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 (
);
};
/* ─────────────────────────────────────────────
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)}
/>
))
)}
)}
);
};