add: layout

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-05-21 11:48:04 +08:00
parent 8454d8bcf3
commit 5241f0f1dd
13 changed files with 657 additions and 507 deletions
+127 -122
View File
@@ -20,11 +20,11 @@ import {
// ─── 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 },
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 },
};
// ─── Label / value row — fully themed by shadcn tokens ───────────────────────
@@ -156,141 +156,146 @@ export default function ViewTaskList() {
);
const groups = taskList.groups ?? [];
const tasks = taskList.tasks ?? [];
const tasks = taskList.tasks ?? [];
return (
<div className="max-w-2xl mx-auto py-8 px-4 space-y-4">
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
<div className="mx-auto space-y-4">
{/* Header */}
<div className="flex items-center justify-between w-full">
<div className="flex gap-2 items-center">
<Button
variant="ghost"
className=""
onClick={() => navigate('/admin/taskList')}
>
<ArrowLeft className="size-4" />
</Button>
<div className="space-y-1">
<h1 className="text-xl font-semibold">{taskList.name}</h1>
{taskList.description && (
<p className="text-sm text-muted-foreground leading-relaxed">
{taskList.description}
</p>
)}
</div>
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
onClick={() => navigate(`/admin/taskList/${taskListId}/edit`)}
>
<Pencil className="h-3.5 w-3.5" />
Edit
</Button>
</div>
{/* Header */}
<div className="flex items-center justify-between w-full">
<div className="flex gap-2 items-center">
<Button
variant="ghost"
{/* Main content — plain div, not Card, to avoid overflow:hidden clipping accordion */}
<div className="rounded-lg border border-border bg-card text-card-foreground shadow-sm">
<div className="p-6 space-y-6">
className=""
onClick={() => navigate('/admin/taskList')}
>
<ArrowLeft className="size-4" />
{/* Assigned Groups */}
<div className="space-y-4">
<div className="flex items-center gap-1.5 text-sm font-medium">
<Equal className="size-4" />
Assigned Groups
</Button>
<div className="space-y-1">
<h1 className="text-xl font-semibold">{taskList.name}</h1>
{taskList.description && (
<p className="text-sm text-muted-foreground leading-relaxed">
{taskList.description}
</p>
)}
</div>
{groups.length > 0 ? (
<div className="flex flex-wrap gap-1.5">
{groups.map((g) => (
<Badge key={g.group_id} variant="secondary">
<Users className="size-3 mr-1" />
{g.name ?? g.group_id}
</Badge>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No groups assigned.</p>
)}
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
onClick={() => navigate(`/admin/taskList/${taskListId}/edit`)}
>
<Pencil className="h-3.5 w-3.5" />
Edit
</Button>
</div>
{/* Tasks */}
<div className="space-y-4">
<div className="flex items-center gap-1.5 text-sm font-medium">
<ClipboardList className="h-4 w-4 text-muted-foreground" />
Tasks
{tasks.length > 0 && (
<span className="ml-auto text-xs text-muted-foreground font-normal">
{tasks.length} task{tasks.length !== 1 ? 's' : ''}
</span>
<div className="lg:w-2xl rounded-lg border border-border bg-card text-card-foreground shadow-sm">
<div className="p-6 space-y-6">
{/* Assigned Groups */}
<div className="space-y-4">
<div className="flex items-center gap-1.5 text-sm font-medium">
<Equal className="size-4" />
Assigned Groups
</div>
{groups.length > 0 ? (
<div className="flex flex-wrap gap-1.5">
{groups.map((g) => (
<Badge key={g.group_id} variant="secondary">
<Users className="size-3 mr-1" />
{g.name ?? g.group_id}
</Badge>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No groups assigned.</p>
)}
</div>
{tasks.length > 0 ? (
<Accordion type="multiple" className="rounded-md border divide-y">
{tasks.map((task, index) => (
<AccordionItem
key={task.task_id ?? index}
value={String(task.task_id ?? index)}
className="px-3 border-0 border-b last:border-b-0"
>
<AccordionTrigger className="py-2.5 hover:no-underline gap-3 [&>svg]:shrink-0">
<div className="flex items-center gap-3 flex-1 min-w-0">
<span className="text-xs text-muted-foreground w-5 text-right shrink-0">
{index + 1}.
</span>
<span className="flex-1 text-sm text-left truncate">
{task.name ?? `Task ${index + 1}`}
</span>
{task.requirements?.length > 0 && (
<Badge variant="secondary" className="text-xs shrink-0">
{task.requirements.length} Requirement{task.requirements.length !== 1 ? 's' : ''}
</Badge>
)}
</div>
</AccordionTrigger>
{/* Tasks */}
<div className="space-y-4">
<div className="flex items-center gap-1.5 text-sm font-medium">
<ClipboardList className="h-4 w-4 text-muted-foreground" />
Tasks
{tasks.length > 0 && (
<span className="ml-auto text-xs text-muted-foreground font-normal">
{tasks.length} task{tasks.length !== 1 ? 's' : ''}
</span>
)}
</div>
<AccordionContent className="pb-4 pt-0 overflow-visible">
<div className="ml-8 space-y-4 text-sm">
{tasks.length > 0 ? (
<Accordion
type="multiple"
className="rounded-md border divide-y"
defaultValue={[String(tasks[0]?.task_id ?? 0)]}
>
{tasks.map((task, index) => (
<AccordionItem
key={task.task_id ?? index}
value={String(task.task_id ?? index)}
className="px-3 border-0 border-b last:border-b-0"
>
<AccordionTrigger className="py-2.5 hover:no-underline gap-3 [&>svg]:shrink-0">
<div className="flex items-center gap-3 flex-1 min-w-0">
<span className="text-xs text-muted-foreground w-5 text-right shrink-0">
{index + 1}.
</span>
<span className="flex-1 text-sm text-left truncate">
{task.name ?? `Task ${index + 1}`}
</span>
{task.requirements?.length > 0 && (
<Badge variant="secondary" className="text-xs shrink-0">
{task.requirements.length} Requirement{task.requirements.length !== 1 ? 's' : ''}
</Badge>
)}
</div>
</AccordionTrigger>
{task.description && (
<div className="flex items-center gap-2 text-muted-foreground">
<FileText className="size-4 shrink-0" />
<p className="leading-relaxed">{task.description}</p>
</div>
)}
<AccordionContent className="pb-4 pt-0">
<div className="ml-8 space-y-4 text-sm">
{task.deadline && (
<div className="flex items-center gap-2 text-muted-foreground">
<CalendarClock className="size-4 shrink-0" />
<span className="text-sm">
Deadline:{' '}
<span className="text-foreground font-medium">
{new Date(task.deadline).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
{task.description && (
<div className="flex items-center gap-2 text-muted-foreground">
<FileText className="size-4 shrink-0" />
<p className="leading-relaxed">{task.description}</p>
</div>
)}
{task.deadline && (
<div className="flex items-center gap-2 text-muted-foreground">
<CalendarClock className="size-4 shrink-0" />
<span className="text-sm">
Deadline:{' '}
<span className="text-foreground font-medium">
{new Date(task.deadline).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
</span>
</span>
</span>
</div>
)}
</div>
)}
<TaskRequirementsSection requirements={task.requirements} />
<TaskRequirementsSection requirements={task.requirements} />
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
<p className="text-sm text-muted-foreground">No tasks in this list yet.</p>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
<p className="text-sm text-muted-foreground">No tasks in this list yet.</p>
)}
</div>
</div>
</div>
</div>