import { useEffect, useRef, useState } from 'react' interface CertificateStatus { eligible: boolean studyTitle: string completedCount: number totalCount: number token: string | null issuedAt: string | null } interface Props { studySlug: string onIssued?: () => void } function formatDate(iso: string | null) { if (!iso) return '' try { return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) } catch { return '' } } function drawCertificate( canvas: HTMLCanvasElement, studyTitle: string, displayName: string, issuedAt: string, ) { const W = 900 const H = 630 canvas.width = W canvas.height = H const ctx = canvas.getContext('2d')! // Background ctx.fillStyle = '#1a1710' ctx.fillRect(0, 0, W, H) // Gold border (outer) ctx.strokeStyle = '#c8952a' ctx.lineWidth = 8 ctx.strokeRect(16, 16, W - 32, H - 32) // Gold border (inner) ctx.strokeStyle = '#e0b840' ctx.lineWidth = 2 ctx.strokeRect(28, 28, W - 56, H - 56) // Header label ctx.fillStyle = '#8a7f5a' ctx.font = '700 15px Georgia, serif' ctx.textAlign = 'center' ctx.fillText('VERSE BY VERSE WITH NATE', W / 2, 80) // "Certificate of Completion" ctx.fillStyle = '#e0b840' ctx.font = 'italic 700 38px Georgia, serif' ctx.fillText('Certificate of Completion', W / 2, 145) // Decorative line ctx.strokeStyle = '#c8952a' ctx.lineWidth = 1.5 ctx.beginPath() ctx.moveTo(W / 2 - 200, 162) ctx.lineTo(W / 2 + 200, 162) ctx.stroke() // "This certifies that" ctx.fillStyle = '#b8a87a' ctx.font = '16px Georgia, serif' ctx.fillText('This certifies that', W / 2, 210) // Name ctx.fillStyle = '#f5f0e8' ctx.font = 'italic 700 44px Georgia, serif' ctx.fillText(displayName, W / 2, 272) // Underline the name const nameWidth = ctx.measureText(displayName).width ctx.strokeStyle = '#e0b840' ctx.lineWidth = 1 ctx.beginPath() ctx.moveTo(W / 2 - nameWidth / 2, 282) ctx.lineTo(W / 2 + nameWidth / 2, 282) ctx.stroke() // "has successfully completed" ctx.fillStyle = '#b8a87a' ctx.font = '16px Georgia, serif' ctx.fillText('has successfully completed', W / 2, 322) // Study title ctx.fillStyle = '#f5f0e8' ctx.font = '700 28px Georgia, serif' ctx.fillText(studyTitle, W / 2, 372) // Date line ctx.fillStyle = '#8a7f5a' ctx.font = '14px Georgia, serif' ctx.fillText(`Awarded on ${formatDate(issuedAt)}`, W / 2, 430) // Decorative flourish bottom ctx.strokeStyle = '#c8952a' ctx.lineWidth = 1.5 ctx.beginPath() ctx.moveTo(W / 2 - 180, 460) ctx.lineTo(W / 2 + 180, 460) ctx.stroke() // Footer ctx.fillStyle = '#5a5040' ctx.font = '12px Georgia, serif' ctx.fillText('versebyversewithnate.us', W / 2, 490) } export function StudyCertificate({ studySlug, onIssued }: Props) { const [status, setStatus] = useState(null) const [loading, setLoading] = useState(true) const [issuing, setIssuing] = useState(false) const [error, setError] = useState(null) const [displayName, setDisplayName] = useState('') const canvasRef = useRef(null) useEffect(() => { let cancelled = false async function load() { try { const res = await fetch(`/api/study-certificate/${studySlug}`) if (!res.ok) throw new Error('unavailable') const data = await res.json() as CertificateStatus if (cancelled) return setStatus(data) // The name printed on the certificate comes from the signed-in account const auth = await fetch('/api/study-auth/status').then(r => (r.ok ? r.json() : null)).catch(() => null) if (cancelled) return if (auth?.displayName) setDisplayName(auth.displayName) else if (auth?.username) setDisplayName(auth.username) } catch { // leave status null; the panel renders nothing } finally { if (!cancelled) setLoading(false) } } void load() return () => { cancelled = true } }, [studySlug]) useEffect(() => { if (status?.token && canvasRef.current) { drawCertificate( canvasRef.current, status.studyTitle, displayName || 'Student', status.issuedAt ?? new Date().toISOString(), ) } }, [status, displayName]) async function handleIssueCertificate() { setIssuing(true) setError(null) try { const res = await fetch(`/api/study-certificate/${studySlug}`, { method: 'POST' }) const data = await res.json() if (!res.ok) { setError(data.message ?? 'Failed to issue certificate.'); return } setStatus(prev => prev ? { ...prev, token: data.token, issuedAt: data.issuedAt } : prev) onIssued?.() } catch { setError('Network error. Please try again.') } finally { setIssuing(false) } } function handleDownload() { if (!canvasRef.current) return const link = document.createElement('a') link.download = `certificate-${studySlug}.png` link.href = canvasRef.current.toDataURL('image/png') link.click() } function handleShare() { if (!status?.token) return const url = `${window.location.origin}/certificate/${status.token}` if (navigator.share) { navigator.share({ title: `Certificate – ${status.studyTitle}`, url }).catch(() => {}) } else { navigator.clipboard.writeText(url).then(() => alert('Certificate link copied!')).catch(() => {}) } } if (loading) return

Checking completion status…

if (!status) return null const progressPct = status.totalCount > 0 ? Math.round((status.completedCount / status.totalCount) * 100) : 0 return (

Certificate of Completion

{!status.eligible && (

{status.completedCount} of {status.totalCount} sections completed ({progressPct}%)

Complete all sections to earn your certificate.

)} {status.eligible && !status.token && (

🎉 Congratulations! You've completed {status.studyTitle}.

{error &&

{error}

}
)} {status.token && (

Issued on {formatDate(status.issuedAt)}

)}
) }