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>
|
<h1 className="text-3xl font-bold tracking-tight">Divide Fractions</h1>
|
||||||
|
|
||||||
<FractionOperationExplorer />
|
<FractionOperationExplorer initialOperation="divide" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user