From 95242709b0516ef5621041bdae3fc44f4a48d103 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Wed, 20 May 2026 13:25:03 +0800 Subject: [PATCH] Adjusted --- package.json | 1 + pnpm-lock.yaml | 10 + src/components/generic/AssetPickerSheet.jsx | 71 +- src/components/generic/BlockList.jsx | 110 ++- src/components/generic/Blocks/TextBlock.jsx | 467 +++++++++- .../generic/Blocks/TextImageBlock.jsx | 24 +- .../generic/Blocks/TextVideoBlock.jsx | 19 +- src/components/generic/Table/DataTable.jsx | 1 + src/components/ui/switch.jsx | 2 - src/contexts/AdminCoursesContext.jsx | 828 +++++++++++++++--- src/data/adminTiles.data.js | 4 +- src/index.css | 1 + .../courses/ArchivedCourseTable.jsx | 132 +++ .../courses/ArchivedLessonsTable.jsx | 0 .../components/courses/ArchivedUnitsTable.jsx | 0 .../admin/components/courses/CourseTable.jsx | 28 +- .../components/courses/LessonsPreview.jsx | 208 +++++ .../admin/components/courses/LessonsTable.jsx | 4 +- .../components/courses/QuestionEditor.jsx | 274 ++++++ .../admin/components/courses/UnitsTable.jsx | 4 +- .../admin/config/assets/rowActions.config.jsx | 4 +- .../config/courses/archive/columns.config.jsx | 70 ++ .../courses/archive/rowActions.config.jsx | 21 + .../courses/archive/selection.config.jsx | 31 + .../config/courses/archive/toolbar.config.jsx | 39 + .../admin/config/courses/columns.config.jsx | 94 +- .../lessons/archive/columns.config.jsx | 47 + .../lessons/archive/rowActions.config.jsx | 41 + .../lessons/archive/selection.config.jsx | 30 + .../lessons/archive/toolbar.config.jsx | 62 ++ .../config/courses/lessons/columns.config.jsx | 85 +- .../courses/lessons/rowActions.config.jsx | 72 +- .../courses/lessons/selection.config.jsx | 30 + .../config/courses/lessons/toolbar.config.jsx | 13 +- .../config/courses/rowActions.config.jsx | 71 +- .../admin/config/courses/selection.config.jsx | 16 +- .../admin/config/courses/toolbar.config.jsx | 13 +- .../courses/units/archive/columns.config.jsx | 47 + .../units/archive/rowActions.config.jsx | 42 + .../units/archive/selection.config.jsx | 30 + .../courses/units/archive/toolbar.config.jsx | 57 ++ .../config/courses/units/columns.config.jsx | 71 +- .../courses/units/rowActions.config.jsx | 71 +- .../config/courses/units/selection.config.jsx | 30 + .../config/courses/units/toolbar.config.jsx | 13 +- .../config/user_groups/rowActions.config.jsx | 4 +- .../admin/config/users/rowActions.config.jsx | 4 +- src/modules/admin/layouts/AdminLayout.jsx | 13 +- src/modules/admin/pages/AdminDashboard.jsx | 4 +- src/modules/admin/pages/courses/AddCourse.jsx | 251 ++++++ .../pages/courses/ArchivedCourseList.jsx | 24 + .../admin/pages/courses/CourseAssessment.jsx | 560 ++++++++++++ .../admin/pages/courses/CourseDetail.jsx | 73 -- .../admin/pages/courses/CreateCourse.jsx | 103 --- .../admin/pages/courses/EditCourse.jsx | 233 ++++- .../admin/pages/courses/LessonDetail.jsx | 99 --- .../admin/pages/courses/LessonPageBuilder.jsx | 157 ---- .../admin/pages/courses/ViewCourse.jsx | 241 +++++ .../AddLesson.jsx} | 95 +- .../courses/{ => lessons}/EditLesson.jsx | 85 +- .../courses/lessons/LessonPageBuilder.jsx | 246 ++++++ .../LessonsList.jsx} | 15 +- .../pages/courses/lessons/ViewLesson.jsx | 127 +++ .../pages/courses/lessons/ViewLessonPage.jsx | 89 ++ .../{CreateUnit.jsx => units/AddUnit.jsx} | 25 +- .../pages/courses/{ => units}/EditUnit.jsx | 22 +- .../admin/pages/courses/units/UnitQuiz.jsx | 542 ++++++++++++ .../admin/pages/courses/units/UnitsList.jsx | 100 +++ .../admin/pages/courses/units/ViewUnit.jsx | 79 ++ src/modules/admin/routes/AdminRoutes.jsx | 77 +- src/utils/timestamp.util.js | 14 +- 71 files changed, 5535 insertions(+), 1035 deletions(-) create mode 100644 src/modules/admin/components/courses/ArchivedCourseTable.jsx create mode 100644 src/modules/admin/components/courses/ArchivedLessonsTable.jsx create mode 100644 src/modules/admin/components/courses/ArchivedUnitsTable.jsx create mode 100644 src/modules/admin/components/courses/LessonsPreview.jsx create mode 100644 src/modules/admin/components/courses/QuestionEditor.jsx create mode 100644 src/modules/admin/config/courses/archive/columns.config.jsx create mode 100644 src/modules/admin/config/courses/archive/rowActions.config.jsx create mode 100644 src/modules/admin/config/courses/archive/selection.config.jsx create mode 100644 src/modules/admin/config/courses/archive/toolbar.config.jsx create mode 100644 src/modules/admin/config/courses/lessons/archive/columns.config.jsx create mode 100644 src/modules/admin/config/courses/lessons/archive/rowActions.config.jsx create mode 100644 src/modules/admin/config/courses/lessons/archive/selection.config.jsx create mode 100644 src/modules/admin/config/courses/lessons/archive/toolbar.config.jsx create mode 100644 src/modules/admin/config/courses/lessons/selection.config.jsx create mode 100644 src/modules/admin/config/courses/units/archive/columns.config.jsx create mode 100644 src/modules/admin/config/courses/units/archive/rowActions.config.jsx create mode 100644 src/modules/admin/config/courses/units/archive/selection.config.jsx create mode 100644 src/modules/admin/config/courses/units/archive/toolbar.config.jsx create mode 100644 src/modules/admin/config/courses/units/selection.config.jsx create mode 100644 src/modules/admin/pages/courses/AddCourse.jsx create mode 100644 src/modules/admin/pages/courses/ArchivedCourseList.jsx create mode 100644 src/modules/admin/pages/courses/CourseAssessment.jsx delete mode 100644 src/modules/admin/pages/courses/CourseDetail.jsx delete mode 100644 src/modules/admin/pages/courses/CreateCourse.jsx delete mode 100644 src/modules/admin/pages/courses/LessonDetail.jsx delete mode 100644 src/modules/admin/pages/courses/LessonPageBuilder.jsx create mode 100644 src/modules/admin/pages/courses/ViewCourse.jsx rename src/modules/admin/pages/courses/{CreateLesson.jsx => lessons/AddLesson.jsx} (50%) rename src/modules/admin/pages/courses/{ => lessons}/EditLesson.jsx (57%) create mode 100644 src/modules/admin/pages/courses/lessons/LessonPageBuilder.jsx rename src/modules/admin/pages/courses/{UnitDetail.jsx => lessons/LessonsList.jsx} (77%) create mode 100644 src/modules/admin/pages/courses/lessons/ViewLesson.jsx create mode 100644 src/modules/admin/pages/courses/lessons/ViewLessonPage.jsx rename src/modules/admin/pages/courses/{CreateUnit.jsx => units/AddUnit.jsx} (85%) rename src/modules/admin/pages/courses/{ => units}/EditUnit.jsx (87%) create mode 100644 src/modules/admin/pages/courses/units/UnitQuiz.jsx create mode 100644 src/modules/admin/pages/courses/units/UnitsList.jsx create mode 100644 src/modules/admin/pages/courses/units/ViewUnit.jsx diff --git a/package.json b/package.json index 7d922d6..cc03954 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "framer-motion": "^12.38.0", "input-otp": "^1.4.2", "lucide-react": "^1.14.0", + "nanoid": "^5.1.11", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19.2.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ae3d87..4218d5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: lucide-react: specifier: ^1.14.0 version: 1.14.0(react@19.2.5) + nanoid: + specifier: ^5.1.11 + version: 5.1.11 next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -2647,6 +2650,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.11: + resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5821,6 +5829,8 @@ snapshots: nanoid@3.3.12: {} + nanoid@5.1.11: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} diff --git a/src/components/generic/AssetPickerSheet.jsx b/src/components/generic/AssetPickerSheet.jsx index 1fe91b4..ab7a77a 100644 --- a/src/components/generic/AssetPickerSheet.jsx +++ b/src/components/generic/AssetPickerSheet.jsx @@ -1,21 +1,13 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { Search, CheckCircle2 } from "lucide-react"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetDescription, -} from "@/components/ui/sheet"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet"; import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; -import { Badge } from "@/components/ui/badge"; import { useAssets } from "@/contexts/AdminAssetsContext"; -// ─── Empty ──────────────────────────────────────────────────────────────────── - function EmptyState({ fileType }) { return (
@@ -26,8 +18,6 @@ function EmptyState({ fileType }) { ); } -// ─── Asset Card ─────────────────────────────────────────────────────────────── - function AssetCard({ asset, selected, onSelect }) { const thumb = asset.thumbnail_url ?? asset.file_url; @@ -43,7 +33,6 @@ function AssetCard({ asset, selected, onSelect }) { : "border-border", ].join(" ")} > - {/* Thumbnail */}
{thumb ? ( )}
- - {/* Name */}

