form 1 topics added
This commit is contained in:
356
components/explorers/number-laws-explorer.tsx
Normal file
356
components/explorers/number-laws-explorer.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
"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 = "commutative" | "associative" | "distributive";
|
||||
type Op = "+" | "-" | "\\times" | "\\div";
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
math: string;
|
||||
match?: boolean; // colour the result green (holds) / red (fails)
|
||||
}
|
||||
|
||||
const OP_META: Record<Op, { apply: (a: number, b: number) => number; commutative: boolean; associative: boolean; name: string }> = {
|
||||
"+": { apply: (a, b) => a + b, commutative: true, associative: true, name: "Addition" },
|
||||
"-": { apply: (a, b) => a - b, commutative: false, associative: false, name: "Subtraction" },
|
||||
"\\times": { apply: (a, b) => a * b, commutative: true, associative: true, name: "Multiplication" },
|
||||
"\\div": { apply: (a, b) => a / b, commutative: false, associative: false, name: "Division" },
|
||||
};
|
||||
|
||||
function fmt(x: number): string {
|
||||
if (Number.isInteger(x)) return `${x}`;
|
||||
return `${parseFloat(x.toFixed(3))}`;
|
||||
}
|
||||
|
||||
function buildCommutativeSteps(a: number, b: number, op: Op): Step[] {
|
||||
const meta = OP_META[op];
|
||||
const r1 = meta.apply(a, b);
|
||||
const r2 = meta.apply(b, a);
|
||||
const same = r1 === r2;
|
||||
return [
|
||||
{
|
||||
label: "Commutative law: does the ORDER of the numbers change the answer?",
|
||||
math: `${a} ${op} ${b} \\quad \\text{vs} \\quad ${b} ${op} ${a}`,
|
||||
},
|
||||
{ label: `Work out the first order.`, math: `${a} ${op} ${b} = ${fmt(r1)}` },
|
||||
{ label: `Now swap the order.`, math: `${b} ${op} ${a} = ${fmt(r2)}` },
|
||||
meta.commutative
|
||||
? {
|
||||
label: `Same answer! ${meta.name} IS commutative — order does not matter.`,
|
||||
math: `a ${op} b = b ${op} a`,
|
||||
match: true,
|
||||
}
|
||||
: {
|
||||
label: same
|
||||
? `They match here only because the numbers are special. In general, ${meta.name.toLowerCase()} is NOT commutative.`
|
||||
: `Different answers! ${meta.name} is NOT commutative — order matters.`,
|
||||
math: `a ${op} b \\neq b ${op} a`,
|
||||
match: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildAssociativeSteps(a: number, b: number, c: number, op: Op): Step[] {
|
||||
const meta = OP_META[op];
|
||||
const left = meta.apply(meta.apply(a, b), c);
|
||||
const right = meta.apply(a, meta.apply(b, c));
|
||||
return [
|
||||
{
|
||||
label: "Associative law: does where we put the BRACKETS change the answer?",
|
||||
math: `(${a} ${op} ${b}) ${op} ${c} \\quad \\text{vs} \\quad ${a} ${op} (${b} ${op} ${c})`,
|
||||
},
|
||||
{ label: "Do the brackets on the LEFT first.", math: `(${a} ${op} ${b}) ${op} ${c} = ${fmt(meta.apply(a, b))} ${op} ${c} = ${fmt(left)}` },
|
||||
{ label: "Now do the brackets on the RIGHT first.", math: `${a} ${op} (${b} ${op} ${c}) = ${a} ${op} ${fmt(meta.apply(b, c))} = ${fmt(right)}` },
|
||||
meta.associative
|
||||
? { label: `Same answer! ${meta.name} IS associative — the grouping does not matter.`, math: `(a ${op} b) ${op} c = a ${op} (b ${op} c)`, match: true }
|
||||
: { label: `Different answers! ${meta.name} is NOT associative — the grouping matters.`, math: `(a ${op} b) ${op} c \\neq a ${op} (b ${op} c)`, match: false },
|
||||
];
|
||||
}
|
||||
|
||||
function buildDistributiveSteps(a: number, b: number, c: number): Step[] {
|
||||
const inside = b + c;
|
||||
const answer = a * inside;
|
||||
return [
|
||||
{ label: "Distributive law: a number outside a bracket multiplies EVERYTHING inside.", math: `${a} \\times (${b} + ${c})` },
|
||||
{
|
||||
label: `Multiply the ${a} by each number inside the bracket separately.`,
|
||||
math: `${a} \\times ${b} \\; + \\; ${a} \\times ${c}`,
|
||||
},
|
||||
{ label: "Working the direct way: add inside the bracket first, then multiply.", math: `${a} \\times (${b} + ${c}) = ${a} \\times ${inside} = ${answer}` },
|
||||
{ label: "Working the expanded way gives the same answer.", math: `${a * b} + ${a * c} = ${answer}`, match: 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() * 4);
|
||||
if (roll === 0) {
|
||||
const a = randInt(2, 9), b = randInt(1, 9), c = randInt(1, 9);
|
||||
return { prompt: `Fill the blank: ${a} × (${b} + ${c}) = ${a} × ${b} + ${a} × ▢`, answer: c };
|
||||
}
|
||||
if (roll === 1) {
|
||||
const a = randInt(3, 12), b = randInt(1, a - 1);
|
||||
return { prompt: `Subtraction is not commutative. What is ${a} − ${b}?`, answer: a - b };
|
||||
}
|
||||
if (roll === 2) {
|
||||
const a = randInt(2, 8), b = randInt(1, 6), c = randInt(1, 6);
|
||||
return { prompt: `Work out ${a} × (${b} + ${c})`, answer: a * (b + c) };
|
||||
}
|
||||
const a = randInt(2, 9), b = randInt(2, 9);
|
||||
return { prompt: `Multiplication is commutative, so ${a} × ${b} equals ${b} × ▢. What is ▢?`, answer: a };
|
||||
}
|
||||
|
||||
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 NumberLawsExplorer() {
|
||||
const [mode, setMode] = useState<Mode>("commutative");
|
||||
const [op, setOp] = useState<Op>("+");
|
||||
const [a, setA] = useState("8");
|
||||
const [b, setB] = useState("3");
|
||||
const [c, setC] = useState("2");
|
||||
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("");
|
||||
// distributive is fixed to + inside; keep whatever op for the others
|
||||
reset();
|
||||
}
|
||||
|
||||
const opChoices: Op[] = mode === "associative" ? ["+", "\\times", "-"] : ["+", "-", "\\times", "\\div"];
|
||||
|
||||
function handleGo() {
|
||||
setError("");
|
||||
const na = parseInt(a, 10);
|
||||
const nb = parseInt(b, 10);
|
||||
const nc = parseInt(c, 10);
|
||||
if (isNaN(na) || isNaN(nb) || (mode !== "commutative" && isNaN(nc))) {
|
||||
return setError("Enter whole numbers in every box.");
|
||||
}
|
||||
const vals = mode === "commutative" ? [na, nb] : [na, nb, nc];
|
||||
if (vals.some((v) => Math.abs(v) > 50)) return setError("Keep the numbers between -50 and 50.");
|
||||
if ((op === "\\div") && (nb === 0 || (mode === "associative" && nc === 0))) {
|
||||
return setError("Cannot divide by zero — change the numbers.");
|
||||
}
|
||||
if (mode === "commutative") setSteps(buildCommutativeSteps(na, nb, op));
|
||||
else if (mode === "associative") setSteps(buildAssociativeSteps(na, nb, nc, op));
|
||||
else setSteps(buildDistributiveSteps(na, nb, nc));
|
||||
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 showThree = mode !== "commutative";
|
||||
const showOp = mode !== "distributive";
|
||||
|
||||
const opLabel: Record<Op, string> = { "+": "+", "-": "−", "\\times": "×", "\\div": "÷" };
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Mode toggle */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["commutative", "associative", "distributive"] as Mode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => switchMode(m)}
|
||||
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold capitalize 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Operation tabs */}
|
||||
{showOp && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{opChoices.map((o) => (
|
||||
<button
|
||||
key={o}
|
||||
onClick={() => {
|
||||
setOp(o);
|
||||
reset();
|
||||
}}
|
||||
className={`h-10 w-12 rounded-full border-2 text-lg font-bold 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"
|
||||
}`}
|
||||
>
|
||||
{opLabel[o]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input card */}
|
||||
<Card>
|
||||
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-8">
|
||||
{mode === "distributive" ? "Enter a, b and c for a × (b + c)" : showThree ? "Enter three numbers" : "Enter two numbers"}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<input type="number" value={a} onChange={(e) => setA(e.target.value)} className="w-20 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="a" />
|
||||
<input type="number" value={b} onChange={(e) => setB(e.target.value)} className="w-20 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="b" />
|
||||
{showThree && (
|
||||
<input type="number" value={c} onChange={(e) => setC(e.target.value)} className="w-20 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="c" />
|
||||
)}
|
||||
<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 numbers 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-2xl ${step.match === true ? "text-correct" : step.match === false ? "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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user