import { useEffect, useState } from "react"; import { api } from "../../../utils/api"; import { type Topic } from "../../../types/topic"; import { useAuthStore } from "../../../stores/authStore"; import { Loader2, Search, ArrowLeft, ChevronRight, Target } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import { slideVariants } from "../../../lib/utils"; import { useAuthToken } from "../../../hooks/useAuthToken"; import { useExamConfigStore } from "../../../stores/useExamConfigStore"; import { useNavigate } from "react-router-dom"; type Step = "topic" | "difficulty" | "review"; const DOTS = [ { size: 10, color: "#f97316", top: "6%", left: "4%", delay: "0s" }, { size: 7, color: "#a855f7", top: "28%", left: "2%", delay: "1.2s" }, { size: 9, color: "#22c55e", top: "62%", left: "3%", delay: "0.6s" }, { size: 12, color: "#3b82f6", top: "10%", right: "4%", delay: "1.8s" }, { size: 7, color: "#f43f5e", top: "48%", right: "2%", delay: "0.9s" }, { size: 9, color: "#eab308", top: "76%", right: "5%", delay: "0.4s" }, ]; const DIFFICULTY_META = { EASY: { emoji: "๐ŸŒฑ", label: "Easy", desc: "Foundational questions to build confidence", color: "#16a34a", bg: "#f0fdf4", border: "#bbf7d0", }, MEDIUM: { emoji: "๐Ÿ”ฅ", label: "Medium", desc: "Balanced mix to solidify your understanding", color: "#f97316", bg: "#fff7ed", border: "#fed7aa", }, HARD: { emoji: "โšก", label: "Hard", desc: "Challenging questions to push your limits", color: "#7c3aed", bg: "#fdf4ff", border: "#e9d5ff", }, } as const; const STEPS: Step[] = ["topic", "difficulty", "review"]; const SECTION_META: Record< string, { color: string; bg: string; border: string; emoji: string } > = { "Reading & Writing": { color: "#0891b2", bg: "#ecfeff", border: "#a5f3fc", emoji: "๐Ÿ“–", }, Math: { color: "#16a34a", bg: "#f0fdf4", border: "#bbf7d0", emoji: "๐Ÿ“" }, default: { color: "#a855f7", bg: "#fdf4ff", border: "#e9d5ff", emoji: "๐Ÿ“š" }, }; const getSectionMeta = (section?: string) => SECTION_META[section ?? ""] ?? SECTION_META["default"]; const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap'); .tp-screen { min-height: 100vh; background: #fffbf4; font-family: 'Nunito', sans-serif; position: relative; overflow-x: hidden; } .tp-blob { position:fixed;pointer-events:none;z-index:0;filter:blur(48px);opacity:0.35; } .tp-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:tpWobble1 14s ease-in-out infinite; } .tp-blob-2 { width:190px;height:190px;background:#a5f3c0;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:tpWobble2 16s ease-in-out infinite; } .tp-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:tpWobble1 18s ease-in-out infinite reverse; } .tp-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:tpWobble2 12s ease-in-out infinite; } @keyframes tpWobble1 { 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 tpWobble2 { 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);} } .tp-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:tpFloat 7s ease-in-out infinite; } @keyframes tpFloat { 0%,100%{transform:translateY(0) rotate(0deg);} 50%{transform:translateY(-12px) rotate(180deg);} } .tp-inner { position: relative; z-index: 1; max-width: 560px; margin: 0 auto; padding: 2rem 1.25rem 8rem; display: flex; flex-direction: column; gap: 1.5rem; } @keyframes tpPopIn { from { opacity:0; transform:scale(0.92) translateY(12px); } to { opacity:1; transform:scale(1) translateY(0); } } .tp-anim { animation: tpPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; } .tp-anim-1 { animation-delay: 0.05s; } .tp-anim-2 { animation-delay: 0.1s; } /* โ”€โ”€ Header โ”€โ”€ */ .tp-back-btn { width: 40px; height: 40px; border-radius: 50%; background: white; border: 2.5px solid #f3f4f6; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 3px 10px rgba(0,0,0,0.05); transition: all 0.15s ease; flex-shrink: 0; } .tp-back-btn:hover { border-color: #e9d5ff; background: #fdf4ff; } .tp-back-btn:active { transform: scale(0.9); } .tp-back-btn.hidden { opacity:0; pointer-events:none; } .tp-header-row { display: flex; align-items: center; gap: 0.75rem; } .tp-header-text { flex: 1; } .tp-eyebrow { font-size: 0.62rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; color: #ef4444; display: flex; align-items: center; gap: 0.35rem; } .tp-title { font-size: 1.75rem; font-weight: 900; color: #1e1b4b; letter-spacing: -0.02em; line-height: 1.15; } .tp-sub { font-family: 'Nunito Sans', sans-serif; font-size: 0.82rem; font-weight: 600; color: #9ca3af; margin-top: 0.2rem; line-height: 1.5; } /* โ”€โ”€ Progress bar โ”€โ”€ */ .tp-progress-wrap { background: white; border: 2.5px solid #f3f4f6; border-radius: 100px; overflow: hidden; height: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); } .tp-progress-fill { height: 100%; background: linear-gradient(90deg, #f97316, #ef4444); border-radius: 100px; transition: width 0.5s cubic-bezier(0.34,1.56,0.64,1); } .tp-progress-labels { display: flex; justify-content: space-between; font-size: 0.6rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #d1d5db; margin-top: 0.35rem; padding: 0 0.1rem; } .tp-progress-labels span.done { color: #f97316; } /* โ”€โ”€ Step content card โ”€โ”€ */ .tp-step-card { background: white; border: 2.5px solid #f3f4f6; border-radius: 24px; padding: 1.25rem; box-shadow: 0 4px 20px rgba(0,0,0,0.05); display: flex; flex-direction: column; gap: 1rem; } .tp-step-title { font-size: 1rem; font-weight: 900; color: #1e1b4b; display: flex; align-items: center; gap: 0.5rem; } .tp-step-badge { font-size: 0.58rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.2rem 0.55rem; border-radius: 100px; background: #fff7ed; border: 2px solid #fed7aa; color: #f97316; } /* โ”€โ”€ Search bar โ”€โ”€ */ .tp-search-wrap { position: relative; } .tp-search-icon { position: absolute; left: 0.85rem; top: 50%; transform: translateY(-50%); pointer-events: none; color: #9ca3af; } .tp-search-input { width: 100%; padding: 0.7rem 1rem 0.7rem 2.5rem; background: #f9fafb; border: 2.5px solid #f3f4f6; border-radius: 14px; font-family: 'Nunito Sans', sans-serif; font-size: 0.85rem; font-weight: 600; color: #1e1b4b; outline: none; transition: border-color 0.2s ease, box-shadow 0.2s ease; box-sizing: border-box; } .tp-search-input:focus { border-color: #c4b5fd; background: white; box-shadow: 0 0 0 3px rgba(168,85,247,0.1); } .tp-search-input::placeholder { color: #9ca3af; } /* โ”€โ”€ Topic grid โ”€โ”€ */ .tp-topic-grid { display: grid; grid-template-columns: 1fr; gap: 0.55rem; max-height: 380px; overflow-y: auto; padding-right: 0.25rem; } @media(min-width: 460px) { .tp-topic-grid { grid-template-columns: 1fr 1fr; } } /* โ”€โ”€ Topic card โ”€โ”€ */ .tp-topic-card { display: flex; align-items: center; gap: 0.75rem; background: white; border: 2.5px solid #f3f4f6; border-radius: 16px; padding: 0.75rem 0.9rem; cursor: pointer; transition: transform 0.15s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease; box-shadow: 0 2px 8px rgba(0,0,0,0.04); user-select: none; -webkit-tap-highlight-color: transparent; } .tp-topic-card:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.07); } .tp-topic-card:active { transform: scale(0.96); } .tp-topic-card.selected { box-shadow: 0 4px 0 var(--tc-shadow), 0 6px 16px rgba(0,0,0,0.07); } .tp-topic-icon { width: 36px; height: 36px; border-radius: 11px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1rem; transition: transform 0.2s cubic-bezier(0.34,1.56,0.64,1); } .tp-topic-card.selected .tp-topic-icon { transform: scale(1.1); } .tp-topic-body { flex: 1; min-width: 0; } .tp-topic-name { font-size: 0.82rem; font-weight: 900; color: #1e1b4b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; } .tp-topic-card.selected .tp-topic-name { color: var(--tc-color); } .tp-topic-sub { font-family: 'Nunito Sans', sans-serif; font-size: 0.68rem; font-weight: 600; color: #9ca3af; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 0.1rem; } .tp-topic-section-pill { font-size: 0.58rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.15rem 0.5rem; border-radius: 100px; flex-shrink: 0; display: none; } @media(min-width: 460px) { .tp-topic-section-pill { display: block; } } .tp-topic-check { width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0; border: 2.5px solid #e5e7eb; display: flex; align-items: center; justify-content: center; transition: all 0.2s cubic-bezier(0.34,1.56,0.64,1); } .tp-topic-card.selected .tp-topic-check { border-color: var(--tc-color); background: var(--tc-color); transform: scale(1.1); } /* โ”€โ”€ Difficulty cards โ”€โ”€ */ .tp-diff-card { display: flex; align-items: center; gap: 1rem; background: white; border: 2.5px solid #f3f4f6; border-radius: 18px; padding: 1rem 1.1rem; cursor: pointer; transition: all 0.18s ease; box-shadow: 0 3px 10px rgba(0,0,0,0.04); } .tp-diff-card:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,0.07); } .tp-diff-card:active { transform: translateY(1px); } .tp-diff-card.selected { box-shadow: 0 6px 0 var(--d-shadow), 0 8px 20px rgba(0,0,0,0.08); } .tp-diff-emoji { width: 44px; height: 44px; border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; flex-shrink: 0; } .tp-diff-label { font-size: 0.95rem; font-weight: 900; color: #1e1b4b; } .tp-diff-desc { font-family: 'Nunito Sans', sans-serif; font-size: 0.75rem; font-weight: 600; color: #9ca3af; margin-top: 0.1rem; } .tp-diff-check { margin-left: auto; width: 24px; height: 24px; border-radius: 50%; border: 2.5px solid #e5e7eb; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; flex-shrink: 0; } .tp-diff-card.selected .tp-diff-check { border-color: var(--d-color); background: var(--d-color); } /* โ”€โ”€ Review card โ”€โ”€ */ .tp-review-row { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.85rem 0; border-bottom: 2px solid #f9fafb; } .tp-review-row:last-child { border-bottom: none; padding-bottom: 0; } .tp-review-icon { width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 0.95rem; } .tp-review-label { font-size: 0.62rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: #9ca3af; } .tp-review-value { font-size: 0.9rem; font-weight: 800; color: #1e1b4b; margin-top: 0.1rem; line-height: 1.4; } /* โ”€โ”€ Topic chips in review โ”€โ”€ */ .tp-chip-wrap { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.35rem; } .tp-chip { background: #fdf4ff; border: 2px solid #e9d5ff; border-radius: 100px; padding: 0.2rem 0.65rem; font-size: 0.72rem; font-weight: 800; color: #9333ea; } /* โ”€โ”€ Loading โ”€โ”€ */ .tp-loading { display: flex; align-items: center; justify-content: center; gap: 0.6rem; padding: 2rem; font-size: 0.85rem; font-weight: 700; color: #9ca3af; } .tp-spinner { animation: tpSpin 0.8s linear infinite; } @keyframes tpSpin { to { transform: rotate(360deg); } } /* โ”€โ”€ Bottom CTA bar โ”€โ”€ */ .tp-cta-bar { position: fixed; bottom: 96px; left: 0; right: 0; z-index: 10; padding: 0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom)); background: rgba(255,251,244,0.9); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-top: 2px solid #f3f4f6; } .tp-cta-inner { max-width: 560px; margin: 0 auto; display: flex; gap: 0.75rem; align-items: center; } /* Next / Start button */ .tp-next-btn { flex: 1; padding: 0.9rem 1.5rem; background: #f97316; color: white; border: none; border-radius: 100px; cursor: pointer; font-family: 'Nunito', sans-serif; font-size: 0.92rem; font-weight: 900; display: flex; align-items: center; justify-content: center; gap: 0.4rem; 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; } .tp-next-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #c2560e,0 12px 24px rgba(249,115,22,0.3); } .tp-next-btn:active { transform:translateY(3px); box-shadow:0 3px 0 #c2560e; } .tp-next-btn:disabled { background: #e5e7eb; color: #9ca3af; cursor: not-allowed; box-shadow: 0 4px 0 #d1d5db; } .tp-next-btn:disabled:hover { transform: none; box-shadow: 0 4px 0 #d1d5db; } /* Start button variant */ .tp-start-btn { flex: 1; padding: 0.9rem 1.5rem; background: linear-gradient(135deg, #7c3aed, #a855f7); color: white; border: none; border-radius: 100px; cursor: pointer; font-family: 'Nunito', sans-serif; font-size: 0.92rem; font-weight: 900; display: flex; align-items: center; justify-content: center; gap: 0.4rem; box-shadow: 0 6px 0 #5b21b6, 0 8px 20px rgba(124,58,237,0.3); transition: transform 0.1s ease, box-shadow 0.1s ease; } .tp-start-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #5b21b6,0 12px 24px rgba(124,58,237,0.35); } .tp-start-btn:active { transform:translateY(3px); box-shadow:0 3px 0 #5b21b6; } /* Empty state */ .tp-empty { text-align: center; padding: 2rem; color: #9ca3af; font-size: 0.85rem; font-weight: 700; } `; export const TargetedPractice = () => { const navigate = useNavigate(); const { storeTopics, storeDuration, setDifficulty: storeDifficulty, setMode, setQuestionCount, } = useExamConfigStore(); const user = useAuthStore((state) => state.user); const token = useAuthToken(); const [direction, setDirection] = useState<1 | -1>(1); const [step, setStep] = useState("topic"); const [selectedTopics, setSelectedTopics] = useState([]); const [difficulty, setDifficulty] = useState< "EASY" | "MEDIUM" | "HARD" | null >(null); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(false); const [topics, setTopics] = useState([]); const difficulties = ["EASY", "MEDIUM", "HARD"] as const; const stepIndex = STEPS.indexOf(step); const progressPct = ((stepIndex + 1) / STEPS.length) * 100; const toggleTopic = (topic: Topic) => { setSelectedTopics((prev) => prev.some((t) => t.id === topic.id) ? prev.filter((t) => t.id !== topic.id) : [...prev, topic], ); }; const goNext = (nextStep: Step) => { setDirection(1); setStep(nextStep); }; const goBack = () => { const prev = STEPS[stepIndex - 1]; if (prev) { setDirection(-1); setStep(prev); } }; async function handleStart() { if (!user || !token || !topics || !difficulty) return; storeDuration(10); navigate(`/student/practice/${topics[0].id}/test`, { replace: true }); } useEffect(() => { const fetchAllTopics = async () => { if (!user) return; try { setLoading(true); const authStorage = localStorage.getItem("auth-storage"); if (!authStorage) return; const { state: { token }, } = JSON.parse(authStorage) as { state?: { token?: string } }; if (!token) return; const response = await api.fetchAllTopics(token); setTopics(response); } catch (error) { console.error("Failed to load topics:", error); } finally { setLoading(false); } }; fetchAllTopics(); }, [user]); const filteredTopics = topics.filter((t) => t.name.toLowerCase().includes(search.toLowerCase()), ); return (
{/* Blobs */}
{/* Dots */} {DOTS.map((d, i) => (
))}
{/* Header */}

