small fixes
This commit is contained in:
@@ -16,7 +16,7 @@ export default function DividePage() {
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight">Divide Fractions</h1>
|
||||
|
||||
<FractionOperationExplorer />
|
||||
<FractionOperationExplorer initialOperation="divide" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function MultiplyPage() {
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight">Multiply Fractions</h1>
|
||||
|
||||
<FractionOperationExplorer />
|
||||
<FractionOperationExplorer initialOperation="multiply" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function WholeFromFractionsPage() {
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight">Calculate the Whole from Fractions</h1>
|
||||
|
||||
<FractionQuantityExplorer />
|
||||
<FractionQuantityExplorer initialMode="findWhole" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function MultiplyDividePage() {
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight">Multiply and Divide Decimals</h1>
|
||||
|
||||
<DecimalArithmeticExplorer />
|
||||
<DecimalArithmeticExplorer initialOperation="multiply" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function SimplifyRatiosPage() {
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight">Simplify Ratios</h1>
|
||||
|
||||
<RatioExplorer />
|
||||
<RatioExplorer initialMode="simplify" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,15 @@ export default function PracticePage() {
|
||||
hover
|
||||
className="cursor-pointer"
|
||||
accent={topic.unitColor}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={() => setSelectedTopic(topic)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedTopic(topic);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold">{topic.label}</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/card";
|
||||
import { StepControls } from "./step-controls";
|
||||
import { MathDisplay } from "@/components/math/math-display";
|
||||
import { simplify, toKatex, gcd } from "@/lib/math/fractions";
|
||||
import { parseStrictDecimal, parseStrictInt } from "@/lib/math/validation";
|
||||
|
||||
type ConvertMode = "decToFrac" | "fracToDec";
|
||||
|
||||
@@ -161,16 +162,16 @@ export function ConversionExplorer() {
|
||||
try {
|
||||
let s: Step[];
|
||||
if (mode === "decToFrac") {
|
||||
const val = parseFloat(decInput);
|
||||
if (isNaN(val) || val < 0 || val >= 10) {
|
||||
const val = parseStrictDecimal(decInput);
|
||||
if (val === null || val < 0 || val >= 10) {
|
||||
setError("Enter a valid decimal between 0 and 10.");
|
||||
return;
|
||||
}
|
||||
s = buildDecToFracSteps(decInput.trim());
|
||||
} else {
|
||||
const n = parseInt(numInput);
|
||||
const d = parseInt(denInput);
|
||||
if (isNaN(n) || isNaN(d) || d === 0) {
|
||||
const n = parseStrictInt(numInput);
|
||||
const d = parseStrictInt(denInput);
|
||||
if (n === null || d === null || d === 0) {
|
||||
setError("Enter a valid fraction (denominator ≠ 0).");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,8 +227,12 @@ function ColumnArithmetic({
|
||||
);
|
||||
}
|
||||
|
||||
export function DecimalArithmeticExplorer() {
|
||||
const [op, setOp] = useState<DecOp>("add");
|
||||
export function DecimalArithmeticExplorer({
|
||||
initialOperation = "add",
|
||||
}: {
|
||||
initialOperation?: DecOp;
|
||||
} = {}) {
|
||||
const [op, setOp] = useState<DecOp>(initialOperation);
|
||||
const [aInput, setAInput] = useState("12.45");
|
||||
const [bInput, setBInput] = useState("3.7");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@@ -174,8 +174,12 @@ function FractionBar({
|
||||
);
|
||||
}
|
||||
|
||||
export function FractionOperationExplorer() {
|
||||
const [op, setOp] = useState<Operation>("add");
|
||||
export function FractionOperationExplorer({
|
||||
initialOperation = "add",
|
||||
}: {
|
||||
initialOperation?: Operation;
|
||||
} = {}) {
|
||||
const [op, setOp] = useState<Operation>(initialOperation);
|
||||
const [n1, setN1] = useState("1");
|
||||
const [d1, setD1] = useState("3");
|
||||
const [n2, setN2] = useState("1");
|
||||
|
||||
@@ -121,8 +121,12 @@ function QuantityBar({ filled, label }: { filled: number; label?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function FractionQuantityExplorer() {
|
||||
const [mode, setMode] = useState<FQMode>("fractionOf");
|
||||
export function FractionQuantityExplorer({
|
||||
initialMode = "fractionOf",
|
||||
}: {
|
||||
initialMode?: FQMode;
|
||||
} = {}) {
|
||||
const [mode, setMode] = useState<FQMode>(initialMode);
|
||||
const [num, setNum] = useState("3");
|
||||
const [den, setDen] = useState("4");
|
||||
const [quantity, setQuantity] = useState("80");
|
||||
|
||||
@@ -182,8 +182,12 @@ function RatioBar({
|
||||
);
|
||||
}
|
||||
|
||||
export function RatioExplorer() {
|
||||
const [mode, setMode] = useState<RatioMode>("divide");
|
||||
export function RatioExplorer({
|
||||
initialMode = "divide",
|
||||
}: {
|
||||
initialMode?: RatioMode;
|
||||
} = {}) {
|
||||
const [mode, setMode] = useState<RatioMode>(initialMode);
|
||||
const [partA, setPartA] = useState("2");
|
||||
const [partB, setPartB] = useState("3");
|
||||
const [partC, setPartC] = useState("");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { StepControls } from "./step-controls";
|
||||
import { parseStrictDecimal } from "@/lib/math/validation";
|
||||
|
||||
type RoundTarget = "whole" | "1dp" | "2dp" | "1sf" | "2sf" | "3sf";
|
||||
|
||||
@@ -384,9 +385,9 @@ export function RoundingExplorer() {
|
||||
function handleGo() {
|
||||
setError("");
|
||||
const trimmed = input.trim();
|
||||
const val = parseFloat(trimmed);
|
||||
if (isNaN(val)) {
|
||||
setError("Enter a valid number.");
|
||||
const val = parseStrictDecimal(trimmed);
|
||||
if (val === null) {
|
||||
setError("Enter a valid number (digits and decimal point only).");
|
||||
return;
|
||||
}
|
||||
if (val < 0) {
|
||||
|
||||
@@ -337,10 +337,11 @@ export function StandardFormExplorer() {
|
||||
<p className="text-sm text-muted">
|
||||
{totalSteps === 0
|
||||
? "Number is already in standard form position — power = 0"
|
||||
: `Step ${currentStep} of ${totalSteps}`}
|
||||
: `Step ${currentStep + 1} of ${totalSteps}`}
|
||||
</p>
|
||||
|
||||
{/* 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">
|
||||
{display.digits.map((ch, i) => {
|
||||
const isLeadingZero = i < fz && fz !== -1;
|
||||
@@ -372,6 +373,7 @@ export function StandardFormExplorer() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Direction label */}
|
||||
{dirText && (
|
||||
@@ -403,7 +405,7 @@ export function StandardFormExplorer() {
|
||||
{/* Controls */}
|
||||
<Card className="p-4">
|
||||
<StepControls
|
||||
currentStep={currentStep}
|
||||
currentStep={currentStep + 1}
|
||||
totalSteps={totalSteps}
|
||||
isPlaying={isPlaying}
|
||||
onStepForward={stepForward}
|
||||
|
||||
@@ -44,8 +44,10 @@ export function StepControls({
|
||||
// Keyboard shortcuts
|
||||
const handleKeyDown = useCallback(
|
||||
(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 (el.closest("button, [role='button'], a")) return;
|
||||
|
||||
if (e.key === "ArrowRight" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { gcd } from "./fractions";
|
||||
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 =
|
||||
| { correct: true; simplified: boolean }
|
||||
| { correct: false; message: string };
|
||||
|
||||
Reference in New Issue
Block a user