- Leaderboard: rank-based percentile tiers (champion/gold/silver/bronze),
deterministic tie-breaking (xp DESC, user_id ASC)
- Mentor chat: md: breakpoints, safe-area-inset-bottom padding
- Learning progress: server-side progressPct, curriculum expansion safety
- Physical orders: hard-disabled with 410 Gone
- Certificate approval: pending/issued flow, admin issue/revoke endpoints,
certificate_requires_approval setting (migration 0031)
- Draft/publish: is_published on lessons and chapters, admin publish UI
(migration 0030)
- Video panel: static thumbnails (no preload), client-side pagination
- Rich editor: TipTap + tiptap-markdown for lesson descriptions,
marked + DOMPurify for SafeMarkdown rendering
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useParams } from "next/navigation";
|
|
import { Loader2 } from "lucide-react";
|
|
import { CertificateCard } from "@/components/certificate-card";
|
|
import { NotFoundState, notFoundSentence } from "@/components/not-found-state";
|
|
import { http, toUserError } from "@/lib/api";
|
|
import { useAuth } from "@/lib/auth-store";
|
|
import type { Certificate } from "@/lib/types";
|
|
|
|
type PublicCertResponse = {
|
|
certificate: { publicId: string; fullName: string; issuedAt: string };
|
|
course: string;
|
|
};
|
|
|
|
export default function CertificatePage() {
|
|
const params = useParams<{ id: string }>();
|
|
const publicId = params?.id ?? "";
|
|
const authUser = useAuth((s) => s.user);
|
|
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ["certificate", publicId],
|
|
queryFn: () => http.get<PublicCertResponse>(`/api/certificates/${publicId}`),
|
|
enabled: !!publicId,
|
|
meta: { silentError: true },
|
|
});
|
|
|
|
const { data: mine } = useQuery({
|
|
queryKey: ["me", "certificate"],
|
|
queryFn: () => http.get<{ certificate: Certificate | null }>("/api/me/certificate"),
|
|
enabled: !!authUser,
|
|
});
|
|
|
|
const isOwner = !!authUser && mine?.certificate?.publicId === publicId;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex min-h-[60vh] items-center justify-center">
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !data?.certificate) {
|
|
return (
|
|
<NotFoundState
|
|
title="گواهینامه پیدا نشد"
|
|
description={notFoundSentence(
|
|
error ? toUserError(error, "") : "",
|
|
"این گواهینامه در سامانه نیست.",
|
|
"گواهینامه پیدا نشد",
|
|
)}
|
|
href="/"
|
|
actionLabel="بازگشت به خانه"
|
|
/>
|
|
);
|
|
}
|
|
|
|
const cert = data.certificate;
|
|
|
|
return (
|
|
<CertificateCard
|
|
cert={{
|
|
publicId: cert.publicId,
|
|
fullName: cert.fullName,
|
|
issuedAt: cert.issuedAt,
|
|
course: data.course,
|
|
}}
|
|
showOwnerLinks={isOwner}
|
|
/>
|
|
);
|
|
}
|