Targeted Practice

{step === "topic" ? "Pick your topics" : step === "difficulty" ? "Set the difficulty" : "Review & launch"}

{step === "topic" ? "Select one or more topics you want to drill." : step === "difficulty" ? "How hard do you want to push yourself today?" : "Everything look good? Let's go."}

{/* Progress */}
{STEPS.map((s, i) => ( {s === "topic" ? "Topics" : s === "difficulty" ? "Difficulty" : "Review"} ))}
{/* Step content */}
{/* โ”€โ”€ Step 1: Topic โ”€โ”€ */} {step === "topic" && (
Choose topics {selectedTopics.length > 0 && ( {selectedTopics.length} selected )}
setSearch(e.target.value)} />
{loading ? (
Loading topics...
) : filteredTopics.length === 0 ? (

No topics match "{search}"

) : (
{filteredTopics.map((t) => { const meta = getSectionMeta(t.section); const isSelected = selectedTopics.some( (st) => st.id === t.id, ); return (
toggleTopic(t)} > {/* Section icon */}
{meta.emoji}
{/* Name + parent */}

{t.name}

{t.parent_name && (

{t.parent_name}

)}
{/* Section pill */} {t.section && ( {t.section === "Reading & Writing" ? "R&W" : t.section} )} {/* Checkmark */}
{isSelected && ( )}
); })}
)}
)} {/* โ”€โ”€ Step 2: Difficulty โ”€โ”€ */} {step === "difficulty" && (

How tough?

{difficulties.map((d) => { const meta = DIFFICULTY_META[d]; const isSelected = difficulty === d; return (
{ setDifficulty(d); storeDifficulty(d); goNext("review"); }} >
{meta.emoji}

{meta.label}

{meta.desc}

{isSelected && ( )}
); })}
)} {/* โ”€โ”€ Step 3: Review โ”€โ”€ */} {step === "review" && (

Your session setup

๐Ÿ“š

Topics

{selectedTopics.map((t) => ( {t.name} ))}
{DIFFICULTY_META[difficulty!]?.emoji}

Difficulty

{DIFFICULTY_META[difficulty!]?.label}

โฑ๏ธ

Questions

7 questions ยท ~10 min

)}
{/* โ”€โ”€ Bottom CTA โ”€โ”€ */}
{step === "topic" && ( )} {step === "difficulty" && ( )} {step === "review" && ( )}
); };