form 1 topics added
This commit is contained in:
372
components/explorers/exponents-explorer.tsx
Normal file
372
components/explorers/exponents-explorer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user