{asset.display_name}

- - {/* Selected checkmark */} {selected && (
@@ -73,33 +58,39 @@ function AssetCard({ asset, selected, onSelect }) { ); } -// ─── Sheet ──────────────────────────────────────────────────────────────────── - export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) { const { fetchAssets, assets, pagination, loading } = useAssets(); - const [search, setSearch] = useState(""); - const [page, setPage] = useState(1); - const [selected, setSelected] = useState(null); + + const [search, setSearch] = useState(""); + const [committed, setCommitted] = useState(""); // ← only updates on search trigger + const [page, setPage] = useState(1); + const [selected, setSelected] = useState(null); const LIMIT = 12; - // ── Fetch on open / search / page change ────────────────────────────────── + const triggerSearch = useCallback(() => { + setCommitted(search); + setPage(1); + }, [search]); + + // ── Fetch only when committed search, page, or open changes ────────────── useEffect(() => { if (!open) return; fetchAssets({ page, limit: LIMIT, filters: [ - ...(fileType ? [{ id: "file_type", value: [fileType] }] : []), - ...(search ? [{ id: "display_name", value: [search] }] : []), + ...(fileType ? [{ id: "file_type", value: [fileType] }] : []), + ...(committed ? [{ id: "display_name", value: [committed] }] : []), ], }); - }, [open, page, search, fileType]); + }, [open, page, committed, fileType]); // ── Reset on close ──────────────────────────────────────────────────────── useEffect(() => { if (!open) { setSearch(""); + setCommitted(""); setPage(1); setSelected(null); } @@ -129,14 +120,24 @@ export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) { {/* ── Search ── */}
-
- - { setSearch(e.target.value); setPage(1); }} - className="pl-9" - /> +
+
+ + setSearch(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && triggerSearch()} + className="pl-9" + /> +
+
diff --git a/src/components/generic/BlockList.jsx b/src/components/generic/BlockList.jsx index 7e1fe54..72a4517 100644 --- a/src/components/generic/BlockList.jsx +++ b/src/components/generic/BlockList.jsx @@ -1,66 +1,84 @@ // components/generic/CMS/BlockList.jsx -import { BlockWrapper } from "./BlockWrapper"; -import { TextBlock } from "./Blocks/TextBlock"; -import { ImageBlock } from "./Blocks/ImageBlock"; +import { BlockWrapper } from "./BlockWrapper"; +import { TextBlock } from "./Blocks/TextBlock"; +import { ImageBlock } from "./Blocks/ImageBlock"; import { TextImageBlock } from "./Blocks/TextImageBlock"; -import { VideoBlock } from "./Blocks/VideoBlock"; +import { VideoBlock } from "./Blocks/VideoBlock"; import { TextVideoBlock } from "./Blocks/TextVideoBlock"; // ─── Block renderer ─────────────────────────────────────────────────────────── +// +// blockId is forwarded to TextBlock (and any future rich-text blocks) so the +// WYSIWYG editor knows exactly when to seed its innerHTML from saved data. -function BlockContent({ type, content, onUpdate }) { - switch (type) { - case "text": return ; - case "image": return ; - case "text-image": return ; - case "video": return ; - case "text-video": return ; - default: return

