form 1 topics added
This commit is contained in:
@@ -1,167 +1,338 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { PracticeSection } from "@/components/practice/practice-section";
|
||||
import { useCollapsibleSidebar, SidebarBackdrop, SidebarToggle, sidebarAsideClass, sidebarNavClass } from "@/components/layout/collapsible-sidebar";
|
||||
import { curriculum, type Unit, type Topic } from "@/lib/curriculum";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Difficulty, MathProblem } from "@/lib/problems/types";
|
||||
|
||||
import { generateFractionAddSubtract, generateFractionMultiply, generateFractionDivide, generateFractionOfQuantity, generateWholeFromFraction } from "@/lib/problems/generators/fraction-problems";
|
||||
import { generateFractionAddSubtract, generateFractionMultiply, generateFractionDivide, generateFractionOfQuantity, generateWholeFromFraction, generateFractionBodmas, generateSignedFraction } from "@/lib/problems/generators/fraction-problems";
|
||||
import { generateDecimalCompareOrder, generateDecimalRounding, generateStandardForm, generateDecimalAddSubtract, generateDecimalMultiplyDivide, generateDecimalConversion } from "@/lib/problems/generators/decimal-problems";
|
||||
import { generateSimplifyRatio, generateDivideInRatio, generateRatioWordProblem } from "@/lib/problems/generators/ratio-problems";
|
||||
import { generateSimplifyRatio, generateDivideInRatio, generateRatioWordProblem, generateDefineRatio, generateFractionsAndRatios } from "@/lib/problems/generators/ratio-problems";
|
||||
import { generateIntegerAddSubtractMixed, generateIntegerMultiplyDivide, generateIntegerOrderCompare } from "@/lib/problems/generators/integer-problems";
|
||||
import { generateDigitValue, generateBinaryToDenary, generateQuaternaryToDenary } from "@/lib/problems/generators/number-system-problems";
|
||||
import { generateIdentityInverse, generateExponentEvaluate, generateDistributive } from "@/lib/problems/generators/number-properties-problems";
|
||||
|
||||
interface TopicGenerator {
|
||||
unitSlug: string;
|
||||
topicSlug: string;
|
||||
label: string;
|
||||
generator: (difficulty: Difficulty) => MathProblem;
|
||||
unitColor: "unit-1" | "unit-2" | "unit-3" | "unit-4";
|
||||
}
|
||||
type UnitColor = Unit["color"];
|
||||
type PracticeUnitColor = Exclude<UnitColor, "unit-7">;
|
||||
type Generator = (difficulty: Difficulty) => MathProblem;
|
||||
|
||||
type UnitColor = TopicGenerator["unitColor"];
|
||||
|
||||
const TOPIC_GENERATORS: TopicGenerator[] = [
|
||||
{ unitSlug: "unit-1-fractions", topicSlug: "add-subtract", label: "Add & Subtract Fractions", generator: generateFractionAddSubtract, unitColor: "unit-1" },
|
||||
{ unitSlug: "unit-1-fractions", topicSlug: "multiply", label: "Multiply Fractions", generator: generateFractionMultiply, unitColor: "unit-1" },
|
||||
{ unitSlug: "unit-1-fractions", topicSlug: "divide", label: "Divide Fractions", generator: generateFractionDivide, unitColor: "unit-1" },
|
||||
{ unitSlug: "unit-1-fractions", topicSlug: "fraction-of-quantity", label: "Fraction of a Quantity", generator: generateFractionOfQuantity, unitColor: "unit-1" },
|
||||
{ unitSlug: "unit-1-fractions", topicSlug: "whole-from-fractions", label: "Find the Whole", generator: generateWholeFromFraction, unitColor: "unit-1" },
|
||||
{ unitSlug: "unit-2-decimals", topicSlug: "compare-order", label: "Compare & Order Decimals", generator: generateDecimalCompareOrder, unitColor: "unit-2" },
|
||||
{ unitSlug: "unit-2-decimals", topicSlug: "approximate", label: "Approximate Decimals", generator: generateDecimalRounding, unitColor: "unit-2" },
|
||||
{ unitSlug: "unit-2-decimals", topicSlug: "standard-form", label: "Standard Form", generator: generateStandardForm, unitColor: "unit-2" },
|
||||
{ unitSlug: "unit-3-decimal-operations", topicSlug: "convert", label: "Convert Decimals & Fractions", generator: generateDecimalConversion, unitColor: "unit-3" },
|
||||
{ unitSlug: "unit-3-decimal-operations", topicSlug: "add-subtract", label: "Add & Subtract Decimals", generator: generateDecimalAddSubtract, unitColor: "unit-3" },
|
||||
{ unitSlug: "unit-3-decimal-operations", topicSlug: "multiply-divide", label: "Multiply & Divide Decimals", generator: generateDecimalMultiplyDivide, unitColor: "unit-3" },
|
||||
{ unitSlug: "unit-4-ratio-proportion", topicSlug: "simplify-ratios", label: "Simplify Ratios", generator: generateSimplifyRatio, unitColor: "unit-4" },
|
||||
{ unitSlug: "unit-4-ratio-proportion", topicSlug: "divide-in-ratio", label: "Divide in a Ratio", generator: generateDivideInRatio, unitColor: "unit-4" },
|
||||
{ unitSlug: "unit-4-ratio-proportion", topicSlug: "word-problems", label: "Ratio Word Problems", generator: generateRatioWordProblem, unitColor: "unit-4" },
|
||||
];
|
||||
|
||||
const unitColors: Record<UnitColor, { activeFilter: string; inactiveFilter: string; label: string }> = {
|
||||
"unit-1": {
|
||||
activeFilter: "border-unit-1 bg-unit-1 text-white shadow-[var(--shadow-sm)]",
|
||||
inactiveFilter: "border-unit-1/40 text-unit-1-dark hover:bg-unit-1-light",
|
||||
label: "Fractions",
|
||||
},
|
||||
"unit-2": {
|
||||
activeFilter: "border-unit-2 bg-unit-2 text-white shadow-[var(--shadow-sm)]",
|
||||
inactiveFilter: "border-unit-2/40 text-unit-2-dark hover:bg-unit-2-light",
|
||||
label: "Decimals",
|
||||
},
|
||||
"unit-3": {
|
||||
activeFilter: "border-unit-3 bg-unit-3 text-white shadow-[var(--shadow-sm)]",
|
||||
inactiveFilter: "border-unit-3/40 text-unit-3-dark hover:bg-unit-3-light",
|
||||
label: "Decimal Operations",
|
||||
},
|
||||
"unit-4": {
|
||||
activeFilter: "border-unit-4 bg-unit-4 text-white shadow-[var(--shadow-sm)]",
|
||||
inactiveFilter: "border-unit-4/40 text-unit-4-dark hover:bg-unit-4-light",
|
||||
label: "Ratio & Proportion",
|
||||
},
|
||||
/**
|
||||
* Practice for each curriculum topic. "lesson-only" topics (the coordinate grid)
|
||||
* need the interactive visual, so they link to their lesson instead of a
|
||||
* fill-in-the-answer generator. Keyed by `${unitSlug}/${topicSlug}`.
|
||||
*/
|
||||
const PRACTICE_BY_TOPIC: Record<string, Generator | "lesson-only"> = {
|
||||
"unit-1-fractions/add-subtract": generateFractionAddSubtract,
|
||||
"unit-1-fractions/multiply": generateFractionMultiply,
|
||||
"unit-1-fractions/divide": generateFractionDivide,
|
||||
"unit-1-fractions/mixed-operations": generateFractionBodmas,
|
||||
"unit-1-fractions/fraction-of-quantity": generateFractionOfQuantity,
|
||||
"unit-1-fractions/whole-from-fractions": generateWholeFromFraction,
|
||||
"unit-2-decimals/compare-order": generateDecimalCompareOrder,
|
||||
"unit-2-decimals/approximate": generateDecimalRounding,
|
||||
"unit-2-decimals/standard-form": generateStandardForm,
|
||||
"unit-3-decimal-operations/convert": generateDecimalConversion,
|
||||
"unit-3-decimal-operations/add-subtract": generateDecimalAddSubtract,
|
||||
"unit-3-decimal-operations/multiply-divide": generateDecimalMultiplyDivide,
|
||||
"unit-4-ratio-proportion/define-ratio": generateDefineRatio,
|
||||
"unit-4-ratio-proportion/fractions-and-ratios": generateFractionsAndRatios,
|
||||
"unit-4-ratio-proportion/simplify-ratios": generateSimplifyRatio,
|
||||
"unit-4-ratio-proportion/divide-in-ratio": generateDivideInRatio,
|
||||
"unit-4-ratio-proportion/word-problems": generateRatioWordProblem,
|
||||
"unit-5-integers/order-compare": generateIntegerOrderCompare,
|
||||
"unit-5-integers/add-subtract": generateIntegerAddSubtractMixed,
|
||||
"unit-5-integers/multiply-divide": generateIntegerMultiplyDivide,
|
||||
"unit-5-integers/negative-fractions": generateSignedFraction,
|
||||
"unit-6-number-system/place-value": generateDigitValue,
|
||||
"unit-6-number-system/binary": generateBinaryToDenary,
|
||||
"unit-6-number-system/quaternary": generateQuaternaryToDenary,
|
||||
"unit-7-coordinates/plot-points": "lesson-only",
|
||||
"unit-7-coordinates/read-coordinates": "lesson-only",
|
||||
"unit-8-number-properties/identity-inverse": generateIdentityInverse,
|
||||
"unit-8-number-properties/number-laws": generateDistributive,
|
||||
"unit-8-number-properties/exponents": generateExponentEvaluate,
|
||||
};
|
||||
|
||||
export default function PracticePage() {
|
||||
const [selectedTopic, setSelectedTopic] = useState<TopicGenerator | null>(null);
|
||||
const [filterUnit, setFilterUnit] = useState<UnitColor | null>(null);
|
||||
function practiceFor(unit: Unit, topic: Topic): Generator | "lesson-only" {
|
||||
return PRACTICE_BY_TOPIC[`${unit.slug}/${topic.slug}`] ?? "lesson-only";
|
||||
}
|
||||
|
||||
const filtered = filterUnit
|
||||
? TOPIC_GENERATORS.filter((t) => t.unitColor === filterUnit)
|
||||
: TOPIC_GENERATORS;
|
||||
interface UnitStyle {
|
||||
heading: string;
|
||||
dot: string;
|
||||
accent: string;
|
||||
iconBg: string;
|
||||
hover: string;
|
||||
activeItem: string;
|
||||
headingHover: string;
|
||||
}
|
||||
|
||||
const UNIT_STYLE: Record<UnitColor, UnitStyle> = {
|
||||
"unit-1": { heading: "text-unit-1-dark", dot: "bg-unit-1", accent: "border-l-unit-1", iconBg: "bg-unit-1-light text-unit-1-dark", hover: "hover:border-unit-1/40", activeItem: "bg-unit-1-light text-unit-1-dark border-unit-1/20", headingHover: "hover:bg-unit-1-light/50" },
|
||||
"unit-2": { heading: "text-unit-2-dark", dot: "bg-unit-2", accent: "border-l-unit-2", iconBg: "bg-unit-2-light text-unit-2-dark", hover: "hover:border-unit-2/40", activeItem: "bg-unit-2-light text-unit-2-dark border-unit-2/20", headingHover: "hover:bg-unit-2-light/50" },
|
||||
"unit-3": { heading: "text-unit-3-dark", dot: "bg-unit-3", accent: "border-l-unit-3", iconBg: "bg-unit-3-light text-unit-3-dark", hover: "hover:border-unit-3/40", activeItem: "bg-unit-3-light text-unit-3-dark border-unit-3/20", headingHover: "hover:bg-unit-3-light/50" },
|
||||
"unit-4": { heading: "text-unit-4-dark", dot: "bg-unit-4", accent: "border-l-unit-4", iconBg: "bg-unit-4-light text-unit-4-dark", hover: "hover:border-unit-4/40", activeItem: "bg-unit-4-light text-unit-4-dark border-unit-4/20", headingHover: "hover:bg-unit-4-light/50" },
|
||||
"unit-5": { heading: "text-unit-5-dark", dot: "bg-unit-5", accent: "border-l-unit-5", iconBg: "bg-unit-5-light text-unit-5-dark", hover: "hover:border-unit-5/40", activeItem: "bg-unit-5-light text-unit-5-dark border-unit-5/20", headingHover: "hover:bg-unit-5-light/50" },
|
||||
"unit-6": { heading: "text-unit-6-dark", dot: "bg-unit-6", accent: "border-l-unit-6", iconBg: "bg-unit-6-light text-unit-6-dark", hover: "hover:border-unit-6/40", activeItem: "bg-unit-6-light text-unit-6-dark border-unit-6/20", headingHover: "hover:bg-unit-6-light/50" },
|
||||
"unit-7": { heading: "text-unit-7-dark", dot: "bg-unit-7", accent: "border-l-unit-7", iconBg: "bg-unit-7-light text-unit-7-dark", hover: "hover:border-unit-7/40", activeItem: "bg-unit-7-light text-unit-7-dark border-unit-7/20", headingHover: "hover:bg-unit-7-light/50" },
|
||||
"unit-8": { heading: "text-unit-8-dark", dot: "bg-unit-8", accent: "border-l-unit-8", iconBg: "bg-unit-8-light text-unit-8-dark", hover: "hover:border-unit-8/40", activeItem: "bg-unit-8-light text-unit-8-dark border-unit-8/20", headingHover: "hover:bg-unit-8-light/50" },
|
||||
};
|
||||
|
||||
interface Selection {
|
||||
unit: Unit;
|
||||
topic: Topic;
|
||||
}
|
||||
|
||||
function PlayIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Sidebar (mirrors the lessons sidebar) ────────────────────────────────────
|
||||
|
||||
function PracticeSidebar({
|
||||
selected,
|
||||
onSelect,
|
||||
onAllTopics,
|
||||
mobileOpen,
|
||||
desktopCollapsed,
|
||||
}: {
|
||||
selected: Selection | null;
|
||||
onSelect: (s: Selection) => void;
|
||||
onAllTopics: () => void;
|
||||
mobileOpen: boolean;
|
||||
desktopCollapsed: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState<Set<number>>(() => new Set([curriculum[0].number]));
|
||||
|
||||
function toggle(num: number) {
|
||||
setOpen((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(num)) next.delete(num);
|
||||
else next.add(num);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={sidebarAsideClass(mobileOpen, desktopCollapsed)}>
|
||||
<nav className={sidebarNavClass}>
|
||||
<button
|
||||
onClick={onAllTopics}
|
||||
className={cn(
|
||||
"mb-4 flex w-full items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200",
|
||||
selected === null
|
||||
? "bg-foreground text-background shadow-[var(--shadow-sm)]"
|
||||
: "text-muted hover:bg-background hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
All Topics
|
||||
</button>
|
||||
|
||||
<div className="mb-3 h-px bg-border/60" />
|
||||
|
||||
{curriculum.map((unit) => {
|
||||
const style = UNIT_STYLE[unit.color];
|
||||
const isOpen = open.has(unit.number);
|
||||
return (
|
||||
<div key={unit.slug} className="mb-1">
|
||||
<button
|
||||
onClick={() => toggle(unit.number)}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-xl px-3 py-2 text-left text-sm font-semibold transition-all duration-200",
|
||||
style.heading,
|
||||
style.headingHover,
|
||||
)}
|
||||
>
|
||||
<span className="truncate">Unit {unit.number}: {unit.title}</span>
|
||||
<svg
|
||||
className={cn("h-4 w-4 shrink-0 transition-transform duration-200", isOpen && "rotate-90")}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="ml-2 mt-0.5 space-y-0.5 border-l-2 border-border/40 pl-2">
|
||||
{unit.topics.map((topic) => {
|
||||
const isActive = selected?.unit.slug === unit.slug && selected?.topic.slug === topic.slug;
|
||||
return (
|
||||
<button
|
||||
key={topic.slug}
|
||||
onClick={() => onSelect({ unit, topic })}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-sm transition-all duration-200",
|
||||
isActive
|
||||
? cn(style.activeItem, "font-medium shadow-[var(--shadow-sm)]")
|
||||
: "text-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<span className={cn("h-1.5 w-1.5 shrink-0 rounded-full transition-colors", isActive ? style.dot : "bg-border")} />
|
||||
<span className="truncate">{topic.shortTitle}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Topic browser (main area, all topics) ────────────────────────────────────
|
||||
|
||||
function TopicBrowser({ onSelect }: { onSelect: (s: Selection) => void }) {
|
||||
return (
|
||||
<div className="space-y-9">
|
||||
{curriculum.map((unit) => {
|
||||
const style = UNIT_STYLE[unit.color];
|
||||
return (
|
||||
<section key={unit.slug}>
|
||||
<div className="mb-3 flex items-center gap-2.5">
|
||||
<span className={cn("h-3.5 w-3.5 rounded-full", style.dot)} />
|
||||
<h2 className={cn("text-sm font-extrabold uppercase tracking-wider", style.heading)}>
|
||||
Unit {unit.number}: {unit.title}
|
||||
</h2>
|
||||
<span className="rounded-full bg-border/50 px-2 py-0.5 text-[0.7rem] font-bold text-muted">
|
||||
{unit.topics.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{unit.topics.map((topic) => (
|
||||
<button
|
||||
key={topic.slug}
|
||||
onClick={() => onSelect({ unit, topic })}
|
||||
className={cn(
|
||||
"group flex items-center gap-3 rounded-2xl border-2 border-l-4 border-border/60 bg-surface p-4 text-left shadow-[var(--shadow-sm)] transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[var(--shadow-md)]",
|
||||
style.accent,
|
||||
style.hover,
|
||||
)}
|
||||
>
|
||||
<span className={cn("flex h-9 w-9 shrink-0 items-center justify-center rounded-xl", style.iconBg)}>
|
||||
<PlayIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="flex-1 font-bold leading-tight">{topic.title}</span>
|
||||
<svg
|
||||
className="h-4 w-4 shrink-0 text-muted/40 transition-all duration-200 group-hover:translate-x-0.5 group-hover:text-muted"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Panel for topics that are practised in the interactive lesson ────────────
|
||||
|
||||
function LessonOnlyPanel({ unit, topic }: { unit: Unit; topic: Topic }) {
|
||||
return (
|
||||
<div className="rounded-3xl border-2 border-border/60 bg-surface p-8 text-center shadow-[var(--shadow-sm)]">
|
||||
<div className={cn("mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl", UNIT_STYLE[unit.color].iconBg)}>
|
||||
<PlayIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-bold">{topic.title}</h2>
|
||||
<p className="mx-auto mb-6 max-w-md text-sm text-muted">
|
||||
This topic uses the interactive coordinate grid, so it's best practised right in the lesson.
|
||||
</p>
|
||||
<Link href={`/lessons/${unit.slug}/${topic.slug}`}>
|
||||
<Button variant="primary" size="md">Open the interactive lesson →</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function PracticePage() {
|
||||
const [selected, setSelected] = useState<Selection | null>(null);
|
||||
const { mobileOpen, desktopCollapsed, toggle, closeMobile } = useCollapsibleSidebar();
|
||||
|
||||
function select(s: Selection) {
|
||||
setSelected(s);
|
||||
closeMobile();
|
||||
}
|
||||
|
||||
const generator = selected ? practiceFor(selected.unit, selected.topic) : null;
|
||||
|
||||
return (
|
||||
<div className="playground-bg flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<main className="playground-frame mx-auto w-full max-w-6xl flex-1 bg-white">
|
||||
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<h1 className="mb-2 text-3xl font-extrabold tracking-tight">Practice</h1>
|
||||
<p className="mb-8 text-muted">
|
||||
Pick a topic and build your confidence with fresh questions each round.
|
||||
</p>
|
||||
<div className="playground-frame relative mx-auto flex w-full max-w-7xl flex-1">
|
||||
<SidebarBackdrop show={mobileOpen} onClick={closeMobile} />
|
||||
|
||||
{/* Unit filters */}
|
||||
<div className="mb-6 flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => setFilterUnit(null)}
|
||||
className={`rounded-full border-2 px-4 py-1.5 text-sm font-extrabold transition-all duration-200 ${
|
||||
filterUnit === null
|
||||
? "border-foreground bg-foreground text-background shadow-[var(--shadow-sm)]"
|
||||
: "border-border text-muted hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
All Topics
|
||||
</button>
|
||||
{(["unit-1", "unit-2", "unit-3", "unit-4"] as const).map((uc) => (
|
||||
<button
|
||||
key={uc}
|
||||
onClick={() => setFilterUnit(filterUnit === uc ? null : uc)}
|
||||
className={`rounded-full border-2 px-4 py-1.5 text-sm font-extrabold transition-all duration-200 ${
|
||||
filterUnit === uc
|
||||
? unitColors[uc].activeFilter
|
||||
: unitColors[uc].inactiveFilter
|
||||
}`}
|
||||
>
|
||||
{unitColors[uc].label}
|
||||
</button>
|
||||
))}
|
||||
<PracticeSidebar
|
||||
selected={selected}
|
||||
onSelect={select}
|
||||
onAllTopics={() => {
|
||||
setSelected(null);
|
||||
closeMobile();
|
||||
}}
|
||||
mobileOpen={mobileOpen}
|
||||
desktopCollapsed={desktopCollapsed}
|
||||
/>
|
||||
|
||||
<main className="min-w-0 flex-1 bg-white">
|
||||
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<SidebarToggle onClick={toggle} />
|
||||
|
||||
{!selected ? (
|
||||
<>
|
||||
<h1 className="mb-2 text-3xl font-extrabold tracking-tight">Practice</h1>
|
||||
<p className="mb-8 text-muted">
|
||||
Pick a topic and build your confidence with fresh questions each round.
|
||||
</p>
|
||||
<TopicBrowser onSelect={select} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="secondary" size="sm" onClick={() => setSelected(null)} className="mb-6">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
All Topics
|
||||
</Button>
|
||||
|
||||
{generator === "lesson-only" || generator === null ? (
|
||||
<LessonOnlyPanel unit={selected.unit} topic={selected.topic} />
|
||||
) : (
|
||||
<PracticeSection
|
||||
key={`${selected.unit.slug}/${selected.topic.slug}`}
|
||||
title={`Practice: ${selected.topic.title}`}
|
||||
generator={generator}
|
||||
unitColor={selected.unit.color as PracticeUnitColor}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Topic grid */}
|
||||
{!selectedTopic && (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{filtered.map((topic) => (
|
||||
<Card
|
||||
key={`${topic.unitSlug}-${topic.topicSlug}`}
|
||||
hover
|
||||
className="cursor-pointer"
|
||||
accent={topic.unitColor}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={() => setSelectedTopic(topic)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedTopic(topic);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold">{topic.label}</span>
|
||||
<Badge variant={topic.unitColor}>
|
||||
{unitColors[topic.unitColor].label}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Practice area */}
|
||||
{selectedTopic && (
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setSelectedTopic(null)}
|
||||
className="mb-6"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Back to Topics
|
||||
</Button>
|
||||
|
||||
<PracticeSection
|
||||
title={`Practice: ${selectedTopic.label}`}
|
||||
generator={selectedTopic.generator}
|
||||
unitColor={selectedTopic.unitColor}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user