mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Layers, Tag, CheckCheck, RefreshCw, SendHorizonal } from "lucide-react";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import ResponsiveModal from "@/components/generic/ResponsiveModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import api from "@/utils/api.util";
|
||||
|
||||
const ReadUnit = ({ title = "Read Units", units = [], groupId, taskListId, taskId }) => {
|
||||
const navigate = useNavigate();
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [details, setDetails] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
units.forEach(async (unit) => {
|
||||
if (!unit.reference_id) return;
|
||||
try {
|
||||
const res = await api.get(`/client/courses/unit/uuid/${unit.reference_id}`);
|
||||
const d = res.data?.data;
|
||||
if (d) setDetails((prev) => ({ ...prev, [unit.reference_id]: d }));
|
||||
} catch (err) {
|
||||
console.error('[ReadUnit] fetch failed:', err?.response?.status, err?.message);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getProgress = (unit) => unit.completed ? 100 : (unit.progress ?? 0);
|
||||
|
||||
if (!units.length) {
|
||||
return (
|
||||
<div className="border rounded-lg bg-card overflow-hidden">
|
||||
<div className="px-6 py-4 border-b flex items-center gap-2">
|
||||
<Layers className="size-4 text-muted-foreground" />
|
||||
<h2 className="font-semibold text-sm">{title}</h2>
|
||||
<Badge variant="secondary" className="ml-auto">0</Badge>
|
||||
</div>
|
||||
<div className="p-4 text-center text-sm text-muted-foreground">No units available.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border rounded-lg bg-card overflow-hidden">
|
||||
<div className="px-6 py-4 border-b flex items-center gap-2">
|
||||
<Layers className="size-4 text-muted-foreground" />
|
||||
<h2 className="font-semibold text-sm">{title}</h2>
|
||||
<Badge variant="secondary" className="ml-auto">{units.length}</Badge>
|
||||
</div>
|
||||
<ScrollArea className="w-full bg-muted overflow-hidden">
|
||||
<div className="flex gap-4 p-4">
|
||||
{units.map((unit) => {
|
||||
const info = details[unit.reference_id];
|
||||
const progress = getProgress(unit);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={unit.id}
|
||||
onClick={() => setSelected(unit)}
|
||||
className="bg-card rounded-2xl border p-4 flex flex-col gap-2.5 hover:bg-muted/60 hover:shadow-sm transition-all cursor-pointer group w-96 shrink-0"
|
||||
>
|
||||
<Badge variant="secondary" className="w-fit truncate max-w-full">
|
||||
<Tag className="size-3 shrink-0" />
|
||||
<span className="truncate">
|
||||
{info ? (info.course?.title ?? 'No course') : 'Loading…'}
|
||||
</span>
|
||||
</Badge>
|
||||
<h1 className="text-base font-medium leading-snug line-clamp-2 group-hover:text-blue-700 dark:group-hover:text-card-foreground transition-colors">
|
||||
{unit.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed line-clamp-2">
|
||||
{info?.description ?? ''}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-3 mt-auto pt-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
{progress >= 100 ? (
|
||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400 font-medium">
|
||||
<CheckCheck className="size-4" /> Completed
|
||||
</span>
|
||||
) : progress > 0 ? (
|
||||
<span className="flex items-center gap-1 font-medium">
|
||||
<RefreshCw className="size-4" /> In Progress
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground font-medium">Not Started</span>
|
||||
)}
|
||||
</div>
|
||||
<Progress
|
||||
value={progress}
|
||||
className={`h-1.5 ${progress >= 100 ? "[&>div]:bg-green-500" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<ResponsiveModal
|
||||
open={!!selected}
|
||||
onOpenChange={(v) => !v && setSelected(null)}
|
||||
title={selected?.title}
|
||||
description="Unit Info"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setSelected(null)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => navigate(
|
||||
`/group/${groupId}/view/${taskListId}/task/${taskId}/requirement`,
|
||||
{ state: { unit: selected } },
|
||||
)}
|
||||
disabled={getProgress(selected ?? {}) >= 100}
|
||||
>
|
||||
<SendHorizonal /> Proceed
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{selected && (() => {
|
||||
const info = details[selected.reference_id];
|
||||
const progress = getProgress(selected);
|
||||
const done = progress >= 100;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{done && (
|
||||
<div className="flex items-center gap-2 bg-green-50 dark:bg-green-950/40 text-green-700 dark:text-green-400 text-sm font-medium rounded-lg px-4 py-3">
|
||||
<CheckCheck className="size-4 shrink-0" />
|
||||
Automatically Turned-in
|
||||
</div>
|
||||
)}
|
||||
|
||||
{info?.course?.title && (
|
||||
<p className="text-sm">
|
||||
<span className="text-muted-foreground">Course: </span>
|
||||
<span className="font-medium">{info.course.title}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-xs uppercase tracking-wide text-muted-foreground font-medium">
|
||||
About this unit
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed">{info?.description ?? '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</ResponsiveModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadUnit;
|
||||
Reference in New Issue
Block a user