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,305 @@
"use client";
import { useState, useCallback } from "react";
import { Card } from "@/components/ui/card";
import { StepControls } from "./step-controls";
import { MathDisplay } from "@/components/math/math-display";
import { toKatex } from "@/lib/math/fractions";
type FQMode = "fractionOf" | "findWhole";
interface Step {
label: string;
math: string;
barFilled: number; // 01 proportion
barLabel?: string;
}
function buildFractionOfSteps(num: number, den: number, quantity: number): Step[] {
const steps: Step[] = [];
steps.push({
label: `Find ${toKatexStr(num, den)} of ${quantity}`,
math: `${toKatex(num, den)} \\text{ of } ${quantity}`,
barFilled: 0,
});
// Step 1: Divide quantity by denominator
const onePart = quantity / den;
steps.push({
label: `Divide ${quantity} by ${den} to find one part`,
math: `${quantity} \\div ${den} = ${fmt(onePart)}`,
barFilled: 1 / den,
barLabel: fmt(onePart),
});
// Step 2: Multiply by numerator
const result = onePart * num;
steps.push({
label: `Multiply one part by ${num}`,
math: `${fmt(onePart)} \\times ${num} = ${fmt(result)}`,
barFilled: num / den,
barLabel: fmt(result),
});
steps.push({
label: `Answer: ${toKatexStr(num, den)} of ${quantity} = ${fmt(result)}`,
math: `${toKatex(num, den)} \\times ${quantity} = ${fmt(result)}`,
barFilled: num / den,
barLabel: fmt(result),
});
return steps;
}
function buildFindWholeSteps(num: number, den: number, part: number): Step[] {
const steps: Step[] = [];
steps.push({
label: `If ${toKatexStr(num, den)} of the whole = ${part}, find the whole`,
math: `${toKatex(num, den)} \\text{ of ? } = ${part}`,
barFilled: num / den,
barLabel: `${part}`,
});
// Step 1: Find one part
const onePart = part / num;
steps.push({
label: `Divide ${part} by ${num} to find one ${den}th`,
math: `${part} \\div ${num} = ${fmt(onePart)}`,
barFilled: 1 / den,
barLabel: fmt(onePart),
});
// Step 2: Multiply by denominator
const whole = onePart * den;
steps.push({
label: `Multiply by ${den} to find the whole`,
math: `${fmt(onePart)} \\times ${den} = ${fmt(whole)}`,
barFilled: 1,
barLabel: fmt(whole),
});
steps.push({
label: `The whole is ${fmt(whole)}`,
math: `\\text{Whole} = ${fmt(whole)}`,
barFilled: 1,
barLabel: fmt(whole),
});
return steps;
}
function toKatexStr(n: number, d: number): string {
return `${n}/${d}`;
}
function fmt(n: number): string {
return Number.isInteger(n) ? n.toString() : n.toFixed(2);
}
function QuantityBar({ filled, label }: { filled: number; label?: string }) {
const pct = Math.max(0, Math.min(1, filled)) * 100;
return (
<div className="w-full max-w-md">
<div className="relative h-12 overflow-hidden rounded-xl border-2 border-border bg-background">
<div
className="flex h-full items-center justify-center bg-unit-1 text-sm font-bold text-white transition-all duration-700"
style={{ width: `${pct}%` }}
>
{label && pct > 15 ? label : ""}
</div>
</div>
{label && pct <= 15 && (
<p className="mt-1 text-center text-sm font-semibold text-unit-1">{label}</p>
)}
<div className="mt-1 flex justify-between text-xs text-muted">
<span>0</span>
<span>{Math.round(pct)}%</span>
</div>
</div>
);
}
export function FractionQuantityExplorer() {
const [mode, setMode] = useState<FQMode>("fractionOf");
const [num, setNum] = useState("3");
const [den, setDen] = useState("4");
const [quantity, setQuantity] = useState("80");
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 n = parseInt(num);
const d = parseInt(den);
const q = parseFloat(quantity);
if (isNaN(n) || isNaN(d) || d === 0) {
setError("Enter a valid fraction (denominator ≠ 0).");
return;
}
if (isNaN(q) || q <= 0) {
setError("Enter a valid positive quantity.");
return;
}
try {
const s =
mode === "fractionOf"
? buildFractionOfSteps(n, d, q)
: buildFindWholeSteps(n, d, q);
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;
return (
<div className="space-y-4">
{/* Mode tabs */}
<div className="flex flex-wrap gap-2">
<button
onClick={() => {
setMode("fractionOf");
reset();
}}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
mode === "fractionOf"
? "border-unit-1 bg-unit-1 text-white"
: "border-unit-1/40 text-unit-1 hover:bg-unit-1-light"
}`}
>
Fraction OF Quantity
</button>
<button
onClick={() => {
setMode("findWhole");
reset();
}}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
mode === "findWhole"
? "border-unit-1 bg-unit-1 text-white"
: "border-unit-1/40 text-unit-1 hover:bg-unit-1-light"
}`}
>
Find the WHOLE
</button>
</div>
{/* Input */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-1">
{mode === "fractionOf" ? "Enter a fraction and quantity" : "Enter a fraction and the part value"}
</p>
<div className="flex flex-wrap items-center gap-3">
<div className="flex flex-col items-center">
<input
type="number"
value={num}
onChange={(e) => setNum(e.target.value)}
className="w-14 rounded-lg border-2 border-border bg-surface px-2 py-1.5 text-center text-lg font-bold outline-none focus:border-unit-1"
aria-label="Numerator"
/>
<div className="my-0.5 h-0.5 w-12 bg-foreground" />
<input
type="number"
value={den}
onChange={(e) => setDen(e.target.value)}
className="w-14 rounded-lg border-2 border-border bg-surface px-2 py-1.5 text-center text-lg font-bold outline-none focus:border-unit-1"
aria-label="Denominator"
/>
</div>
<span className="text-lg font-semibold text-muted">
{mode === "fractionOf" ? "of" : "= "}
</span>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleGo()}
className="w-24 rounded-lg border-2 border-border bg-surface px-3 py-2.5 text-center text-lg font-bold outline-none focus:border-unit-1"
aria-label={mode === "fractionOf" ? "Quantity" : "Part value"}
/>
<button
onClick={handleGo}
className="rounded-lg bg-unit-1 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-1-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 values above and click <strong>Go</strong>
</p>
) : (
<>
<p className="text-sm font-medium text-muted">{step.label}</p>
<MathDisplay math={step.math} className="text-2xl" />
<QuantityBar filled={step.barFilled} label={step.barLabel} />
</>
)}
</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 || !steps ? (
<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>
<div className="text-3xl font-extrabold max-sm:text-2xl">
<MathDisplay math={steps[steps.length - 1].math} />
</div>
</>
)}
</Card>
</div>
);
}