709 lines
24 KiB
TypeScript
709 lines
24 KiB
TypeScript
"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";
|
||
|
||
type Tab = "multiply" | "divide" | "thermometer";
|
||
|
||
interface Step {
|
||
label: string;
|
||
math: string;
|
||
signVisual?: { signA: "+" | "-"; signB: "+" | "-"; resultSign: "+" | "-" };
|
||
thermometer?: { startTemp: number; change: number; endTemp: number };
|
||
}
|
||
|
||
function getSign(n: number): "+" | "-" {
|
||
return n >= 0 ? "+" : "-";
|
||
}
|
||
|
||
function buildMultiplySteps(a: number, b: number): Step[] {
|
||
const steps: Step[] = [];
|
||
const result = a * b;
|
||
const signA = getSign(a);
|
||
const signB = getSign(b);
|
||
const signR = getSign(result);
|
||
|
||
steps.push({
|
||
label: "Start with the multiplication",
|
||
math: `${a < 0 ? `(${a})` : a} \\times ${b < 0 ? `(${b})` : b}`,
|
||
});
|
||
|
||
// Step: Multiply the absolute values
|
||
steps.push({
|
||
label: `First, multiply the absolute values: ${Math.abs(a)} × ${Math.abs(b)}`,
|
||
math: `|${a}| \\times |${b}| = ${Math.abs(a)} \\times ${Math.abs(b)} = ${Math.abs(result)}`,
|
||
});
|
||
|
||
// Step: Determine the sign
|
||
const sameSign = (a >= 0 && b >= 0) || (a < 0 && b < 0);
|
||
if (sameSign) {
|
||
steps.push({
|
||
label: `Same signs (${signA} × ${signB}): the result is POSITIVE`,
|
||
math: `(${signA}) \\times (${signB}) = (+)`,
|
||
signVisual: { signA, signB, resultSign: "+" },
|
||
});
|
||
} else {
|
||
steps.push({
|
||
label: `Different signs (${signA} × ${signB}): the result is NEGATIVE`,
|
||
math: `(${signA}) \\times (${signB}) = (-)`,
|
||
signVisual: { signA, signB, resultSign: "-" },
|
||
});
|
||
}
|
||
|
||
// Step: Combine
|
||
steps.push({
|
||
label: "Combine the magnitude and sign",
|
||
math: `${a < 0 ? `(${a})` : a} \\times ${b < 0 ? `(${b})` : b} = ${result}`,
|
||
signVisual: { signA, signB, resultSign: signR },
|
||
});
|
||
|
||
// Story time for negative × negative
|
||
if (a < 0 && b < 0) {
|
||
steps.push({
|
||
label: "Why does negative × negative = positive? Think of it like a double-negative in English!",
|
||
math: `\\text{\"Do NOT not eat!\"} = \\text{\"Eat!\"} \\quad (-) \\times (-) = (+)`,
|
||
signVisual: { signA: "-", signB: "-", resultSign: "+" },
|
||
});
|
||
}
|
||
|
||
return steps;
|
||
}
|
||
|
||
function buildDivideSteps(a: number, b: number): Step[] {
|
||
const steps: Step[] = [];
|
||
const result = a / b;
|
||
const signA = getSign(a);
|
||
const signB = getSign(b);
|
||
const signR = getSign(result);
|
||
const isWhole = Number.isInteger(result);
|
||
const displayResult = isWhole ? result.toString() : result.toFixed(2);
|
||
|
||
steps.push({
|
||
label: "Start with the division",
|
||
math: `${a < 0 ? `(${a})` : a} \\div ${b < 0 ? `(${b})` : b}`,
|
||
});
|
||
|
||
steps.push({
|
||
label: `First, divide the absolute values: ${Math.abs(a)} ÷ ${Math.abs(b)}`,
|
||
math: `|${a}| \\div |${b}| = ${Math.abs(a)} \\div ${Math.abs(b)} = ${isWhole ? Math.abs(result) : Math.abs(result).toFixed(2)}`,
|
||
});
|
||
|
||
const sameSign = (a >= 0 && b >= 0) || (a < 0 && b < 0);
|
||
if (sameSign) {
|
||
steps.push({
|
||
label: `Same signs (${signA} ÷ ${signB}): the result is POSITIVE`,
|
||
math: `(${signA}) \\div (${signB}) = (+)`,
|
||
signVisual: { signA, signB, resultSign: "+" },
|
||
});
|
||
} else {
|
||
steps.push({
|
||
label: `Different signs (${signA} ÷ ${signB}): the result is NEGATIVE`,
|
||
math: `(${signA}) \\div (${signB}) = (-)`,
|
||
signVisual: { signA, signB, resultSign: "-" },
|
||
});
|
||
}
|
||
|
||
steps.push({
|
||
label: "Combine the magnitude and sign",
|
||
math: `${a < 0 ? `(${a})` : a} \\div ${b < 0 ? `(${b})` : b} = ${displayResult}`,
|
||
signVisual: { signA, signB, resultSign: signR },
|
||
});
|
||
|
||
return steps;
|
||
}
|
||
|
||
function buildThermometerSteps(startTemp: number, change: number): Step[] {
|
||
const steps: Step[] = [];
|
||
const endTemp = startTemp + change;
|
||
const direction = change >= 0 ? "rose" : "dropped";
|
||
const absChange = Math.abs(change);
|
||
|
||
steps.push({
|
||
label: "Read the starting temperature on the thermometer",
|
||
math: `\\text{Starting temperature: } ${startTemp}°C`,
|
||
thermometer: { startTemp, change: 0, endTemp: startTemp },
|
||
});
|
||
|
||
steps.push({
|
||
label: `The temperature ${direction} by ${absChange}°C`,
|
||
math: change >= 0
|
||
? `${startTemp}°C + ${absChange}°C`
|
||
: `${startTemp}°C - ${absChange}°C`,
|
||
thermometer: { startTemp, change: 0, endTemp: startTemp },
|
||
});
|
||
|
||
steps.push({
|
||
label: change >= 0 ? "Rising temperature means we ADD" : "Dropping temperature means we SUBTRACT",
|
||
math: `${startTemp} + (${change}) = ${endTemp}`,
|
||
thermometer: { startTemp, change, endTemp },
|
||
});
|
||
|
||
steps.push({
|
||
label: `The new temperature is ${endTemp}°C`,
|
||
math: `\\text{New temperature: } ${endTemp}°C`,
|
||
thermometer: { startTemp, change, endTemp },
|
||
});
|
||
|
||
return steps;
|
||
}
|
||
|
||
function SignRuleGrid({ visual, operation }: { visual?: Step["signVisual"]; operation: "×" | "÷" }) {
|
||
const rules = [
|
||
{ a: "+", b: "+", result: "+", example: operation === "×" ? "3 × 2 = 6" : "6 ÷ 2 = 3" },
|
||
{ a: "-", b: "-", result: "+", example: operation === "×" ? "(-3) × (-2) = 6" : "(-6) ÷ (-2) = 3" },
|
||
{ a: "+", b: "-", result: "-", example: operation === "×" ? "3 × (-2) = -6" : "6 ÷ (-2) = -3" },
|
||
{ a: "-", b: "+", result: "-", example: operation === "×" ? "(-3) × 2 = -6" : "(-6) ÷ 2 = -3" },
|
||
];
|
||
|
||
return (
|
||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||
{rules.map((r) => {
|
||
const isActive = visual && visual.signA === r.a && visual.signB === r.b;
|
||
return (
|
||
<div
|
||
key={`${r.a}${r.b}`}
|
||
className={`rounded-xl border-2 p-3 text-center transition-all duration-300 ${
|
||
isActive
|
||
? "border-unit-5 bg-unit-5-light shadow-md scale-105"
|
||
: "border-border/40 bg-surface"
|
||
}`}
|
||
>
|
||
<p className={`text-lg font-extrabold ${isActive ? "text-unit-5-dark" : "text-foreground"}`}>
|
||
({r.a}) {operation} ({r.b}) = ({r.result})
|
||
</p>
|
||
<p className={`mt-1 text-xs ${isActive ? "text-unit-5-dark/70" : "text-muted"}`}>
|
||
{r.example}
|
||
</p>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Thermometer({ data }: { data?: Step["thermometer"] }) {
|
||
const minTemp = -20;
|
||
const maxTemp = 30;
|
||
const range = maxTemp - minTemp;
|
||
|
||
const startTemp = data?.startTemp ?? 0;
|
||
const endTemp = data?.endTemp ?? 0;
|
||
|
||
const startY = ((startTemp - minTemp) / range) * 100;
|
||
const endY = ((endTemp - minTemp) / range) * 100;
|
||
const fillY = ((endTemp - minTemp) / range) * 100;
|
||
|
||
const ticks = [];
|
||
for (let t = minTemp; t <= maxTemp; t += 5) {
|
||
ticks.push(t);
|
||
}
|
||
|
||
return (
|
||
<div className="mx-auto flex items-end gap-6">
|
||
{/* Thermometer */}
|
||
<div className="relative flex flex-col items-center">
|
||
<div className="relative h-64 w-8 overflow-hidden rounded-t-full border-2 border-foreground/30 bg-gradient-to-b from-red-50 to-blue-50">
|
||
{/* Mercury fill */}
|
||
<div
|
||
className="absolute bottom-0 left-0 right-0 rounded-t-sm transition-all duration-700"
|
||
style={{
|
||
height: `${Math.max(0, Math.min(100, fillY))}%`,
|
||
background: endTemp >= 0
|
||
? `linear-gradient(to top, #dc2626, #f97316)`
|
||
: `linear-gradient(to top, #2563eb, #60a5fa)`,
|
||
}}
|
||
/>
|
||
{/* Tick marks */}
|
||
{ticks.map((t) => {
|
||
const y = ((t - minTemp) / range) * 100;
|
||
return (
|
||
<div
|
||
key={t}
|
||
className="absolute left-0 right-0 flex items-center"
|
||
style={{ bottom: `${y}%`, transform: "translateY(50%)" }}
|
||
>
|
||
<div className={`h-px flex-1 ${t === 0 ? "bg-foreground" : "bg-foreground/20"}`} />
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
{/* Bulb */}
|
||
<div className="relative -mt-1 flex h-12 w-12 items-center justify-center rounded-full border-2 border-foreground/30 bg-red-500">
|
||
<span className="text-[8px] font-bold text-white">°C</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Scale labels */}
|
||
<div className="relative h-64">
|
||
{ticks.map((t) => {
|
||
const y = ((t - minTemp) / range) * 100;
|
||
const isStart = t === startTemp;
|
||
const isEnd = data && data.change !== 0 && t === endTemp;
|
||
return (
|
||
<div
|
||
key={t}
|
||
className="absolute flex items-center gap-1"
|
||
style={{ bottom: `${y}%`, transform: "translateY(50%)" }}
|
||
>
|
||
<span
|
||
className={`text-xs transition-all duration-300 ${
|
||
isEnd
|
||
? "font-extrabold text-correct text-sm"
|
||
: isStart
|
||
? "font-bold text-unit-5"
|
||
: t === 0
|
||
? "font-semibold text-foreground"
|
||
: "text-muted/60"
|
||
}`}
|
||
>
|
||
{t}°C
|
||
</span>
|
||
{isStart && (
|
||
<span className="rounded bg-unit-5 px-1.5 py-0.5 text-[9px] font-bold text-white">
|
||
START
|
||
</span>
|
||
)}
|
||
{isEnd && (
|
||
<span className="rounded bg-correct px-1.5 py-0.5 text-[9px] font-bold text-white">
|
||
END
|
||
</span>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ThermometerPractice() {
|
||
const scenarios = [
|
||
{ city: "London", startTemp: -3, change: 7, question: "The temperature rose by 7°C. What is the new temperature?" },
|
||
{ city: "Moscow", startTemp: -12, change: -5, question: "The temperature dropped by 5°C. What is the new temperature?" },
|
||
{ city: "Kingston", startTemp: 28, change: -10, question: "The temperature dropped by 10°C. What is the new temperature?" },
|
||
{ city: "Ottawa", startTemp: -8, change: 15, question: "The temperature rose by 15°C. What is the new temperature?" },
|
||
{ city: "Reykjavik", startTemp: -1, change: -6, question: "The temperature dropped by 6°C. What is the new temperature?" },
|
||
];
|
||
|
||
const [scenarioIdx, setScenarioIdx] = useState(0);
|
||
const [userAnswer, setUserAnswer] = useState("");
|
||
const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null);
|
||
|
||
const scenario = scenarios[scenarioIdx];
|
||
const correctAnswer = scenario.startTemp + scenario.change;
|
||
|
||
function check() {
|
||
const parsed = parseInt(userAnswer);
|
||
if (isNaN(parsed)) return;
|
||
setFeedback(parsed === correctAnswer ? "correct" : "incorrect");
|
||
}
|
||
|
||
function next() {
|
||
setScenarioIdx((i) => (i + 1) % scenarios.length);
|
||
setUserAnswer("");
|
||
setFeedback(null);
|
||
}
|
||
|
||
return (
|
||
<Card className="space-y-4">
|
||
<p className="text-xs font-bold uppercase tracking-wider text-unit-5">
|
||
Real-World Challenge
|
||
</p>
|
||
<div className="rounded-xl border-2 border-unit-5/20 bg-unit-5-light/50 p-4">
|
||
<p className="text-sm font-bold text-unit-5-dark">
|
||
In {scenario.city}, the temperature is {scenario.startTemp}°C.
|
||
</p>
|
||
<p className="mt-1 text-sm text-unit-5-dark/80">{scenario.question}</p>
|
||
</div>
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<input
|
||
type="number"
|
||
value={userAnswer}
|
||
onChange={(e) => {
|
||
setUserAnswer(e.target.value);
|
||
setFeedback(null);
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
if (feedback !== null) next();
|
||
else check();
|
||
}
|
||
}}
|
||
className={`w-24 rounded-lg border-2 px-3 py-2 text-center text-lg 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-5"
|
||
}`}
|
||
placeholder="°C"
|
||
aria-label="Temperature answer"
|
||
/>
|
||
<span className="text-sm font-semibold text-muted">°C</span>
|
||
{feedback === null ? (
|
||
<button
|
||
onClick={check}
|
||
className="rounded-lg bg-unit-5 px-5 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-5-dark"
|
||
>
|
||
Check
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={next}
|
||
className="rounded-lg bg-foreground px-5 py-2.5 text-sm font-bold text-background transition-colors hover:bg-foreground/80"
|
||
>
|
||
Next City →
|
||
</button>
|
||
)}
|
||
</div>
|
||
{feedback === "correct" && (
|
||
<p className="text-sm font-bold text-correct">
|
||
Correct! {scenario.startTemp} + ({scenario.change}) = {correctAnswer}°C
|
||
</p>
|
||
)}
|
||
{feedback === "incorrect" && (
|
||
<p className="text-sm font-bold text-incorrect">
|
||
Not quite. {scenario.startTemp} + ({scenario.change}) = {correctAnswer}°C
|
||
</p>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
function MultiplyDividePractice() {
|
||
const [problem, setProblem] = useState(() => generateProblem());
|
||
const [userAnswer, setUserAnswer] = useState("");
|
||
const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null);
|
||
const [score, setScore] = useState({ correct: 0, total: 0 });
|
||
|
||
function generateProblem() {
|
||
const ops = ["×", "÷"] as const;
|
||
const op = ops[Math.floor(Math.random() * ops.length)];
|
||
|
||
if (op === "×") {
|
||
const a = Math.floor(Math.random() * 25) - 12;
|
||
const b = Math.floor(Math.random() * 25) - 12;
|
||
return { a, b, op, answer: a * b };
|
||
} else {
|
||
// Generate division with clean integer results
|
||
const b = Math.floor(Math.random() * 11) - 5;
|
||
const nonZeroB = b === 0 ? 3 : b;
|
||
const answer = Math.floor(Math.random() * 21) - 10;
|
||
const a = answer * nonZeroB;
|
||
return { a, b: nonZeroB, op, answer };
|
||
}
|
||
}
|
||
|
||
function check() {
|
||
const parsed = parseInt(userAnswer);
|
||
if (isNaN(parsed)) return;
|
||
const isCorrect = parsed === problem.answer;
|
||
setFeedback(isCorrect ? "correct" : "incorrect");
|
||
setScore((prev) => ({
|
||
correct: prev.correct + (isCorrect ? 1 : 0),
|
||
total: prev.total + 1,
|
||
}));
|
||
}
|
||
|
||
function next() {
|
||
setProblem(generateProblem());
|
||
setUserAnswer("");
|
||
setFeedback(null);
|
||
}
|
||
|
||
const displayA = problem.a < 0 ? `(${problem.a})` : `${problem.a}`;
|
||
const displayB = problem.b < 0 ? `(${problem.b})` : `${problem.b}`;
|
||
|
||
return (
|
||
<Card className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-xs font-bold uppercase tracking-wider text-unit-5">Quick Practice</p>
|
||
<span className="rounded-full bg-unit-5-light px-3 py-1 text-xs font-bold text-unit-5-dark">
|
||
{score.correct}/{score.total}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||
<span className="text-2xl font-extrabold">
|
||
{displayA} {problem.op} {displayB} =
|
||
</span>
|
||
<input
|
||
type="number"
|
||
value={userAnswer}
|
||
onChange={(e) => {
|
||
setUserAnswer(e.target.value);
|
||
setFeedback(null);
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
if (feedback !== null) next();
|
||
else check();
|
||
}
|
||
}}
|
||
className={`w-20 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-5"
|
||
}`}
|
||
placeholder="?"
|
||
aria-label="Your answer"
|
||
/>
|
||
{feedback === null ? (
|
||
<button onClick={check} className="rounded-lg bg-unit-5 px-5 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-5-dark">
|
||
Check
|
||
</button>
|
||
) : (
|
||
<button onClick={next} className="rounded-lg bg-foreground px-5 py-2.5 text-sm font-bold text-background transition-colors 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 {problem.answer}.
|
||
</p>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
export function IntegerMultiplyDivideExplorer() {
|
||
const [tab, setTab] = useState<Tab>("multiply");
|
||
const [inputA, setInputA] = useState("-3");
|
||
const [inputB, setInputB] = useState("5");
|
||
const [startTemp, setStartTemp] = useState("-3");
|
||
const [tempChange, setTempChange] = useState("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("");
|
||
|
||
if (tab === "thermometer") {
|
||
const st = parseInt(startTemp);
|
||
const ch = parseInt(tempChange);
|
||
if (isNaN(st) || isNaN(ch)) {
|
||
setError("Enter valid integers.");
|
||
return;
|
||
}
|
||
if (Math.abs(st) > 20 || Math.abs(ch) > 30) {
|
||
setError("Keep temperatures reasonable (-20 to 30).");
|
||
return;
|
||
}
|
||
const s = buildThermometerSteps(st, ch);
|
||
setSteps(s);
|
||
setCurrentStep(0);
|
||
setIsPlaying(false);
|
||
return;
|
||
}
|
||
|
||
const a = parseInt(inputA);
|
||
const b = parseInt(inputB);
|
||
if (isNaN(a) || isNaN(b)) {
|
||
setError("Enter valid integers.");
|
||
return;
|
||
}
|
||
if (tab === "divide" && b === 0) {
|
||
setError("Cannot divide by zero!");
|
||
return;
|
||
}
|
||
if (Math.abs(a) > 99 || Math.abs(b) > 99) {
|
||
setError("Keep numbers under 100.");
|
||
return;
|
||
}
|
||
if (tab === "divide" && !Number.isInteger(a / b)) {
|
||
setError("Choose numbers that divide evenly for now.");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const s = tab === "multiply" ? buildMultiplySteps(a, b) : 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;
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* Tab selector */}
|
||
<div className="flex flex-wrap gap-2">
|
||
{(["multiply", "divide", "thermometer"] as Tab[]).map((t) => (
|
||
<button
|
||
key={t}
|
||
onClick={() => {
|
||
setTab(t);
|
||
reset();
|
||
}}
|
||
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
|
||
tab === t
|
||
? "border-unit-5 bg-unit-5 text-white"
|
||
: "border-unit-5/40 text-unit-5 hover:bg-unit-5-light"
|
||
}`}
|
||
>
|
||
{t === "multiply"
|
||
? "Multiply (×)"
|
||
: t === "divide"
|
||
? "Divide (÷)"
|
||
: "Thermometer"}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Input Card */}
|
||
<Card>
|
||
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-5">
|
||
{tab === "thermometer" ? "Enter temperature scenario" : "Enter two integers"}
|
||
</p>
|
||
|
||
{tab === "thermometer" ? (
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<div className="flex flex-col">
|
||
<label className="mb-1 text-xs font-semibold text-muted">Start temp (°C)</label>
|
||
<input
|
||
type="number"
|
||
value={startTemp}
|
||
onChange={(e) => setStartTemp(e.target.value)}
|
||
className="w-24 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-5"
|
||
aria-label="Starting temperature"
|
||
/>
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<label className="mb-1 text-xs font-semibold text-muted">Change (°C)</label>
|
||
<input
|
||
type="number"
|
||
value={tempChange}
|
||
onChange={(e) => setTempChange(e.target.value)}
|
||
className="w-24 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-5"
|
||
aria-label="Temperature change"
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={handleGo}
|
||
className="mt-5 rounded-lg bg-unit-5 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-5-dark"
|
||
>
|
||
Go →
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<input
|
||
type="number"
|
||
value={inputA}
|
||
onChange={(e) => setInputA(e.target.value)}
|
||
className="w-20 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-5"
|
||
aria-label="First integer"
|
||
/>
|
||
<span className="text-xl font-bold text-muted">
|
||
{tab === "multiply" ? "×" : "÷"}
|
||
</span>
|
||
<input
|
||
type="number"
|
||
value={inputB}
|
||
onChange={(e) => setInputB(e.target.value)}
|
||
className="w-20 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-5"
|
||
aria-label="Second integer"
|
||
/>
|
||
<button
|
||
onClick={handleGo}
|
||
className="rounded-lg bg-unit-5 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-5-dark"
|
||
>
|
||
Go →
|
||
</button>
|
||
</div>
|
||
)}
|
||
{error && <p className="mt-2 text-sm text-incorrect">{error}</p>}
|
||
</Card>
|
||
|
||
{/* Sign Rules Grid (for multiply/divide) */}
|
||
{tab !== "thermometer" && (
|
||
<SignRuleGrid
|
||
visual={step?.signVisual}
|
||
operation={tab === "multiply" ? "×" : "÷"}
|
||
/>
|
||
)}
|
||
|
||
{/* Display Card */}
|
||
<Card className="flex min-h-[300px] 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" />
|
||
{step.thermometer && <Thermometer data={step.thermometer} />}
|
||
</>
|
||
)}
|
||
</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>
|
||
|
||
{/* Practice sections */}
|
||
{tab === "thermometer" ? <ThermometerPractice /> : <MultiplyDividePractice />}
|
||
</div>
|
||
);
|
||
}
|