import { useEffect, useMemo, useState } from "react"; import { BookOpen, Clock, FileText, Layers, LayoutGrid, List, Lock, PlayCircle, Search, Sparkles, Tag, X, Zap, } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { api } from "../../../utils/api"; import { type PracticeSheet } from "../../../types/sheet"; import { useAuthStore } from "../../../stores/authStore"; /* ───────────────────────────────────────────── Ambient decoration ───────────────────────────────────────────── */ const DOTS = [ { size: 10, color: "#f97316", top: "7%", left: "4%", delay: "0s" }, { size: 7, color: "#a855f7", top: "28%", left: "2%", delay: "1.2s" }, { size: 9, color: "#22c55e", top: "60%", left: "3%", delay: "0.6s" }, { size: 12, color: "#3b82f6", top: "11%", right: "4%", delay: "1.8s" }, { size: 7, color: "#f43f5e", top: "47%", right: "2%", delay: "0.9s" }, { size: 9, color: "#eab308", top: "76%", right: "5%", delay: "0.4s" }, ]; /* ───────────────────────────────────────────── Helpers ───────────────────────────────────────────── */ const DIFF_META: Record< string, { label: string; color: string; bg: string; border: string } > = { EASY: { label: "Easy", color: "#16a34a", bg: "#f0fdf4", border: "#bbf7d0" }, MEDIUM: { label: "Medium", color: "#d97706", bg: "#fffbeb", border: "#fde68a", }, HARD: { label: "Hard", color: "#dc2626", bg: "#fff5f5", border: "#fecaca" }, }; const getDiff = (d: string) => DIFF_META[d?.toUpperCase()] ?? { label: d, color: "#6b7280", bg: "#f9fafb", border: "#e5e7eb", }; const STATUS_META: Record< string, { label: string; color: string; bg: string } > = { COMPLETED: { label: "Completed", color: "#16a34a", bg: "#f0fdf4" }, IN_PROGRESS: { label: "In Progress", color: "#d97706", bg: "#fffbeb" }, NOT_STARTED: { label: "Not Started", color: "#9ca3af", bg: "#f9fafb" }, }; const getStatus = (s: string) => STATUS_META[s?.toUpperCase()] ?? { label: s ?? "—", color: "#9ca3af", bg: "#f9fafb", }; const PALETTES = [ { bg: "#fff5f5", blob: "#fee2e2", accent: "#fca5a5", pop: "#ef4444", line: "#fecaca", }, { bg: "#ecfeff", blob: "#cffafe", accent: "#67e8f9", pop: "#06b6d4", line: "#a5f3fc", }, { bg: "#f7ffe4", blob: "#d9f99d", accent: "#bef264", pop: "#84cc16", line: "#d9f99d", }, { bg: "#fffbeb", blob: "#fef3c7", accent: "#fde68a", pop: "#f59e0b", line: "#fcd34d", }, ]; const formatTime = (mins: number) => { if (!mins) return "—"; if (mins < 60) return `${mins}m`; return `${Math.floor(mins / 60)}h ${mins % 60 > 0 ? `${mins % 60}m` : ""}`.trim(); }; const initials = (name: string) => name ?.split(" ") .map((w) => w[0]) .slice(0, 2) .join("") .toUpperCase() ?? "?"; /* ───────────────────────────────────────────── Card illustration ───────────────────────────────────────────── */ const CardIllo = ({ index, locked }: { index: number; locked: boolean }) => { const p = PALETTES[index % PALETTES.length]; return ( {locked && ( <> )} ); }; /* ───────────────────────────────────────────── Compact row card ───────────────────────────────────────────── */ const CompactCard = ({ sheet, index, onStart, }: { sheet: PracticeSheet; index: number; onStart: () => void; }) => { const p = PALETTES[index % PALETTES.length]; const status = getStatus(sheet.user_status); return (

{sheet.title} {sheet.is_locked && ( )}

{status.label} {sheet.time_limit > 0 && ( {formatTime(sheet.time_limit)} )} {sheet.modules_count > 0 && ( {sheet.modules_count} module{sheet.modules_count !== 1 ? "s" : ""} )}
{sheet.is_locked ? (
) : ( )}
); }; /* ───────────────────────────────────────────── Skeletons ───────────────────────────────────────────── */ const SkeletonCard = () => (
); const SkeletonCompact = () => (
); /* ───────────────────────────────────────────── Empty state ───────────────────────────────────────────── */ const EmptyState = ({ query }: { query: string }) => (

{query ? `No sheets match "${query}"` : "No practice sheets yet"}

Try a different search or check back later.

); /* ───────────────────────────────────────────── Styles ───────────────────────────────────────────── */ const STYLES = ` :root { --content-max: 1100px; } .ps-screen { min-height: 100vh; background: #fffbf4; font-family: 'Nunito', sans-serif; position: relative; overflow-x: hidden; } @media (min-width: 768px) { .ps-screen { padding-left: calc(17rem + 1.25rem); } } .ps-blob { position:fixed;pointer-events:none;z-index:0;filter:blur(48px);opacity:0.35; } .ps-blob-1 { width:240px;height:240px;background:#fde68a;top:-80px;left:-80px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:psWobble1 14s ease-in-out infinite; } .ps-blob-2 { width:190px;height:190px;background:#a5f3c0;bottom:-50px;left:6%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:psWobble2 16s ease-in-out infinite; } .ps-blob-3 { width:210px;height:210px;background:#fbcfe8;top:15%;right:-60px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:psWobble1 18s ease-in-out infinite reverse; } .ps-blob-4 { width:150px;height:150px;background:#bfdbfe;bottom:12%;right:2%;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:psWobble2 12s ease-in-out infinite; } @keyframes psWobble1 { 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 psWobble2 { 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);} } .ps-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.3;animation:psFloat 7s ease-in-out infinite; } @keyframes psFloat { 0%,100%{transform:translateY(0) rotate(0deg);} 50%{transform:translateY(-12px) rotate(180deg);} } .ps-inner { position:relative;z-index:1; max-width:580px;margin:0 auto; padding:2rem 1.25rem 4rem; display:flex;flex-direction:column;gap:1.5rem; } @media(min-width:900px){ .ps-inner { max-width:var(--content-max);padding:3rem 1.5rem 6rem; } .ps-blob-1 { left:calc((100vw - var(--content-max)) / 2 - 120px);top:-120px;width:300px;height:300px; } .ps-blob-2 { left:calc((100vw - var(--content-max)) / 2 + 20px);bottom:-80px;width:220px;height:220px; } .ps-blob-3 { right:calc((100vw - var(--content-max)) / 2 - 40px);top:10%;width:260px;height:260px; } .ps-blob-4 { right:calc((100vw - var(--content-max)) / 2 + 10px);bottom:6%;width:180px;height:180px; } } @keyframes psPopIn { from{opacity:0;transform:scale(0.92) translateY(12px);} to{opacity:1;transform:scale(1) translateY(0);} } .ps-anim { animation:psPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; } .ps-anim-1 { animation-delay:0.05s; } .ps-anim-2 { animation-delay:0.10s; } .ps-anim-3 { animation-delay:0.15s; } /* Page header */ .ps-page-header { display:flex;flex-direction:column;gap:0.3rem; } .ps-eyebrow { font-size:0.65rem;font-weight:800;letter-spacing:0.18em; text-transform:uppercase;color:#f59e0b; display:flex;align-items:center;gap:0.4rem; } .ps-title { font-size:clamp(1.6rem,5vw,2.1rem);font-weight:900; color:#1e1b4b;letter-spacing:-0.03em;line-height:1.1; } .ps-subtitle { font-family:'Nunito Sans',sans-serif; font-size:0.88rem;font-weight:600;color:#9ca3af;margin-top:0.2rem; } /* Search */ .ps-search-wrap { position:relative; } .ps-search-icon { position:absolute;left:1rem;top:50%;transform:translateY(-50%); color:#9ca3af;pointer-events:none; } .ps-search-input { width:100%;box-sizing:border-box; background:white;border:2.5px solid #e9d5ff;border-radius:100px; padding:0.75rem 3rem 0.75rem 2.8rem; font-family:'Nunito',sans-serif;font-size:0.92rem;font-weight:700; color:#1e1b4b;outline:none; box-shadow:0 4px 14px rgba(0,0,0,0.04); transition:border-color 0.15s,box-shadow 0.15s; } .ps-search-input::placeholder { color:#c4b5fd;font-weight:600; } .ps-search-input:focus { border-color:#a855f7;box-shadow:0 4px 20px rgba(168,85,247,0.15); } .ps-search-clear { position:absolute;right:0.85rem;top:50%;transform:translateY(-50%); background:#f3f4f6;border:none;border-radius:50%;width:28px;height:28px; display:flex;align-items:center;justify-content:center; cursor:pointer;color:#6b7280;transition:background 0.15s; } .ps-search-clear:hover { background:#e5e7eb; } /* ── Toolbar ── */ .ps-toolbar { display:flex;align-items:center;justify-content:space-between;gap:0.75rem; } .ps-results-meta { font-size:0.78rem;font-weight:800;color:#9ca3af;letter-spacing:0.04em; } .ps-results-meta span { color:#7c3aed; } .ps-view-toggle { display:flex;align-items:center; background:white;border:2.5px solid #e9d5ff;border-radius:100px; padding:3px;gap:2px; box-shadow:0 2px 8px rgba(0,0,0,0.04); flex-shrink:0; } .ps-toggle-btn { display:flex;align-items:center;gap:0.3rem; padding:0.3rem 0.75rem;border-radius:100px;border:none; cursor:pointer; font-family:'Nunito',sans-serif;font-size:0.72rem;font-weight:800; color:#9ca3af;background:transparent; transition:background 0.15s,color 0.15s,box-shadow 0.15s; white-space:nowrap; } .ps-toggle-btn.active { background:linear-gradient(135deg,#a855f7,#7c3aed); color:white; box-shadow:0 2px 8px rgba(124,58,237,0.25); } .ps-toggle-btn:not(.active):hover { color:#7c3aed;background:#f5f3ff; } /* ── Standard grid ── */ .ps-grid { display:grid;grid-template-columns:1fr;gap:1rem; } @media(min-width:520px){ .ps-grid { grid-template-columns:1fr 1fr; } } @media(min-width:900px){ .ps-grid { grid-template-columns:repeat(3,1fr); } } .ps-card { background:white;border:2.5px solid #f3f4f6;border-radius:22px; overflow:hidden;cursor:pointer; box-shadow:0 4px 14px rgba(0,0,0,0.04); display:flex;flex-direction:column; transition:transform 0.15s ease,box-shadow 0.15s ease,border-color 0.15s ease; animation:psPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; } .ps-card:hover { transform:translateY(-4px);box-shadow:0 14px 32px rgba(0,0,0,0.09);border-color:#e9d5ff; } .ps-card:active { transform:translateY(1px);box-shadow:0 3px 8px rgba(0,0,0,0.06); } .ps-card.locked { opacity:0.8; } .ps-card-illo { width:100%;height:90px;flex-shrink:0;position:relative;overflow:hidden; } .ps-card-body { padding:1rem 1.1rem 1rem;display:flex;flex-direction:column;gap:0.55rem;flex:1; } .ps-card-title-row { display:flex;align-items:flex-start;justify-content:space-between;gap:0.5rem; } .ps-card-title { font-size:0.97rem;font-weight:900;color:#1e1b4b;line-height:1.3;flex:1; } .ps-lock-icon { color:#9ca3af;flex-shrink:0;margin-top:1px; } .ps-card-desc { font-family:'Nunito Sans',sans-serif; font-size:0.75rem;font-weight:600;color:#9ca3af;line-height:1.45; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; } .ps-pill-row { display:flex;flex-wrap:wrap;gap:0.4rem; } .ps-pill { display:inline-flex;align-items:center;gap:0.28rem; padding:0.25rem 0.65rem;border-radius:100px; font-size:0.7rem;font-weight:800;border:1.5px solid transparent;white-space:nowrap; } .ps-stats-row { display:flex;flex-wrap:wrap;gap:0.75rem;margin-top:0.1rem; } .ps-stat { display:flex;align-items:center;gap:0.3rem;font-size:0.72rem;font-weight:700;color:#6b7280; } .ps-stat svg { flex-shrink:0; } .ps-tags-row { display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.1rem;align-items:center; } .ps-tag { background:#f3f4f6;border-radius:6px;padding:0.18rem 0.5rem;font-size:0.65rem;font-weight:700;color:#6b7280;white-space:nowrap; } .ps-card-footer { display:flex;align-items:center;justify-content:space-between; padding:0.6rem 1.1rem 0.85rem;border-top:1.5px solid #f3f4f6;margin-top:auto; } .ps-author { display:flex;align-items:center;gap:0.4rem;font-size:0.7rem;font-weight:700;color:#9ca3af; } .ps-author-avatar { width:22px;height:22px;border-radius:50%; background:linear-gradient(135deg,#a855f7,#7c3aed); display:flex;align-items:center;justify-content:center; font-size:0.6rem;font-weight:800;color:white;flex-shrink:0; } .ps-cta-arrow { font-size:0.72rem;font-weight:800;color:#7c3aed;display:flex;align-items:center;gap:0.2rem;transition:gap 0.2s ease; } .ps-card:hover .ps-cta-arrow { gap:0.45rem; } /* ── Compact list ── */ .ps-compact-list { display:flex;flex-direction:column;gap:0.45rem; } .ps-compact-card { background:white;border:2px solid #f3f4f6;border-radius:16px; display:flex;align-items:center;gap:0.85rem; padding:0 1rem 0 0; overflow:hidden;cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,0.04); transition:transform 0.12s ease,box-shadow 0.12s ease,border-color 0.12s ease; animation:psPopIn 0.35s cubic-bezier(0.34,1.56,0.64,1) both; min-height:56px; } .ps-compact-card:hover { transform:translateX(3px); box-shadow:0 6px 20px rgba(0,0,0,0.08); border-color:#e9d5ff; } .ps-compact-card:active { transform:translateX(1px); } .ps-compact-card.locked { opacity:0.75;cursor:default; } .ps-compact-bar { width:5px;align-self:stretch;flex-shrink:0;border-radius:0; } .ps-compact-main { flex:1;display:flex;flex-direction:column;gap:0.28rem; padding:0.6rem 0;min-width:0; } .ps-compact-title { font-size:0.87rem;font-weight:900;color:#1e1b4b; line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; display:flex;align-items:center;gap:4px; } .ps-compact-meta { display:flex;flex-wrap:wrap;align-items:center;gap:0.28rem; } .ps-compact-chip { display:inline-flex;align-items:center;gap:0.2rem; padding:0.16rem 0.45rem;border-radius:100px; font-size:0.63rem;font-weight:800;white-space:nowrap; } .ps-compact-chip-neutral { background:#f3f4f6;color:#6b7280; } .ps-compact-start-btn { display:inline-flex;align-items:center;gap:0.3rem; padding:0.38rem 0.85rem;border-radius:100px;border:none; cursor:pointer; font-family:'Nunito',sans-serif;font-size:0.72rem;font-weight:800;color:white; flex-shrink:0; box-shadow:0 3px 0 rgba(0,0,0,0.12); transition:transform 0.1s ease,box-shadow 0.1s ease; } .ps-compact-start-btn:hover { transform:translateY(-1px);box-shadow:0 4px 0 rgba(0,0,0,0.12); } .ps-compact-start-btn:active { transform:translateY(1px);box-shadow:0 1px 0 rgba(0,0,0,0.12); } .ps-compact-lock-pill { display:inline-flex;align-items:center;justify-content:center; width:30px;height:30px;border-radius:50%; background:#f3f4f6;color:#9ca3af;flex-shrink:0; } /* Skeleton */ .ps-skeleton { pointer-events:none; } .ps-skel-block, .ps-skel-line { background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb 50%,#f3f4f6 75%); background-size:200% 100%; animation:psSkelShimmer 1.4s ease-in-out infinite; border-radius:8px; } @keyframes psSkelShimmer { 0%{background-position:200% 0;} 100%{background-position:-200% 0;} } /* Empty */ .ps-empty { display:flex;flex-direction:column;align-items:center; gap:0.75rem;padding:3rem 1rem;text-align:center;grid-column:1/-1; } .ps-empty-title { font-size:1rem;font-weight:900;color:#1e1b4b; } .ps-empty-sub { font-family:'Nunito Sans',sans-serif;font-size:0.82rem;font-weight:600;color:#9ca3af; } /* Error */ .ps-error { background:#fff5f5;border:2px solid #fecaca;border-radius:16px; padding:1rem 1.25rem;font-size:0.85rem;font-weight:700;color:#dc2626; display:flex;align-items:center;gap:0.6rem; } `; /* ───────────────────────────────────────────── Main component ───────────────────────────────────────────── */ type ViewMode = "standard" | "compact"; export const PracticeSheetList = () => { const navigate = useNavigate(); const token = useAuthStore((state) => state.token); const [sheets, setSheets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [query, setQuery] = useState(""); const [viewMode, setViewMode] = useState("compact"); useEffect(() => { if (!token) return; setLoading(true); api .getPracticeSheets(token, 1, 10) .then((data) => { const normalized: PracticeSheet[] = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : Array.isArray(data?.results) ? data.results : Array.isArray(data?.practice_sheets) ? data.practice_sheets : []; setSheets(normalized); setError(null); }) .catch(() => setError("Couldn't load practice sheets. Please try again.")) .finally(() => setLoading(false)); }, []); const filtered = useMemo(() => { if (!query.trim()) return sheets; const q = query.toLowerCase(); return sheets.filter( (s) => s.title.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) || s.topics?.some((t) => t.name.toLowerCase().includes(q)) || s.difficulty?.toLowerCase().includes(q), ); }, [sheets, query]); const handleStart = (sheet: PracticeSheet) => { if (!sheet.is_locked) navigate(`/student/practice/${sheet.id}`); }; return (
{DOTS.map((d, i) => (
))}
{/* Page heading */}

Practice Sheets

Pick your sheet,
own your score 📄

Curated question sets designed to sharpen specific skills — work through them at your own pace.

{/* Search */}
setQuery(e.target.value)} /> {query && ( )}
{/* Toolbar: count + view toggle */} {!loading && !error && (

{query ? ( <> {filtered.length} result {filtered.length !== 1 ? "s" : ""} for "{query}" ) : ( <> {sheets.length} sheet {sheets.length !== 1 ? "s" : ""} available )}

)} {/* Error */} {error && (
⚠️ {error}
)} {/* Standard grid */} {viewMode === "standard" && (
{loading ? ( Array.from({ length: 6 }).map((_, i) => ) ) : filtered.length === 0 ? ( ) : ( filtered.map((sheet, idx) => { const diff = getDiff(sheet.difficulty); const status = getStatus(sheet.user_status); return (
handleStart(sheet)} >

{sheet.title}

{sheet.is_locked && ( )}
{sheet.description && (

{sheet.description}

)}
{diff.label} {status.label}
{sheet.questions_count > 0 && ( {sheet.questions_count} Q )} {sheet.modules_count > 0 && ( {sheet.modules_count} module {sheet.modules_count !== 1 ? "s" : ""} )} {sheet.time_limit > 0 && ( {formatTime(sheet.time_limit)} )}
{sheet.topics?.length > 0 && (
{sheet.topics.slice(0, 4).map((t) => ( {t.name} ))} {sheet.topics.length > 4 && ( +{sheet.topics.length - 4} )}
)}
{initials(sheet.created_by?.name ?? "")}
{sheet.created_by?.name ?? "Unknown"}
{sheet.is_locked ? ( 🔒 Locked ) : ( Start → )}
); }) )}
)} {/* Compact list */} {viewMode === "compact" && (
{loading ? ( Array.from({ length: 8 }).map((_, i) => ( )) ) : filtered.length === 0 ? ( ) : ( filtered.map((sheet, idx) => ( handleStart(sheet)} /> )) )}
)}
); };