"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { badgeSvgMarkup, badgeFullSvg } from "./BadgeArt"; import LineDraw from "./LineDraw"; import { loadProgressDrawing, saveProgressDrawing, saveFinalDrawing, exportCanvasToDataURL, fileToDataURL } from "./drawingPersistence"; interface StepData { n: number; title: string; instruction: string; tip: string; lines: string[] } interface Meta { level: string; levelName: string; sublevel: number; title: string; emoji: string; intro: string; badgeKey: string; badgeName: string; baseSvg: string; phaseLabel: string; } const COLORS = ["#2b2440", "#4f7cff", "#34c6c6", "#58c777", "#ffc843", "#ff7eb6", "#e85a5a", "#ffffff", "#c4c8cc"]; const SIZES = [4, 8, 16]; interface Point { x: number; y: number } interface Stroke { color: string; size: number; points: Point[] } export default function TraceRunner({ meta, steps, loggedIn, username, completedSteps, alreadyEarned, carryImage = "", nextHref = "", nextLabel = "" }: { meta: Meta; steps: StepData[]; loggedIn: boolean; username: string; completedSteps: number[]; alreadyEarned: boolean; carryImage?: string; nextHref?: string; nextLabel?: string; }) { const router = useRouter(); const lastIndex = steps.length - 1; const [idx, setIdx] = useState(Math.min(completedSteps.length, lastIndex)); // Always open the lesson (so you can redraw/continue); show the finished screen only after finishing here. const [finished, setFinished] = useState(false); const [earned, setEarned] = useState(false); const [playKey, setPlayKey] = useState(0); const [playing, setPlaying] = useState(true); const [finalImage, setFinalImage] = useState(null); const [uploadMsg, setUploadMsg] = useState(""); const uploadRef = useRef(null); const step = steps[idx]; // faint guide = base outline + every line up to & including the current step const guideUpTo = steps.slice(0, idx + 1).flatMap((s) => s.lines).join("\n"); const canvasRef = useRef(null); const stageRef = useRef(null); const strokesRef = useRef([]); const drawingRef = useRef(null); const baseImgRef = useRef(null); const saveTimer = useRef | undefined>(undefined); const [color, setColor] = useState(COLORS[0]); const [size, setSize] = useState(SIZES[1]); const [, force] = useState(0); const redraw = useCallback(() => { const c = canvasRef.current; if (!c) return; const ctx = c.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, c.width, c.height); if (baseImgRef.current) ctx.drawImage(baseImgRef.current, 0, 0, c.width, c.height); ctx.lineCap = "round"; ctx.lineJoin = "round"; const dpr = Math.min(window.devicePixelRatio || 1, 2); for (const s of strokesRef.current) { if (!s.points.length) continue; ctx.strokeStyle = s.color; ctx.lineWidth = s.size * dpr; ctx.beginPath(); ctx.moveTo(s.points[0].x * c.width, s.points[0].y * c.height); if (s.points.length === 1) ctx.lineTo(s.points[0].x * c.width + 0.1, s.points[0].y * c.height + 0.1); else for (let i = 1; i < s.points.length; i++) ctx.lineTo(s.points[i].x * c.width, s.points[i].y * c.height); ctx.stroke(); } }, []); const resize = useCallback(() => { const c = canvasRef.current, st = stageRef.current; if (!c || !st) return; const r = st.getBoundingClientRect(); const dpr = Math.min(window.devicePixelRatio || 1, 2); c.width = Math.round(r.width * dpr); c.height = Math.round(r.height * dpr); redraw(); }, [redraw]); useEffect(() => { resize(); window.addEventListener("resize", resize); return () => window.removeEventListener("resize", resize); }, [resize]); useEffect(() => { if (!finished) resize(); }, [finished, resize]); useEffect(() => { if (!loggedIn) return; let alive = true; loadProgressDrawing(meta.level, meta.sublevel).then((img) => { // Use this lesson's own saved canvas if any; otherwise carry over the previous lesson's drawing const src = img || carryImage; if (!src || !alive) return; const im = new Image(); im.onload = () => { baseImgRef.current = im; redraw(); }; im.src = src; }); return () => { alive = false; }; }, [loggedIn, alreadyEarned, meta.level, meta.sublevel, carryImage, redraw]); // Auto-play the line video whenever the step changes useEffect(() => { setPlaying(true); setPlayKey((k) => k + 1); }, [idx]); function hasContent() { return strokesRef.current.length > 0 || !!baseImgRef.current; } function flushSave() { if (!loggedIn) return; const c = canvasRef.current; if (!c || !hasContent()) return; saveProgressDrawing(meta.level, meta.sublevel, exportCanvasToDataURL(c, 800, false)); } function scheduleSave() { if (!loggedIn) return; clearTimeout(saveTimer.current); saveTimer.current = setTimeout(flushSave, 1200); } function pe(e: React.PointerEvent): Point { const r = canvasRef.current!.getBoundingClientRect(); return { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height }; } function onDown(e: React.PointerEvent) { if (finished) return; (e.target as Element).setPointerCapture?.(e.pointerId); const s: Stroke = { color, size, points: [pe(e)] }; drawingRef.current = s; strokesRef.current.push(s); } function onMove(e: React.PointerEvent) { const s = drawingRef.current; if (!s) return; s.points.push(pe(e)); redraw(); } function onUp() { drawingRef.current = null; force((n) => n + 1); scheduleSave(); } function clearCanvas() { strokesRef.current = []; baseImgRef.current = null; redraw(); force((n) => n + 1); const c = canvasRef.current; if (loggedIn && c) saveProgressDrawing(meta.level, meta.sublevel, exportCanvasToDataURL(c, 800, false)); } function undo() { strokesRef.current.pop(); redraw(); force((n) => n + 1); scheduleSave(); } async function saveStep(stepN: number, badgeKey?: string) { if (!loggedIn) return; try { await fetch("/api/progress", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ level: meta.level, sublevel: meta.sublevel, step: stepN, badgeKey }) }); } catch { /* */ } } function next() { saveStep(step.n); flushSave(); if (idx < lastIndex) setIdx(idx + 1); else finish(); } function prev() { flushSave(); if (idx > 0) setIdx(idx - 1); } async function finish() { await saveStep(step.n, meta.badgeKey || undefined); // Only completing the level (the badge lesson) saves to the profile gallery. // Earlier lessons (e.g. Outline) keep their working canvas so it carries forward, but aren't saved as finals. if (meta.badgeKey) { const c = canvasRef.current; if (c && hasContent()) { const data = exportCanvasToDataURL(c); setFinalImage(data); if (loggedIn) await saveFinalDrawing(meta.level, meta.sublevel, "canvas", data); } } else { flushSave(); // persist the outline canvas so the Details lesson can carry it over } setEarned(loggedIn && !!meta.badgeKey); setFinished(true); router.refresh(); // refresh cached pages (Learn, gating) with the new progress } async function onUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; if (!loggedIn) { setUploadMsg("Log in to save your drawing to your gallery."); return; } setUploadMsg("Uploading…"); try { const data = await fileToDataURL(file); const ok = await saveFinalDrawing(meta.level, meta.sublevel, "upload", data); setUploadMsg(ok ? "Saved to your gallery! πŸ–ΌοΈ" : "Couldn't save that file."); } catch { setUploadMsg("Couldn't read that image."); } finally { if (uploadRef.current) uploadRef.current.value = ""; } } async function rasterizeBadge(): Promise { const blob = new Blob([badgeFullSvg(meta.badgeName, meta.emoji)], { type: "image/svg+xml" }); const url = URL.createObjectURL(blob); try { const img = await loadImage(url); const c = document.createElement("canvas"); c.width = 600; c.height = 600; c.getContext("2d")!.drawImage(img, 0, 0, 600, 600); return await new Promise((res) => c.toBlob((b) => res(b), "image/png")); } finally { URL.revokeObjectURL(url); } } async function shareBadge() { const blob = await rasterizeBadge(); if (!blob) return; const file = new File([blob], "drawit-badge.png", { type: "image/png" }); const nav = navigator as Navigator & { canShare?: (d: ShareData) => boolean }; if (nav.share && nav.canShare && nav.canShare({ files: [file] })) { try { await nav.share({ files: [file], title: "My DrawIt badge!", text: `I earned the ${meta.badgeName} badge on DrawIt!` }); return; } catch { /* */ } } downloadBlob(blob, "drawit-badge.png"); } async function downloadBadge() { const b = await rasterizeBadge(); if (b) downloadBlob(b, "drawit-badge.png"); } if (finished && !meta.badgeKey) { // Outline (or any non-badge step): completed, but the level isn't finished β€” no profile saving here. return (

