import { useNavigate } from "react-router-dom"; import { useResults } from "../../../stores/useResults"; import { LucideArrowLeft } from "lucide-react"; import { CircularLevelProgress } from "../../../components/CircularLevelProgress"; import { useEffect, useState } from "react"; import { useExamConfigStore } from "../../../stores/useExamConfigStore"; // ─── Shared styles injected once ───────────────────────────────────────────── const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600&display=swap'); :root { --content-max: 1100px; } .results-screen { min-height: 100vh; background: #fffbf4; font-family: 'Nunito', sans-serif; position: relative; overflow-x: hidden; padding: 0 0 3rem 0; } /* ── Blobs ── */ .r-blob { position: fixed; pointer-events: none; z-index: 0; } .r-blob-1 { width: 260px; height: 260px; background: #fde68a; top: -80px; left: -80px; border-radius: 60% 40% 70% 30% / 50% 60% 40% 50%; animation: rWobble1 6s ease-in-out infinite; } .r-blob-2 { width: 200px; height: 200px; background: #a5f3c0; bottom: -60px; left: 8%; border-radius: 40% 60% 30% 70% / 60% 40% 60% 40%; animation: rWobble2 7s ease-in-out infinite; } .r-blob-3 { width: 220px; height: 220px; background: #fbcfe8; top: 12%; right: -60px; border-radius: 70% 30% 50% 50% / 40% 60% 40% 60%; animation: rWobble1 8s ease-in-out infinite reverse; } .r-blob-4 { width: 160px; height: 160px; background: #bfdbfe; bottom: 15%; right: 3%; border-radius: 50% 50% 30% 70% / 60% 40% 60% 40%; animation: rWobble2 5s ease-in-out infinite; } @keyframes rWobble1 { 0%,100% { border-radius:60% 40% 70% 30%/50% 60% 40% 50%; transform:translate(0,0) rotate(0deg); } 50% { border-radius:40% 60% 30% 70%/60% 40% 60% 40%; transform:translate(12px,16px) rotate(8deg); } } @keyframes rWobble2 { 0%,100% { border-radius:40% 60% 30% 70%/60% 40% 60% 40%; transform:translate(0,0) rotate(0deg); } 50% { border-radius:60% 40% 70% 30%/40% 60% 40% 60%; transform:translate(-10px,12px) rotate(-6deg); } } /* ── Floating dots ── */ .r-dot { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; animation: rFloat 4s ease-in-out infinite; } @keyframes rFloat { 0%,100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-14px) rotate(180deg); } } /* ── Content wrapper ── */ .results-inner { position: relative; z-index: 1; max-width: 520px; margin: 0 auto; padding: 2.5rem 1.5rem 2rem; display: flex; flex-direction: column; gap: 1rem; } /* Desktop / wide layout */ @media (min-width: 900px) { .results-inner { max-width: var(--content-max); padding: 3rem 1.5rem 4rem; } .stats-grid { grid-template-columns: repeat(4, 1fr); } .r-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 120px); top: -120px; width: 300px; height: 300px; } .r-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 20px); bottom: -80px; width: 220px; height: 220px; } .r-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 40px); top: 10%; width: 260px; height: 260px; } .r-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 10px); bottom: 6%; width: 180px; height: 180px; } } /* ── Header ── */ .results-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.25rem; animation: rPopIn 0.45s cubic-bezier(0.34,1.56,0.64,1) both; } .results-back-btn { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; border: 2px solid #c084fc; background: linear-gradient(135deg, #c084fc, #a855f7); cursor: pointer; flex-shrink: 0; box-shadow: 0 4px 0 #7e22ce55; transition: transform 0.1s ease, box-shadow 0.1s ease; } .results-back-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 0 #7e22ce55; } .results-back-btn:active { transform: translateY(2px); box-shadow: 0 2px 0 #7e22ce55; } .results-title { font-size: 2rem; font-weight: 900; color: #1e1b4b; letter-spacing: -0.02em; } /* ── Mode badge ── */ .mode-badge { display: flex; align-items: center; gap: 0.6rem; background: white; border: 2.5px solid #e9d5ff; border-radius: 16px; padding: 0.75rem 1.1rem; box-shadow: 0 2px 8px rgba(0,0,0,0.04); animation: rPopIn 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.05s both; } .mode-badge-text { font-size: 0.85rem; font-weight: 700; color: #7e22ce; } .mode-badge-sub { font-size: 0.78rem; font-weight: 600; color: #a78bfa; } /* ── Hero congratulations banner ── */ .congrats-banner { background: white; border: 2.5px solid #f3f4f6; border-radius: 24px; padding: 1.5rem; text-align: center; box-shadow: 0 6px 20px rgba(0,0,0,0.05); animation: rPopIn 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.1s both; display: flex; flex-direction: column; align-items: center; gap: 0.25rem; } .congrats-emoji { font-size: 3rem; animation: rBounce 2s ease-in-out infinite; display: block; margin-bottom: 0.25rem; } @keyframes rBounce { 0%,100% { transform: translateY(0) rotate(-4deg); } 50% { transform: translateY(-10px) rotate(4deg); } } .congrats-title { font-size: 1.4rem; font-weight: 900; color: #1e1b4b; } .congrats-title span { color: #f97316; } .congrats-sub { font-family: 'Nunito Sans', sans-serif; font-size: 0.85rem; font-weight: 600; color: #9ca3af; } /* ── XP ring section ── */ .xp-section { display: flex; justify-content: center; animation: rPopIn 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.15s both; } /* ── Stat cards grid ── */ .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; animation: rPopIn 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.2s both; } .stat-card { background: white; border: 2.5px solid #f3f4f6; border-radius: 20px; padding: 1.1rem 1rem; box-shadow: 0 4px 12px rgba(0,0,0,0.04); display: flex; flex-direction: column; gap: 0.2rem; } .stat-card-label { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: #9ca3af; display: flex; align-items: center; gap: 0.35rem; } .stat-card-value { font-size: 1.9rem; font-weight: 900; color: #1e1b4b; line-height: 1; } .stat-card-sub { font-family: 'Nunito Sans', sans-serif; font-size: 0.75rem; font-weight: 600; color: #d1d5db; } /* Accent colours per card */ .stat-card.orange { border-color: #fed7aa; } .stat-card.orange .stat-card-value { color: #f97316; } .stat-card.green { border-color: #bbf7d0; } .stat-card.green .stat-card-value { color: #16a34a; } .stat-card.purple { border-color: #e9d5ff; } .stat-card.purple .stat-card-value { color: #9333ea; } .stat-card.blue { border-color: #bfdbfe; } .stat-card.blue .stat-card-value { color: #2563eb; } /* ── Improvement tip card ── */ .tip-card { background: white; border: 2.5px solid #f3f4f6; border-radius: 20px; padding: 1.1rem 1.25rem; box-shadow: 0 4px 12px rgba(0,0,0,0.04); animation: rPopIn 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.25s both; } .tip-card-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; } .tip-card-title { font-size: 0.95rem; font-weight: 900; color: #1e1b4b; } .tip-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; } .tip-chip { background: #fafafa; border: 2px solid #f3f4f6; border-radius: 100px; padding: 0.35rem 0.85rem; font-size: 0.75rem; font-weight: 700; color: #374151; display: flex; align-items: center; gap: 0.3rem; } /* ── Done button ── */ .done-btn { width: 100%; background: #f97316; color: white; border: none; border-radius: 100px; padding: 1rem 2.5rem; font-family: 'Nunito', sans-serif; font-size: 1rem; font-weight: 800; cursor: pointer; box-shadow: 0 6px 0 #c2560e, 0 8px 20px rgba(249,115,22,0.25); transition: transform 0.1s ease, box-shadow 0.1s ease; animation: rPopIn 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.35s both; letter-spacing: 0.01em; } .done-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 0 #c2560e, 0 12px 24px rgba(249,115,22,0.3); } .done-btn:active { transform: translateY(3px); box-shadow: 0 3px 0 #c2560e, 0 4px 12px rgba(249,115,22,0.2); } @keyframes rPopIn { from { opacity:0; transform: scale(0.88) translateY(12px); } to { opacity:1; transform: scale(1) translateY(0); } } `; // ─── Animated counter ───────────────────────────────────────────────────────── function useCountUp(target: number, duration = 900) { const [val, setVal] = useState(0); useEffect(() => { if (!target) return; let start: number | null = null; const tick = (t: number) => { if (!start) start = t; const p = Math.min((t - start) / duration, 1); setVal(Math.floor(p * target)); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, [target]); return val; } // ─── Floating dots config (shared) ─────────────────────────────────────────── const DOTS = [ { size: 12, color: "#f97316", top: "18%", left: "12%", delay: "0s" }, { size: 8, color: "#a855f7", top: "33%", left: "5%", delay: "1s" }, { size: 10, color: "#22c55e", top: "62%", left: "15%", delay: "0.5s" }, { size: 14, color: "#3b82f6", top: "22%", right: "10%", delay: "1.5s" }, { size: 8, color: "#f43f5e", top: "52%", right: "6%", delay: "0.8s" }, { size: 10, color: "#eab308", top: "72%", right: "18%", delay: "0.3s" }, ]; // ─── Targeted results ───────────────────────────────────────────────────────── const TARGETED_XP = 15; const TARGETED_SCORE = 15; const TargetedResults = ({ onFinish }: { onFinish: () => void }) => { const { userMetrics, setUserMetrics } = useExamConfigStore(); const previousXP = userMetrics.xp ?? 0; const gainedXP = TARGETED_XP; const levelMinXP = Math.floor(previousXP / 100) * 100; const levelMaxXP = levelMinXP + 100; const currentLevel = Math.floor(previousXP / 100) + 1; const displayXP = useCountUp(gainedXP); useEffect(() => { setUserMetrics({ xp: previousXP, questions: 0, streak: 0, }); }, []); return (
{/* Blobs */}
{/* Dots */} {DOTS.map((d, i) => (
))}
{/* Header */}

