Initial Commit

This commit is contained in:
2026-03-01 18:50:29 -04:00
parent 261c52d602
commit 364facd9f0
69 changed files with 7829 additions and 87 deletions

View File

@@ -0,0 +1,403 @@
"use client";
import { useState, useCallback } from "react";
import { Card } from "@/components/ui/card";
import { StepControls } from "./step-controls";
type DecOp = "add" | "subtract" | "multiply" | "divide";
interface Step {
label: string;
columns: ColumnDisplay[];
carry?: string;
resultRow?: string;
}
interface ColumnDisplay {
rows: string[];
highlight?: number; // which row is highlighted
separator?: boolean; // line above this row
}
function padAndAlign(a: string, b: string): { aStr: string; bStr: string; dotPos: number } {
const aParts = a.split(".");
const bParts = b.split(".");
const aInt = aParts[0] || "0";
const bInt = bParts[0] || "0";
const aDec = aParts[1] || "";
const bDec = bParts[1] || "";
const maxInt = Math.max(aInt.length, bInt.length);
const maxDec = Math.max(aDec.length, bDec.length);
const aAligned = aInt.padStart(maxInt, " ") + (maxDec > 0 ? "." + aDec.padEnd(maxDec, "0") : "");
const bAligned = bInt.padStart(maxInt, " ") + (maxDec > 0 ? "." + bDec.padEnd(maxDec, "0") : "");
return { aStr: aAligned, bStr: bAligned, dotPos: maxInt };
}
function buildAddSubSteps(a: number, b: number, op: DecOp): Step[] {
const steps: Step[] = [];
const aStr = a.toString();
const bStr = b.toString();
const { aStr: aAligned, bStr: bAligned } = padAndAlign(aStr, bStr);
const opSymbol = op === "add" ? "+" : "";
const result = op === "add" ? a + b : a - b;
const resultStr = parseFloat(result.toFixed(10)).toString();
// Step 0: Show the problem
steps.push({
label: `${aStr} ${opSymbol} ${bStr}`,
columns: [],
});
// Step 1: Align decimal points
steps.push({
label: "Align the decimal points",
columns: [
{ rows: [aAligned, `${opSymbol} ${bAligned}`] },
],
});
// Step 2: Add trailing zeros
steps.push({
label: "Fill in zeros as placeholders",
columns: [
{ rows: [aAligned, `${opSymbol} ${bAligned}`], separator: true },
],
});
// Step 3: Compute
const { aStr: aFinal, bStr: bFinal } = padAndAlign(aStr, bStr);
const resultAligned = padAndAlign(resultStr, aStr).aStr;
steps.push({
label: `Compute column by column`,
columns: [
{ rows: [aFinal, `${opSymbol} ${bFinal}`, resultAligned], separator: true, highlight: 2 },
],
});
// Step 4: Result
steps.push({
label: `${aStr} ${opSymbol} ${bStr} = ${resultStr}`,
columns: [
{ rows: [aFinal, `${opSymbol} ${bFinal}`, resultAligned], separator: true, highlight: 2 },
],
resultRow: resultStr,
});
return steps;
}
function buildMultiplySteps(a: number, b: number): Step[] {
const steps: Step[] = [];
const aStr = a.toString();
const bStr = b.toString();
const aDec = (aStr.split(".")[1] || "").length;
const bDec = (bStr.split(".")[1] || "").length;
const totalDec = aDec + bDec;
// Remove decimals for integer multiplication
const aInt = Math.round(a * Math.pow(10, aDec));
const bInt = Math.round(b * Math.pow(10, bDec));
const result = a * b;
const resultStr = parseFloat(result.toFixed(10)).toString();
steps.push({
label: `${aStr} × ${bStr}`,
columns: [],
});
if (totalDec > 0) {
steps.push({
label: `Count decimal places: ${aDec} + ${bDec} = ${totalDec}`,
columns: [
{ rows: [`${aStr}${aDec} d.p.`, `${bStr}${bDec} d.p.`, `Total: ${totalDec} d.p.`] },
],
});
steps.push({
label: `Multiply as whole numbers: ${aInt} × ${bInt}`,
columns: [
{ rows: [`${aInt}`, `× ${bInt}`, `${aInt * bInt}`], separator: true, highlight: 2 },
],
});
steps.push({
label: `Place decimal point ${totalDec} place${totalDec !== 1 ? "s" : ""} from the right`,
columns: [
{ rows: [`${aInt * bInt}`, `${resultStr}`], highlight: 1 },
],
resultRow: resultStr,
});
} else {
const intResult = a * b;
steps.push({
label: `Multiply: ${aStr} × ${bStr} = ${intResult}`,
columns: [
{ rows: [`${aStr}`, `× ${bStr}`, `${intResult}`], separator: true, highlight: 2 },
],
resultRow: intResult.toString(),
});
}
return steps;
}
function buildDivideSteps(a: number, b: number): Step[] {
const steps: Step[] = [];
const aStr = a.toString();
const bStr = b.toString();
const result = a / b;
const resultStr = parseFloat(result.toFixed(10)).toString();
steps.push({
label: `${aStr} ÷ ${bStr}`,
columns: [],
});
// Make divisor a whole number
const bDec = (bStr.split(".")[1] || "").length;
if (bDec > 0) {
const factor = Math.pow(10, bDec);
const newA = parseFloat((a * factor).toFixed(10));
const newB = parseFloat((b * factor).toFixed(10));
steps.push({
label: `Make divisor whole: multiply both by ${factor}`,
columns: [
{ rows: [`${aStr} × ${factor} = ${newA}`, `${bStr} × ${factor} = ${newB}`] },
],
});
steps.push({
label: `Now divide: ${newA} ÷ ${newB}`,
columns: [
{ rows: [`${newA} ÷ ${newB}`, `= ${resultStr}`], highlight: 1 },
],
});
} else {
steps.push({
label: `Divide: ${aStr} ÷ ${bStr}`,
columns: [
{ rows: [`${aStr} ÷ ${bStr}`, `= ${resultStr}`], highlight: 1 },
],
});
}
steps.push({
label: `${aStr} ÷ ${bStr} = ${resultStr}`,
columns: [
{ rows: [`${aStr} ÷ ${bStr} = ${resultStr}`] },
],
resultRow: resultStr,
});
return steps;
}
function ColumnArithmetic({
columns,
}: {
columns: ColumnDisplay[];
}) {
if (columns.length === 0) return null;
return (
<div className="flex flex-col items-center gap-0.5 font-mono text-xl">
{columns.map((col, ci) =>
col.rows.map((row, ri) => (
<div key={`${ci}-${ri}`}>
{col.separator && ri === col.rows.length - 1 && (
<div className="mb-1 h-0.5 bg-foreground" />
)}
<div
className={`rounded-lg px-6 py-1.5 text-right tracking-wider transition-all duration-300 ${
ri === col.highlight
? "bg-unit-3-light font-bold text-foreground"
: "text-foreground"
}`}
>
{row}
</div>
</div>
)),
)}
</div>
);
}
export function DecimalArithmeticExplorer() {
const [op, setOp] = useState<DecOp>("add");
const [aInput, setAInput] = useState("12.45");
const [bInput, setBInput] = useState("3.7");
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;
function handleGo() {
setError("");
const a = parseFloat(aInput);
const b = parseFloat(bInput);
if (isNaN(a) || isNaN(b)) {
setError("Enter valid decimal numbers.");
return;
}
if (op === "divide" && b === 0) {
setError("Cannot divide by zero.");
return;
}
try {
let s: Step[];
if (op === "add" || op === "subtract") {
s = buildAddSubSteps(a, b, op);
} else if (op === "multiply") {
s = buildMultiplySteps(a, b);
} else {
s = buildDivideSteps(a, b);
}
setSteps(s);
setCurrentStep(0);
setIsPlaying(false);
} catch {
setError("Could not compute. Check your inputs.");
}
}
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 reset = useCallback(() => {
setSteps(null);
setCurrentStep(0);
setIsPlaying(false);
}, []);
const step = steps ? steps[currentStep] : null;
const opLabels: Record<DecOp, string> = {
add: "Add (+)",
subtract: "Subtract ()",
multiply: "Multiply (×)",
divide: "Divide (÷)",
};
return (
<div className="space-y-4">
{/* Operation tabs */}
<div className="flex flex-wrap gap-2">
{(["add", "subtract", "multiply", "divide"] as DecOp[]).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-3 bg-unit-3 text-white"
: "border-unit-3/40 text-unit-3 hover:bg-unit-3-light"
}`}
>
{opLabels[o]}
</button>
))}
</div>
{/* Input */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-3">
Enter two decimal numbers
</p>
<div className="flex flex-wrap items-center gap-2.5">
<input
type="text"
value={aInput}
onChange={(e) => setAInput(e.target.value)}
placeholder="e.g. 12.45"
autoComplete="off"
className="max-w-[140px] rounded-lg border-2 border-border bg-surface px-3.5 py-2.5 text-lg font-medium outline-none transition-colors focus:border-unit-3"
aria-label="First number"
/>
<span className="text-xl font-bold text-muted">
{{ add: "+", subtract: "", multiply: "×", divide: "÷" }[op]}
</span>
<input
type="text"
value={bInput}
onChange={(e) => setBInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleGo()}
placeholder="e.g. 3.7"
autoComplete="off"
className="max-w-[140px] rounded-lg border-2 border-border bg-surface px-3.5 py-2.5 text-lg font-medium outline-none transition-colors focus:border-unit-3"
aria-label="Second number"
/>
<button
onClick={handleGo}
className="rounded-lg bg-unit-3 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-3-dark"
>
Go
</button>
</div>
{error && <p className="mt-2 text-sm text-incorrect">{error}</p>}
</Card>
{/* Display */}
<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-sm font-medium text-muted">{step.label}</p>
<ColumnArithmetic columns={step.columns} />
</>
)}
</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>
{/* Result */}
<Card className="flex min-h-[80px] flex-col items-center justify-center gap-1.5 text-center">
{!done || !step?.resultRow ? (
<p className="text-sm text-muted/40">Result will appear here when steps are complete</p>
) : (
<>
<p className="text-xs uppercase tracking-wider text-muted">Answer</p>
<p className="text-3xl font-extrabold text-foreground max-sm:text-2xl">
{step.resultRow}
</p>
</>
)}
</Card>
</div>
);
}