Unknown block type.

; - } +function BlockContent({ block, onUpdate }) { + const { id, type, content } = block; + + switch (type) { + case "text": + return ( + + ); + case "image": + return ; + case "text-image": + return ; + case "video": + return ; + case "text-video": + return ; + default: + return

Unknown block type.

; + } } // ─── Default content per type ───────────────────────────────────────────────── export const DEFAULT_CONTENT = { - "text": { body: "" }, - "image": { asset_id: null, url: "", alt: "" }, - "text-image": { body: "", asset_id: null, url: "", alt: "", image_position: "right" }, - "video": { asset_id: null, url: "", thumbnail_url: "" }, - "text-video": { body: "", asset_id: null, url: "", thumbnail_url: "", video_position: "right" }, + "text": { body: "" }, + "image": { asset_id: null, url: "", alt: "" }, + "text-image": { body: "", asset_id: null, url: "", alt: "", image_position: "right" }, + "video": { asset_id: null, url: "", thumbnail_url: "" }, + "text-video": { body: "", asset_id: null, url: "", thumbnail_url: "", video_position: "right" }, }; // ─── List ───────────────────────────────────────────────────────────────────── export function BlockList({ blocks, onUpdate, onMove, onDelete }) { - if (!blocks.length) { - return ( -
-

