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 (
+ + + {isEditing ? 'Edit Course' : 'New Course'} + +
+ + {/* Header */} +
+

+ {isEditing ? 'Edit Course' : 'Create New Course'} +

+
+ + + +
+
+ + + + {error && ( +
+ {error} +
+ )} + + +
+

Course Details

+
+
+
+
+ + handleChange('courseCode', e.target.value)} + className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary" + /> +
+
+ + handleChange('title', e.target.value)} + className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary" + /> +
+
+ +
+
+ + +
+
+ + /courses/ + + handleChange('slug', e.target.value)} + className="border-0 bg-transparent font-mono focus:ring-0 focus:bg-transparent" + /> +
+
+ +
+ +