form 1 topics added
This commit is contained in:
351
components/explorers/place-value-explorer.tsx
Normal file
351
components/explorers/place-value-explorer.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
"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";
|
||||
|
||||
interface Column {
|
||||
exponent: number;
|
||||
placeValue: number;
|
||||
digit: string;
|
||||
contrib: number;
|
||||
}
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
math: string;
|
||||
activeCol: number | null;
|
||||
revealed: number; // how many columns show their contribution
|
||||
}
|
||||
|
||||
const PLACE_NAMES = ["ones", "tens", "hundreds", "thousands", "ten thousands", "hundred thousands"];
|
||||
|
||||
function columnsFor(n: number): Column[] {
|
||||
const str = `${n}`;
|
||||
const len = str.length;
|
||||
return str.split("").map((d, i) => {
|
||||
const exponent = len - 1 - i;
|
||||
return {
|
||||
exponent,
|
||||
placeValue: Math.pow(10, exponent),
|
||||
digit: d,
|
||||
contrib: parseInt(d, 10) * Math.pow(10, exponent),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildSteps(n: number): Step[] {
|
||||
const cols = columnsFor(n);
|
||||
const len = cols.length;
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: "In the denary (base 10) system, each digit has a place value that is a power of 10.",
|
||||
math: `${n}`,
|
||||
activeCol: null,
|
||||
revealed: 0,
|
||||
},
|
||||
];
|
||||
|
||||
cols.forEach((c, i) => {
|
||||
steps.push({
|
||||
label: `The ${PLACE_NAMES[c.exponent]} digit is ${c.digit}. Its place value is 10^${c.exponent} = ${c.placeValue.toLocaleString("en-US")}.`,
|
||||
math: `${c.digit} \\times 10^{${c.exponent}} = ${c.contrib.toLocaleString("en-US")}`,
|
||||
activeCol: i,
|
||||
revealed: i + 1,
|
||||
});
|
||||
});
|
||||
|
||||
const expanded = cols.map((c) => `${c.digit} \\times 10^{${c.exponent}}`).join(" + ");
|
||||
steps.push({
|
||||
label: "Put it all together — this is the number in expanded form.",
|
||||
math: `${n} = ${expanded}`,
|
||||
activeCol: null,
|
||||
revealed: len,
|
||||
});
|
||||
|
||||
const sumParts = cols.map((c) => c.contrib.toLocaleString("en-US")).join(" + ");
|
||||
steps.push({
|
||||
label: "Add every part back up and you return to the original number.",
|
||||
math: `${sumParts} = ${n.toLocaleString("en-US")}`,
|
||||
activeCol: null,
|
||||
revealed: len,
|
||||
});
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
function PlaceValueChart({ columns, step }: { columns: Column[]; step: Step }) {
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-xl border-2 border-unit-6/30 bg-unit-6-light/40 p-3">
|
||||
<table className="mx-auto border-collapse font-mono">
|
||||
<tbody>
|
||||
<tr>
|
||||
{columns.map((c, i) => (
|
||||
<td
|
||||
key={`p-${i}`}
|
||||
className={`min-w-[64px] border-2 border-unit-6/40 px-3 py-2 text-center text-sm font-bold text-unit-6-dark ${
|
||||
step.activeCol === i ? "bg-unit-6 text-white" : "bg-unit-6-light"
|
||||
}`}
|
||||
>
|
||||
10<sup>{c.exponent}</sup>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{columns.map((c, i) => (
|
||||
<td
|
||||
key={`v-${i}`}
|
||||
className={`min-w-[64px] border-2 border-unit-6/40 px-3 py-2 text-center text-sm font-bold text-unit-6 ${
|
||||
step.activeCol === i ? "bg-unit-6-light" : "bg-surface"
|
||||
}`}
|
||||
>
|
||||
{c.placeValue.toLocaleString("en-US")}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{columns.map((c, i) => (
|
||||
<td
|
||||
key={`d-${i}`}
|
||||
className={`min-w-[64px] border-2 border-unit-6/40 px-3 py-2 text-center text-2xl font-extrabold ${
|
||||
step.activeCol === i ? "bg-unit-6-light text-unit-6-dark" : "bg-surface text-foreground"
|
||||
}`}
|
||||
>
|
||||
{c.digit}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{columns.map((c, i) => (
|
||||
<td
|
||||
key={`c-${i}`}
|
||||
className={`min-w-[64px] border-2 border-unit-6/40 px-3 py-2 text-center text-sm font-bold ${
|
||||
i < step.revealed ? "bg-correct-light text-correct" : "bg-[#fafbfc] text-muted/50"
|
||||
}`}
|
||||
>
|
||||
{i < step.revealed ? c.contrib.toLocaleString("en-US") : "_"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 {
|
||||
if (Math.random() < 0.5) {
|
||||
// value of a digit
|
||||
const n = randInt(1000, 99999);
|
||||
const cols = columnsFor(n);
|
||||
const nonZero = cols.filter((c) => c.digit !== "0");
|
||||
const pick = nonZero[Math.floor(Math.random() * nonZero.length)];
|
||||
return {
|
||||
prompt: `In ${n.toLocaleString("en-US")}, what is the VALUE of the digit ${pick.digit}?`,
|
||||
answer: pick.contrib,
|
||||
};
|
||||
}
|
||||
// exponent of a named column
|
||||
const exp = randInt(0, 5);
|
||||
return {
|
||||
prompt: `Which power of 10 is the ${PLACE_NAMES[exp]} column? Enter the exponent.`,
|
||||
answer: exp,
|
||||
};
|
||||
}
|
||||
|
||||
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-6">Quick Practice</p>
|
||||
<span className="rounded-full bg-unit-6-light px-3 py-1 text-xs font-bold text-unit-6-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-32 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-6"
|
||||
}`}
|
||||
placeholder="?"
|
||||
aria-label="Your answer"
|
||||
/>
|
||||
{feedback === null ? (
|
||||
<button onClick={check} className="rounded-lg bg-unit-6 px-5 py-2.5 text-sm font-bold text-white hover:bg-unit-6-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.toLocaleString("en-US")}.</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main explorer ────────────────────────────────────────────────────────────
|
||||
|
||||
export function PlaceValueExplorer() {
|
||||
const [input, setInput] = useState("983275");
|
||||
const [error, setError] = useState("");
|
||||
const [columns, setColumns] = useState<Column[] | null>(null);
|
||||
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);
|
||||
setColumns(null);
|
||||
setCurrentStep(0);
|
||||
setIsPlaying(false);
|
||||
}, []);
|
||||
|
||||
function handleGo() {
|
||||
setError("");
|
||||
const cleaned = input.replace(/[,\s]/g, "");
|
||||
const n = parseInt(cleaned, 10);
|
||||
if (isNaN(n) || !/^\d+$/.test(cleaned)) return setError("Enter a whole number (digits only).");
|
||||
if (n < 1 || n > 999999) return setError("Use a whole number between 1 and 999,999.");
|
||||
setColumns(columnsFor(n));
|
||||
setSteps(buildSteps(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;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Input card */}
|
||||
<Card>
|
||||
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-6">
|
||||
Enter a whole number (up to 999,999)
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleGo();
|
||||
}}
|
||||
className="w-44 rounded-lg border-2 border-unit-6 bg-surface px-3 py-2 text-center font-mono text-2xl font-bold text-unit-6 outline-none focus:border-unit-6-dark"
|
||||
aria-label="Number input"
|
||||
/>
|
||||
<button
|
||||
onClick={handleGo}
|
||||
className="rounded-lg bg-unit-6 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-6-dark"
|
||||
>
|
||||
Break Down →
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="mt-2 text-sm text-incorrect">{error}</p>}
|
||||
</Card>
|
||||
|
||||
{/* Display card */}
|
||||
<Card className="flex min-h-[280px] flex-col items-center justify-center gap-4 p-6">
|
||||
{!step || !columns ? (
|
||||
<p className="text-muted/50">
|
||||
Enter a number above and click <strong>Break Down</strong>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-center text-sm font-medium text-muted">{step.label}</p>
|
||||
<MathDisplay math={step.math} className="text-xl" />
|
||||
<PlaceValueChart columns={columns} step={step} />
|
||||
</>
|
||||
)}
|
||||
</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