No blocks yet.

-

Use the button below to add your first block.

-
- ); - } - + if (!blocks.length) { return ( -
- {blocks.map((block, index) => ( - onMove(block.id, "up")} - onMoveDown={() => onMove(block.id, "down")} - onDelete={() => onDelete(block.id)} - > - onUpdate(block.id, updated)} - /> - - ))} -
+
+

No blocks yet.

+

+ Use the button below to add your first block. +

+
); + } + + return ( +
+ {blocks.map((block, index) => ( + onMove(block.id, "up")} + onMoveDown={() => onMove(block.id, "down")} + onDelete={() => onDelete(block.id)} + > + onUpdate(block.id, updated)} + /> + + ))} +
+ ); } \ No newline at end of file diff --git a/src/components/generic/Blocks/TextBlock.jsx b/src/components/generic/Blocks/TextBlock.jsx index d456cf6..3bf1f65 100644 --- a/src/components/generic/Blocks/TextBlock.jsx +++ b/src/components/generic/Blocks/TextBlock.jsx @@ -1,18 +1,457 @@ -// components/generic/CMS/blocks/TextBlock.jsx +// components/generic/CMS/Blocks/TextBlock.jsx -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; +import { useEffect, useRef, useState } from "react"; +import { + Bold, Italic, Underline, + AlignLeft, AlignCenter, AlignRight, AlignJustify, + List, ListOrdered, Indent, Outdent, + Link2, FileText, Table, +} from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { AssetPickerSheet } from "../AssetPickerSheet"; +import { cn } from "@/lib/utils"; -export function TextBlock({ content, onUpdate }) { - return ( -
- -