add: more commits

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-03 16:21:27 +08:00
parent 17326b2c2e
commit 7e964f2432
112 changed files with 9160 additions and 3461 deletions
@@ -1,10 +1,18 @@
import { Trophy, Clock } from "lucide-react";
import { Trophy, Clock, CheckCircle2 } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
/**
* Props:
* course — { title, ... } the completed course
* course — { title, pending_certificate, certificate, ... } the completed course
*/
const CourseCompleteBlock = ({ course }) => {
const { fmtDate } = useDateFormat();
const certificate = course?.certificate ?? null;
const pendingCert = course?.pending_certificate ?? null;
const isIssued = !!certificate;
const isPending = !isIssued && !!pendingCert;
return (
<div className="max-w-2xl mx-auto">
<div className="rounded-xl border bg-card p-6 space-y-6 text-center sm:p-10">
@@ -25,18 +33,33 @@ const CourseCompleteBlock = ({ course }) => {
You've passed all required units and the final assessment for this course.
</p>
</div>
<div className="flex items-start gap-2.5 rounded-lg border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-left">
<Clock className="size-4 text-amber-500 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-700 dark:text-amber-400">Certificate Pending</p>
<p className="text-xs text-muted-foreground">
Certificates are issued automatically every hour. Yours will be ready within the next hour — check your notifications.
</p>
{isIssued ? (
<div className="flex items-start gap-2.5 rounded-lg border border-green-500/30 bg-green-500/5 px-4 py-3 text-left">
<CheckCircle2 className="size-4 text-green-500 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-green-700 dark:text-green-400">Certificate Issued</p>
<p className="text-xs text-muted-foreground">
Issued on {fmtDate(certificate.issued_at)}. View and download it from your Certificates page.
</p>
</div>
</div>
</div>
) : (
<div className="flex items-start gap-2.5 rounded-lg border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-left">
<Clock className="size-4 text-amber-500 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-700 dark:text-amber-400">Certificate Pending</p>
<p className="text-xs text-muted-foreground">
{isPending
? `Certificates are issued automatically every hour. Yours will be ready by ${fmtDate(pendingCert.issue_at)} — check your notifications.`
: "Certificates are issued automatically every hour. Yours will be ready within the next hour — check your notifications."}
</p>
</div>
</div>
)}
</div>
</div>
);
};
export default CourseCompleteBlock;
export default CourseCompleteBlock;
@@ -179,18 +179,13 @@ const ReadCourse = ({ title = "Read Course", courses = [], groupId, taskListId,
<div>
<h1
onClick={(e) => {
if (!taskId || !groupId || !taskListId) return;
if (!info?.course_id) return;
e.stopPropagation();
navigate(
`/group/${groupId}/view/${taskListId}/task/${taskId}/requirement`,
{ state: { course: { id: course.id, reference_id: course.reference_id, title: course.title } } }
);
navigate(`/course/${info.course_id}/unit`, {
state: taskId ? { taskCtx: { has_task: true, groupId, taskListId, taskId } } : {},
});
}}
className={`text-base font-semibold leading-snug line-clamp-2 transition-colors ${
taskId
? 'text-blue-600 dark:text-blue-400 hover:underline cursor-pointer'
: 'group-hover:text-blue-700 dark:group-hover:text-blue-400'
}`}
className="text-base font-semibold leading-snug line-clamp-2 transition-colors text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
>
{course.title}
</h1>
@@ -242,18 +237,12 @@ const ReadCourse = ({ title = "Read Course", courses = [], groupId, taskListId,
<Button
onClick={() => {
if (!info?.course_id) return;
if (taskId && groupId && taskListId) {
// Task context — read inside ViewRequirement
navigate(
`/group/${groupId}/view/${taskListId}/task/${taskId}/requirement`,
{ state: { course: { id: selected.id, reference_id: selected.reference_id, title: selected.title } } }
);
} else {
// No task context — fall back to standalone course reader
navigate(`/course/${info.course_id}/unit`, {
state: allRead ? { seekFirstIncomplete: true } : {},
});
}
navigate(`/course/${info.course_id}/unit`, {
state: {
...(allRead ? { seekFirstIncomplete: true } : {}),
...(taskId ? { taskCtx: { has_task: true, groupId, taskListId, taskId } } : {}),
},
});
}}
disabled={done || !info?.course_id}
>
@@ -147,10 +147,16 @@ const ReadLesson = ({ title = "Read Lessons", lessons = [], groupId, taskListId,
return (
<div
key={lesson.id}
onClick={() => !isFetching && navigate(
`/group/${groupId}/view/${taskListId}/task/${taskId}/requirement`,
{ state: { lesson } },
)}
onClick={() => {
if (isFetching || !info?.unit?.course?.course_id) return;
navigate(`/course/${info.unit.course.course_id}/unit`, {
state: {
lessonId: info.lesson_id,
unitId: info.unit.unit_id,
...(taskId ? { taskCtx: { has_task: true, groupId, taskListId, taskId } } : {}),
},
});
}}
className={`bg-card rounded-2xl border p-4 flex flex-col gap-3 transition-all w-96 shrink-0 ${
isFetching
? 'opacity-60 cursor-wait'
@@ -194,18 +194,13 @@ const ReadUnit = ({ title = "Read Units", units = [], groupId, taskListId, taskI
<div>
<h1
onClick={(e) => {
if (!taskId || !groupId || !taskListId) return;
if (!info?.course?.course_id) return;
e.stopPropagation();
navigate(
`/group/${groupId}/view/${taskListId}/task/${taskId}/requirement`,
{ state: { unit } }
);
navigate(`/course/${info.course.course_id}/unit`, {
state: taskId ? { taskCtx: { has_task: true, groupId, taskListId, taskId } } : {},
});
}}
className={`text-base font-semibold leading-snug line-clamp-2 transition-colors ${
taskId
? 'text-blue-600 dark:text-blue-400 hover:underline cursor-pointer'
: 'group-hover:text-blue-700 dark:group-hover:text-blue-400'
}`}
className="text-base font-semibold leading-snug line-clamp-2 transition-colors text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
>
{unit.title}
</h1>
@@ -251,10 +246,13 @@ const ReadUnit = ({ title = "Read Units", units = [], groupId, taskListId, taskI
<>
<Button variant="outline" onClick={() => setSelected(null)}>Cancel</Button>
<Button
onClick={() => navigate(
`/group/${groupId}/view/${taskListId}/task/${taskId}/requirement`,
{ state: { unit: selected } },
)}
onClick={() => {
const info = details[selected?.reference_id];
if (!info?.course?.course_id) return;
navigate(`/course/${info.course.course_id}/unit`, {
state: taskId ? { taskCtx: { has_task: true, groupId, taskListId, taskId } } : {},
});
}}
disabled={
getProgress(selected ?? {}) >= 100 ||
locked[selected?.reference_id] ||