form 1 topics added
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
22
app/lessons/unit-5-integers/negative-fractions/page.tsx
Normal file
22
app/lessons/unit-5-integers/negative-fractions/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
app/lessons/unit-5-integers/order-compare/page.tsx
Normal file
22
app/lessons/unit-5-integers/order-compare/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
app/lessons/unit-6-number-system/place-value/page.tsx
Normal file
22
app/lessons/unit-6-number-system/place-value/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
app/lessons/unit-7-coordinates/page.tsx
Normal file
40
app/lessons/unit-7-coordinates/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
app/lessons/unit-7-coordinates/plot-points/page.tsx
Normal file
22
app/lessons/unit-7-coordinates/plot-points/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
app/lessons/unit-7-coordinates/read-coordinates/page.tsx
Normal file
22
app/lessons/unit-7-coordinates/read-coordinates/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
app/lessons/unit-8-number-properties/exponents/page.tsx
Normal file
22
app/lessons/unit-8-number-properties/exponents/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
22
app/lessons/unit-8-number-properties/number-laws/page.tsx
Normal file
22
app/lessons/unit-8-number-properties/number-laws/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
app/lessons/unit-8-number-properties/page.tsx
Normal file
40
app/lessons/unit-8-number-properties/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
12
app/page.tsx
12
app/page.tsx
@@ -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
|
||||
|
||||
@@ -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