mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
134 lines
5.5 KiB
React
134 lines
5.5 KiB
React
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { Award, Download, LockIcon, ArrowLeft, BadgeCheck } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { useProfile } from "@/contexts/ProfileProvider";
|
|
import { useDateFormat } from "@/hooks/useDateFormat";
|
|
import api from "@/utils/api.util";
|
|
import { toast } from "sonner";
|
|
|
|
const CertBadgeIcon = ({ className }) => (
|
|
<svg className={className} width="120" height="120" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Prism logo">
|
|
<defs>
|
|
<linearGradient id="prism-mc" x1="0" y1="0" x2="1" y2="1">
|
|
<stop offset="0" stopColor="#8EA2F6"/>
|
|
<stop offset="1" stopColor="#5061E6"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<g transform="rotate(45 60 60)">
|
|
<rect x="24" y="24" width="72" height="72" rx="16" fill="url(#prism-mc)"/>
|
|
<rect x="46" y="46" width="28" height="28" rx="6" fill="#FFFFFF"/>
|
|
</g>
|
|
</svg>
|
|
);
|
|
|
|
const CertificateCard = ({ courseTitle, issuedAt, courseUuid }) => {
|
|
const [downloading, setDownloading] = useState(false);
|
|
const { fmtDate } = useDateFormat();
|
|
|
|
const issuedLabel = issuedAt ? fmtDate(issuedAt) : "—";
|
|
|
|
const handleDownload = async () => {
|
|
setDownloading(true);
|
|
try {
|
|
const res = await api.get(`/client/certificates/${courseUuid}`, { responseType: "blob" });
|
|
const disposition = res.headers["content-disposition"] ?? "";
|
|
const match = disposition.match(/filename="([^"]+)"/);
|
|
const filename = match ? match[1] : `certificate-${courseUuid}.pdf`;
|
|
const url = URL.createObjectURL(new Blob([res.data], { type: "application/pdf" }));
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
} catch {
|
|
toast.error("Could not download certificate.");
|
|
} finally {
|
|
setDownloading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-2xl border bg-card p-6 flex flex-col items-center gap-4 shadow-sm">
|
|
<CertBadgeIcon className="w-[160px] h-[160px]" />
|
|
<p className="text-lg font-bold text-center leading-snug">Certificate of Completion</p>
|
|
<div className="w-full rounded-lg border px-3 py-2.5">
|
|
<p className="text-[10px] uppercase tracking-widest text-muted-foreground font-semibold">Course</p>
|
|
<p className="text-sm font-medium mt-1 line-clamp-2">{courseTitle}</p>
|
|
</div>
|
|
<div className="w-full flex items-end justify-between">
|
|
<div>
|
|
<p className="text-[10px] uppercase tracking-widest text-muted-foreground font-semibold">Issued</p>
|
|
<p className="text-sm font-medium">{issuedLabel}</p>
|
|
</div>
|
|
<Badge className="bg-green-100 text-green-700 border-green-400 dark:bg-green-900/40 dark:text-green-400 dark:border-green-700 gap-1">
|
|
<BadgeCheck className="size-3" /> Verified
|
|
</Badge>
|
|
</div>
|
|
<Button size="sm" variant="outline" className="w-full" onClick={handleDownload} disabled={downloading}>
|
|
<Download className="size-3.5" />
|
|
{downloading ? "Downloading…" : "Download PDF"}
|
|
</Button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function MyCertificates() {
|
|
const navigate = useNavigate();
|
|
const { achievements, achievementsLoading, getAchievements } = useProfile();
|
|
|
|
useEffect(() => { getAchievements(); }, []);
|
|
|
|
const certAchievements = achievements.filter((a) => a.key.startsWith("course_completed_"));
|
|
|
|
return (
|
|
<section className="mt-17 bg-muted min-h-full">
|
|
<div className="p-6 lg:container lg:max-w-3xl lg:mx-auto space-y-5">
|
|
|
|
<div className="flex items-center gap-3">
|
|
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h1 className="text-xl font-semibold">My Certificates</h1>
|
|
<Badge variant="outline" className="gap-1 text-xs">
|
|
<LockIcon className="h-3 w-3" /> Only you
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">Certificates earned from completed courses.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{achievementsLoading ? (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{[...Array(2)].map((_, i) => (
|
|
<Skeleton key={i} className="h-80 rounded-2xl" />
|
|
))}
|
|
</div>
|
|
) : certAchievements.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-24 text-center">
|
|
<Award className="h-10 w-10 text-muted-foreground/40 mb-3" />
|
|
<p className="text-sm font-medium">No certificates yet</p>
|
|
<p className="text-xs text-muted-foreground">Pass a course assessment to earn your first certificate.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{certAchievements.map((cert) => (
|
|
<CertificateCard
|
|
key={cert.achievement_id}
|
|
courseTitle={cert.description}
|
|
issuedAt={cert.granted_at}
|
|
courseUuid={cert.metadata?.courseUuid ?? cert.key.replace("course_completed_", "")}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|