From 3c7da8944f7fe513219a32db0bd5c70a4e9c9148 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Mon, 3 Aug 2026 17:19:12 +0800 Subject: [PATCH] added more things --- .../components/courses/CreateLessonDialog.jsx | 11 +- .../components/courses/CreateUnitDialog.jsx | 11 +- .../components/courses/RoadmapBuilder.jsx | 8 +- .../library/AttachLessonsDialog.jsx | 173 +++++-- .../task_list/TasksTable.jsx} | 163 +------ .../config/task_list/rowActions.config.jsx | 10 +- .../pages/library/units/AddLibraryUnit.jsx | 100 ++++- .../admin/pages/task_list/ViewTaskList.jsx | 422 +++++++----------- .../pages/task_list/task/ArchiveTask.jsx | 2 +- .../admin/pages/task_list/task/CreateTask.jsx | 6 +- .../admin/pages/task_list/task/EditTask.jsx | 2 +- .../admin/pages/task_list/task/ViewTask.jsx | 2 +- .../task_list/task/ViewTaskCompletion.jsx | 2 +- src/modules/admin/routes/AdminRoutes.jsx | 5 +- 14 files changed, 422 insertions(+), 495 deletions(-) rename src/modules/admin/{pages/task_list/task/Tasks.jsx => components/task_list/TasksTable.jsx} (56%) diff --git a/src/modules/admin/components/courses/CreateLessonDialog.jsx b/src/modules/admin/components/courses/CreateLessonDialog.jsx index b382c61..f07d19b 100644 --- a/src/modules/admin/components/courses/CreateLessonDialog.jsx +++ b/src/modules/admin/components/courses/CreateLessonDialog.jsx @@ -75,7 +75,16 @@ export default function CreateLessonDialog({ open, onOpenChange, courseId, unitI New Lesson -
+ { + // Dialog content is portalled out of the DOM, but React still bubbles + // synthetic events through the React tree — without this, submitting + // here also fires the wizard's outer , advancing the step. + e.stopPropagation(); + handleSubmit(onValid)(e); + }} + className="space-y-4" + >
- - {/* Created */} -
- - Created - - - {taskList ? formattedCreated : } - -
- - {/* Last updated */} -
- - Last Updated - - - {taskList ? formattedUpdated : } - -
- - - - - {/* ── Tasks DataTable ───────────────────────────────────────────── */} + <> ( - {/* ── All Groups Dialog ─────────────────────────────────────────── */} - - - - - - Assigned Groups - - {assignedGroups.length} - - - -
- {assignedGroups.map((g) => ( - - - {g.name} - - ))} -
-
-
- {/* ── Single archive ────────────────────────────────────────────── */} !v && setArchiveTarget(null)}> @@ -366,7 +228,6 @@ export default function Tasks() { loading={loading} onSuccess={afterMutation} /> - - + ); -} \ No newline at end of file +} diff --git a/src/modules/admin/config/task_list/rowActions.config.jsx b/src/modules/admin/config/task_list/rowActions.config.jsx index 0de9834..94ed169 100644 --- a/src/modules/admin/config/task_list/rowActions.config.jsx +++ b/src/modules/admin/config/task_list/rowActions.config.jsx @@ -1,4 +1,4 @@ -import { Eye, Archive, ArchiveRestore, NotebookPen, Info } from "lucide-react"; +import { Eye, Archive, ArchiveRestore } from "lucide-react"; export function buildRowActions({ navigate, onArchive, onRestore, showArchived }) { return [ @@ -8,14 +8,6 @@ export function buildRowActions({ navigate, onArchive, onRestore, showArchived } icon: , onClick: (row) => navigate(`${row.task_list_id}/view`), }, - { - key: "tasks", - label: "View Tasks", - icon: , - onClick: (row) => navigate(`${row.task_list_id}/tasks`), - separator: true, - className: "text-sky-800" - }, { key: "archive", label: "Archive", diff --git a/src/modules/admin/pages/library/units/AddLibraryUnit.jsx b/src/modules/admin/pages/library/units/AddLibraryUnit.jsx index 0d159b0..53956f2 100644 --- a/src/modules/admin/pages/library/units/AddLibraryUnit.jsx +++ b/src/modules/admin/pages/library/units/AddLibraryUnit.jsx @@ -7,7 +7,7 @@ import { nanoid } from "nanoid"; import { ArrowLeft, ChevronLeft, ChevronRight, Check, FileText, BookOpen, LayoutTemplate, ClipboardCheck, ListChecks, - Plus, Trash2, Link2, + Plus, Trash2, Link2, Eye, } from "lucide-react"; import { useLibrary } from "@/contexts/AdminLibraryContext"; @@ -250,12 +250,13 @@ function StepLessons({ control, register, errors, existingLessons, onRemoveExist } // ─── Step 3 — Page Builder ───────────────────────────────────────────────────── -function StepPageBuilder({ control, setValue }) { +function StepPageBuilder({ control, setValue, existingLessons }) { const lessons = useWatch({ control, name: "lessons" }) ?? []; const [rawIndex, setActiveIndex] = useState(0); const [drawerOpen, setDrawerOpen] = useState(false); + const [viewingExisting, setViewingExisting] = useState(null); - if (lessons.length === 0) { + if (lessons.length === 0 && existingLessons.length === 0) { return (

Add at least one lesson in the previous step to build its page content here. @@ -288,6 +289,29 @@ function StepPageBuilder({ control, setValue }) { return (

+ {existingLessons.map((l) => ( +
+
+
+

{l.title}

+ existing +
+

+ {(l.blocks?.length ?? 0)} block{(l.blocks?.length ?? 0) !== 1 ? "s" : ""} · content lives in the Lessons library +

+
+ +
+ ))} + {lessons.map((l, i) => (
@@ -345,13 +369,13 @@ function StepPageBuilder({ control, setValue }) {
Live Preview
- +
({ objective_id: oi, text: o.value })), + title: activeLesson?.title, + description: activeLesson?.description, + objectives: (activeLesson?.objectives ?? []).map((o, oi) => ({ objective_id: oi, text: o.value })), }} blocks={blocks} empty="Your content will appear here as you build." @@ -369,6 +393,37 @@ function StepPageBuilder({ control, setValue }) { + + !open && setViewingExisting(null)} shouldScaleBackground> + + + Page Content + {viewingExisting?.title} · attached from the Lessons library + + +
+ +
+ +
+
+
+ + + + + + +
+
); } @@ -612,7 +667,7 @@ export default function AddLibraryUnit() { /> )} {step === 2 && ( - + )} {step === 3 && ( @@ -657,12 +712,33 @@ export default function AddLibraryUnit() { open={attachLessonsOpen} onOpenChange={setAttachLessonsOpen} attachedLessonIds={existingLessons.map((l) => l.lesson_id)} - onAttach={(lessonIds) => { + onAttach={async (lessonIds) => { const picked = lessonIds .map((id) => lessonsFlat.find((l) => l.lesson_id === id)) - .filter(Boolean) - .map((l) => ({ lesson_id: l.lesson_id, title: l.title })); - setExistingLessons((prev) => [...prev, ...picked]); + .filter(Boolean); + + // Fetch each lesson's page content so it can be previewed in the + // Page Builder step — the flat list used by the picker only carries + // title/duration, not blocks. + const withContent = await Promise.all( + picked.map(async (l) => { + try { + const { data } = await api.get(`/admin/lessons/${l.lesson_id}`); + const full = data?.data?.data; + return { + lesson_id: l.lesson_id, + title: l.title, + description: full?.description ?? "", + objectives: full?.objectives ?? [], + blocks: full?.page?.blocks ?? [], + }; + } catch { + return { lesson_id: l.lesson_id, title: l.title, blocks: [] }; + } + }) + ); + + setExistingLessons((prev) => [...prev, ...withContent]); }} /> diff --git a/src/modules/admin/pages/task_list/ViewTaskList.jsx b/src/modules/admin/pages/task_list/ViewTaskList.jsx index 19c6cf3..57c7105 100644 --- a/src/modules/admin/pages/task_list/ViewTaskList.jsx +++ b/src/modules/admin/pages/task_list/ViewTaskList.jsx @@ -1,145 +1,144 @@ import { useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useAdminTask } from '@/contexts/AdminTaskContext'; import { useDateFormat } from '@/hooks/useDateFormat'; +import { PageMeta } from '@/contexts/MetadataContext'; + +import TasksTable from '../../components/task_list/TasksTable'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; +import { Separator } from '@/components/ui/separator'; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from '@/components/ui/accordion'; -import { - ArrowLeft, Pencil, Users, ClipboardList, FileText, - Link2, Upload, BookOpen, BookMarked, FileCheck2, - CalendarClock, Equal, Tag, Globe, Copy, File, Bookmark, GitBranch, + ArrowLeft, Pencil, Users, ListTodo, Info, + NotebookPen, FileText, CalendarClock, } from 'lucide-react'; -// ─── All styling uses shadcn tokens — only label/icon differs per type -const REQUIREMENT_CONFIG = { - visit_link: { label: 'Visit Link', badgeLabel: 'Link', Icon: Link2 }, - upload_file: { label: 'Upload File', badgeLabel: 'Upload', Icon: Upload }, - read_course: { label: 'Read Course', badgeLabel: 'Course', Icon: BookOpen }, - read_unit: { label: 'Read Unit', badgeLabel: 'Unit', Icon: BookMarked }, - read_lesson: { label: 'Read Lesson', badgeLabel: 'Lesson', Icon: FileCheck2 }, -}; +// ─── Helpers ──────────────────────────────────────────────────────────────── -// ─── Label / value row — fully themed by shadcn tokens ─────────────────────── -function MetaRow({ icon: Icon, label, children }) { +function InfoRow({ label, children }) { return ( -
-
- - {label} -
-
- {children} -
+
+ {label} + {children ?? —}
); } -// ─── Requirement card ───────────────────────────────────────────────────────── -function RequirementCard({ req }) { - const cfg = REQUIREMENT_CONFIG[req.type] ?? { - label: req.type, badgeLabel: req.type, Icon: FileText, accent: 'text-muted-foreground', - }; - const { Icon } = cfg; +function SectionCard({ icon: Icon, title, children }) { + return ( +
+
+ {Icon && } +

{title}

+
+ + {children} +
+ ); +} + +function LoadingSkeleton() { + return ( +
+ + +
+ ); +} + +// ─── Tab: Details ─────────────────────────────────────────────────────────── + +function DetailsTab({ taskList }) { + const { fmtDateTime } = useDateFormat(); + + if (!taskList) return ; + + const groups = taskList.groups ?? []; + const taskCount = taskList.tasks?.length ?? 0; return ( -
- - {/* Header */} -
-
- +
+ +
+ {taskList.name} + {taskList.description && ( + + + {taskList.description} + + + )}
- - {cfg.label} - - - {cfg.badgeLabel} - -
+ - {/* Rows */} - {req.type === 'visit_link' && ( - <> - {req.link_label && ( - - {req.link_label} - - )} - {req.link_url && ( - - - {req.link_url} - - - )} - - )} + +
+ +
+ + {taskCount} +
+
+ +
+ + {taskList.group_count ?? groups.length} +
+
+
+
- {req.type === 'upload_file' && ( - <> - {req.max_file_count != null && ( - - {req.max_file_count} - - )} - {req.allowed_file_types?.length > 0 && ( - - {req.allowed_file_types.join(', ')} - - )} - - )} + + {groups.length > 0 ? ( +
+ {groups.map((g) => ( + + + {g.name ?? g.group_id} + + ))} +
+ ) : ( +

No groups assigned.

+ )} +
- {['read_course', 'read_unit', 'read_lesson'].includes(req.type) && req.reference_label && ( - - {req.reference_label} - - )} + +
+ + {taskList.createdAt ? fmtDateTime(taskList.createdAt) : '—'} + + + {taskList.updatedAt ? fmtDateTime(taskList.updatedAt) : '—'} + +
+
); } -// ─── Requirements section ───────────────────────────────────────────────────── -function TaskRequirementsSection({ requirements = [] }) { - const sorted = [...requirements].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); - return ( -
-

- Requirements -

- {sorted.length > 0 ? ( - sorted.map((req) => ( - - )) - ) : ( -

No other requirements.

- )} -
- ); -} +// ─── Tabs config ──────────────────────────────────────────────────────────── + +const TABS = [ + { key: 'details', label: 'Details', icon: Info }, + { key: 'tasks', label: 'Tasks', icon: NotebookPen }, +]; + +// ─── Page ─────────────────────────────────────────────────────────────────── -// ─── Page ───────────────────────────────────────────────────────────────────── export default function ViewTaskList() { const navigate = useNavigate(); const { taskListId } = useParams(); + const [searchParams] = useSearchParams(); const { fetchTaskList } = useAdminTask(); - const { fmtDateTime } = useDateFormat(); const [taskList, setTaskList] = useState(null); + const [activeTab, setActiveTab] = useState( + searchParams.get('tab') === 'tasks' ? 'tasks' : 'details' + ); useEffect(() => { fetchTaskList(taskListId).then((data) => { @@ -148,166 +147,71 @@ export default function ViewTaskList() { }); }, [taskListId]); - if (!taskList) return ( -
- - - - -
- ); - - const groups = taskList.groups ?? []; - const tasks = taskList.tasks ?? []; - return ( -
-
- - {/* Header */} -
-
- -
-

{taskList.name}

- {taskList.description && ( -

- {taskList.description} -

+
+

+ + View Task List +

+ {taskList && ( +

{taskList.name}

)}
+ {activeTab === 'details' && ( + + )} +
+ + {/* Underline tabs */} +
+ {TABS.map(({ key, label, icon: Icon }) => ( + + ))}
-
+
-
-
- - {/* Assigned Groups */} -
-
- - Assigned Groups -
- {groups.length > 0 ? ( -
- {groups.map((g) => ( - - - {g.name ?? g.group_id} - - ))} -
- ) : ( -

No groups assigned.

- )} -
- - {/* Tasks */} -
-
- - Tasks - {tasks.length > 0 && ( - - {tasks.length} task{tasks.length !== 1 ? 's' : ''} - - )} -
- - {tasks.length > 0 ? ( - - {tasks.map((task, index) => ( - - -
- - {index + 1}. - - - {task.name ?? `Task ${index + 1}`} - - {task.requirements?.length > 0 && ( - - {task.requirements.length} Requirement{task.requirements.length !== 1 ? 's' : ''} - - )} -
-
- - -
- - {task.description && ( -
- -

{task.description}

-
- )} - - {task.deadline && ( -
- - - Deadline:{' '} - - {fmtDateTime(task.deadline)} - - -
- )} - - {task.prerequisites?.length > 0 && ( -
- - - Requires:{' '} - - {task.prerequisites.map((p) => p.name).join(', ')} - - -
- )} - - - -
-
-
- ))} -
- ) : ( -

No tasks in this list yet.

- )} -
- -
+ {/* ── Content ── */} +
+
+ {activeTab === 'details' && } + {activeTab === 'tasks' && }
); -} \ No newline at end of file +} diff --git a/src/modules/admin/pages/task_list/task/ArchiveTask.jsx b/src/modules/admin/pages/task_list/task/ArchiveTask.jsx index f725dd1..e4e1612 100644 --- a/src/modules/admin/pages/task_list/task/ArchiveTask.jsx +++ b/src/modules/admin/pages/task_list/task/ArchiveTask.jsx @@ -8,7 +8,7 @@ export default function ArchivedTask() { const items = [ { label: "Home", icon: , to: "/admin" }, { label: "Task List", to: "/admin/taskList" }, - { label: "Tasks", to: `/admin/taskList/${taskListId}/tasks` }, + { label: "Tasks", to: `/admin/taskList/${taskListId}/view?tab=tasks` }, { label: "Archived" }, ]; diff --git a/src/modules/admin/pages/task_list/task/CreateTask.jsx b/src/modules/admin/pages/task_list/task/CreateTask.jsx index 0cf2570..4960dee 100644 --- a/src/modules/admin/pages/task_list/task/CreateTask.jsx +++ b/src/modules/admin/pages/task_list/task/CreateTask.jsx @@ -89,7 +89,7 @@ export default function CreateTask() { }; const handleBack = () => { - if (step === 0) navigate(`/admin/taskList/${taskListId}/tasks`); + if (step === 0) navigate(`/admin/taskList/${taskListId}/view?tab=tasks`); else setStep((s) => s - 1); }; @@ -117,7 +117,7 @@ export default function CreateTask() { prerequisite_task_ids: form.prerequisite_task_ids, }); - if (created) navigate(`/admin/taskList/${taskListId}/tasks`); + if (created) navigate(`/admin/taskList/${taskListId}/view?tab=tasks`); }; const formattedDeadline = (() => { @@ -132,7 +132,7 @@ export default function CreateTask() { {/* Header */}
-

Create Task

diff --git a/src/modules/admin/pages/task_list/task/EditTask.jsx b/src/modules/admin/pages/task_list/task/EditTask.jsx index c501bd7..779b5e5 100644 --- a/src/modules/admin/pages/task_list/task/EditTask.jsx +++ b/src/modules/admin/pages/task_list/task/EditTask.jsx @@ -103,7 +103,7 @@ export default function EditTask() { requirements, prerequisite_task_ids: form.prerequisite_task_ids ?? [], }); - if (updated) navigate(`/admin/taskList/${taskListId}/tasks`); + if (updated) navigate(`/admin/taskList/${taskListId}/view?tab=tasks`); }; const handleSubmit = (e) => { diff --git a/src/modules/admin/pages/task_list/task/ViewTask.jsx b/src/modules/admin/pages/task_list/task/ViewTask.jsx index e2bb85f..31abfc8 100644 --- a/src/modules/admin/pages/task_list/task/ViewTask.jsx +++ b/src/modules/admin/pages/task_list/task/ViewTask.jsx @@ -328,7 +328,7 @@ export default function ViewTask() {
diff --git a/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx b/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx index f62a6b5..3602089 100644 --- a/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx +++ b/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx @@ -100,7 +100,7 @@ export default function ViewTaskCompletion() { const breadcrumbs = [ { label: 'Home', icon: , to: '/admin' }, { label: 'Task Lists', to: '/admin/taskList' }, - { label: taskList?.name ?? '…', to: `/admin/taskList/${taskListId}/tasks` }, + { label: taskList?.name ?? '…', to: `/admin/taskList/${taskListId}/view?tab=tasks` }, { label: task?.name ?? '…', to: `/admin/taskList/${taskListId}/tasks/${taskId}/view` }, { label: 'Completions', to: `/admin/taskList/${taskListId}/tasks/${taskId}/view?tab=completions` }, { label: 'View' }, diff --git a/src/modules/admin/routes/AdminRoutes.jsx b/src/modules/admin/routes/AdminRoutes.jsx index 7d34dd5..3afd78e 100644 --- a/src/modules/admin/routes/AdminRoutes.jsx +++ b/src/modules/admin/routes/AdminRoutes.jsx @@ -1,4 +1,4 @@ -import { Outlet } from 'react-router-dom' +import { Outlet, Navigate } from 'react-router-dom' import ProtectedRoute from '../../../routes/ProtectedRoute' // Layouts @@ -75,7 +75,6 @@ import ArchivedLessonLibraryList from '../pages/library/lessons/ArchivedLessonLi import TaskList from '../pages/task_list/TaskList' import CreateTaskList from '../pages/task_list/CreateTaskList' import EditTaskList from '../pages/task_list/EditTaskList' -import Tasks from '../pages/task_list/task/Tasks' import ArchiveTaskList from '../pages/task_list/ArchiveTaskList' import ViewTaskList from '../pages/task_list/ViewTaskList' @@ -284,7 +283,7 @@ export const AdminRoutes = { path: ':taskListId/tasks', element: , children: [ - { index: true, element: }, + { index: true, element: }, { path: 'create', element: }, { path: 'archived', element: }, { path: ':taskId/view', element: },