✏️ Outline complete!

Awesome lines! Your outline carries over to the next lesson, so you can add the details right on top.

{nextHref && {nextLabel || "Next"} β†’}
Back to lessons
); } if (finished) { return (

πŸŽ‰ Great work!

You finished {meta.title}{earned ? <> and earned the {meta.badgeName} badge! : "."}

{earned &&
} {earned && (
πŸ† Download award (PDF)
)} {finalImage && ( )}
Drew it on paper?

Take a photo and add it to your gallery.

{uploadMsg &&

{uploadMsg}

}
{nextHref &&
{nextLabel || "Next"} β†’
}
{loggedIn && πŸ–ΌοΈ My drawings} Back to lessons
); } return (
{meta.levelName} Β· {meta.phaseLabel}

{meta.emoji} {meta.title}

Report a problem
{idx === 0 &&

{meta.intro}

}
{steps.map((s, i) => ( ))}
{/* the already-drawn outline shown clearly so the child knows what they're adding to */} {meta.baseSvg && ( )} {/* faint trace target for this lesson's lines so far */} {/* the child's drawing (transparent base so the guide + video show through) */} {/* the slowly-drawn video for the current step β€” on top so it always shows, even over the imported outline */} {playing && setPlaying(false)} />}
{COLORS.map((c) => ( ))}
{loggedIn &&

Your drawing is saved automatically.

}
Step {step.n}: {step.title}

{step.instruction}

{step.tip &&

πŸ’‘ {step.tip}

}
{!loggedIn &&

Log in to save your progress.

}
); } const toolBtn: React.CSSProperties = { minHeight: 40, padding: "8px 14px", fontSize: "0.9rem", boxShadow: "none" }; function loadImage(src: string): Promise { return new Promise((res, rej) => { const i = new Image(); i.onload = () => res(i); i.onerror = rej; i.src = src; }); } function downloadBlob(blob: Blob, name: string) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }