small fixes

This commit is contained in:
2026-03-01 19:48:29 -04:00
parent 364facd9f0
commit 1764adf0a5
15 changed files with 68 additions and 24 deletions

View File

@@ -16,7 +16,7 @@ export default function DividePage() {
<h1 className="text-3xl font-bold tracking-tight">Divide Fractions</h1> <h1 className="text-3xl font-bold tracking-tight">Divide Fractions</h1>
<FractionOperationExplorer /> <FractionOperationExplorer initialOperation="divide" />
</div> </div>
); );
} }

View File

@@ -16,7 +16,7 @@ export default function MultiplyPage() {
<h1 className="text-3xl font-bold tracking-tight">Multiply Fractions</h1> <h1 className="text-3xl font-bold tracking-tight">Multiply Fractions</h1>
<FractionOperationExplorer /> <FractionOperationExplorer initialOperation="multiply" />
</div> </div>
); );
} }

View File

@@ -16,7 +16,7 @@ export default function WholeFromFractionsPage() {
<h1 className="text-3xl font-bold tracking-tight">Calculate the Whole from Fractions</h1> <h1 className="text-3xl font-bold tracking-tight">Calculate the Whole from Fractions</h1>
<FractionQuantityExplorer /> <FractionQuantityExplorer initialMode="findWhole" />
</div> </div>
); );
} }

View File

@@ -16,7 +16,7 @@ export default function MultiplyDividePage() {
<h1 className="text-3xl font-bold tracking-tight">Multiply and Divide Decimals</h1> <h1 className="text-3xl font-bold tracking-tight">Multiply and Divide Decimals</h1>
<DecimalArithmeticExplorer /> <DecimalArithmeticExplorer initialOperation="multiply" />
</div> </div>
); );
} }

View File

@@ -16,7 +16,7 @@ export default function SimplifyRatiosPage() {
<h1 className="text-3xl font-bold tracking-tight">Simplify Ratios</h1> <h1 className="text-3xl font-bold tracking-tight">Simplify Ratios</h1>
<RatioExplorer /> <RatioExplorer initialMode="simplify" />
</div> </div>
); );
} }

View File

@@ -117,7 +117,15 @@ export default function PracticePage() {
hover hover
className="cursor-pointer" className="cursor-pointer"
accent={topic.unitColor} accent={topic.unitColor}
tabIndex={0}
role="button"
onClick={() => setSelectedTopic(topic)} onClick={() => setSelectedTopic(topic)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setSelectedTopic(topic);
}
}}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="font-bold">{topic.label}</span> <span className="font-bold">{topic.label}</span>

View File