Results

{/* Mode badge */}
🎯

Targeted Mode Complete!

You answered all questions correctly.

{/* Congrats banner */}
🏆

Nailed it, champ!

Perfect run — every question down.

{/* XP ring */}
{/* Stats grid */}
⚡ XP Gained +{displayXP} experience points
🎯 Score {TARGETED_SCORE} total points
✅ Accuracy 100% all correct
🔥 Streak Perfect no mistakes
{/* Tip card */}
🚀 Keep the momentum going!
📖 Review mistakes
⏱️ Try timed mode
🎯 Next topic
); }; // ─── Main Results ───────────────────────────────────────────────────────────── export const Results = () => { const navigate = useNavigate(); const results = useResults((s) => s.results); const clearResults = useResults((s) => s.clearResults); const { setUserMetrics, payload } = useExamConfigStore(); const isTargeted = payload?.mode === "TARGETED"; useEffect(() => { if (results) setUserMetrics({ xp: results.total_xp, questions: results.correct_count, streak: 0, }); }, [results]); function handleFinishExam() { useExamConfigStore.getState().clearPayload(); clearResults(); navigate("/student/home"); } if (isTargeted) return ; // ── Standard mode values ─────────────────────────────────────────────────── const previousXP = results ? results.total_xp - results.xp_gained : 0; const accuracy = results && results.total_questions > 0 ? Math.round((results.correct_count / results.total_questions) * 100) : 0; const displayXP = useCountUp(results?.xp_gained ?? 0); const displayScore = useCountUp(results?.score ?? 0); // Motivational headline based on accuracy const headline = accuracy >= 90 ? { emoji: "🏆", text: "Absolutely crushing it!" } : accuracy >= 70 ? { emoji: "🎉", text: "Solid work, keep going!" } : accuracy >= 50 ? { emoji: "💪", text: "Good effort, room to grow!" } : { emoji: "📚", text: "Every attempt makes you better!" }; return (
{/* Blobs */}
{/* Dots */} {DOTS.map((d, i) => (
))}
{/* Header */}

Results

{/* Congrats banner — dynamic */}
{headline.emoji}

{headline.text.split(" ").slice(0, -1).join(" ")}{" "} {headline.text.split(" ").slice(-1)}

Here's how you performed today

{/* XP ring */}
{results && ( )}
{/* Stats grid */}
⚡ XP Gained +{displayXP} experience points
🎯 Score {displayScore} total points
= 70 ? "purple" : "blue"}`}> ✅ Accuracy {accuracy}% {results?.correct_count ?? 0} of {results?.total_questions ?? 0}{" "} correct
❌ Missed {results ? results.total_questions - results.correct_count : 0} questions to review
{/* Tip card */}
💡 How to improve faster
📖 Review wrong answers
🔁 Retry missed questions
⏱️ Work on pacing
📈 Track your trends
); };