Files
edbridge-scholars/src/pages/student/targeted-practice/page.tsx

914 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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');
:root { --content-max: 1100px; }
.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: 5;
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;
}
@media (min-width: 900px) {
.tp-inner { max-width: var(--content-max); padding: 3rem 1.5rem 10rem; }
.tp-topic-grid { grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
.tp-cta-bar { left: var(--sidebar-width); right: 0; }
/* Align decorative blobs relative to the centered content container */
.tp-blob-3 { right: calc((100vw - var(--content-max)) / 2 - 48px); }
.tp-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 56px); }
.tp-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 12px); }
.tp-blob-4 { right: calc((100vw - var(--content-max)) / 2 + 12px); }
}
/* 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<Step>("topic");
const [selectedTopics, setSelectedTopics] = useState<Topic[]>([]);
const [difficulty, setDifficulty] = useState<
"EASY" | "MEDIUM" | "HARD" | null
>(null);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [topics, setTopics] = useState<Topic[]>([]);
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 (
<div className="tp-screen">
<style>{STYLES}</style>
{/* Blobs */}
<div className="tp-blob tp-blob-1" />
<div className="tp-blob tp-blob-2" />
<div className="tp-blob tp-blob-3" />
<div className="tp-blob tp-blob-4" />
{/* Dots */}
{DOTS.map((d, i) => (
<div
key={i}
className="tp-dot"
style={
{
width: d.size,
height: d.size,
background: d.color,
top: d.top,
left: d.left,
right: d.right,
animationDelay: d.delay,
animationDuration: `${5 + i * 0.5}s`,
} as React.CSSProperties
}
/>
))}
<div className="tp-inner">
{/* Header */}
<div className="tp-header-row tp-anim">
<button
className={`tp-back-btn${step === "topic" ? " hidden" : ""}`}
onClick={goBack}
>
<ArrowLeft size={17} color="#6b7280" />
</button>
<div className="tp-header-text">
<p className="tp-eyebrow">
<Target size={11} /> Targeted Practice
</p>
<h1 className="tp-title">
{step === "topic"
? "Pick your topics"
: step === "difficulty"
? "Set the difficulty"
: "Review & launch"}
</h1>
<p className="tp-sub">
{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."}
</p>
</div>
</div>
{/* Progress */}
<div className="tp-anim tp-anim-1">
<div className="tp-progress-wrap">
<div
className="tp-progress-fill"
style={{ width: `${progressPct}%` }}
/>
</div>
<div className="tp-progress-labels">
{STEPS.map((s, i) => (
<span key={s} className={i <= stepIndex ? "done" : ""}>
{s === "topic"
? "Topics"
: s === "difficulty"
? "Difficulty"
: "Review"}
</span>
))}
</div>
</div>
{/* Step content */}
<div style={{ position: "relative", overflow: "hidden" }}>
<AnimatePresence mode="wait" custom={direction}>
{/* ── Step 1: Topic ── */}
{step === "topic" && (
<motion.div
key="topic"
custom={direction}
variants={slideVariants}
initial="initial"
animate="animate"
exit="exit"
>
<div className="tp-step-card">
<div className="tp-step-title">
Choose topics
{selectedTopics.length > 0 && (
<span className="tp-step-badge">
{selectedTopics.length} selected
</span>
)}
</div>
<div className="tp-search-wrap">
<Search size={15} className="tp-search-icon" />
<input
className="tp-search-input"
placeholder="Search topics..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
{loading ? (
<div className="tp-loading">
<Loader2
size={22}
color="#a855f7"
className="tp-spinner"
/>
Loading topics...
</div>
) : filteredTopics.length === 0 ? (
<p className="tp-empty">No topics match "{search}"</p>
) : (
<div className="tp-topic-grid">
{filteredTopics.map((t) => {
const meta = getSectionMeta(t.section);
const isSelected = selectedTopics.some(
(st) => st.id === t.id,
);
return (
<div
key={t.id}
className={`tp-topic-card${isSelected ? " selected" : ""}`}
style={
{
borderColor: isSelected
? meta.border
: "#f3f4f6",
background: isSelected ? meta.bg : "white",
"--tc-color": meta.color,
"--tc-shadow": meta.border,
} as React.CSSProperties
}
onClick={() => toggleTopic(t)}
>
{/* Section icon */}
<div
className="tp-topic-icon"
style={{
background: meta.bg,
border: `2px solid ${meta.border}`,
}}
>
{meta.emoji}
</div>
{/* Name + parent */}
<div className="tp-topic-body">
<p className="tp-topic-name">{t.name}</p>
{t.parent_name && (
<p className="tp-topic-sub">{t.parent_name}</p>
)}
</div>
{/* Section pill */}
{t.section && (
<span
className="tp-topic-section-pill"
style={{
background: meta.bg,
border: `2px solid ${meta.border}`,
color: meta.color,
}}
>
{t.section === "Reading & Writing"
? "R&W"
: t.section}
</span>
)}
{/* Checkmark */}
<div className="tp-topic-check">
{isSelected && (
<svg
width="11"
height="11"
viewBox="0 0 12 12"
fill="none"
>
<path
d="M2 6L5 9L10 3"
stroke="white"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
</div>
);
})}
</div>
)}
</div>
</motion.div>
)}
{/* ── Step 2: Difficulty ── */}
{step === "difficulty" && (
<motion.div
key="difficulty"
custom={direction}
variants={slideVariants}
initial="initial"
animate="animate"
exit="exit"
>
<div className="tp-step-card">
<p className="tp-step-title">How tough?</p>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "0.6rem",
}}
>
{difficulties.map((d) => {
const meta = DIFFICULTY_META[d];
const isSelected = difficulty === d;
return (
<div
key={d}
className={`tp-diff-card${isSelected ? " selected" : ""}`}
style={
{
borderColor: isSelected ? meta.border : "#f3f4f6",
background: isSelected ? meta.bg : "white",
"--d-color": meta.color,
"--d-shadow": meta.border,
} as React.CSSProperties
}
onClick={() => {
setDifficulty(d);
storeDifficulty(d);
goNext("review");
}}
>
<div
className="tp-diff-emoji"
style={{
background: meta.bg,
border: `2px solid ${meta.border}`,
}}
>
{meta.emoji}
</div>
<div style={{ flex: 1 }}>
<p
className="tp-diff-label"
style={{
color: isSelected ? meta.color : "#1e1b4b",
}}
>
{meta.label}
</p>
<p className="tp-diff-desc">{meta.desc}</p>
</div>
<div className="tp-diff-check">
{isSelected && (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<path
d="M2 6L5 9L10 3"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
</div>
);
})}
</div>
</div>
</motion.div>
)}
{/* ── Step 3: Review ── */}
{step === "review" && (
<motion.div
key="review"
custom={direction}
variants={slideVariants}
initial="initial"
animate="animate"
exit="exit"
>
<div className="tp-step-card">
<p className="tp-step-title">Your session setup</p>
<div className="tp-review-row">
<div
className="tp-review-icon"
style={{
background: "#fdf4ff",
border: "2px solid #e9d5ff",
}}
>
📚
</div>
<div>
<p className="tp-review-label">Topics</p>
<div className="tp-chip-wrap">
{selectedTopics.map((t) => (
<span key={t.id} className="tp-chip">
{t.name}
</span>
))}
</div>
</div>
</div>
<div className="tp-review-row">
<div
className="tp-review-icon"
style={{
background: DIFFICULTY_META[difficulty!]?.bg,
border: `2px solid ${DIFFICULTY_META[difficulty!]?.border}`,
}}
>
{DIFFICULTY_META[difficulty!]?.emoji}
</div>
<div>
<p className="tp-review-label">Difficulty</p>
<p
className="tp-review-value"
style={{ color: DIFFICULTY_META[difficulty!]?.color }}
>
{DIFFICULTY_META[difficulty!]?.label}
</p>
</div>
</div>
<div className="tp-review-row">
<div
className="tp-review-icon"
style={{
background: "#fff7ed",
border: "2px solid #fed7aa",
}}
>
</div>
<div>
<p className="tp-review-label">Questions</p>
<p className="tp-review-value">7 questions · ~10 min</p>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* ── Bottom CTA ── */}
<div className="tp-cta-bar">
<div className="tp-cta-inner">
{step === "topic" && (
<button
className="tp-next-btn"
disabled={selectedTopics.length === 0}
onClick={() => {
storeTopics(selectedTopics.map((t) => t.id));
setMode("TARGETED");
setQuestionCount(7);
goNext("difficulty");
}}
>
Next Difficulty <ChevronRight size={17} />
</button>
)}
{step === "difficulty" && (
<button className="tp-next-btn" disabled>
Select a difficulty to continue
</button>
)}
{step === "review" && (
<button className="tp-start-btn" onClick={handleStart}>
🚀 Start Practice
</button>
)}
</div>
</div>
</div>
);
};