Files
cabrits-math/components/explorers/fraction-quantity-explorer.tsx
2026-03-01 18:50:29 -04:00

306 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}