From 963ca25313abaeea1dfd68fa3fbc946bbeb36db2 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Mon, 3 Aug 2026 14:31:51 +0800 Subject: [PATCH] fix wip --- .../courses/CourseReadingProgressList.jsx | 90 ++----- .../admin/components/courses/LessonsTable.jsx | 4 +- .../admin/components/courses/UnitsTable.jsx | 3 - .../courses/lessons/rowActions.config.jsx | 34 +-- .../courses/units/rowActions.config.jsx | 38 +-- .../pages/courses/lessons/ViewLesson.jsx | 222 +++++++++++++----- .../admin/pages/courses/units/ViewUnit.jsx | 175 +++++++++++--- 7 files changed, 332 insertions(+), 234 deletions(-) diff --git a/src/modules/admin/components/courses/CourseReadingProgressList.jsx b/src/modules/admin/components/courses/CourseReadingProgressList.jsx index 69775ba..c1c6e36 100644 --- a/src/modules/admin/components/courses/CourseReadingProgressList.jsx +++ b/src/modules/admin/components/courses/CourseReadingProgressList.jsx @@ -1,6 +1,6 @@ import { useEffect, useState, useMemo } from 'react'; import { - CheckCircle2, Circle, BookOpen, Users, Search, ChevronLeft, ChevronRight, RefreshCcw, AlertTriangle + CheckCircle2, Circle, BookOpen, Users, Search, RefreshCcw, AlertTriangle } from 'lucide-react'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; @@ -12,15 +12,10 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, } from '@/components/ui/dialog'; -import { - Pagination, PaginationContent, PaginationItem, - PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis, -} from '@/components/ui/pagination'; +import { TablePagination } from '@/components/generic/Table/TablePagination'; import { useAdminCourseReadingProgress } from '@/contexts/AdminCourseReadingProgressContext'; import { useDateFormat } from '@/hooks/useDateFormat'; -const PAGE_SIZE = 10; - // ─── Helpers ────────────────────────────────────────────────────────────────── function StatusBadge({ status }) { @@ -228,63 +223,6 @@ function UserCard({ entry, onOpen }) { ); } -// ─── Pagination controls ────────────────────────────────────────────────────── - -function PaginationControls({ page, totalPages, onPage }) { - if (totalPages <= 1) return null; - - const pages = []; - for (let i = 1; i <= totalPages; i++) pages.push(i); - - // Show at most 5 page numbers around current - const getVisible = () => { - if (totalPages <= 5) return pages; - if (page <= 3) return [1, 2, 3, 4, null, totalPages]; - if (page >= totalPages - 2) return [1, null, totalPages - 3, totalPages - 2, totalPages - 1, totalPages]; - return [1, null, page - 1, page, page + 1, null, totalPages]; - }; - - return ( - - - - { e.preventDefault(); if (page > 1) onPage(page - 1); }} - className={page === 1 ? 'pointer-events-none opacity-50' : ''} - /> - - - {getVisible().map((p, i) => - p === null ? ( - - - - ) : ( - - { e.preventDefault(); onPage(p); }} - > - {p} - - - ) - )} - - - { e.preventDefault(); if (page < totalPages) onPage(page + 1); }} - className={page === totalPages ? 'pointer-events-none opacity-50' : ''} - /> - - - - ); -} - // ─── Main component ─────────────────────────────────────────────────────────── export default function CourseReadingProgressList({ courseId }) { @@ -293,6 +231,7 @@ export default function CourseReadingProgressList({ courseId }) { const [searchInput, setSearchInput] = useState(''); const [query, setQuery] = useState(''); const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); const [dialogEntry, setDialogEntry] = useState(null); useEffect(() => { @@ -316,8 +255,19 @@ export default function CourseReadingProgressList({ courseId }) { }, [progressList, query]); // ── Paginate ────────────────────────────────────────────────────────────── - const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); - const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); + const paginated = filtered.slice((page - 1) * pageSize, page * pageSize); + const pagination = { + page, + limit: pageSize, + totalPages, + totalRecords: filtered.length, + hasPrevPage: page > 1, + hasNextPage: page < totalPages, + }; + + const handlePageChange = (p) => setPage(p); + const handlePageSizeChange = (size) => { setPageSize(size); setPage(1); }; const completedCount = progressList.filter((e) => e.course_status === 'completed').length; const inProgressCount = progressList.length - completedCount; @@ -407,7 +357,13 @@ export default function CourseReadingProgressList({ courseId }) { )} {/* ── Pagination ── */} - + {/* ── Detail Dialog ── */} buildRowActions({ - onViewPage: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/page/view`), - onCreatePage: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/page`), onView: (row) => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${row.lesson_id}/view`), onArchive: (row) => setArchiveTarget(row), }), [courseId, unitId]); diff --git a/src/modules/admin/components/courses/UnitsTable.jsx b/src/modules/admin/components/courses/UnitsTable.jsx index 53ecd1d..c400ad3 100644 --- a/src/modules/admin/components/courses/UnitsTable.jsx +++ b/src/modules/admin/components/courses/UnitsTable.jsx @@ -55,9 +55,6 @@ export default function UnitsTable({ courseId, returnTo }) { ); const rowActions = useMemo(() => buildRowActions({ - onViewQuiz: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/quiz/view`), - onQuiz: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/quiz/edit`), - onViewLessons: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/lessons`), onView: (row) => navigate(`/admin/courses/${courseId}/units/${row.unit_id}/view`), onArchive: (row) => setArchiveTarget(row), }), [courseId]); diff --git a/src/modules/admin/config/courses/lessons/rowActions.config.jsx b/src/modules/admin/config/courses/lessons/rowActions.config.jsx index 60f08db..9610654 100644 --- a/src/modules/admin/config/courses/lessons/rowActions.config.jsx +++ b/src/modules/admin/config/courses/lessons/rowActions.config.jsx @@ -1,10 +1,6 @@ -import { Eye, Archive, SquarePlus, SquareChartGantt } from "lucide-react"; +import { Eye, Archive } from "lucide-react"; -// duration_seconds is derived from the lesson's authored content blocks -// (see duration.util.js) — zero means no page content exists yet. -const hasContent = (row) => !!row.duration_seconds; - -export function buildRowActions({ onCreatePage, onViewPage, onView, onArchive }) { +export function buildRowActions({ onView, onArchive }) { return [ { key: "view", @@ -12,32 +8,6 @@ export function buildRowActions({ onCreatePage, onViewPage, onView, onArchive }) icon: , onClick: (row) => onView(row), }, - { - key: "create_content", - label: "Create Content", - icon: , - className: "text-sky-700 hover:text-sky-600", - onClick: (row) => onCreatePage(row), - hidden: (row) => hasContent(row), - separator: true, - }, - { - key: "view_content", - label: "View Content", - icon: , - className: "text-sky-700 hover:text-sky-600", - onClick: (row) => onViewPage(row), - hidden: (row) => !hasContent(row), - separator: true, - }, - { - key: "modify_content", - label: "Modify Content", - icon: , - className: "text-sky-700 hover:text-sky-600", - onClick: (row) => onCreatePage(row), - hidden: (row) => !hasContent(row), - }, { key: "archive", label: "Archive", diff --git a/src/modules/admin/config/courses/units/rowActions.config.jsx b/src/modules/admin/config/courses/units/rowActions.config.jsx index d597c86..137bed4 100644 --- a/src/modules/admin/config/courses/units/rowActions.config.jsx +++ b/src/modules/admin/config/courses/units/rowActions.config.jsx @@ -1,6 +1,6 @@ -import { Eye, Archive, BookCheck, NotebookPen, ClipboardList, PlusCircle } from "lucide-react"; +import { Eye, Archive } from "lucide-react"; -export function buildRowActions({ onViewLessons, onView, onArchive, onQuiz, onViewQuiz }) { +export function buildRowActions({ onView, onArchive }) { return [ { key: "view", @@ -8,40 +8,6 @@ export function buildRowActions({ onViewLessons, onView, onArchive, onQuiz, onVi icon: , onClick: (row) => onView(row), }, - { - key: "view_units", - label: "View Lessons", - icon: , - className: "text-sky-700 hover:text-sky-600", - onClick: (row) => onViewLessons(row), - separator: true, - }, - { - key: "create_quiz", - label: "Create Quiz", - icon: , - className: "text-purple-700 hover:text-purple-600", - onClick: (row) => onQuiz(row), - hidden: (row) => !!(row.quiz_id || row.quiz), - separator: true, - }, - { - key: "view_quiz", - label: "View Quiz", - icon: , - className: "text-purple-700 hover:text-purple-600", - onClick: (row) => onViewQuiz(row), - hidden: (row) => !(row.quiz_id || row.quiz), - separator: true, - }, - { - key: "modify_quiz", - label: "Modify Quiz", - icon: , - className: "text-purple-700 hover:text-purple-600", - onClick: (row) => onQuiz(row), - hidden: (row) => !(row.quiz_id || row.quiz), - }, { key: "archive", label: "Archive", diff --git a/src/modules/admin/pages/courses/lessons/ViewLesson.jsx b/src/modules/admin/pages/courses/lessons/ViewLesson.jsx index caf971d..c29ff15 100644 --- a/src/modules/admin/pages/courses/lessons/ViewLesson.jsx +++ b/src/modules/admin/pages/courses/lessons/ViewLesson.jsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { House, Pencil, ArrowLeft, Clock, ListChecks, FileText } from "lucide-react"; +import { + Pencil, ArrowLeft, Clock, ListChecks, Info, FileText, SquarePlus, SquareChartGantt, +} from "lucide-react"; import { useCourses } from "@/contexts/AdminCoursesContext"; import { useDateFormat } from "@/hooks/useDateFormat"; @@ -18,13 +20,136 @@ function InfoField({ label, value }) { ); } +// ─── Tabs ───────────────────────────────────────────────────────────────────── + +const TABS = [ + { key: "details", label: "Lesson Details", icon: Info }, + { key: "content", label: "Content", icon: FileText }, +]; + +// ─── Details tab ────────────────────────────────────────────────────────────── + +function LessonDetailsTab({ lesson, fmtDate }) { + return ( +
+ {/* Stats row */} +
+
+

