mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
fix wip
This commit is contained in:
@@ -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 (
|
||||
<Pagination className="mt-4">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => { e.preventDefault(); if (page > 1) onPage(page - 1); }}
|
||||
className={page === 1 ? 'pointer-events-none opacity-50' : ''}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{getVisible().map((p, i) =>
|
||||
p === null ? (
|
||||
<PaginationItem key={`ellipsis-${i}`}>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
) : (
|
||||
<PaginationItem key={p}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
isActive={p === page}
|
||||
onClick={(e) => { e.preventDefault(); onPage(p); }}
|
||||
>
|
||||
{p}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => { e.preventDefault(); if (page < totalPages) onPage(page + 1); }}
|
||||
className={page === totalPages ? 'pointer-events-none opacity-50' : ''}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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 ── */}
|
||||
<PaginationControls page={page} totalPages={totalPages} onPage={setPage} />
|
||||
<TablePagination
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
rowCount={paginated.length}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
recordLabel="student"
|
||||
/>
|
||||
|
||||
{/* ── Detail Dialog ── */}
|
||||
<UserDetailDialog
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState, useEffect, useCallback } from "react";
|
||||
import { useMemo, useRef, useState, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
@@ -58,8 +58,6 @@ export default function LessonsTable({ courseId, unitId }) {
|
||||
);
|
||||
|
||||
const rowActions = useMemo(() => 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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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: <Eye className="h-3.5 w-3.5" />,
|
||||
onClick: (row) => onView(row),
|
||||
},
|
||||
{
|
||||
key: "create_content",
|
||||
label: "Create Content",
|
||||
icon: <SquarePlus className="h-3.5 w-3.5" />,
|
||||
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: <SquareChartGantt className="h-3.5 w-3.5" />,
|
||||
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: <SquarePlus className="h-3.5 w-3.5" />,
|
||||
className: "text-sky-700 hover:text-sky-600",
|
||||
onClick: (row) => onCreatePage(row),
|
||||
hidden: (row) => !hasContent(row),
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
label: "Archive",
|
||||
|
||||
@@ -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: <Eye className="h-3.5 w-3.5" />,
|
||||
onClick: (row) => onView(row),
|
||||
},
|
||||
{
|
||||
key: "view_units",
|
||||
label: "View Lessons",
|
||||
icon: <BookCheck className="h-3.5 w-3.5" />,
|
||||
className: "text-sky-700 hover:text-sky-600",
|
||||
onClick: (row) => onViewLessons(row),
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
key: "create_quiz",
|
||||
label: "Create Quiz",
|
||||
icon: <PlusCircle className="h-3.5 w-3.5" />,
|
||||
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: <ClipboardList className="h-3.5 w-3.5" />,
|
||||
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: <NotebookPen className="h-3.5 w-3.5" />,
|
||||
className: "text-purple-700 hover:text-purple-600",
|
||||
onClick: (row) => onQuiz(row),
|
||||
hidden: (row) => !(row.quiz_id || row.quiz),
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
label: "Archive",
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Order</p>
|
||||
<p className="font-semibold text-sm">#{lesson?.order_index ?? 0}</p>
|
||||
</div>
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" /> Duration
|
||||
</p>
|
||||
<p className="font-semibold text-sm">{lesson?.duration_formatted ?? "0 mins"}</p>
|
||||
</div>
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Created</p>
|
||||
<p className="font-semibold text-sm">{fmtDate(lesson?.createdAt)}</p>
|
||||
</div>
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Last Updated</p>
|
||||
<p className="font-semibold text-sm">{fmtDate(lesson?.updatedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||||
<InfoField label="Title" value={lesson?.title} />
|
||||
<InfoField label="Description" value={lesson?.description || "No description provided."} />
|
||||
<InfoField label="Unit" value={lesson?.unit?.title} />
|
||||
</div>
|
||||
|
||||
{/* Objectives */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ListChecks className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium">Objectives</p>
|
||||
<Badge variant="secondary" className="ml-auto">{lesson?.objectives?.length ?? 0}</Badge>
|
||||
</div>
|
||||
|
||||
{lesson?.objectives?.length === 0 || !lesson?.objectives ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">No objectives defined.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{lesson.objectives.map((obj, i) => (
|
||||
<li key={obj.objective_id} className="flex items-start gap-3 text-sm">
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span>{obj.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Content tab ──────────────────────────────────────────────────────────────
|
||||
|
||||
function LessonContentTab({ lesson, courseId, unitId, lessonId }) {
|
||||
const navigate = useNavigate();
|
||||
const hasContent = !!lesson?.duration_seconds;
|
||||
|
||||
if (!hasContent) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed bg-card p-12 flex flex-col items-center gap-3 text-center">
|
||||
<FileText className="h-8 w-8 text-muted-foreground/40" />
|
||||
<p className="text-sm text-muted-foreground">No content has been created for this lesson yet.</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${lessonId}/page`)}
|
||||
>
|
||||
<SquarePlus className="h-4 w-4 mr-2" />
|
||||
Create Content
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{lesson.title}</p>
|
||||
<p className="text-xs text-muted-foreground">{lesson.duration_formatted ?? "0 mins"} of content</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${lessonId}/page/view`)}
|
||||
>
|
||||
<SquareChartGantt className="h-4 w-4 mr-2" />
|
||||
View Content
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${lessonId}/page`)}
|
||||
>
|
||||
<SquarePlus className="h-4 w-4 mr-2" />
|
||||
Modify Content
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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() {
|
||||
<p className="text-sm text-muted-foreground">Lesson details.</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${lessonId}/edit`)}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Order</p>
|
||||
<p className="font-semibold text-sm">#{lesson?.order_index ?? 0}</p>
|
||||
</div>
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" /> Duration
|
||||
</p>
|
||||
<p className="font-semibold text-sm">{lesson?.duration_formatted ?? "0 mins"}</p>
|
||||
</div>
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Created</p>
|
||||
<p className="font-semibold text-sm">{fmtDate(lesson?.createdAt)}</p>
|
||||
</div>
|
||||
<div className="bg-card rounded-lg border p-3 space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Last Updated</p>
|
||||
<p className="font-semibold text-sm">{fmtDate(lesson?.updatedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||||
<InfoField label="Title" value={lesson?.title} />
|
||||
<InfoField label="Description" value={lesson?.description || "No description provided."} />
|
||||
<InfoField label="Unit" value={lesson?.unit?.title} />
|
||||
</div>
|
||||
|
||||
{/* Objectives */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ListChecks className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium">Objectives</p>
|
||||
<Badge variant="secondary" className="ml-auto">{lesson?.objectives?.length ?? 0}</Badge>
|
||||
</div>
|
||||
|
||||
{lesson?.objectives?.length === 0 || !lesson?.objectives ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">No objectives defined.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{lesson.objectives.map((obj, i) => (
|
||||
<li key={obj.objective_id} className="flex items-start gap-3 text-sm">
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span>{obj.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{activeTab === "details" && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/lessons/${lessonId}/edit`)}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 pb-0 -mb-px border-b">
|
||||
{TABS.map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveTab(key)}
|
||||
className={[
|
||||
"flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors",
|
||||
activeTab === key
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground",
|
||||
].join(" ")}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === "details" && <LessonDetailsTab lesson={lesson} fmtDate={fmtDate} />}
|
||||
{activeTab === "content" && (
|
||||
<LessonContentTab lesson={lesson} courseId={courseId} unitId={unitId} lessonId={lessonId} />
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Title</p>
|
||||
<p className="text-sm font-medium">{unit?.title ?? "—"}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Description</p>
|
||||
<p className="text-sm text-muted-foreground">{unit?.description || "No description provided."}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Order</p>
|
||||
<p className="text-sm font-medium">{unit?.order ?? 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Quiz tab ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function UnitQuizTab({ courseId, unitId }) {
|
||||
const navigate = useNavigate();
|
||||
const { fetchQuiz, quiz, loading } = useCourses();
|
||||
|
||||
useEffect(() => {
|
||||
fetchQuiz(courseId, unitId);
|
||||
}, [courseId, unitId]);
|
||||
|
||||
if (loading && !quiz) {
|
||||
return <Skeleton className="h-32 w-full rounded-lg" />;
|
||||
}
|
||||
|
||||
if (!quiz) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed bg-card p-12 flex flex-col items-center gap-3 text-center">
|
||||
<HelpCircle className="h-8 w-8 text-muted-foreground/40" />
|
||||
<p className="text-sm text-muted-foreground">No quiz has been created for this unit yet.</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/quiz/edit`)}
|
||||
>
|
||||
<NotebookPen className="h-4 w-4 mr-2" />
|
||||
Create Quiz
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const questions = quiz.questions ?? [];
|
||||
const totalPoints = questions.reduce((sum, q) => sum + (q.points ?? 1), 0);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{quiz.title || "Unit Quiz"}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/quiz/view`)}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
View Quiz
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/quiz/edit`)}
|
||||
>
|
||||
<NotebookPen className="h-4 w-4 mr-2" />
|
||||
Modify Quiz
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<section className="bg-muted h-full">
|
||||
<PageMeta title={unit ? `${unit.title} - STARR` : undefined} />
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-center px-4 py-10">
|
||||
|
||||
<div className="w-full max-w-2xl">
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="sticky z-20 shrink-0 border-b bg-white/70 dark:bg-zinc-900/70 backdrop-blur-md" style={{ top: "var(--navbar-h)" }}>
|
||||
<div className="lg:container lg:mx-auto lg:px-6 px-4">
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(`/admin/courses/${courseId}/units`)}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
@@ -47,35 +151,44 @@ export default function ViewUnit() {
|
||||
<p className="text-sm text-muted-foreground">View unit information.</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/edit`)}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
{activeTab === "details" && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => navigate(`/admin/courses/${courseId}/units/${unitId}/edit`)}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Title</p>
|
||||
<p className="text-sm font-medium">{unit?.title ?? "—"}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Description</p>
|
||||
<p className="text-sm text-muted-foreground">{unit?.description || "No description provided."}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Order</p>
|
||||
<p className="text-sm font-medium">{unit?.order ?? 0}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 pb-0 -mb-px">
|
||||
{TABS.map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveTab(key)}
|
||||
className={[
|
||||
"flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors",
|
||||
activeTab === key
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground",
|
||||
].join(" ")}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:container lg:mx-auto lg:px-6 px-4 py-6">
|
||||
<div className={WIDE_TABS.has(activeTab) ? "pb-16" : "max-w-2xl mx-auto pb-16"}>
|
||||
{activeTab === "details" && <UnitDetailsTab unit={unit} />}
|
||||
{activeTab === "lessons" && <LessonsTable courseId={courseId} unitId={unitId} />}
|
||||
{activeTab === "quiz" && <UnitQuizTab courseId={courseId} unitId={unitId} />}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user