Files
cabrits-math/app/practice/page.tsx
2026-03-01 19:48:29 -04:00

169 lines
8.1 KiB
TypeScript

"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}
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>
<Footer />
</div>
);
}