Order

+

#{lesson?.order_index ?? 0}

+
+
+

+ Duration +

+

{lesson?.duration_formatted ?? "0 mins"}

+
+
+

Created

+

{fmtDate(lesson?.createdAt)}

+
+
+

Last Updated

+

{fmtDate(lesson?.updatedAt)}

+
+
+ + {/* Details */} +
+ + + +
+ + {/* Objectives */} +
+
+ +

Objectives

+ {lesson?.objectives?.length ?? 0} +
+ + {lesson?.objectives?.length === 0 || !lesson?.objectives ? ( +

No objectives defined.

+ ) : ( +
    + {lesson.objectives.map((obj, i) => ( +
  • + + {i + 1} + + {obj.text} +
  • + ))} +
+ )} +
+
+ ); +} + +// ─── Content tab ────────────────────────────────────────────────────────────── + +function LessonContentTab({ lesson, courseId, unitId, lessonId }) { + const navigate = useNavigate(); + const hasContent = !!lesson?.duration_seconds; + + if (!hasContent) { + return ( +
+ +

No content has been created for this lesson yet.

+ +
+ ); + } + + return ( +
+
+
+

{lesson.title}

+

{lesson.duration_formatted ?? "0 mins"} of content

+
+
+ + +
+
+
+ ); +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + export default function ViewLesson() { const navigate = useNavigate(); const { courseId, unitId, lessonId } = useParams(); - const { fetchLesson, course, unit } = useCourses(); + const { fetchLesson } = useCourses(); const { fmtDate } = useDateFormat(); const [lesson, setLesson] = useState(null); const [initializing, setInitializing] = useState(true); + const [activeTab, setActiveTab] = useState("details"); useEffect(() => { (async () => { @@ -59,70 +184,43 @@ export default function ViewLesson() {

Lesson details.

- - - - {/* Stats row */} -
-
-

Order

-

#{lesson?.order_index ?? 0}

-
-
-

- Duration -

-

{lesson?.duration_formatted ?? "0 mins"}

-
-
-

Created

-

{fmtDate(lesson?.createdAt)}

-
-
-

Last Updated

-

{fmtDate(lesson?.updatedAt)}

-
-
- - {/* Details */} -
- - - -
- - {/* Objectives */} -
-
- -

Objectives

- {lesson?.objectives?.length ?? 0} -
- - {lesson?.objectives?.length === 0 || !lesson?.objectives ? ( -

No objectives defined.

- ) : ( -
    - {lesson.objectives.map((obj, i) => ( -
  • - - {i + 1} - - {obj.text} -
  • - ))} -
+ {activeTab === "details" && ( + )}
+ {/* Tabs */} +
+ {TABS.map(({ key, label, icon: Icon }) => ( + + ))} +
+ + {activeTab === "details" && } + {activeTab === "content" && ( + + )} + ); -} \ No newline at end of file +} diff --git a/src/modules/admin/pages/courses/units/ViewUnit.jsx b/src/modules/admin/pages/courses/units/ViewUnit.jsx index 5b6d817..70024da 100644 --- a/src/modules/admin/pages/courses/units/ViewUnit.jsx +++ b/src/modules/admin/pages/courses/units/ViewUnit.jsx @@ -1,18 +1,123 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { House, Pencil, ArrowLeft } from "lucide-react"; +import { + Pencil, ArrowLeft, Info, BookCheck, ClipboardList, NotebookPen, Eye, HelpCircle, +} from "lucide-react"; import { useCourses } from "@/contexts/AdminCoursesContext"; import { PageMeta } from "@/contexts/MetadataContext"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; +import { Skeleton } from "@/components/ui/skeleton"; +import LessonsTable from "../../../components/courses/LessonsTable"; + +// ─── Tabs ───────────────────────────────────────────────────────────────────── + +const TABS = [ + { key: "details", label: "Unit Details", icon: Info }, + { key: "lessons", label: "Lessons", icon: BookCheck }, + { key: "quiz", label: "Quiz", icon: ClipboardList }, +]; + +const WIDE_TABS = new Set(["lessons"]); + +// ─── Details tab ────────────────────────────────────────────────────────────── + +function UnitDetailsTab({ unit }) { + return ( +
+
+

Title

+

{unit?.title ?? "—"}

+
+ +
+

Description

+

{unit?.description || "No description provided."}

+
+ +
+

Order

+

{unit?.order ?? 0}

+
+
+ ); +} + +// ─── Quiz tab ───────────────────────────────────────────────────────────────── + +function UnitQuizTab({ courseId, unitId }) { + const navigate = useNavigate(); + const { fetchQuiz, quiz, loading } = useCourses(); + + useEffect(() => { + fetchQuiz(courseId, unitId); + }, [courseId, unitId]); + + if (loading && !quiz) { + return ; + } + + if (!quiz) { + return ( +
+ +

No quiz has been created for this unit yet.

+ +
+ ); + } + + const questions = quiz.questions ?? []; + const totalPoints = questions.reduce((sum, q) => sum + (q.points ?? 1), 0); + + return ( +
+
+
+

{quiz.title || "Unit Quiz"}

+

+ {questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""} +

+
+
+ + +
+
+
+ ); +} + +// ─── Page ───────────────────────────────────────────────────────────────────── export default function ViewUnit() { const navigate = useNavigate(); const { courseId, unitId } = useParams(); - const { fetchUnit, course } = useCourses(); + const { fetchUnit } = useCourses(); const [unit, setUnit] = useState(null); const [initializing, setInitializing] = useState(true); + const [activeTab, setActiveTab] = useState("details"); useEffect(() => { (async () => { @@ -33,11 +138,10 @@ export default function ViewUnit() { return (
-
-
- -
+
+
+
- + {activeTab === "details" && ( + + )}
-
- -
-

Title

-

{unit?.title ?? "—"}

-
- -
-

Description

-

{unit?.description || "No description provided."}

-
- -
-

Order

-

{unit?.order ?? 0}

-
- +
+ {TABS.map(({ key, label, icon: Icon }) => ( + + ))}
+ +
+
+ {activeTab === "details" && } + {activeTab === "lessons" && } + {activeTab === "quiz" && } +
+
); -} \ No newline at end of file +}