From b435b656e9f715a436f3e6001aa55df539d4bae8 Mon Sep 17 00:00:00 2001 From: shafin-r Date: Fri, 13 Mar 2026 01:24:13 +0600 Subject: [PATCH] feat(practice-sheet): add practice sheet page --- src/App.tsx | 5 + src/components/AppSidebar.tsx | 19 + src/pages/student/Practice.tsx | 503 ++++++++-- src/pages/student/practice-sheet/page.tsx | 1009 +++++++++++++++++++++ 4 files changed, 1465 insertions(+), 71 deletions(-) create mode 100644 src/pages/student/practice-sheet/page.tsx diff --git a/src/App.tsx b/src/App.tsx index 8f07a0d..47d824b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ import { Drills } from "./pages/student/drills/page"; import { HardTestModules } from "./pages/student/hard-test-modules/page"; import { QuestMap } from "./pages/student/QuestMap"; import { Register } from "./pages/auth/Register"; +import { PracticeSheetList } from "./pages/student/practice-sheet/page"; function App() { const router = createBrowserRouter([ @@ -80,6 +81,10 @@ function App() { path: "practice/hard-test-modules", element: , }, + { + path: "practice/practice-sheet", + element: , + }, ], }, { diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 0c2c28e..be4f2f5 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -19,6 +19,7 @@ import { Trophy, Map, SquareLibrary, + ListIcon, } from "lucide-react"; import { useState } from "react"; @@ -362,6 +363,7 @@ export function AppSidebar() { /> Targeted Practice + @@ -396,6 +398,23 @@ export function AppSidebar() { /> Hard Test Modules + + `flex items-center gap-2.5 rounded-2xl px-2 py-2 text-sm font-satoshi transition-colors duration-200 ${ + isActive + ? "bg-white text-slate-900" + : "text-slate-500 hover:bg-white hover:text-slate-900" + }` + } + > + + Practice Sheet + )} diff --git a/src/pages/student/Practice.tsx b/src/pages/student/Practice.tsx index 4af3279..d424093 100644 --- a/src/pages/student/Practice.tsx +++ b/src/pages/student/Practice.tsx @@ -1,12 +1,4 @@ -import { - BookOpen, - Clock, - DraftingCompass, - Loader2, - Target, - Trophy, - Zap, -} from "lucide-react"; +import { DraftingCompass, FileText, Target, Trophy, Zap } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { InfoHeader } from "../../components/InfoHeader"; @@ -26,13 +18,13 @@ const STYLES = ` .pr-screen { min-height: 100vh; + padding-bottom: 40px; background: #fffbf4; font-family: 'Nunito', sans-serif; position: relative; overflow-x: hidden; } - /* On desktop, account for sidebar */ @media (min-width: 768px) { .pr-screen { padding-left: calc(17rem + 1.25rem); @@ -70,10 +62,9 @@ const STYLES = ` display: flex; flex-direction: column; gap: 1.5rem; } - /* Desktop / wide layout */ @media (min-width: 900px) { .pr-inner { max-width: var(--content-max); padding: 3rem 1.5rem 6rem; } - .pr-grid { grid-template-columns: repeat(3, 1fr); gap: 1rem; } + .pr-grid { grid-template-columns: repeat(4, 1fr) !important; gap: 1rem !important; } .pr-blob-1 { left: calc((100vw - var(--content-max)) / 2 - 120px); top: -120px; width: 300px; height: 300px; } .pr-blob-2 { left: calc((100vw - var(--content-max)) / 2 + 20px); bottom: -80px; width: 220px; height: 220px; } @@ -95,29 +86,6 @@ const STYLES = ` .pr-anim-3 { animation-delay: 0.15s; } .pr-anim-4 { animation-delay: 0.2s; } - /* ── Header ── */ - .pr-header { - display: flex; align-items: center; justify-content: space-between; - animation: prPopIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both; - } - .pr-logo-btn { - width: 44px; height: 44px; border-radius: 14px; - background: linear-gradient(135deg, #a855f7, #7c3aed); - display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 0 #5b21b644; - } - .pr-xp-chip { - display: flex; align-items: center; gap: 0.5rem; - background: white; border: 2.5px solid #e9d5ff; - border-radius: 100px; padding: 0.45rem 1rem; - font-size: 0.85rem; font-weight: 800; color: #7c3aed; - box-shadow: 0 3px 10px rgba(0,0,0,0.05); - } - .pr-xp-dot { - width: 8px; height: 8px; border-radius: 50%; - background: linear-gradient(135deg, #a855f7, #7c3aed); - } - /* ── Hero banner ── */ .pr-hero { border-radius: 24px; @@ -175,15 +143,15 @@ const STYLES = ` /* ── Mode card ── */ .pr-mode-card { background: white; border: 2.5px solid #f3f4f6; border-radius: 22px; - padding: 1.1rem 1.25rem; + padding: 0; box-shadow: 0 4px 14px rgba(0,0,0,0.04); - cursor: pointer; display: flex; flex-direction: column; gap: 0.85rem; + cursor: pointer; display: flex; flex-direction: column; transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; position: relative; overflow: hidden; } .pr-mode-card:hover { - transform: translateY(-3px); - box-shadow: 0 10px 24px rgba(0,0,0,0.08); + transform: translateY(-4px); + box-shadow: 0 14px 32px rgba(0,0,0,0.1); } .pr-mode-card:active { transform: translateY(1px); box-shadow: 0 3px 8px rgba(0,0,0,0.06); } @@ -193,71 +161,455 @@ const STYLES = ` .pr-mode-card.cyan:hover { border-color: #67e8f9; } .pr-mode-card.lime { border-color: #d9f99d; } .pr-mode-card.lime:hover { border-color: #bef264; } + .pr-mode-card.amber { border-color: #fde68a; } + .pr-mode-card.amber:hover { border-color: #fcd34d; } + + /* Illustration strip at top of card */ + .pr-card-illo { + width: 100%; + height: 110px; + position: relative; + overflow: hidden; + flex-shrink: 0; + } + .pr-card-illo svg { + width: 100%; + height: 100%; + } + + /* Card body below illustration */ + .pr-card-body { + padding: 1rem 1.1rem 1rem; + display: flex; flex-direction: column; gap: 0.6rem; + flex: 1; + } .pr-mode-top { - display: flex; align-items: flex-start; justify-content: space-between; + display: flex; align-items: center; gap: 0.6rem; } .pr-mode-icon { - width: 44px; height: 44px; border-radius: 14px; + width: 36px; height: 36px; border-radius: 12px; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; } - .pr-mode-icon.red { background: linear-gradient(135deg, #f87171, #ef4444); box-shadow: 0 4px 0 #b91c1c44; } - .pr-mode-icon.cyan { background: linear-gradient(135deg, #22d3ee, #06b6d4); box-shadow: 0 4px 0 #0e7490aa; } - .pr-mode-icon.lime { background: linear-gradient(135deg, #a3e635, #84cc16); box-shadow: 0 4px 0 #4d7c0f44; } - - .pr-mode-badge { - width: 36px; height: 36px; border-radius: 50%; - display: flex; align-items: center; justify-content: center; - } - .pr-mode-badge.red { background: #fff5f5; } - .pr-mode-badge.cyan { background: #ecfeff; } - .pr-mode-badge.lime { background: #f7ffe4; } + .pr-mode-icon.red { background: linear-gradient(135deg, #f87171, #ef4444); box-shadow: 0 3px 0 #b91c1c44; } + .pr-mode-icon.cyan { background: linear-gradient(135deg, #22d3ee, #06b6d4); box-shadow: 0 3px 0 #0e7490aa; } + .pr-mode-icon.lime { background: linear-gradient(135deg, #a3e635, #84cc16); box-shadow: 0 3px 0 #4d7c0f44; } + .pr-mode-icon.amber{ background: linear-gradient(135deg, #fbbf24, #f59e0b); box-shadow: 0 3px 0 #92400e44; } .pr-mode-title { - font-size: 1rem; font-weight: 900; color: #1e1b4b; + font-size: 0.95rem; font-weight: 900; color: #1e1b4b; } .pr-mode-desc { font-family: 'Nunito Sans', sans-serif; - font-size: 0.78rem; font-weight: 600; color: #9ca3af; + font-size: 0.76rem; font-weight: 600; color: #9ca3af; + line-height: 1.4; } .pr-mode-arrow { font-size: 0.75rem; font-weight: 800; margin-top: auto; display: flex; align-items: center; gap: 0.25rem; transition: gap 0.2s ease; + padding-top: 0.25rem; } .pr-mode-card:hover .pr-mode-arrow { gap: 0.5rem; } .pr-mode-arrow.red { color: #ef4444; } .pr-mode-arrow.cyan { color: #06b6d4; } .pr-mode-arrow.lime { color: #84cc16; } + .pr-mode-arrow.amber{ color: #f59e0b; } + + /* Wiggle on hover for illustration elements */ + .pr-mode-card:hover .illo-wiggle { + animation: illoWiggle 0.5s ease; + } + @keyframes illoWiggle { + 0%,100%{transform:rotate(0deg);} + 25%{transform:rotate(-5deg);} + 75%{transform:rotate(5deg);} + } `; +/* ── SVG Illustrations for each card ── */ + +const IlloTargeted = () => ( + + + {/* soft bg circles */} + + + {/* Target rings */} + + + + + {/* Arrow hitting bullseye */} + + + {/* Arrow fletching */} + + + {/* Sparkles */} + + + + + + +); + +const IlloDrills = () => ( + + + + + {/* Stopwatch body */} + + + {/* Clock hands */} + + + + {/* Crown / button */} + + + {/* Lightning bolts (speed) */} + + + + {/* Speed lines */} + + + + {/* dots */} + + + + +); + +const IlloHard = () => ( + + + + + {/* Trophy */} + + + + + + {/* Star inside trophy */} + + {/* Mountain / difficulty hills */} + + + + {/* flag on tallest */} + + + {/* sparkles */} + + + +); + +const IlloSheet = () => ( + + + + + {/* Paper sheet */} + + {/* Folded corner */} + + + {/* Lines on paper */} + + + + + {/* Checkmark on first line */} + + {/* Pencil */} + + + + + + + {/* Stars / highlights */} + + + + + +); + const MODE_CARDS = [ { color: "red", - icon: , - badge: , + icon: , title: "Targeted Practice", desc: "Focus on your weak spots and improve fast", route: "/student/practice/targeted-practice", arrow: "Practice →", + Illo: IlloTargeted, }, { color: "cyan", - icon: , - badge: , + icon: , title: "Drills", desc: "Train speed and accuracy under pressure", route: "/student/practice/drills", arrow: "Drill →", + Illo: IlloDrills, }, { color: "lime", - icon: , - badge: , + icon: , title: "Hard Modules", desc: "Push yourself with the toughest questions", route: "/student/practice/hard-test-modules", arrow: "Challenge →", + Illo: IlloHard, + }, + { + color: "amber", + icon: , + title: "Practice Sheet", + desc: "Work through curated question sets at your own pace", + route: "/student/practice/practice-sheet", + arrow: "Start Sheet →", + Illo: IlloSheet, }, ] as const; @@ -297,6 +649,7 @@ export const Practice = () => {
{/* ── Header ── */} + {/* ── Hero banner ── */}
@@ -307,7 +660,12 @@ export const Practice = () => {

Take a full adaptive test and benchmark your SAT readiness.

- +
{/* ── Practice modes ── */} @@ -322,19 +680,22 @@ export const Practice = () => { className={`pr-mode-card ${card.color}`} onClick={() => navigate(card.route)} > -
-
- {card.icon} -
-
- {card.badge} -
+ {/* Illustration */} +
+
-
-

{card.title}

+ + {/* Body */} +
+
+
+ {card.icon} +
+

{card.title}

+

{card.desc}

+

{card.arrow}

-

{card.arrow}

))}
diff --git a/src/pages/student/practice-sheet/page.tsx b/src/pages/student/practice-sheet/page.tsx new file mode 100644 index 0000000..1e0c6fb --- /dev/null +++ b/src/pages/student/practice-sheet/page.tsx @@ -0,0 +1,1009 @@ +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"; + +/* ───────────────────────────────────────────── + 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 = ` + @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; } + + .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 [sheets, setSheets] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [query, setQuery] = useState(""); + const [viewMode, setViewMode] = useState("compact"); + + useEffect(() => { + setLoading(true); + const authStorage = localStorage.getItem("auth-storage"); + if (!authStorage) return; + const { + state: { token }, + } = JSON.parse(authStorage); + if (!token) return; + 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)} + /> + )) + )} +
+ )} +
+
+ ); +};