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,372 @@
"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 = "expand" | "index";
type IndexOp = "multiply" | "divide";
interface Step {
label: string;
math: string;
}
function repeat(base: number, count: number): string {
return Array.from({ length: count }, () => `${base}`).join(" \\times ");
}
function buildExpandSteps(base: number, exp: number): Step[] {
if (exp === 0) {
return [
{ label: "The power (index) is 0.", math: `${base}^{0}` },
{ label: "Any number raised to the power 0 is equal to 1.", math: `${base}^{0} = 1` },
];
}
if (exp === 1) {
return [
{ label: "The power (index) is 1.", math: `${base}^{1}` },
{ label: "Any number raised to the power 1 is itself.", math: `${base}^{1} = ${base}` },
];
}
const steps: Step[] = [
{
label: `The base is ${base} and the power is ${exp}: multiply the base by itself ${exp} times.`,
math: `${base}^{${exp}}`,
},
];
let running = base;
for (let k = 2; k <= exp; k++) {
running *= base;
steps.push({
label: `Multiply on another ${base}.`,
math: `${repeat(base, k)} = ${running}`,
});
}
steps.push({ label: "So the power is worked out.", math: `${base}^{${exp}} = ${running}` });
return steps;
}
function buildIndexSteps(base: number, m: number, n: number, op: IndexOp): Step[] {
if (op === "multiply") {
const total = m + n;
const value = Math.pow(base, total);
const steps: Step[] = [
{ label: "Multiplying powers of the SAME base.", math: `${base}^{${m}} \\times ${base}^{${n}}` },
{ label: "Write each power out in full.", math: `(${repeat(base, m)}) \\times (${repeat(base, n)})` },
{ label: `Count the factors: ${m} + ${n} = ${total}. When multiplying, ADD the powers.`, math: `${base}^{${m}} \\times ${base}^{${n}} = ${base}^{${m}+${n}} = ${base}^{${total}}` },
];
if (value <= 1e7) steps.push({ label: "Work out the value.", math: `${base}^{${total}} = ${value}` });
return steps;
}
// divide, m >= n
const diff = m - n;
const value = Math.pow(base, diff);
const steps: Step[] = [
{ label: "Dividing powers of the SAME base.", math: `\\frac{${base}^{${m}}}{${base}^{${n}}}` },
{ label: "Write each power out in full.", math: `\\frac{${repeat(base, m)}}{${repeat(base, n)}}` },
{ label: `Cancel the ${n} matching factor${n === 1 ? "" : "s"} from top and bottom.`, math: `${base}^{${m}} \\div ${base}^{${n}} = ${base}^{${m}-${n}} = ${base}^{${diff}}` },
];
if (value <= 1e7) steps.push({ label: "Work out the value.", math: `${base}^{${diff}} = ${value}` });
return steps;
}
// ── Quick practice ───────────────────────────────────────────────────────────
interface PracticeQ {
prompt: string;
answer: number;
}
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const SUPERSCRIPT: Record<string, string> = {
"0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴",
"5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹",
};
function sup(n: number): string {
return String(n).split("").map((c) => SUPERSCRIPT[c] ?? c).join("");
}
function makeQuestion(): PracticeQ {
const roll = Math.floor(Math.random() * 5);
if (roll === 0) {
const base = randInt(2, 7);
const exp = randInt(2, 3);
return { prompt: `What is ${base}${sup(exp)}?`, answer: Math.pow(base, exp) };
}
if (roll === 1) {
const base = randInt(2, 9);
return { prompt: `What is ${base}${sup(0)}?`, answer: 1 };
}
if (roll === 2) {
const base = randInt(2, 4);
const a = randInt(2, 3);
const b = 1;
return { prompt: `What is ${base}${sup(a)} ${base}${sup(b)}?`, answer: Math.pow(base, a) - Math.pow(base, b) };
}
if (roll === 3) {
const base = randInt(2, 5);
const m = randInt(3, 6);
const n = randInt(1, m - 1);
return { prompt: `Simplify ${base}${sup(m)} ÷ ${base}${sup(n)}. The answer is ${base} to what power?`, answer: m - n };
}
const base = randInt(2, 6);
const m = randInt(1, 4);
const n = randInt(1, 4);
return { prompt: `Simplify ${base}${sup(m)} × ${base}${sup(n)}. The answer is ${base} to what power?`, answer: m + 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 ExponentsExplorer() {
const [mode, setMode] = useState<Mode>("expand");
const [indexOp, setIndexOp] = useState<IndexOp>("multiply");
const [base, setBase] = useState("2");
const [power, setPower] = useState("4");
const [m, setM] = useState("4");
const [n, setN] = 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(md: Mode) {
setMode(md);
setError("");
reset();
}
function handleGo() {
setError("");
const b = parseInt(base, 10);
if (isNaN(b) || b < 2 || b > 12) return setError("Use a base between 2 and 12.");
if (mode === "expand") {
const e = parseInt(power, 10);
if (isNaN(e) || e < 0 || e > 6) return setError("Use a power between 0 and 6.");
if (Math.pow(b, e) > 1_000_000) return setError("That gets too big. Try a smaller base or power.");
setSteps(buildExpandSteps(b, e));
} else {
const mm = parseInt(m, 10);
const nn = parseInt(n, 10);
if (isNaN(mm) || isNaN(nn) || mm < 1 || nn < 1 || mm > 8 || nn > 8) return setError("Use powers between 1 and 8.");
if (indexOp === "divide" && mm < nn) return setError("For division, the top power must be at least the bottom power.");
setSteps(buildIndexSteps(b, mm, nn, indexOp));
}
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;
return (
<div className="space-y-4">
{/* Mode toggle */}
<div className="flex flex-wrap gap-2">
{(["expand", "index"] as Mode[]).map((md) => (
<button
key={md}
onClick={() => switchMode(md)}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
mode === md
? "border-unit-8 bg-unit-8 text-white"
: "border-unit-8/40 text-unit-8 hover:bg-unit-8-light"
}`}
>
{md === "expand" ? "Expand a Power" : "Index Laws"}
</button>
))}
</div>
{/* Index op tabs */}
{mode === "index" && (
<div className="flex flex-wrap gap-2">
{(["multiply", "divide"] as IndexOp[]).map((o) => (
<button
key={o}
onClick={() => {
setIndexOp(o);
reset();
}}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
indexOp === 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 === "multiply" ? "Multiply (add powers)" : "Divide (subtract powers)"}
</button>
))}
</div>
)}
{/* Input card */}
<Card>
{mode === "expand" ? (
<>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-8">Enter a base and a power</p>
<div className="flex flex-wrap items-center gap-3">
<input type="number" value={base} onChange={(e) => setBase(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="Base" />
<span className="text-sm font-bold text-muted">to the power</span>
<input type="number" value={power} onChange={(e) => setPower(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="Power" />
<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>
</>
) : (
<>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-8">
Same base, two powers {indexOp === "multiply" ? "to multiply" : "to divide"}
</p>
<div className="flex flex-wrap items-center gap-3">
<span className="text-sm font-bold text-muted">base</span>
<input type="number" value={base} onChange={(e) => setBase(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="Base" />
<span className="text-sm font-bold text-muted">powers</span>
<input type="number" value={m} onChange={(e) => setM(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="First power" />
<span className="text-lg font-bold text-muted">{indexOp === "multiply" ? "×" : "÷"}</span>
<input type="number" value={n} onChange={(e) => setN(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="Second power" />
<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 values 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" />
</>
)}
</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>
);
}