form 1 topics added

This commit is contained in:
2026-07-05 11:12:45 -04:00
parent a614532810
commit a23fa53772
58 changed files with 7089 additions and 475 deletions

View File

@@ -33,6 +33,14 @@
--unit-6-light: #eef2ff;
--unit-6-dark: #3730a3;
--unit-7: #16a34a;
--unit-7-light: #dcfce7;
--unit-7-dark: #15803d;
--unit-8: #e11d48;
--unit-8-light: #ffe4e9;
--unit-8-dark: #be123c;
--correct: #16a34a;
--correct-light: #dcfce7;
--incorrect: #dc2626;
@@ -73,6 +81,12 @@
--color-unit-6: var(--unit-6);
--color-unit-6-light: var(--unit-6-light);
--color-unit-6-dark: var(--unit-6-dark);
--color-unit-7: var(--unit-7);
--color-unit-7-light: var(--unit-7-light);
--color-unit-7-dark: var(--unit-7-dark);
--color-unit-8: var(--unit-8);
--color-unit-8-light: var(--unit-8-light);
--color-unit-8-dark: var(--unit-8-dark);
--color-correct: var(--correct);
--color-correct-light: var(--correct-light);

View File

@@ -1,7 +1,6 @@
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import { Sidebar } from "@/components/layout/sidebar";
import { MobileNav } from "@/components/layout/mobile-nav";
import { LessonsShell } from "@/components/layout/lessons-shell";
export default function LessonsLayout({
children,
@@ -11,17 +10,7 @@ export default function LessonsLayout({
return (
<div className="playground-bg flex min-h-screen flex-col">
<Header />
<div className="playground-frame relative mx-auto flex w-full max-w-6xl flex-1">
<Sidebar />
<main className="flex-1 bg-white">
<div className="relative p-4 lg:hidden">
<MobileNav />
</div>
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
{children}
</div>
</main>
</div>
<LessonsShell>{children}</LessonsShell>
<Footer />
</div>
);

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { NegativeFractionsExplorer } from "@/components/explorers/negative-fractions-explorer";
export default function NegativeFractionsPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 5: Integers", href: "/lessons/unit-5-integers" },
{ label: "Negative Fractions" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Negative Fractions</h1>
<NegativeFractionsExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { IntegerOrderCompareExplorer } from "@/components/explorers/integer-order-compare-explorer";
export default function IntegerOrderComparePage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 5: Integers", href: "/lessons/unit-5-integers" },
{ label: "Order & Compare" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Order and Compare Integers</h1>
<IntegerOrderCompareExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { PlaceValueExplorer } from "@/components/explorers/place-value-explorer";
export default function PlaceValuePage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 6: Number System", href: "/lessons/unit-6-number-system" },
{ label: "Place Value" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Denary Place Value (Base 10)</h1>
<PlaceValueExplorer />
</div>
);
}

View File

@@ -0,0 +1,40 @@
import Link from "next/link";
import { curriculum } from "@/lib/curriculum";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
export default function Unit7Overview() {
const unit = curriculum[6];
return (
<div>
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: `Unit 7: ${unit.title}` },
]}
/>
<div className="mb-10">
<Badge variant="unit-7" className="mb-3">Unit 7</Badge>
<h1 className="mb-2 text-3xl font-bold tracking-tight">{unit.title}</h1>
<p className="text-muted leading-relaxed">{unit.description}</p>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{unit.topics.map((topic, i) => (
<Link key={topic.slug} href={`/lessons/${unit.slug}/${topic.slug}`}>
<Card accent="unit-7" hover className="group h-full">
<div className="mb-2 flex items-center gap-2">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-unit-7-light text-xs font-bold text-unit-7-dark shadow-[var(--shadow-sm)]">
{i + 1}
</span>
</div>
<h3 className="mb-1 font-semibold">{topic.title}</h3>
<p className="text-sm leading-relaxed text-muted">{topic.description}</p>
</Card>
</Link>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { PlotPointsExplorer } from "@/components/explorers/plot-points-explorer";
export default function PlotPointsPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 7: Coordinates", href: "/lessons/unit-7-coordinates" },
{ label: "Plot Points" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Plot Points on the Cartesian Plane</h1>
<PlotPointsExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { ReadCoordinatesExplorer } from "@/components/explorers/read-coordinates-explorer";
export default function ReadCoordinatesPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 7: Coordinates", href: "/lessons/unit-7-coordinates" },
{ label: "Read Coordinates" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">State Coordinates of Points</h1>
<ReadCoordinatesExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { ExponentsExplorer } from "@/components/explorers/exponents-explorer";
export default function ExponentsPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 8: Number Properties", href: "/lessons/unit-8-number-properties" },
{ label: "Exponents" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Powers and Exponents</h1>
<ExponentsExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { IdentityInverseExplorer } from "@/components/explorers/identity-inverse-explorer";
export default function IdentityInversePage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 8: Number Properties", href: "/lessons/unit-8-number-properties" },
{ label: "Identity & Inverse" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Identity and Inverse</h1>
<IdentityInverseExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { NumberLawsExplorer } from "@/components/explorers/number-laws-explorer";
export default function NumberLawsPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 8: Number Properties", href: "/lessons/unit-8-number-properties" },
{ label: "Number Laws" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Commutative, Associative and Distributive Laws</h1>
<NumberLawsExplorer />
</div>
);
}

View File

@@ -0,0 +1,40 @@
import Link from "next/link";
import { curriculum } from "@/lib/curriculum";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
export default function Unit8Overview() {
const unit = curriculum[7];
return (
<div>
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: `Unit 8: ${unit.title}` },
]}
/>
<div className="mb-10">
<Badge variant="unit-8" className="mb-3">Unit 8</Badge>
<h1 className="mb-2 text-3xl font-bold tracking-tight">{unit.title}</h1>
<p className="text-muted leading-relaxed">{unit.description}</p>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{unit.topics.map((topic, i) => (
<Link key={topic.slug} href={`/lessons/${unit.slug}/${topic.slug}`}>
<Card accent="unit-8" hover className="group h-full">
<div className="mb-2 flex items-center gap-2">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-unit-8-light text-xs font-bold text-unit-8-dark shadow-[var(--shadow-sm)]">
{i + 1}
</span>
</div>
<h3 className="mb-1 font-semibold">{topic.title}</h3>
<p className="text-sm leading-relaxed text-muted">{topic.description}</p>
</Card>
</Link>
))}
</div>
</div>
);
}

View File

@@ -6,7 +6,7 @@ export default function NotFound() {
return (
<div className="playground-bg flex min-h-screen flex-col">
<Header />
<main className="playground-frame hero-gradient relative mx-auto flex w-full max-w-6xl flex-1 items-center justify-center">
<main className="playground-frame hero-gradient relative mx-auto flex w-full max-w-7xl flex-1 items-center justify-center">
<div className="dot-pattern absolute inset-0 opacity-25" />
<div className="relative text-center">
<p className="mb-2 text-sm font-semibold uppercase tracking-widest text-muted">

View File

@@ -35,6 +35,16 @@ const unitStyles = {
unitCard: "border-[#a5b4fc] bg-[#eef2ff]",
chip: "bg-[#3730a3]",
},
"unit-7": {
tile: "from-[#4ade80] to-[#15803d]",
unitCard: "border-[#86efac] bg-[#f0fdf4]",
chip: "bg-[#15803d]",
},
"unit-8": {
tile: "from-[#fb7185] to-[#be123c]",
unitCard: "border-[#fda4af] bg-[#fff1f2]",
chip: "bg-[#be123c]",
},
};
const topicTiles = [
@@ -59,7 +69,7 @@ export default function Home() {
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 px-3 pb-10 sm:px-5">
<main className="playground-frame mx-auto w-full max-w-7xl flex-1 px-3 pb-10 sm:px-5">
<section className="mt-4 rounded-md border-2 border-[#1f50bf] bg-[#f1f6ff] p-4 sm:p-6">
<p className="text-center text-sm font-bold text-[#26438b]">
Interactive maths practice for secondary school students

View File

@@ -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&apos;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>
);