Initial Commit

This commit is contained in:
2026-03-01 18:50:29 -04:00
parent 261c52d602
commit 364facd9f0
69 changed files with 7829 additions and 87 deletions

View File

@@ -1,26 +1,183 @@
@import "tailwindcss";
@import "katex/dist/katex.min.css";
:root {
--background: #ffffff;
--foreground: #171717;
--background: #f4f8ff;
--foreground: #10223f;
--muted: #4f6588;
--border: #cbdaef;
--surface: #FFFFFF;
--surface-raised: #FFFFFF;
--unit-1: #2563eb;
--unit-1-light: #e8f1ff;
--unit-1-dark: #1d4ed8;
--unit-2: #0ea5a4;
--unit-2-light: #e6fffb;
--unit-2-dark: #0f766e;
--unit-3: #f97316;
--unit-3-light: #fff1e7;
--unit-3-dark: #c2410c;
--unit-4: #e11d48;
--unit-4-light: #ffe8ef;
--unit-4-dark: #be123c;
--correct: #16a34a;
--correct-light: #dcfce7;
--incorrect: #dc2626;
--incorrect-light: #fee2e2;
--hint: #d97706;
--hint-light: #ffedd5;
--shadow-sm: 0 1px 2px rgb(16 34 63 / 0.06), 0 2px 8px rgb(37 99 235 / 0.04);
--shadow-md: 0 8px 18px rgb(16 34 63 / 0.09), 0 2px 8px rgb(37 99 235 / 0.08);
--shadow-lg: 0 16px 30px rgb(16 34 63 / 0.11), 0 6px 20px rgb(37 99 235 / 0.1);
--shadow-xl: 0 24px 44px rgb(16 34 63 / 0.14), 0 12px 28px rgb(37 99 235 / 0.12);
--shadow-glow: 0 0 24px rgb(37 99 235 / 0.18);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-muted: var(--muted);
--color-border: var(--border);
--color-surface: var(--surface);
--color-surface-raised: var(--surface-raised);
--color-unit-1: var(--unit-1);
--color-unit-1-light: var(--unit-1-light);
--color-unit-1-dark: var(--unit-1-dark);
--color-unit-2: var(--unit-2);
--color-unit-2-light: var(--unit-2-light);
--color-unit-2-dark: var(--unit-2-dark);
--color-unit-3: var(--unit-3);
--color-unit-3-light: var(--unit-3-light);
--color-unit-3-dark: var(--unit-3-dark);
--color-unit-4: var(--unit-4);
--color-unit-4-light: var(--unit-4-light);
--color-unit-4-dark: var(--unit-4-dark);
--color-correct: var(--correct);
--color-correct-light: var(--correct-light);
--color-incorrect: var(--incorrect);
--color-incorrect-light: var(--incorrect-light);
--color-hint: var(--hint);
--color-hint-light: var(--hint-light);
--font-sans: "Nunito", "Trebuchet MS", "Segoe UI", sans-serif;
--font-display: "Avenir Next", "Poppins", "Trebuchet MS", sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", "Consolas", monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--background);
background:
radial-gradient(circle at 15% 0%, color-mix(in srgb, var(--unit-1) 9%, transparent), transparent 46%),
radial-gradient(circle at 90% 8%, color-mix(in srgb, var(--unit-3) 9%, transparent), transparent 34%),
radial-gradient(circle at 85% 88%, color-mix(in srgb, var(--unit-2) 7%, transparent), transparent 40%),
linear-gradient(180deg, #f7fbff 0%, var(--background) 100%);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-sans);
text-wrap: pretty;
}
h1,
h2,
h3,
h4 {
font-family: var(--font-display);
letter-spacing: -0.02em;
}
::selection {
background: color-mix(in srgb, var(--unit-1) 20%, transparent);
}
/* Subtle gradient background pattern */
.hero-gradient {
background:
radial-gradient(ellipse 100% 70% at 55% -15%, color-mix(in srgb, var(--unit-1) 14%, transparent), transparent),
radial-gradient(ellipse 70% 40% at 95% 45%, color-mix(in srgb, var(--unit-3) 12%, transparent), transparent),
radial-gradient(ellipse 70% 50% at 10% 75%, color-mix(in srgb, var(--unit-2) 10%, transparent), transparent),
linear-gradient(180deg, rgb(255 255 255 / 0.94), rgb(255 255 255 / 0.82));
}
/* Dot pattern overlay */
.dot-pattern {
background-image:
linear-gradient(to right, rgb(203 218 239 / 0.45) 1px, transparent 1px),
linear-gradient(to bottom, rgb(203 218 239 / 0.45) 1px, transparent 1px);
background-size: 22px 22px;
}
@keyframes float-card {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
.float-card {
animation: float-card 3.2s ease-in-out infinite;
}
.playground-bg {
background: linear-gradient(180deg, #2f65e8 0%, #2a5fdc 100%);
}
.playground-frame {
border-left: 4px solid #1e4fc6;
border-right: 4px solid #1e4fc6;
background: #ffffff;
box-shadow: 0 16px 40px rgb(15 35 86 / 0.22);
}
.section-ribbon {
border: 2px solid #1f4fbe;
background: linear-gradient(180deg, #2f65e8 0%, #2051c3 100%);
color: white;
}
.game-thumb-grid {
background-image:
linear-gradient(90deg, rgb(255 255 255 / 0.22) 1px, transparent 1px),
linear-gradient(0deg, rgb(255 255 255 / 0.22) 1px, transparent 1px);
background-size: 14px 14px;
}
/* Smooth scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--muted);
}
/* Focus ring utility */
.focus-ring {
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus-ring:focus-visible {
outline-color: var(--unit-1);
}

View File

@@ -1,20 +1,10 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Cabrits Math Lab",
description:
"Build confidence in mathematics with interactive lessons and practice for secondary school students.",
};
export default function RootLayout({
@@ -24,9 +14,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body className="antialiased">
{children}
</body>
</html>

28
app/lessons/layout.tsx Normal file
View File

@@ -0,0 +1,28 @@
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";
export default function LessonsLayout({
children,
}: {
children: React.ReactNode;
}) {
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>
<Footer />
</div>
);
}

42
app/lessons/page.tsx Normal file
View File

@@ -0,0 +1,42 @@
import Link from "next/link";
import { curriculum } from "@/lib/curriculum";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
export default function LessonsOverview() {
return (
<div>
<h1 className="mb-2 text-3xl font-bold tracking-tight">All Topics</h1>
<p className="mb-10 text-muted">Form 1, Term 2 &mdash; Select a topic to explore</p>
{curriculum.map((unit) => (
<section key={unit.slug} className="mb-12">
<div className="mb-4 flex items-center gap-3">
<Badge variant={unit.color}>Unit {unit.number}</Badge>
<h2 className="text-xl font-semibold">{unit.title}</h2>
<span className="hidden text-sm text-muted sm:inline">{unit.weeks}</span>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{unit.topics.map((topic) => (
<Link key={topic.slug} href={`/lessons/${unit.slug}/${topic.slug}`}>
<Card accent={unit.color} hover className="group h-full">
<h3 className="mb-1 font-semibold">{topic.title}</h3>
<p className="mb-3 text-sm leading-relaxed text-muted">{topic.description}</p>
<div className="flex items-center justify-between text-xs text-muted">
<span>Week {topic.week}</span>
<span className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
Explore
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</span>
</div>
</Card>
</Link>
))}
</div>
</section>
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { FractionOperationExplorer } from "@/components/explorers/fraction-operation-explorer";
export default function AddSubtractPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 1: Fractions", href: "/lessons/unit-1-fractions" },
{ label: "Add & Subtract" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Add and Subtract Fractions</h1>
<FractionOperationExplorer />
</div>
);
}

View File

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

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { FractionQuantityExplorer } from "@/components/explorers/fraction-quantity-explorer";
export default function FractionOfQuantityPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 1: Fractions", href: "/lessons/unit-1-fractions" },
{ label: "Of a Quantity" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Fraction of a Quantity</h1>
<FractionQuantityExplorer />
</div>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
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 Unit1Overview() {
const unit = curriculum[0];
return (
<div>
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: `Unit 1: ${unit.title}` },
]}
/>
<div className="mb-10">
<Badge variant="unit-1" className="mb-3">Unit 1 &mdash; {unit.weeks}</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-1" 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-1-light text-xs font-bold text-unit-1-dark shadow-[var(--shadow-sm)]">
{i + 1}
</span>
<span className="text-xs text-muted">Week {topic.week}</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 { FractionQuantityExplorer } from "@/components/explorers/fraction-quantity-explorer";
export default function WholeFromFractionsPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 1: Fractions", href: "/lessons/unit-1-fractions" },
{ label: "Find the Whole" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Calculate the Whole from Fractions</h1>
<FractionQuantityExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { RoundingExplorer } from "@/components/explorers/rounding-explorer";
export default function ApproximatePage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 2: Decimals", href: "/lessons/unit-2-decimals" },
{ label: "Approximate" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Approximate Decimals</h1>
<RoundingExplorer />
</div>
);
}

View File

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

View File

@@ -0,0 +1,41 @@
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 Unit2Overview() {
const unit = curriculum[1];
return (
<div>
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: `Unit 2: ${unit.title}` },
]}
/>
<div className="mb-10">
<Badge variant="unit-2" className="mb-3">Unit 2 &mdash; {unit.weeks}</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-2" 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-2-light text-xs font-bold text-unit-2-dark shadow-[var(--shadow-sm)]">
{i + 7}
</span>
<span className="text-xs text-muted">Week {topic.week}</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 { StandardFormExplorer } from "@/components/explorers/standard-form-explorer";
export default function StandardFormPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 2: Decimals", href: "/lessons/unit-2-decimals" },
{ label: "Standard Form" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Standard Form (Scientific Notation)</h1>
<StandardFormExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { DecimalArithmeticExplorer } from "@/components/explorers/decimal-arithmetic-explorer";
export default function AddSubtractPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 3: Decimal Operations", href: "/lessons/unit-3-decimal-operations" },
{ label: "Add & Subtract" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Add and Subtract Decimals</h1>
<DecimalArithmeticExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { ConversionExplorer } from "@/components/explorers/conversion-explorer";
export default function ConvertPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 3: Decimal Operations", href: "/lessons/unit-3-decimal-operations" },
{ label: "Convert" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Convert Decimals and Fractions</h1>
<ConversionExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { DecimalArithmeticExplorer } from "@/components/explorers/decimal-arithmetic-explorer";
export default function MultiplyDividePage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 3: Decimal Operations", href: "/lessons/unit-3-decimal-operations" },
{ label: "Multiply & Divide" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Multiply and Divide Decimals</h1>
<DecimalArithmeticExplorer />
</div>
);
}

View File

@@ -0,0 +1,41 @@
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 Unit3Overview() {
const unit = curriculum[2];
return (
<div>
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: `Unit 3: ${unit.title}` },
]}
/>
<div className="mb-10">
<Badge variant="unit-3" className="mb-3">Unit 3 &mdash; {unit.weeks}</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-3" 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-3-light text-xs font-bold text-unit-3-dark shadow-[var(--shadow-sm)]">
{i + 10}
</span>
<span className="text-xs text-muted">Week {topic.week}</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 { RatioExplorer } from "@/components/explorers/ratio-explorer";
export default function DefineRatioPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 4: Ratio & Proportion", href: "/lessons/unit-4-ratio-proportion" },
{ label: "Define Ratio" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Define a Ratio</h1>
<RatioExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { RatioExplorer } from "@/components/explorers/ratio-explorer";
export default function DivideInRatioPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 4: Ratio & Proportion", href: "/lessons/unit-4-ratio-proportion" },
{ label: "Divide in Ratio" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Divide a Quantity in a Given Ratio</h1>
<RatioExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { RatioExplorer } from "@/components/explorers/ratio-explorer";
export default function FractionsAndRatiosPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 4: Ratio & Proportion", href: "/lessons/unit-4-ratio-proportion" },
{ label: "Fractions & Ratios" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Fractions and Ratios</h1>
<RatioExplorer />
</div>
);
}

View File

@@ -0,0 +1,41 @@
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 Unit4Overview() {
const unit = curriculum[3];
return (
<div>
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: `Unit 4: ${unit.title}` },
]}
/>
<div className="mb-10">
<Badge variant="unit-4" className="mb-3">Unit 4 &mdash; {unit.weeks}</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-4" 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-4-light text-xs font-bold text-unit-4-dark shadow-[var(--shadow-sm)]">
{String.fromCharCode(97 + i)}
</span>
<span className="text-xs text-muted">Week {topic.week}</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 { RatioExplorer } from "@/components/explorers/ratio-explorer";
export default function SimplifyRatiosPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 4: Ratio & Proportion", href: "/lessons/unit-4-ratio-proportion" },
{ label: "Simplify Ratios" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Simplify Ratios</h1>
<RatioExplorer />
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { RatioExplorer } from "@/components/explorers/ratio-explorer";
export default function WordProblemsPage() {
return (
<div className="space-y-8">
<Breadcrumbs
items={[
{ label: "Lessons", href: "/lessons" },
{ label: "Unit 4: Ratio & Proportion", href: "/lessons/unit-4-ratio-proportion" },
{ label: "Word Problems" },
]}
/>
<h1 className="text-3xl font-bold tracking-tight">Proportional Parts Word Problems</h1>
<RatioExplorer />
</div>
);
}

37
app/not-found.tsx Normal file
View File

@@ -0,0 +1,37 @@
import Link from "next/link";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
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">
<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">
Page not found
</p>
<h1 className="mb-4 text-8xl font-bold tracking-tighter">
<span className="bg-gradient-to-r from-unit-1 via-unit-4 to-unit-2 bg-clip-text text-transparent">
404
</span>
</h1>
<p className="mb-8 text-lg text-muted">
This page doesn&apos;t exist or has been moved.
</p>
<Link
href="/"
className="inline-flex items-center gap-2 rounded-xl bg-foreground px-6 py-3 font-medium text-background shadow-[var(--shadow-md)] transition-all duration-200 hover:shadow-[var(--shadow-lg)] hover:-translate-y-0.5"
>
<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 Home
</Link>
</div>
</main>
<Footer />
</div>
);
}

View File

@@ -1,65 +1,133 @@
import Image from "next/image";
import Link from "next/link";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import { curriculum } from "@/lib/curriculum";
const unitStyles = {
"unit-1": {
tile: "from-[#4e95ff] to-[#1f63ea]",
unitCard: "border-[#8cb2ff] bg-[#eaf2ff]",
chip: "bg-[#2059cc]",
},
"unit-2": {
tile: "from-[#29c6c5] to-[#0f8d8c]",
unitCard: "border-[#89e5dd] bg-[#eafffb]",
chip: "bg-[#0f8d8c]",
},
"unit-3": {
tile: "from-[#ffab55] to-[#e97617]",
unitCard: "border-[#ffd2a3] bg-[#fff4e8]",
chip: "bg-[#d26a17]",
},
"unit-4": {
tile: "from-[#ff5f85] to-[#d11b56]",
unitCard: "border-[#ffb2c7] bg-[#fff0f4]",
chip: "bg-[#c6174e]",
},
};
const topicTiles = [
"from-[#4e95ff] via-[#357fe9] to-[#1f63ea]",
"from-[#2ac3be] via-[#1da8a3] to-[#0f8d8c]",
"from-[#ffb067] via-[#f2913a] to-[#d86e16]",
"from-[#ff6f98] via-[#ef3d72] to-[#cb1c54]",
"from-[#7d95ff] via-[#6078f2] to-[#3f56d9]",
"from-[#4ccf7b] via-[#2fb45f] to-[#1f944b]",
"from-[#ff8f8f] via-[#f26b6b] to-[#d94545]",
"from-[#5eb7ff] via-[#3f99ef] to-[#2a7fda]",
];
export default function Home() {
const allTopics = curriculum.flatMap((unit) =>
unit.topics.map((topic) => ({
unit,
topic,
})),
);
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
<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">
<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
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
<h1 className="mt-2 text-center text-3xl font-extrabold text-[#17367d] sm:text-5xl">
Math Topics Students Love to Explore
</h1>
<p className="mx-auto mt-3 max-w-3xl text-center text-sm font-semibold text-[#33508e] sm:text-base">
Explore each topic with visual examples, short explanations, and guided lessons.
Pick a unit, jump into a topic, and start solving.
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-3">
<Link
href="/lessons"
className="rounded-sm bg-[#e6503e] px-5 py-2.5 text-sm font-extrabold text-white shadow-md transition-colors hover:bg-[#cc4231]"
>
Browse Lessons
</Link>
<Link
href="/practice"
className="rounded-sm bg-[#0e9d50] px-5 py-2.5 text-sm font-extrabold text-white shadow-md transition-colors hover:bg-[#0c7f41]"
>
Start Practice
</Link>
</div>
</section>
<section className="mt-5">
<h2 className="section-ribbon rounded-sm px-3 py-2 text-lg font-extrabold sm:text-xl">
Unit Worlds
</h2>
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{curriculum.map((unit) => (
<Link
key={unit.slug}
href={`/lessons/${unit.slug}`}
className={`rounded-md border-2 p-3 transition-transform hover:-translate-y-0.5 ${unitStyles[unit.color].unitCard}`}
>
<div className="flex items-center justify-between gap-2">
<h3 className="text-lg font-extrabold text-[#142f75]">{unit.title}</h3>
<span className={`rounded-full px-2 py-1 text-xs font-extrabold text-white ${unitStyles[unit.color].chip}`}>
Unit {unit.number}
</span>
</div>
<p className="mt-2 text-sm font-semibold text-[#35508b]">{unit.description}</p>
<p className="mt-2 text-xs font-bold uppercase tracking-wide text-[#4262aa]">{unit.weeks}</p>
</Link>
))}
</div>
</section>
<section className="mt-6">
<h2 className="section-ribbon rounded-sm px-3 py-2 text-lg font-extrabold sm:text-xl">
Topic Explorer
</h2>
<div className="mt-3 grid grid-cols-2 gap-2.5 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{allTopics.map(({ unit, topic }, index) => (
<Link
key={`${unit.slug}-${topic.slug}`}
href={`/lessons/${unit.slug}/${topic.slug}`}
className="rounded-sm border border-[#8ea9df] bg-white p-2 transition-transform hover:-translate-y-0.5 hover:shadow-md"
>
<div className={`game-thumb-grid mb-2 flex aspect-[4/3] flex-col justify-between rounded-sm bg-gradient-to-br p-2 text-white ${topicTiles[index % topicTiles.length]}`}>
<span className="w-fit rounded-sm bg-black/25 px-1.5 py-0.5 text-[10px] font-extrabold uppercase tracking-wide">
Week {topic.week}
</span>
<span className="text-center text-xs font-extrabold leading-tight">
{topic.shortTitle}
</span>
</div>
<p className="min-h-[2.5rem] text-xs font-extrabold text-[#193981]">
{topic.title}
</p>
</Link>
))}
</div>
</section>
</main>
<Footer />
</div>
);
}

160
app/practice/page.tsx Normal file
View File

@@ -0,0 +1,160 @@
"use client";
import { useState } from "react";
import { Card } from "@/components/ui/card";
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 type { Difficulty, MathProblem } from "@/lib/problems/types";
import { generateFractionAddSubtract, generateFractionMultiply, generateFractionDivide, generateFractionOfQuantity, generateWholeFromFraction } 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";
interface TopicGenerator {
unitSlug: string;
topicSlug: string;
label: string;
generator: (difficulty: Difficulty) => MathProblem;
unitColor: "unit-1" | "unit-2" | "unit-3" | "unit-4";
}
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",
},
};
export default function PracticePage() {
const [selectedTopic, setSelectedTopic] = useState<TopicGenerator | null>(null);
const [filterUnit, setFilterUnit] = useState<UnitColor | null>(null);
const filtered = filterUnit
? TOPIC_GENERATORS.filter((t) => t.unitColor === filterUnit)
: TOPIC_GENERATORS;
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>
{/* 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>
))}
</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}
onClick={() => 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>
<Footer />
</div>
);
}