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

@@ -0,0 +1,331 @@
"use client";
import { useState, useCallback } from "react";
import { Card } from "@/components/ui/card";
import { ClientOnly } from "@/components/ui/client-only";
import { StepControls } from "./step-controls";
import { MathDisplay } from "@/components/math/math-display";
type Mode = "identity" | "inverse" | "zero";
type Op = "add" | "multiply";
interface Step {
label: string;
math: string;
danger?: boolean;
}
function buildIdentitySteps(n: number, op: Op): Step[] {
if (op === "add") {
return [
{ label: "The identity for addition is 0 — it leaves a number unchanged.", math: `${n} + 0` },
{ label: "Adding 0 changes nothing.", math: `${n} + 0 = ${n}` },
{ label: "So 0 is the identity element for addition.", math: `n + 0 = n` },
];
}
return [
{ label: "The identity for multiplication is 1 — it leaves a number unchanged.", math: `${n} \\times 1` },
{ label: "Multiplying by 1 changes nothing.", math: `${n} \\times 1 = ${n}` },
{ label: "So 1 is the identity element for multiplication.", math: `n \\times 1 = n` },
];
}
function buildInverseSteps(n: number, op: Op): Step[] {
if (op === "add") {
const inv = -n;
const invText = inv < 0 ? `(${inv})` : `${inv}`;
return [
{ label: "The additive inverse is the number that brings you back to the identity, 0.", math: `${n} + \\square = 0` },
{ label: `Flip the sign of ${n} to get its additive inverse.`, math: `\\text{inverse of } ${n} = ${inv}` },
{ label: "Check: adding a number and its inverse gives 0.", math: `${n} + ${invText} = 0` },
];
}
// multiplicative inverse (reciprocal)
const absN = Math.abs(n);
const recip = n < 0 ? `-\\frac{1}{${absN}}` : `\\frac{1}{${absN}}`;
const recipParen = n < 0 ? `\\left(-\\frac{1}{${absN}}\\right)` : `\\frac{1}{${absN}}`;
return [
{ label: "The multiplicative inverse (reciprocal) brings you back to the identity, 1.", math: `${n} \\times \\square = 1` },
{ label: `Write ${n} as a fraction and flip it. The sign stays in front.`, math: `\\text{inverse of } ${n} = ${recip}` },
{ label: "Check: a number times its reciprocal gives 1.", math: `${n} \\times ${recipParen} = 1` },
];
}
function buildZeroSteps(n: number): Step[] {
return [
{ label: "Multiplying any number by zero always gives zero.", math: `${n} \\times 0 = 0` },
{ label: "Zero shared into any number of groups is still zero.", math: `0 \\div ${n === 0 ? 5 : n} = 0` },
{
label: "But you can never divide BY zero — there is no answer. It is undefined.",
math: `${n} \\div 0 = \\text{undefined}`,
danger: true,
},
];
}
// ── Quick practice ───────────────────────────────────────────────────────────
interface PracticeQ {
prompt: string;
answer: number;
}
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function makeQuestion(): PracticeQ {
const roll = Math.floor(Math.random() * 5);
if (roll === 0) {
const n = randInt(-12, 12) || 3;
return { prompt: `What is the additive inverse of ${n}?`, answer: -n };
}
if (roll === 1) {
const n = randInt(1, 20);
return { prompt: `What is ${n} × 0?`, answer: 0 };
}
if (roll === 2) {
return { prompt: `What number is the identity for addition?`, answer: 0 };
}
if (roll === 3) {
return { prompt: `What number is the identity for multiplication?`, answer: 1 };
}
const n = randInt(2, 12);
return { prompt: `The multiplicative inverse of ${n} is 1 over what number?`, answer: n };
}
function QuickPractice() {
const [q, setQ] = useState<PracticeQ>(() => makeQuestion());
const [val, setVal] = useState("");
const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null);
const [score, setScore] = useState({ correct: 0, total: 0 });
function check() {
const parsed = parseInt(val, 10);
if (isNaN(parsed)) return;
const correct = parsed === q.answer;
setFeedback(correct ? "correct" : "incorrect");
setScore((s) => ({ correct: s.correct + (correct ? 1 : 0), total: s.total + 1 }));
}
function next() {
setQ(makeQuestion());
setVal("");
setFeedback(null);
}
return (
<Card className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-xs font-bold uppercase tracking-wider text-unit-8">Quick Practice</p>
<span className="rounded-full bg-unit-8-light px-3 py-1 text-xs font-bold text-unit-8-dark">
{score.correct}/{score.total}
</span>
</div>
<p className="text-center text-lg font-bold">{q.prompt}</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<input
type="number"
value={val}
onChange={(e) => {
setVal(e.target.value);
setFeedback(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (feedback !== null) next();
else check();
}
}}
className={`w-24 rounded-lg border-2 px-3 py-2 text-center text-xl font-bold outline-none transition-colors ${
feedback === "correct"
? "border-correct bg-correct-light"
: feedback === "incorrect"
? "border-incorrect bg-incorrect-light"
: "border-border focus:border-unit-8"
}`}
placeholder="?"
aria-label="Your answer"
/>
{feedback === null ? (
<button onClick={check} className="rounded-lg bg-unit-8 px-5 py-2.5 text-sm font-bold text-white hover:bg-unit-8-dark">
Check
</button>
) : (
<button onClick={next} className="rounded-lg bg-foreground px-5 py-2.5 text-sm font-bold text-background hover:bg-foreground/80">
Next
</button>
)}
</div>
{feedback === "correct" && <p className="text-center text-sm font-bold text-correct">Correct!</p>}
{feedback === "incorrect" && (
<p className="text-center text-sm font-bold text-incorrect">Not quite the answer is {q.answer}.</p>
)}
</Card>
);
}
// ── Main explorer ────────────────────────────────────────────────────────────
export function IdentityInverseExplorer() {
const [mode, setMode] = useState<Mode>("identity");
const [op, setOp] = useState<Op>("add");
const [input, setInput] = useState("3");
const [error, setError] = useState("");
const [steps, setSteps] = useState<Step[] | null>(null);
const [currentStep, setCurrentStep] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const done = steps ? currentStep >= steps.length - 1 : false;
const reset = useCallback(() => {
setSteps(null);
setCurrentStep(0);
setIsPlaying(false);
}, []);
function switchMode(m: Mode) {
setMode(m);
setError("");
reset();
}
function handleGo() {
setError("");
const n = parseInt(input, 10);
if (isNaN(n)) return setError("Enter a valid whole number.");
if (Math.abs(n) > 100) return setError("Keep the number between -100 and 100.");
if (mode === "inverse" && op === "multiply" && n === 0) {
return setError("Zero has no multiplicative inverse — try another number.");
}
if (mode === "identity") setSteps(buildIdentitySteps(n, op));
else if (mode === "inverse") setSteps(buildInverseSteps(n, op));
else setSteps(buildZeroSteps(n));
setCurrentStep(0);
setIsPlaying(false);
}
const stepForward = useCallback(() => {
if (!steps || currentStep >= steps.length - 1) return;
setCurrentStep((s) => s + 1);
if (currentStep + 1 >= steps.length - 1) setIsPlaying(false);
}, [steps, currentStep]);
const stepBack = useCallback(() => {
if (currentStep <= 0) return;
setCurrentStep((s) => s - 1);
}, [currentStep]);
const togglePlay = useCallback(() => setIsPlaying((p) => !p), []);
const step = steps ? steps[currentStep] : null;
const showOp = mode !== "zero";
return (
<div className="space-y-4">
{/* Mode toggle */}
<div className="flex flex-wrap gap-2">
{(["identity", "inverse", "zero"] as Mode[]).map((m) => (
<button
key={m}
onClick={() => switchMode(m)}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
mode === m
? "border-unit-8 bg-unit-8 text-white"
: "border-unit-8/40 text-unit-8 hover:bg-unit-8-light"
}`}
>
{m === "identity" ? "Identity" : m === "inverse" ? "Inverse" : "Zero Facts"}
</button>
))}
</div>
{/* Operation tabs */}
{showOp && (
<div className="flex flex-wrap gap-2">
{(["add", "multiply"] as Op[]).map((o) => (
<button
key={o}
onClick={() => {
setOp(o);
reset();
}}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
op === o
? "border-unit-8-dark bg-unit-8-dark text-white"
: "border-unit-8/30 text-unit-8-dark hover:bg-unit-8-light"
}`}
>
{o === "add" ? "Addition" : "Multiplication"}
</button>
))}
</div>
)}
{/* Input card */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-8">
{mode === "identity"
? "See how the identity leaves a number unchanged"
: mode === "inverse"
? "Find the inverse of a number"
: "Explore what happens with zero"}
</p>
<div className="flex flex-wrap items-center gap-3">
<input
type="number"
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-24 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-8"
aria-label="Number"
/>
<button
onClick={handleGo}
className="rounded-lg bg-unit-8 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-8-dark"
>
Go
</button>
</div>
{error && <p className="mt-2 text-sm text-incorrect">{error}</p>}
</Card>
{/* Display card */}
<Card className="flex min-h-[220px] flex-col items-center justify-center gap-4 p-6">
{!step ? (
<p className="text-muted/50">
Enter a number above and click <strong>Go</strong>
</p>
) : (
<>
<p className="text-center text-sm font-medium text-muted">{step.label}</p>
<MathDisplay
math={step.math}
className={`text-3xl ${step.danger ? "text-incorrect" : ""}`}
/>
</>
)}
</Card>
{/* Controls */}
<Card className="p-4">
<StepControls
currentStep={currentStep + 1}
totalSteps={steps?.length ?? 0}
isPlaying={isPlaying}
onStepForward={stepForward}
onStepBack={stepBack}
onTogglePlay={togglePlay}
onReset={reset}
canStepForward={!!steps && !done}
canStepBack={!!steps && currentStep > 0}
/>
</Card>
{/* Quick practice */}
<ClientOnly fallback={<Card className="min-h-[220px]" />}>
<QuickPractice />
</ClientOnly>
</div>
);
}