mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
units,lesson as standalone
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// LessonCard — grid card for a standalone Lesson. Shared by LessonsList.jsx and
|
||||
// Dashboard.jsx's "Featured Lessons" section. Mirrors UnitCard.jsx; a Lesson has
|
||||
// no lesson_count/quiz_id of its own — shows unit_count instead (how many Units
|
||||
// it's attached to).
|
||||
|
||||
import { Timer, LockIcon, Layers } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function formatDuration(seconds = 0) {
|
||||
if (!seconds) return null;
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (h && m) return `${h}h ${m}m`;
|
||||
if (h) return `${h}h`;
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
export const LessonCard = ({ lesson, onViewDetails }) => {
|
||||
const locked = lesson.is_locked;
|
||||
const duration = formatDuration(lesson.duration_seconds);
|
||||
const unitCount = Number(lesson.unit_count ?? 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card rounded-2xl border p-4 flex flex-col gap-2.5 transition-all cursor-pointer group",
|
||||
"hover:shadow-sm",
|
||||
locked
|
||||
? "opacity-80 hover:opacity-100 hover:border-muted-foreground/40"
|
||||
: "hover:bg-muted/60 dark:hover:border-blue-500"
|
||||
)}
|
||||
onClick={() => onViewDetails(lesson)}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{locked ? (
|
||||
<Badge variant="secondary">
|
||||
<LockIcon className="size-3" /> Locked
|
||||
</Badge>
|
||||
) : unitCount > 0 ? (
|
||||
<Badge variant="outline"><Layers className="size-3" /> In {unitCount} unit{unitCount === 1 ? "" : "s"}</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-muted-foreground">Standalone</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-lg font-medium leading-snug line-clamp-3 transition-colors group-hover:text-blue-700 dark:group-hover:text-blue-400">
|
||||
{lesson.title}
|
||||
</h1>
|
||||
{lesson.description && (
|
||||
<p className="text-sm leading-relaxed line-clamp-2 group-hover:text-blue-700 dark:group-hover:text-blue-400">
|
||||
{lesson.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 mt-auto border-t">
|
||||
<div className={`flex items-center gap-3 text-sm [&_svg]:size-4 ${locked ? "text-muted-foreground" : "text-card-foreground group-hover:text-blue-700 dark:group-hover:text-blue-400"}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Timer /> {duration ?? "—"}
|
||||
</div>
|
||||
</div>
|
||||
{locked && (
|
||||
<span className="text-xs text-muted-foreground">Upgrade to unlock</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LessonCardSkeleton = () => (
|
||||
<div className="bg-card rounded-2xl border p-4 flex flex-col gap-2.5">
|
||||
<div className="flex gap-1.5">
|
||||
<Skeleton className="h-5 w-20 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-6 w-3/4" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<div className="pt-2 mt-auto border-t">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
// LessonUpsellModal — shown when a learner clicks a locked standalone Lesson.
|
||||
// Mirrors UnitUpsellModal.jsx: a Lesson isn't independently purchasable, so this
|
||||
// lists every course that would unlock it (aggregated across all its attached
|
||||
// Units, since a Lesson can sit in more than one) plus a generic "View Plans"
|
||||
// fallback. Shared by LessonsList and Dashboard.
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LockIcon, BookOpen } from "lucide-react";
|
||||
import ResponsiveModal from "@/components/generic/ResponsiveModal";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { resolveTierBadge } from "@/utils/tierBadge.util";
|
||||
|
||||
export default function LessonUpsellModal({ open, onOpenChange, lesson, tierMap = {} }) {
|
||||
const navigate = useNavigate();
|
||||
const courses = lesson?.courses ?? [];
|
||||
|
||||
return (
|
||||
<ResponsiveModal
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={lesson?.title ?? "Lesson Details"}
|
||||
description="This lesson is part of one or more courses that require a plan upgrade."
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>Close</Button>
|
||||
<Button onClick={() => { onOpenChange(false); navigate("/plans"); }}>
|
||||
<LockIcon /> View Plans
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3 py-2">
|
||||
{courses.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upgrade your plan to access this content.
|
||||
</p>
|
||||
) : (
|
||||
courses.map((course) => {
|
||||
const { label, cls } = resolveTierBadge(course.subscription, tierMap);
|
||||
return (
|
||||
<div
|
||||
key={course.course_id}
|
||||
className="flex items-center justify-between gap-3 p-4 rounded-xl border bg-muted/40"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<BookOpen className="size-4 text-muted-foreground shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{course.title}</p>
|
||||
<Badge className={`${cls} mt-1`}>
|
||||
<LockIcon className="size-3" /> {label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0"
|
||||
onClick={() => { onOpenChange(false); navigate(`/course/${course.course_id}`); }}
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</ResponsiveModal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// LockedContentPanel — full-page inline panel shown when a deep-link lands on
|
||||
// content the learner can't access (locked unit or a lesson under one).
|
||||
// Distinct from UnitUpsellModal (a dialog triggered from card grids) — this
|
||||
// renders in place of the page body itself. Shared by UnitDetails and
|
||||
// LessonDetails.
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LockIcon, Zap } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function LockedContentPanel({ course, tierMap = {} }) {
|
||||
const navigate = useNavigate();
|
||||
const tier = course?.subscription ? tierMap[course.subscription] : null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-6 py-32 text-center px-4">
|
||||
<div className="h-16 w-16 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
|
||||
<LockIcon className="size-7 text-amber-500" />
|
||||
</div>
|
||||
<div className="space-y-2 max-w-sm">
|
||||
<h2 className="text-xl font-semibold">Premium / Exclusive Content</h2>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{course
|
||||
? `This content is part of "${course.title}"${tier?.name ? ` (${tier.name} plan)` : ""}. Upgrade your plan or view the course to unlock it.`
|
||||
: "Upgrade your plan to access this content."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Button onClick={() => navigate('/plans')} className="gap-1.5">
|
||||
<Zap className="size-4" /> View Available Plans
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">Already subscribed? Your plan may not cover this tier.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// PlanComparisonTable — side-by-side feature matrix, an alternate view next to
|
||||
// the card grid on PlanList.jsx. Rows are the union of every active plan's
|
||||
// admin-authored `features` list; a plan gets a checkmark for a row if that
|
||||
// exact feature text is present in its own list.
|
||||
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { Check, Minus, Tag } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { resolveTierBadge } from "@/utils/tierBadge.util";
|
||||
|
||||
export default function PlanComparisonTable({ plans, myTier, tierMap, fmtCurrency, onSelect }) {
|
||||
const featureRows = [...new Set(
|
||||
plans.flatMap((p) => (p.features ?? []).map((f) => f.text))
|
||||
)];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-xl border bg-card">
|
||||
<table className="w-full text-sm border-collapse min-w-[640px]">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left p-4 w-48 align-bottom text-muted-foreground font-medium">Plan</th>
|
||||
{plans.map((plan) => {
|
||||
const { label, cls } = resolveTierBadge(plan.tier, tierMap);
|
||||
const Icon = LucideIcons[tierMap[plan.tier]?.badge_icon] ?? Tag;
|
||||
const isCurrent = myTier?.tier === plan.tier && myTier?.status === "active";
|
||||
return (
|
||||
<th key={plan.plan_id} className="p-4 text-center align-bottom min-w-[160px]">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Badge className={cls}><Icon className="size-3" />{label}</Badge>
|
||||
<span className="font-semibold text-foreground">{plan.label}</span>
|
||||
<span className="text-lg font-bold">{fmtCurrency(plan.price, plan.currency)}</span>
|
||||
{isCurrent && <Badge variant="secondary" className="text-xs">Current Plan</Badge>}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b bg-muted/30">
|
||||
<td className="p-3 font-medium text-muted-foreground">Courses included</td>
|
||||
{plans.map((plan) => (
|
||||
<td key={plan.plan_id} className="p-3 text-center">
|
||||
{plan.course_count > 0 ? plan.course_count : "—"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
{featureRows.map((text, i) => (
|
||||
<tr key={text} className={`border-b ${i % 2 === 1 ? "bg-muted/30" : ""}`}>
|
||||
<td className="p-3 text-muted-foreground">{text}</td>
|
||||
{plans.map((plan) => {
|
||||
const included = (plan.features ?? []).some((f) => f.text === text);
|
||||
return (
|
||||
<td key={plan.plan_id} className="p-3 text-center">
|
||||
{included
|
||||
? <Check className="size-4 mx-auto text-green-500" />
|
||||
: <Minus className="size-4 mx-auto text-muted-foreground/30" />
|
||||
}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
{featureRows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={plans.length + 1} className="p-6 text-center text-muted-foreground">
|
||||
No features have been added to these plans yet.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td className="p-4" />
|
||||
{plans.map((plan) => {
|
||||
const isCurrent = myTier?.tier === plan.tier && myTier?.status === "active";
|
||||
return (
|
||||
<td key={plan.plan_id} className="p-4 text-center">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isCurrent ? "secondary" : "default"}
|
||||
disabled={isCurrent || !plan.is_active}
|
||||
onClick={() => onSelect(plan)}
|
||||
>
|
||||
{isCurrent ? "Current" : !plan.is_active ? "Not Available" : "Select"}
|
||||
</Button>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ClipboardCheck, CheckCheck, SendHorizonal, Lock, Zap, Info, Tag } from "lucide-react";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import ResponsiveModal from "@/components/generic/ResponsiveModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import api from "@/utils/api.util";
|
||||
import { useClientTiers } from "@/contexts/ClientTiersProvider";
|
||||
import { resolveTierBadge } from "@/utils/tierBadge.util";
|
||||
|
||||
function TierBadge({ tier }) {
|
||||
const { tierMap } = useClientTiers();
|
||||
const { rank, label, cls } = resolveTierBadge(tier ?? 'free', tierMap);
|
||||
return (
|
||||
<Badge className={`gap-1 ${cls} w-fit shrink-0`}>
|
||||
{rank > 0 ? <Lock className="size-3" /> : <Tag className="size-3" />}
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── PassQuiz — task requirement block for pass_quiz type ─────────────────────
|
||||
// Mirrors ReadUnit.jsx's fetch-detail-and-navigate pattern. A quiz is always
|
||||
// unit-scoped; navigation goes into the course reader if the unit is attached
|
||||
// to a course, otherwise the standalone unit reader.
|
||||
const PassQuiz = ({ title = "Pass Quizzes", quizzes = [], groupId, taskListId, taskId }) => {
|
||||
const navigate = useNavigate();
|
||||
const [details, setDetails] = useState({});
|
||||
const [locked, setLocked] = useState({});
|
||||
const [lockedInfo, setLockedInfo] = useState({});
|
||||
const [unavailable, setUnavailable] = useState({});
|
||||
const [fetching, setFetching] = useState({});
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
quizzes.forEach(async (q) => {
|
||||
if (!q.reference_id) return;
|
||||
setFetching((prev) => ({ ...prev, [q.reference_id]: true }));
|
||||
try {
|
||||
const res = await api.get(`/client/courses/quiz/uuid/${q.reference_id}`);
|
||||
const d = res.data?.data;
|
||||
if (d) setDetails((prev) => ({ ...prev, [q.reference_id]: d }));
|
||||
} catch (err) {
|
||||
if (err?.response?.status === 403) {
|
||||
setLocked((prev) => ({ ...prev, [q.reference_id]: true }));
|
||||
const course = err.response?.data?.course;
|
||||
if (course) setLockedInfo((prev) => ({ ...prev, [q.reference_id]: course }));
|
||||
} else {
|
||||
setUnavailable((prev) => ({ ...prev, [q.reference_id]: true }));
|
||||
}
|
||||
} finally {
|
||||
setFetching((prev) => ({ ...prev, [q.reference_id]: false }));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const goToQuiz = (info) => {
|
||||
if (!info) return;
|
||||
const taskCtx = taskId ? { has_task: true, groupId, taskListId, taskId } : undefined;
|
||||
if (info.unit?.course?.course_id) {
|
||||
navigate(`/course/${info.unit.course.course_id}/unit`, {
|
||||
state: { quizUnitId: info.unit.unit_id, ...(taskCtx ? { taskCtx } : {}) },
|
||||
});
|
||||
} else if (info.unit?.uuid) {
|
||||
navigate(`/units/${info.unit.uuid}/read`, { state: { quizId: true } });
|
||||
}
|
||||
};
|
||||
|
||||
if (!quizzes.length) return null;
|
||||
|
||||
const hasLocked = Object.values(locked).some(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border rounded-lg bg-card overflow-hidden">
|
||||
<div className="px-6 py-4 border-b flex items-center gap-2">
|
||||
<ClipboardCheck className="size-4 text-muted-foreground" />
|
||||
<h2 className="font-semibold text-sm">{title}</h2>
|
||||
<Badge variant="secondary" className="ml-auto">{quizzes.length}</Badge>
|
||||
</div>
|
||||
|
||||
{hasLocked && (
|
||||
<div className="flex items-start gap-3 px-5 py-3.5 border-b bg-amber-50 dark:bg-amber-950/30 text-amber-800 dark:text-amber-300">
|
||||
<Info className="size-4 mt-0.5 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium leading-snug">Subscription Required</p>
|
||||
<p className="text-xs mt-0.5 text-amber-700 dark:text-amber-400 leading-relaxed">
|
||||
To complete this activity, subscribe to one of our available tier plans.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" className="shrink-0 border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/40" onClick={() => navigate('/plans')}>
|
||||
<Zap className="size-3.5" /> View Plans
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ScrollArea className="w-full bg-muted overflow-hidden">
|
||||
<div className="flex gap-4 p-4">
|
||||
{quizzes.map((q) => {
|
||||
const info = details[q.reference_id];
|
||||
const courseInfo = lockedInfo[q.reference_id];
|
||||
const isLocked = locked[q.reference_id];
|
||||
const isUnavailable = unavailable[q.reference_id];
|
||||
const isFetching = fetching[q.reference_id];
|
||||
const passed = info?.has_passed || q.completed;
|
||||
|
||||
if (isLocked) {
|
||||
return (
|
||||
<div
|
||||
key={q.id}
|
||||
onClick={() => navigate('/plans')}
|
||||
className="bg-card rounded-2xl border border-dashed p-4 flex flex-col gap-3 hover:bg-muted/60 hover:shadow-sm transition-all cursor-pointer group w-80 shrink-0 opacity-80"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClipboardCheck className="size-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="text-xs text-muted-foreground font-medium">Quiz</span>
|
||||
<div className="ml-auto"><TierBadge tier={courseInfo?.subscription} /></div>
|
||||
</div>
|
||||
<h1 className="text-base font-semibold leading-snug line-clamp-2 text-muted-foreground">{q.title}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Subscribe to <span className="font-medium">{courseInfo?.title ?? 'this course'}</span> to unlock this quiz.
|
||||
</p>
|
||||
<div className="mt-auto">
|
||||
<Button size="sm" className="w-full gap-1.5" onClick={(e) => { e.stopPropagation(); navigate('/plans'); }}>
|
||||
<Zap className="size-3.5" /> Upgrade to unlock
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isUnavailable) {
|
||||
return (
|
||||
<div key={q.id} className="bg-card rounded-2xl border border-dashed p-4 flex flex-col gap-3 w-80 shrink-0 opacity-50 cursor-not-allowed">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClipboardCheck className="size-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="text-xs text-muted-foreground font-medium">Quiz</span>
|
||||
<Badge variant="secondary" className="ml-auto gap-1 text-muted-foreground text-xs">
|
||||
<Lock className="size-3" /> Unavailable
|
||||
</Badge>
|
||||
</div>
|
||||
<h1 className="text-base font-semibold leading-snug line-clamp-2 text-muted-foreground">{q.title}</h1>
|
||||
<p className="text-sm text-muted-foreground">This quiz is no longer available.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={q.id}
|
||||
onClick={() => !isFetching && setSelected(q)}
|
||||
className={`bg-card rounded-2xl border p-4 flex flex-col gap-3 transition-all w-80 shrink-0 ${
|
||||
isFetching ? 'opacity-60 cursor-wait' : 'hover:bg-muted/60 hover:shadow-sm cursor-pointer group'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClipboardCheck className="size-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="text-xs text-muted-foreground font-medium">Quiz</span>
|
||||
<div className="ml-auto">
|
||||
{info?.unit?.course?.subscription
|
||||
? <TierBadge tier={info.unit.course.subscription} />
|
||||
: isFetching && <Badge variant="secondary" className="opacity-40 text-xs">Loading…</Badge>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{info?.unit?.title && (
|
||||
<p className="text-xs text-muted-foreground truncate -mt-1">
|
||||
in <span className="text-foreground/70 font-medium">{info.unit.title}</span>
|
||||
</p>
|
||||
)}
|
||||
<h1 className="text-base font-semibold leading-snug line-clamp-2">{q.title}</h1>
|
||||
<div className="flex items-center justify-between text-sm mt-auto pt-2 border-t">
|
||||
{passed ? (
|
||||
<span className="flex items-center gap-1.5 text-green-600 dark:text-green-400 font-medium">
|
||||
<CheckCheck className="size-4" /> Passed
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground font-medium">Not Attempted</span>
|
||||
)}
|
||||
{info?.passing_score && (
|
||||
<span className="text-xs text-muted-foreground">{info.passing_score}% to pass</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<ResponsiveModal
|
||||
open={!!selected}
|
||||
onOpenChange={(v) => !v && setSelected(null)}
|
||||
title={selected?.title}
|
||||
description="Quiz Info"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setSelected(null)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => goToQuiz(details[selected?.reference_id])}
|
||||
disabled={!details[selected?.reference_id]}
|
||||
>
|
||||
<SendHorizonal /> Take Quiz
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{selected && (() => {
|
||||
const info = details[selected.reference_id];
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{(info?.has_passed || selected.completed) && (
|
||||
<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" /> Already Passed
|
||||
</div>
|
||||
)}
|
||||
{info?.unit?.title && (
|
||||
<p className="text-sm">
|
||||
<span className="text-muted-foreground">Unit: </span>
|
||||
<span className="font-medium">{info.unit.title}</span>
|
||||
</p>
|
||||
)}
|
||||
{info?.passing_score && (
|
||||
<p className="text-sm">
|
||||
<span className="text-muted-foreground">Passing score: </span>
|
||||
<span className="font-medium">{info.passing_score}%</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</ResponsiveModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PassQuiz;
|
||||
Reference in New Issue
Block a user