diff --git a/package-lock.json b/package-lock.json
index 82bcc48..6d8cc63 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@radix-ui/react-slot": "^1.3.3",
"@radix-ui/react-switch": "^1.3.7",
"@radix-ui/react-tabs": "^1.1.21",
+ "axios": "^1.20.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"react": "^19.2.8",
@@ -1934,6 +1935,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
@@ -1951,6 +1964,24 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
+ "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.6",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -2021,6 +2052,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001809",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
@@ -2063,6 +2107,18 @@
"node": ">=6"
}
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2109,7 +2165,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2130,6 +2185,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2140,6 +2204,20 @@
"node": ">=8"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.405",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz",
@@ -2161,6 +2239,51 @@
"node": ">=10.13.0"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -2459,6 +2582,42 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2474,6 +2633,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2484,6 +2652,43 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2510,6 +2715,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2517,6 +2734,45 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -2534,6 +2790,19 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2981,6 +3250,36 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
@@ -3001,7 +3300,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -3169,6 +3467,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
diff --git a/package.json b/package.json
index 2bb3fb2..e31101e 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@radix-ui/react-slot": "^1.3.3",
"@radix-ui/react-switch": "^1.3.7",
"@radix-ui/react-tabs": "^1.1.21",
+ "axios": "^1.20.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"react": "^19.2.8",
diff --git a/src/App.jsx b/src/App.jsx
index 839cc18..d631855 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -2,6 +2,7 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import MainLayout from './layouts/MainLayout.jsx';
import CoursesPage from './pages/Course/CoursesPage.jsx';
import CourseDetail from './pages/Course/CourseDetails.jsx';
+import CourseFormPage from './pages/Course/CourseFormPage.jsx';
import LessonPage from './pages/lesson/LessonPage.jsx';
import LessonFormPage from './pages/lesson/LessonFormPage.jsx';
import MCQPage from './pages/question-bank/MCQPage.jsx';
@@ -20,7 +21,9 @@ function App() {
}>
} />
} />
+ } />
} />
+ } />
} />
} />
} />
diff --git a/src/components/CourseCard.jsx b/src/components/CourseCard.jsx
index 3d1c075..c91dc1b 100644
--- a/src/components/CourseCard.jsx
+++ b/src/components/CourseCard.jsx
@@ -7,7 +7,7 @@ const statusStyles = {
Inactive: 'bg-app-neutral-badge text-app-neutral-badge-text'
};
-function CourseCard({ course, image, handleClick }) {
+function CourseCard({ course, image, handleClick, onEdit }) {
const { code, title, description, status, lastUpdate } = course;
const formattedDate = new Date(lastUpdate).toLocaleDateString('en-GB', {
@@ -65,7 +65,7 @@ function CourseCard({ course, image, handleClick }) {
size="sm"
className="rounded-xl bg-app-surface-muted text-app-text hover:bg-app-interactive-hover"
aria-label={`Edit ${title}`}
- onClick={(e) => e.stopPropagation()}
+ onClick={(e) => { e.stopPropagation(); onEdit && onEdit(); }}
>
diff --git a/src/lib/api.js b/src/lib/api.js
new file mode 100644
index 0000000..72a4a0e
--- /dev/null
+++ b/src/lib/api.js
@@ -0,0 +1,19 @@
+import axios from 'axios';
+
+const api = axios.create({
+ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+api.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ const message = error.response?.data?.message || error.message || 'Something went wrong';
+ console.error('API Error:', message);
+ return Promise.reject(error);
+ }
+);
+
+export default api;
diff --git a/src/pages/Course/CourseDetails.jsx b/src/pages/Course/CourseDetails.jsx
index e155c2b..d6a39cb 100644
--- a/src/pages/Course/CourseDetails.jsx
+++ b/src/pages/Course/CourseDetails.jsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
@@ -23,116 +23,9 @@ import {
HiOutlineArrowUpTray,
HiOutlineFunnel,
} from 'react-icons/hi2';
-
-const coursesData = [
- {
- code: 'LDR-101',
- title: 'Foundations of Modern Leadership',
- description:
- 'An introductory course covering essential leadership principles for new managers.',
- status: 'Active',
- lastUpdate: '2026-08-19',
- image:
- 'https://images.unsplash.com/photo-1556761175-b413da4baf72?auto=format&fit=crop&w=800&q=80',
- },
- {
- code: 'TEC-204',
- title: 'Advanced Cloud Architecture Patterns',
- description:
- 'Deep dive into scalable cloud infrastructure design and implementation strategies.',
- status: 'Inactive',
- lastUpdate: '2026-08-19',
- image:
- 'https://images.unsplash.com/photo-1533090161767-e6ffed986c88?auto=format&fit=crop&w=800&q=80',
- },
- {
- code: 'CMP-099',
- title: '2022 Annual Compliance Review',
- description: 'Outdated compliance materials from the previous fiscal year.',
- status: 'Inactive',
- lastUpdate: '2026-08-19',
- image:
- 'https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=800&q=80',
- },
-];
-
-const lessonsByCourse = {
- 'LDR-101': [
- {
- id: 'les_001',
- title: '1.1 What is Leadership?',
- position: 1,
- type: 'video',
- duration: '12:30',
- status: 'Published',
- isFreePreview: true,
- updatedAt: '2026-08-15',
- },
- {
- id: 'les_002',
- title: '1.2 Leadership Styles Overview',
- position: 2,
- type: 'text',
- duration: '~8 min read',
- status: 'Published',
- isFreePreview: false,
- updatedAt: '2026-08-16',
- },
- {
- id: 'les_003',
- title: '1.3 Self-Assessment Quiz',
- position: 3,
- type: 'quiz',
- duration: '10:00',
- status: 'Draft',
- isFreePreview: false,
- updatedAt: '2026-08-17',
- },
- {
- id: 'les_004',
- title: '2.1 Communication Fundamentals',
- position: 4,
- type: 'video',
- duration: '15:20',
- status: 'Published',
- isFreePreview: false,
- updatedAt: '2026-08-18',
- },
- {
- id: 'les_005',
- title: '2.2 Active Listening Techniques',
- position: 5,
- type: 'text',
- duration: '~6 min read',
- status: 'Draft',
- isFreePreview: false,
- updatedAt: '2026-08-19',
- },
- ],
- 'TEC-204': [
- {
- id: 'les_006',
- title: '1.1 Cloud Fundamentals Recap',
- position: 1,
- type: 'video',
- duration: '18:45',
- status: 'Published',
- isFreePreview: true,
- updatedAt: '2026-08-10',
- },
- {
- id: 'les_007',
- title: '1.2 Microservices Architecture',
- position: 2,
- type: 'embed',
- duration: 'Figma iFrame',
- status: 'Draft',
- isFreePreview: false,
- updatedAt: '2026-08-12',
- },
- ],
- 'CMP-099': [],
-};
+import courseService from '@/services/courseService';
+import lessonService from '@/services/lessonService';
+import sectionService from '@/services/sectionService';
const typeConfig = {
video: { label: 'Video', icon: HiOutlineVideoCamera, color: 'bg-blue-100 text-blue-700' },
@@ -146,20 +39,120 @@ const statusStyles = {
Draft: 'bg-app-neutral-badge text-app-neutral-badge-text',
};
+function formatDuration(seconds) {
+ if (!seconds) return '00:00';
+ const m = Math.floor(seconds / 60);
+ const s = seconds % 60;
+ return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
+}
+
+function mapCourse(c) {
+ return {
+ id: c.id,
+ code: c.courseCode,
+ title: c.title,
+ description: c.description,
+ status: c.isPublished ? 'Active' : 'Inactive',
+ lastUpdate: c.updatedAt,
+ image: c.image,
+ isPublished: c.isPublished,
+ };
+}
+
function CourseDetail() {
const { courseId } = useParams();
const navigate = useNavigate();
- const course = coursesData.find((e) => e.code === courseId);
- const lessons = lessonsByCourse[courseId] || [];
+ const [course, setCourse] = useState(null);
+ const [lessons, setLessons] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState('overview');
- if (!course) {
+ const handleTogglePublish = async () => {
+ try {
+ await courseService.update(courseId, {
+ courseCode: course.code,
+ title: course.title,
+ description: course.description,
+ image: course.image,
+ isPublished: !course.isPublished,
+ });
+ setCourse((prev) => ({ ...prev, isPublished: !prev.isPublished, status: prev.isPublished ? 'Inactive' : 'Active' }));
+ } catch (err) {
+ alert(err.message || 'Failed to update course');
+ }
+ };
+
+ const handleDeleteLesson = async (lessonId) => {
+ if (!window.confirm('Are you sure you want to delete this lesson?')) return;
+ try {
+ await lessonService.delete(lessonId);
+ setLessons((prev) => prev.filter((l) => l.id !== lessonId));
+ } catch (err) {
+ alert(err.message || 'Failed to delete lesson');
+ }
+ };
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const courseData = await courseService.getById(courseId);
+ setCourse(mapCourse(courseData));
+
+ const allLessons = await lessonService.getAll();
+ const courseLessons = allLessons.filter((l) => l.courseId === Number(courseId));
+
+ const lessonsWithSections = await Promise.all(
+ courseLessons.map(async (lesson) => {
+ const sections = await sectionService.getByLessonId(lesson.id);
+ const firstSection = sections[0];
+ return {
+ id: lesson.id,
+ title: lesson.title,
+ position: lesson.position,
+ type: firstSection?.type || 'text',
+ duration: firstSection?.type === 'video'
+ ? formatDuration(firstSection.durationSeconds)
+ : firstSection?.type === 'text'
+ ? `~${Math.max(1, Math.ceil((firstSection.content || '').split(/\s+/).length / 200))} min read`
+ : firstSection?.type === 'quiz'
+ ? `${firstSection.durationSeconds ? Math.floor(firstSection.durationSeconds / 60) : 10}:00`
+ : firstSection?.type === 'embed'
+ ? 'iFrame'
+ : '00:00',
+ status: lesson.isPublished ? 'Published' : 'Draft',
+ isFreePreview: firstSection?.isFreePreview || false,
+ updatedAt: lesson.updatedAt,
+ };
+ })
+ );
+
+ setLessons(lessonsWithSections);
+ } catch (err) {
+ setError(err.message || 'Failed to load course');
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchData();
+ }, [courseId]);
+
+ if (loading) {
+ return (
+ <>
+
+
Loading course...
+ >
+ );
+ }
+
+ if (error || !course) {
return (
<>
-
Course not found
+
{error || 'Course not found'}
+
-
@@ -283,7 +288,9 @@ function CourseDetail() {
Last updated:{' '}
- {course.lastUpdate}
+
+ {course.lastUpdate ? new Date(course.lastUpdate).toLocaleDateString('en-GB') : '-'}
+
Total lessons:{' '}
@@ -368,7 +375,7 @@ function CourseDetail() {
{/* Table Rows */}
{lessons.map((lesson) => {
- const tc = typeConfig[lesson.type];
+ const tc = typeConfig[lesson.type] || typeConfig.text;
const TypeIcon = tc.icon;
return (
- navigate(`/courses/${courseId}/lessons/new`)
+ navigate(`/courses/${courseId}/lessons/${lesson.id}/edit`)
}
className="w-8 h-8 rounded-lg flex items-center justify-center text-app-text-muted hover:text-app-primary hover:bg-app-primary-soft transition-colors"
title="Edit"
@@ -427,6 +434,7 @@ function CourseDetail() {
handleDeleteLesson(lesson.id)}
className="w-8 h-8 rounded-lg flex items-center justify-center text-app-text-muted hover:text-red-500 hover:bg-red-50 transition-colors"
title="Delete"
>
diff --git a/src/pages/Course/CourseFormPage.jsx b/src/pages/Course/CourseFormPage.jsx
new file mode 100644
index 0000000..d239435
--- /dev/null
+++ b/src/pages/Course/CourseFormPage.jsx
@@ -0,0 +1,267 @@
+import { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import PageHeader from '../../components/PageHeader';
+import { Card } from '../../components/ui/card';
+import { Button } from '../../components/ui/button';
+import { Input } from '../../components/ui/input';
+import { Label } from '../../components/ui/label';
+import { Textarea } from '../../components/ui/textarea';
+import { Switch } from '../../components/ui/switch';
+import { Separator } from '../../components/ui/separator';
+import {
+ HiOutlineArrowLeft,
+ HiOutlineBookOpen,
+ HiOutlineChevronRight,
+} from 'react-icons/hi2';
+import courseService from '@/services/courseService';
+
+function CourseFormPage() {
+ const { courseId } = useParams();
+ const navigate = useNavigate();
+ const isEditing = Boolean(courseId);
+
+ const [form, setForm] = useState({
+ courseCode: '',
+ title: '',
+ slug: '',
+ description: '',
+ image: '',
+ isPublished: false,
+ });
+ const [loading, setLoading] = useState(isEditing);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!isEditing) return;
+ const fetchCourse = async () => {
+ try {
+ const data = await courseService.getById(courseId);
+ setForm({
+ courseCode: data.courseCode || '',
+ title: data.title || '',
+ slug: data.slug || '',
+ description: data.description || '',
+ image: data.image || '',
+ isPublished: data.isPublished || false,
+ });
+ } catch (err) {
+ setError(err.message || 'Failed to load course');
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchCourse();
+ }, [courseId, isEditing]);
+
+ const handleChange = (field, value) => {
+ setForm((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const generateSlug = () => {
+ const slug = form.title
+ .toLowerCase()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .replace(/đ/g, 'd')
+ .replace(/[^a-z0-9\s-]/g, '')
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-')
+ .trim();
+ handleChange('slug', slug);
+ };
+
+ const handleSave = async (publish) => {
+ if (!form.courseCode.trim() || !form.title.trim()) {
+ setError('Course code and title are required');
+ return;
+ }
+ setSaving(true);
+ setError(null);
+ try {
+ const payload = { ...form, isPublished: publish };
+ if (isEditing) {
+ await courseService.update(courseId, payload);
+ } else {
+ const created = await courseService.create(payload);
+ navigate(`/courses/${created.id}`);
+ return;
+ }
+ navigate(`/courses/${courseId}`);
+ } catch (err) {
+ setError(err.message || 'Failed to save course');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return (
+ <>
+
+ Loading course...
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+ {/* Breadcrumb */}
+
+ navigate('/courses')}
+ className="hover:text-app-primary transition-colors flex items-center gap-1"
+ >
+
+ Courses
+
+
+
+ {isEditing ? 'Edit Course' : 'New Course'}
+
+
+
+ {/* Header */}
+
+
+ {isEditing ? 'Edit Course' : 'Create New Course'}
+
+
+ navigate(isEditing ? `/courses/${courseId}` : '/courses')}
+ disabled={saving}
+ >
+
+ Cancel
+
+ handleSave(false)}
+ disabled={saving}
+ >
+ {saving ? 'Saving...' : 'Save Draft'}
+
+ handleSave(true)}
+ disabled={saving}
+ >
+ {saving ? 'Saving...' : isEditing ? 'Update & Publish' : 'Save & Publish'}
+
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
Course Details
+
+
+
+
+
+
+
+
+ Auto-generate from title
+
+
+
+
+ /courses/
+
+ handleChange('slug', e.target.value)}
+ className="border-0 bg-transparent font-mono focus:ring-0 focus:bg-transparent"
+ />
+
+
+
+
+
+
+
+
+
+ handleChange('image', e.target.value)}
+ className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary"
+ />
+
+
+
+
+ Published
+ Make this course visible to students
+
+
handleChange('isPublished', checked)}
+ />
+
+
+
+
+ >
+ );
+}
+
+export default CourseFormPage;
diff --git a/src/pages/Course/CoursesPage.jsx b/src/pages/Course/CoursesPage.jsx
index ff9eb46..c47cc03 100644
--- a/src/pages/Course/CoursesPage.jsx
+++ b/src/pages/Course/CoursesPage.jsx
@@ -1,3 +1,4 @@
+import { useState, useEffect } from 'react';
import CourseCard from '../../components/CourseCard.jsx';
import PageHeader from '../../components/PageHeader.jsx';
import { Button } from '../../components/ui/button.jsx';
@@ -6,45 +7,58 @@ import { Input } from '../../components/ui/input.jsx';
import { Separator } from '../../components/ui/separator.jsx';
import { HiOutlineFunnel, HiOutlinePlus, HiOutlineMagnifyingGlass } from 'react-icons/hi2';
import { useNavigate } from 'react-router-dom';
+import courseService from '@/services/courseService';
-const stats = [
- {
- code: 'LDR-101',
- title: 'Foundations of Modern Leadership',
- description:
- 'An introductory course covering essential leadership principles for new managers.',
- status: 'Active',
- lastUpdate: '2026-08-19',
- image:
- 'https://images.unsplash.com/photo-1556761175-b413da4baf72?auto=format&fit=crop&w=800&q=80'
- },
- {
- code: 'TEC-204',
- title: 'Advanced Cloud Architecture Patterns',
- description:
- 'Deep dive into scalable cloud infrastructure design and implementation strategies.',
- status: 'Inactive',
- lastUpdate: '2026-08-19',
- image:
- 'https://images.unsplash.com/photo-1533090161767-e6ffed986c88?auto=format&fit=crop&w=800&q=80'
- },
- {
- code: 'CMP-099',
- title: '2022 Annual Compliance Review',
- description: 'Outdated compliance materials from the previous fiscal year.',
- status: 'Inactive',
- lastUpdate: '2026-08-19',
- image:
- 'https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=800&q=80'
- }
-];
+function mapCourse(c) {
+ return {
+ id: c.id,
+ code: c.courseCode,
+ title: c.title,
+ description: c.description,
+ status: c.isPublished ? 'Active' : 'Inactive',
+ lastUpdate: c.updatedAt,
+ image: c.image,
+ };
+}
function CoursePage() {
const navigate = useNavigate();
+ const [courses, setCourses] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [activeTab, setActiveTab] = useState('all');
+ const [search, setSearch] = useState('');
+
+ useEffect(() => {
+ const fetchCourses = async () => {
+ try {
+ const data = await courseService.getAll();
+ setCourses(data.map(mapCourse));
+ } catch (err) {
+ setError(err.message || 'Failed to load courses');
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchCourses();
+ }, []);
+
+ const filtered = courses.filter((c) => {
+ const matchTab =
+ activeTab === 'all' ||
+ (activeTab === 'active' && c.status === 'Active') ||
+ (activeTab === 'inactive' && c.status === 'Inactive');
+ const matchSearch =
+ !search ||
+ c.title.toLowerCase().includes(search.toLowerCase()) ||
+ c.code.toLowerCase().includes(search.toLowerCase());
+ return matchTab && matchSearch;
+ });
const handleClick = (course) => {
- navigate(`/courses/${course.code}`);
+ navigate(`/courses/${course.id}`);
};
+
return (
<>
@@ -65,7 +79,10 @@ function CoursePage() {
Filter
-
+ navigate('/courses/new')}
+ >
New Course
@@ -73,7 +90,7 @@ function CoursePage() {
-
+
All
Active
@@ -85,19 +102,36 @@ function CoursePage() {
setSearch(e.target.value)}
/>
-
- {stats.map((course) => (
- handleClick(course)}
- />
- ))}
-
+ {loading ? (
+ Loading courses...
+ ) : error ? (
+
+
{error}
+
window.location.reload()}>
+ Retry
+
+
+ ) : (
+
+ {filtered.map((course) => (
+
handleClick(course)}
+ onEdit={() => navigate(`/courses/${course.id}/edit`)}
+ />
+ ))}
+ {filtered.length === 0 && (
+ No courses found.
+ )}
+
+ )}
>
);
}
diff --git a/src/pages/lesson/LessonFormPage.jsx b/src/pages/lesson/LessonFormPage.jsx
index 9071709..78a228c 100644
--- a/src/pages/lesson/LessonFormPage.jsx
+++ b/src/pages/lesson/LessonFormPage.jsx
@@ -1,60 +1,183 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import PageHeader from '../../components/PageHeader';
import ActionBar from './components/ActionBar';
import LessonMetadataForm from './components/LessonMetadataForm';
import SectionManager from './components/SectionManager';
import LessonSidebar from './components/LessonSidebar';
+import lessonService from '@/services/lessonService';
+import sectionService from '@/services/sectionService';
-const initialMetadata = {
+const emptyMetadata = {
title: '',
position: 1,
slug: '',
description: '',
};
-const initialSections = [
- {
- id: 'section_1',
- type: 'video',
- title: 'Introduction to Grid 8pt System',
- url: 'https://player.vimeo.com/video/847291039',
- duration: '08:45',
- thumbnail: '',
- isFreePreview: true,
- },
- {
- id: 'section_2',
- type: 'text',
- title: 'Breakpoint Standards & Responsive Guide',
- content:
- '## 1. Standard Breakpoints\n\nIn modern responsive design, the grid must follow the soft 8-pt grid structure.\n\n```\n$screens: ( sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px );\n```\n\nAll spacing (gutter, margin) must be a multiple of 8: 8px, 16px, 24px, 32px...',
- isFreePreview: false,
- },
- {
- id: 'section_3',
- type: 'quiz',
- title: 'Knowledge Check: Breakpoints & Fluid Grids',
- questionCount: 5,
- timeLimit: 10,
- passScore: 80,
- maxAttempts: 3,
- isFreePreview: false,
- },
- {
- id: 'section_4',
- type: 'embed',
- title: 'Figma Prototype Interaction',
- embedUrl: 'https://www.figma.com/embed?embed_host=share&url=https://www.figma.com/file/rN7X3d/Responsive-Design-Library',
- isFreePreview: false,
- },
-];
+function mapSectionFromBE(s) {
+ const base = {
+ id: `section_${s.id}`,
+ serverId: s.id,
+ type: s.type,
+ title: s.title,
+ isFreePreview: s.isFreePreview,
+ position: s.position,
+ };
+
+ switch (s.type) {
+ case 'video':
+ return {
+ ...base,
+ url: s.content || '',
+ duration: s.durationSeconds
+ ? `${String(Math.floor(s.durationSeconds / 60)).padStart(2, '0')}:${String(s.durationSeconds % 60).padStart(2, '0')}`
+ : '',
+ thumbnail: '',
+ };
+ case 'text':
+ return { ...base, content: s.content || '' };
+ case 'quiz': {
+ let parsed = {};
+ try { parsed = JSON.parse(s.content || '{}'); } catch { /* ignore */ }
+ return {
+ ...base,
+ questionCount: parsed.questionCount || 5,
+ timeLimit: parsed.timeLimit || 10,
+ passScore: parsed.passScore || 80,
+ maxAttempts: parsed.maxAttempts || 3,
+ };
+ }
+ case 'embed':
+ return { ...base, embedUrl: s.content || '' };
+ default:
+ return { ...base, content: s.content || '' };
+ }
+}
+
+function mapSectionToBE(section, lessonId) {
+ const base = {
+ lessonId,
+ title: section.title,
+ type: section.type,
+ position: section.position,
+ isFreePreview: section.isFreePreview,
+ };
+
+ switch (section.type) {
+ case 'video': {
+ const parts = (section.duration || '00:00').split(':').map(Number);
+ return {
+ ...base,
+ content: section.url || '',
+ durationSeconds: (parts[0] || 0) * 60 + (parts[1] || 0),
+ };
+ }
+ case 'text':
+ return { ...base, content: section.content || '' };
+ case 'quiz':
+ return {
+ ...base,
+ content: JSON.stringify({
+ questionCount: section.questionCount,
+ timeLimit: section.timeLimit,
+ passScore: section.passScore,
+ maxAttempts: section.maxAttempts,
+ }),
+ durationSeconds: (section.timeLimit || 10) * 60,
+ };
+ case 'embed':
+ return { ...base, content: section.embedUrl || '' };
+ default:
+ return { ...base, content: section.content || '' };
+ }
+}
function LessonFormPage() {
- const { courseId } = useParams();
- const [metadata, setMetadata] = useState(initialMetadata);
- const [sections, setSections] = useState(initialSections);
+ const { courseId, lessonId } = useParams();
+ const isEditing = Boolean(lessonId);
+
+ const [metadata, setMetadata] = useState(emptyMetadata);
+ const [sections, setSections] = useState([]);
const [isPublished, setIsPublished] = useState(false);
+ const [loading, setLoading] = useState(isEditing);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!isEditing) return;
+
+ const fetchLesson = async () => {
+ try {
+ const lesson = await lessonService.getById(lessonId);
+ setMetadata({
+ title: lesson.title,
+ position: lesson.position,
+ slug: lesson.slug || '',
+ description: lesson.description || '',
+ });
+ setIsPublished(lesson.isPublished);
+
+ const sectionData = await sectionService.getByLessonId(lessonId);
+ setSections(sectionData.sort((a, b) => a.position - b.position).map(mapSectionFromBE));
+ } catch (err) {
+ setError(err.message || 'Failed to load lesson');
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchLesson();
+ }, [lessonId, isEditing]);
+
+ const handleSave = async (publish) => {
+ setSaving(true);
+ setError(null);
+ try {
+ const lessonPayload = {
+ courseId: Number(courseId),
+ title: metadata.title,
+ slug: metadata.slug,
+ description: metadata.description,
+ position: Number(metadata.position) || 1,
+ isPublished: publish,
+ };
+
+ let savedLesson;
+ if (isEditing) {
+ savedLesson = await lessonService.update(lessonId, lessonPayload);
+ } else {
+ savedLesson = await lessonService.create(lessonPayload);
+ }
+
+ const savedLessonId = savedLesson.id;
+
+ for (let i = 0; i < sections.length; i++) {
+ const section = sections[i];
+ const sectionPayload = mapSectionToBE({ ...section, position: i + 1 }, savedLessonId);
+
+ if (section.serverId) {
+ await sectionService.update(section.serverId, sectionPayload);
+ } else {
+ await sectionService.create(sectionPayload);
+ }
+ }
+
+ alert(isEditing ? 'Lesson updated successfully!' : 'Lesson created successfully!');
+ } catch (err) {
+ setError(err.message || 'Failed to save lesson');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return (
+ <>
+
+ Loading lesson...
+ >
+ );
+ }
return (
<>
@@ -64,9 +187,17 @@ function LessonFormPage() {
setIsPublished(!isPublished)}
+ onTogglePublish={() => handleSave(true)}
+ onSaveDraft={() => handleSave(false)}
+ saving={saving}
/>
+ {error && (
+
+ {error}
+
+ )}
+
diff --git a/src/pages/lesson/LessonPage.jsx b/src/pages/lesson/LessonPage.jsx
index e26cac1..fe20d9d 100644
--- a/src/pages/lesson/LessonPage.jsx
+++ b/src/pages/lesson/LessonPage.jsx
@@ -1,18 +1,21 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
import PageHeader from '../../components/PageHeader';
import { DataTable } from '../../components/ui/data-table';
+import lessonService from '@/services/lessonService';
-const mockLessons = [
- { id: 1, code: 'LES001', name: 'Introduction to Programming', description: 'Basic concepts of programming', createdBy: 'Admin', createdAt: '2026-08-01' },
- { id: 2, code: 'LES002', name: 'Data Types and Variables', description: 'Understanding data types', createdBy: 'Admin', createdAt: '2026-08-03' },
- { id: 3, code: 'LES003', name: 'Control Flow', description: 'If/else and loops', createdBy: 'Teacher1', createdAt: '2026-08-05' },
- { id: 4, code: 'LES004', name: 'Functions', description: 'Function declaration and usage', createdBy: 'Teacher1', createdAt: '2026-08-07' },
- { id: 5, code: 'LES005', name: 'Arrays', description: 'Working with arrays', createdBy: 'Admin', createdAt: '2026-08-10' },
- { id: 6, code: 'LES006', name: 'Introduction to Programming', description: 'Basic concepts of programming', createdBy: 'Admin', createdAt: '2026-08-01' },
- { id: 7, code: 'LES007', name: 'Data Types and Variables', description: 'Understanding data types', createdBy: 'Admin', createdAt: '2026-08-03' },
- { id: 8, code: 'LES008', name: 'Control Flow', description: 'If/else and loops', createdBy: 'Teacher1', createdAt: '2026-08-05' },
- { id: 9, code: 'LES009', name: 'Functions', description: 'Function declaration and usage', createdBy: 'Teacher1', createdAt: '2026-08-07' },
- { id: 10, code: 'LES010', name: 'Arrays', description: 'Working with arrays', createdBy: 'Admin', createdAt: '2026-08-10' },
-];
+function mapLesson(l) {
+ return {
+ id: l.id,
+ code: `LES${String(l.id).padStart(3, '0')}`,
+ name: l.title,
+ description: l.description || '',
+ courseId: l.courseId,
+ position: l.position,
+ isPublished: l.isPublished,
+ createdAt: l.createdAt,
+ };
+}
const actionBtn = {
padding: '6px 12px',
@@ -39,44 +42,95 @@ const actionBtnDelete = {
transition: 'background 160ms ease, color 160ms ease',
};
-const columns = [
- { key: '#', label: '#', render: (_row, idx) => idx },
- {
- key: 'code',
- label: 'LessonCode',
- tdStyle: { fontFamily: "'SF Mono', Consolas, monospace", fontSize: 13, color: '#5741d8', fontWeight: 500 },
- },
- { key: 'name', label: 'LessonName', tdStyle: { fontWeight: 500 } },
- { key: 'description', label: 'Description', tdStyle: { color: '#686b82' } },
- { key: 'createdBy', label: 'Created By' },
- { key: 'createdAt', label: 'Created At', tdStyle: { color: '#9497a9', fontSize: 13 } },
- {
- key: 'actions',
- label: 'Action',
- render: () => (
-
- { e.target.style.background = '#7132f5'; e.target.style.color = '#ffffff'; }}
- onMouseLeave={(e) => { e.target.style.background = '#ffffff'; e.target.style.color = '#5741d8'; }}
- >
- Edit
-
- { e.target.style.background = 'rgba(20,158,97,0.16)'; e.target.style.color = '#026b3f'; e.target.style.borderColor = '#149e61'; }}
- onMouseLeave={(e) => { e.target.style.background = 'transparent'; e.target.style.color = '#9497a9'; e.target.style.borderColor = '#dedee5'; }}
- >
- Delete
-
-
- ),
- },
-];
-
function LessonPage() {
+ const navigate = useNavigate();
+ const [lessons, setLessons] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchLessons = async () => {
+ try {
+ const data = await lessonService.getAll();
+ setLessons(data.map(mapLesson));
+ } catch (err) {
+ setError(err.message || 'Failed to load lessons');
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchLessons();
+ }, []);
+
+ const handleDelete = async (id) => {
+ if (!window.confirm('Are you sure you want to delete this lesson?')) return;
+ try {
+ await lessonService.delete(id);
+ setLessons((prev) => prev.filter((l) => l.id !== id));
+ } catch (err) {
+ alert(err.message || 'Failed to delete lesson');
+ }
+ };
+
+ const columns = [
+ { key: '#', label: '#', render: (_row, idx) => idx },
+ {
+ key: 'code',
+ label: 'LessonCode',
+ tdStyle: { fontFamily: "'SF Mono', Consolas, monospace", fontSize: 13, color: '#5741d8', fontWeight: 500 },
+ },
+ { key: 'name', label: 'LessonName', tdStyle: { fontWeight: 500 } },
+ { key: 'description', label: 'Description', tdStyle: { color: '#686b82' } },
+ {
+ key: 'isPublished',
+ label: 'Status',
+ render: (row) => (
+
+ {row.isPublished ? 'Published' : 'Draft'}
+
+ ),
+ },
+ {
+ key: 'createdAt',
+ label: 'Created At',
+ render: (row) => row.createdAt ? new Date(row.createdAt).toLocaleDateString('en-GB') : '-',
+ tdStyle: { color: '#9497a9', fontSize: 13 },
+ },
+ {
+ key: 'actions',
+ label: 'Action',
+ render: (row) => (
+
+ navigate(`/courses/${row.courseId}/lessons/${row.id}/edit`)}
+ onMouseEnter={(e) => { e.target.style.background = '#7132f5'; e.target.style.color = '#ffffff'; }}
+ onMouseLeave={(e) => { e.target.style.background = '#ffffff'; e.target.style.color = '#5741d8'; }}
+ >
+ Edit
+
+ handleDelete(row.id)}
+ onMouseEnter={(e) => { e.target.style.background = 'rgba(20,158,97,0.16)'; e.target.style.color = '#026b3f'; e.target.style.borderColor = '#149e61'; }}
+ onMouseLeave={(e) => { e.target.style.background = 'transparent'; e.target.style.color = '#9497a9'; e.target.style.borderColor = '#dedee5'; }}
+ >
+ Delete
+
+
+ ),
+ },
+ ];
+
return (
<>
@@ -95,18 +149,32 @@ function LessonPage() {
fontSize: 14,
fontWeight: 500,
}}
+ onClick={() => {
+ const firstCourseId = lessons.length > 0 ? lessons[0].courseId : null;
+ if (firstCourseId) {
+ navigate(`/courses/${firstCourseId}/lessons/new`);
+ } else {
+ alert('Please create a course first before adding lessons.');
+ }
+ }}
>
+ Add Lesson
-
+ {loading ? (
+
Loading lessons...
+ ) : error ? (
+
{error}
+ ) : (
+
+ )}
>
);
diff --git a/src/pages/lesson/components/ActionBar.jsx b/src/pages/lesson/components/ActionBar.jsx
index 51c5b40..b19c5b9 100644
--- a/src/pages/lesson/components/ActionBar.jsx
+++ b/src/pages/lesson/components/ActionBar.jsx
@@ -1,17 +1,27 @@
import { Button } from '../../../components/ui/button';
-import { HiOutlineArrowLeft, HiOutlineBookmark, HiOutlineRocketLaunch } from 'react-icons/hi2';
+import {
+ HiOutlineArrowLeft,
+ HiOutlineBookmark,
+ HiOutlineRocketLaunch
+} from 'react-icons/hi2';
import { Link } from 'react-router-dom';
-function ActionBar({ courseId, isPublished, onTogglePublish }) {
+function ActionBar({ courseId, isPublished, onTogglePublish, onSaveDraft, saving }) {
return (
-
+
Courses
/
-
+
{courseId}
/
@@ -26,6 +36,7 @@ function ActionBar({ courseId, isPublished, onTogglePublish }) {
Back
@@ -33,16 +44,19 @@ function ActionBar({ courseId, isPublished, onTogglePublish }) {
- Save Draft
+ {saving ? 'Saving...' : 'Save Draft'}
- {isPublished ? 'Update & Publish' : 'Save & Publish'}
+ {saving ? 'Saving...' : isPublished ? 'Update & Publish' : 'Save & Publish'}
diff --git a/src/services/courseService.js b/src/services/courseService.js
new file mode 100644
index 0000000..e57d03f
--- /dev/null
+++ b/src/services/courseService.js
@@ -0,0 +1,31 @@
+import api from '@/lib/api';
+
+const BASE_PATH = '/api/courses';
+
+const courseService = {
+ getAll: async () => {
+ const response = await api.get(BASE_PATH);
+ return response.data;
+ },
+
+ getById: async (id) => {
+ const response = await api.get(`${BASE_PATH}/${id}`);
+ return response.data;
+ },
+
+ create: async (data) => {
+ const response = await api.post(BASE_PATH, data);
+ return response.data;
+ },
+
+ update: async (id, data) => {
+ const response = await api.put(`${BASE_PATH}/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id) => {
+ await api.delete(`${BASE_PATH}/${id}`);
+ },
+};
+
+export default courseService;
diff --git a/src/services/lessonService.js b/src/services/lessonService.js
new file mode 100644
index 0000000..236a794
--- /dev/null
+++ b/src/services/lessonService.js
@@ -0,0 +1,31 @@
+import api from '@/lib/api';
+
+const BASE_PATH = '/api/lessons';
+
+const lessonService = {
+ getAll: async () => {
+ const response = await api.get(BASE_PATH);
+ return response.data;
+ },
+
+ getById: async (id) => {
+ const response = await api.get(`${BASE_PATH}/${id}`);
+ return response.data;
+ },
+
+ create: async (data) => {
+ const response = await api.post(BASE_PATH, data);
+ return response.data;
+ },
+
+ update: async (id, data) => {
+ const response = await api.put(`${BASE_PATH}/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id) => {
+ await api.delete(`${BASE_PATH}/${id}`);
+ },
+};
+
+export default lessonService;
diff --git a/src/services/sectionService.js b/src/services/sectionService.js
new file mode 100644
index 0000000..f9b1bf3
--- /dev/null
+++ b/src/services/sectionService.js
@@ -0,0 +1,36 @@
+import api from '@/lib/api';
+
+const BASE_PATH = '/api/sections';
+
+const sectionService = {
+ getAll: async () => {
+ const response = await api.get(BASE_PATH);
+ return response.data;
+ },
+
+ getById: async (id) => {
+ const response = await api.get(`${BASE_PATH}/${id}`);
+ return response.data;
+ },
+
+ getByLessonId: async (lessonId) => {
+ const response = await api.get(`${BASE_PATH}/by-lesson/${lessonId}`);
+ return response.data;
+ },
+
+ create: async (data) => {
+ const response = await api.post(BASE_PATH, data);
+ return response.data;
+ },
+
+ update: async (id, data) => {
+ const response = await api.put(`${BASE_PATH}/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id) => {
+ await api.delete(`${BASE_PATH}/${id}`);
+ },
+};
+
+export default sectionService;