@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/card";
import { StepControls } from "./step-controls"; import { StepControls } from "./step-controls";
import { MathDisplay } from "@/components/math/math-display"; import { MathDisplay } from "@/components/math/math-display";
import { simplify, toKatex, gcd } from "@/lib/math/fractions"; import { simplify, toKatex, gcd } from "@/lib/math/fractions";
import { parseStrictDecimal, parseStrictInt } from "@/lib/math/validation";
type ConvertMode = "decToFrac" | "fracToDec"; type ConvertMode = "decToFrac" | "fracToDec";
@@ -161,16 +162,16 @@ export function ConversionExplorer() {
try { try {
let s: Step[]; let s: Step[];
if (mode === "decToFrac") { if (mode === "decToFrac") {
const val = parseFloat(decInput); const val = parseStrictDecimal(decInput);
if (isNaN(val) || val < 0 || val >= 10) { if (val === null || val < 0 || val >= 10) {
setError("Enter a valid decimal between 0 and 10."); setError("Enter a valid decimal between 0 and 10.");
return; return;
} }
s = buildDecToFracSteps(decInput.trim()); s = buildDecToFracSteps(decInput.trim());
} else { } else {
const n = parseInt(numInput); const n = parseStrictInt(numInput);
const d = parseInt(denInput); const d = parseStrictInt(denInput);
if (isNaN(n) || isNaN(d) || d === 0) { if (n === null || d === null || d === 0) {
setError("Enter a valid fraction (denominator ≠ 0)."); setError("Enter a valid fraction (denominator ≠ 0).");
return; return;
} }

View File

@@ -227,8 +227,12 @@ function ColumnArithmetic({
); );
} }
export function DecimalArithmeticExplorer() { export function DecimalArithmeticExplorer({
const [op, setOp] = useState<DecOp>("add"); initialOperation = "add",
}: {
initialOperation?: DecOp;
} = {}) {
const [op, setOp] = useState<DecOp>(initialOperation);
const [aInput, setAInput] = useState("12.45"); const [aInput, setAInput] = useState("12.45");
const [bInput, setBInput] = useState("3.7"); const [bInput, setBInput] = useState("3.7");
const [error, setError] = useState(""); const [error, setError] = useState("");

View File

@@ -174,8 +174,12 @@ function FractionBar({
); );
} }
export function FractionOperationExplorer() { export function FractionOperationExplorer({
const [op, setOp] = useState<Operation>("add"); initialOperation = "add",
}: {
initialOperation?: Operation;
} = {}) {
const [op, setOp] = useState<Operation>(initialOperation);
const [n1, setN1] = useState("1"); const [n1, setN1] = useState("1");
const [d1, setD1] = useState("3"); const [d1, setD1] = useState("3");
const [n2, setN2] = useState("1"); const [n2, setN2] = useState("1");

View File

@@ -121,8 +121,12 @@ function QuantityBar({ filled, label }: { filled: number; label?: string }) {
); );
} }
export function FractionQuantityExplorer() { export function FractionQuantityExplorer({
const [mode, setMode] = useState<FQMode>("fractionOf"); initialMode = "fractionOf",
}: {
initialMode?: FQMode;
} = {}) {
const [mode, setMode] = useState<FQMode>(initialMode);
const [num, setNum] = useState("3"); const [num, setNum] = useState("3");
const [den, setDen] = useState("4"); const [den, setDen] = useState("4");
const [quantity, setQuantity] = useState("80"); const [quantity, setQuantity] = useState("80");

View File

@@ -182,8 +182,12 @@ function RatioBar({
); );
} }
export function RatioExplorer() { export function RatioExplorer({
const [mode, setMode] = useState<RatioMode>("divide"); initialMode = "divide",
}: {
initialMode?: RatioMode;
} = {}) {
const [mode, setMode] = useState<RatioMode>(initialMode);
const [partA, setPartA] = useState("2"); const [partA, setPartA] = useState("2");
const [partB, setPartB] = useState("3"); const [partB, setPartB] = useState("3");
const [partC, setPartC] = useState(""); const [partC, setPartC] = useState("");

View File

@@ -3,6 +3,7 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { StepControls } from "./step-controls"; import { StepControls } from "./step-controls";
import { parseStrictDecimal } from "@/lib/math/validation";
type RoundTarget = "whole" | "1dp" | "2dp" | "1sf" | "2sf" | "3sf"; type RoundTarget = "whole" | "1dp" | "2dp" | "1sf" | "2sf" | "3sf";
@@ -384,9 +385,9 @@ export function RoundingExplorer() {
function handleGo() { function handleGo() {
setError(""); setError("");
const trimmed = input.trim(); const trimmed = input.trim();
const val = parseFloat(trimmed); const val = parseStrictDecimal(trimmed);
if (isNaN(val)) { if (val === null) {
setError("Enter a valid number."); setError("Enter a valid number (digits and decimal point only).");
return; return;
} }
if (val < 0) { if (val < 0) {

View File

@@ -337,10 +337,11 @@ export function StandardFormExplorer() {
<p className="text-sm text-muted"> <p className="text-sm text-muted">
{totalSteps === 0 {totalSteps === 0
? "Number is already in standard form position — power = 0" ? "Number is already in standard form position — power = 0"
: `Step ${currentStep} of ${totalSteps}`} : `Step ${currentStep + 1} of ${totalSteps}`}
</p> </p>
{/* Digit row */} {/* Digit row */}
<div className="overflow-x-auto max-sm:pb-2">
<div ref={numberRowRef} className="relative inline-flex items-end gap-2.5 px-4 pb-7 pt-2"> <div ref={numberRowRef} className="relative inline-flex items-end gap-2.5 px-4 pb-7 pt-2">
{display.digits.map((ch, i) => { {display.digits.map((ch, i) => {
const isLeadingZero = i < fz && fz !== -1; const isLeadingZero = i < fz && fz !== -1;
@@ -372,6 +373,7 @@ export function StandardFormExplorer() {
}} }}
/> />
</div> </div>
</div>
{/* Direction label */} {/* Direction label */}
{dirText && ( {dirText && (
@@ -403,7 +405,7 @@ export function StandardFormExplorer() {
{/* Controls */} {/* Controls */}
<Card className="p-4"> <Card className="p-4">
<StepControls <StepControls
currentStep={currentStep} currentStep={currentStep + 1}
totalSteps={totalSteps} totalSteps={totalSteps}
isPlaying={isPlaying} isPlaying={isPlaying}
onStepForward={stepForward} onStepForward={stepForward}

View File

@@ -44,8 +44,10 @@ export function StepControls({
// Keyboard shortcuts // Keyboard shortcuts
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
const tag = (e.target as HTMLElement).tagName; const el = e.target as HTMLElement;
const tag = el.tagName;
if (["INPUT", "TEXTAREA", "SELECT"].includes(tag)) return; if (["INPUT", "TEXTAREA", "SELECT"].includes(tag)) return;
if (el.closest("button, [role='button'], a")) return;
if (e.key === "ArrowRight" || e.key === " ") { if (e.key === "ArrowRight" || e.key === " ") {
e.preventDefault(); e.preventDefault();

View File

@@ -1,6 +1,20 @@
import { gcd } from "./fractions"; import { gcd } from "./fractions";
import { simplifyRatio } from "./ratios"; import { simplifyRatio } from "./ratios";
/** Parse a strict decimal string (digits and optional single dot). Rejects scientific notation, trailing letters, etc. */
export function parseStrictDecimal(input: string): number | null {
const s = input.trim();
if (!/^\d+(\.\d+)?$/.test(s)) return null;
return parseFloat(s);
}
/** Parse a strict integer string (digits only, no dot or letters). */
export function parseStrictInt(input: string): number | null {
const s = input.trim();
if (!/^\d+$/.test(s)) return null;
return parseInt(s, 10);
}
export type AnswerResult = export type AnswerResult =
| { correct: true; simplified: boolean } | { correct: true; simplified: boolean }
| { correct: false; message: string }; | { correct: false; message: string };