Initial Commit
This commit is contained in:
305
components/explorers/fraction-quantity-explorer.tsx
Normal file
305
components/explorers/fraction-quantity-explorer.tsx
Normal 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; // 0–1 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user