form 1 topics added
This commit is contained in:
258
components/explorers/read-coordinates-explorer.tsx
Normal file
258
components/explorers/read-coordinates-explorer.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ClientOnly } from "@/components/ui/client-only";
|
||||
import { StepControls } from "./step-controls";
|
||||
import { MathDisplay } from "@/components/math/math-display";
|
||||
import { CartesianGrid, type GridPoint } from "./cartesian-grid";
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
math: string;
|
||||
points: GridPoint[];
|
||||
highlight?: { x: number; y: number };
|
||||
guide?: "x" | "y" | "both";
|
||||
}
|
||||
|
||||
function randInt(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function quadrantName(x: number, y: number): string {
|
||||
if (x === 0 && y === 0) return "the origin";
|
||||
if (x === 0) return "on the y-axis";
|
||||
if (y === 0) return "on the x-axis";
|
||||
if (x > 0 && y > 0) return "the top-right quadrant";
|
||||
if (x < 0 && y > 0) return "the top-left quadrant";
|
||||
if (x < 0 && y < 0) return "the bottom-left quadrant";
|
||||
return "the bottom-right quadrant";
|
||||
}
|
||||
|
||||
function buildReadSteps(x: number, y: number): Step[] {
|
||||
const point: GridPoint = { x, y, tone: "solid" };
|
||||
return [
|
||||
{
|
||||
label: "Here is a point. Let's read off its coordinates.",
|
||||
math: `(?,\\ ?)`,
|
||||
points: [point],
|
||||
},
|
||||
{
|
||||
label: `Trace straight ${y >= 0 ? "DOWN" : "UP"} to the x-axis. It lands on ${x}.`,
|
||||
math: `x = ${x}`,
|
||||
points: [point],
|
||||
highlight: { x, y },
|
||||
guide: "x",
|
||||
},
|
||||
{
|
||||
label: `Now trace straight across to the y-axis. It lands on ${y}.`,
|
||||
math: `y = ${y}`,
|
||||
points: [point],
|
||||
highlight: { x, y },
|
||||
guide: "both",
|
||||
},
|
||||
{
|
||||
label: `The coordinates are (${x}, ${y}) — x first, then y. This point is in ${quadrantName(x, y)}.`,
|
||||
math: `(${x},\\ ${y})`,
|
||||
points: [{ x, y, label: `(${x}, ${y})`, tone: "solid" }],
|
||||
highlight: { x, y },
|
||||
guide: "both",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ── Quick practice: read the coordinates ─────────────────────────────────────
|
||||
|
||||
function ReadPractice() {
|
||||
const [target, setTarget] = useState(() => ({ x: randInt(-9, 9), y: randInt(-9, 9) }));
|
||||
const [xVal, setXVal] = useState("");
|
||||
const [yVal, setYVal] = useState("");
|
||||
const [feedback, setFeedback] = useState<"correct" | "swapped" | "incorrect" | null>(null);
|
||||
const [score, setScore] = useState({ correct: 0, total: 0 });
|
||||
|
||||
function check() {
|
||||
const gx = parseInt(xVal, 10);
|
||||
const gy = parseInt(yVal, 10);
|
||||
if (isNaN(gx) || isNaN(gy)) return;
|
||||
if (gx === target.x && gy === target.y) {
|
||||
setFeedback("correct");
|
||||
setScore((s) => ({ correct: s.correct + 1, total: s.total + 1 }));
|
||||
} else if (gx === target.y && gy === target.x && target.x !== target.y) {
|
||||
setFeedback("swapped");
|
||||
setScore((s) => ({ correct: s.correct, total: s.total + 1 }));
|
||||
} else {
|
||||
setFeedback("incorrect");
|
||||
setScore((s) => ({ correct: s.correct, total: s.total + 1 }));
|
||||
}
|
||||
}
|
||||
|
||||
function next() {
|
||||
setTarget({ x: randInt(-9, 9), y: randInt(-9, 9) });
|
||||
setXVal("");
|
||||
setYVal("");
|
||||
setFeedback(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-unit-7">Quick Practice</p>
|
||||
<span className="rounded-full bg-unit-7-light px-3 py-1 text-xs font-bold text-unit-7-dark">
|
||||
{score.correct}/{score.total}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-center text-lg font-bold">What are the coordinates of this point?</p>
|
||||
<div className="mx-auto max-w-md">
|
||||
<CartesianGrid points={[{ x: target.x, y: target.y, tone: "solid" }]} highlight={feedback ? target : undefined} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
<span className="text-lg font-bold text-muted">(</span>
|
||||
<input
|
||||
type="number"
|
||||
value={xVal}
|
||||
onChange={(e) => {
|
||||
setXVal(e.target.value);
|
||||
setFeedback(null);
|
||||
}}
|
||||
disabled={feedback === "correct"}
|
||||
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-7"
|
||||
placeholder="x"
|
||||
aria-label="x coordinate"
|
||||
/>
|
||||
<span className="text-lg font-bold text-muted">,</span>
|
||||
<input
|
||||
type="number"
|
||||
value={yVal}
|
||||
onChange={(e) => {
|
||||
setYVal(e.target.value);
|
||||
setFeedback(null);
|
||||
}}
|
||||
disabled={feedback === "correct"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (feedback === "correct") next();
|
||||
else check();
|
||||
}
|
||||
}}
|
||||
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-7"
|
||||
placeholder="y"
|
||||
aria-label="y coordinate"
|
||||
/>
|
||||
<span className="text-lg font-bold text-muted">)</span>
|
||||
{feedback === "correct" ? (
|
||||
<button onClick={next} className="rounded-lg bg-foreground px-5 py-2.5 text-sm font-bold text-background hover:bg-foreground/80">
|
||||
Next →
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={check} className="rounded-lg bg-unit-7 px-5 py-2.5 text-sm font-bold text-white hover:bg-unit-7-dark">
|
||||
Check
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{feedback === "correct" && <p className="text-center text-sm font-bold text-correct">Correct! ({target.x}, {target.y})</p>}
|
||||
{feedback === "swapped" && (
|
||||
<p className="text-center text-sm font-bold text-incorrect">You swapped them — the x-coordinate comes first! Try again.</p>
|
||||
)}
|
||||
{feedback === "incorrect" && (
|
||||
<p className="text-center text-sm font-bold text-incorrect">Not quite. Trace down to the x-axis, then across to the y-axis.</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main explorer ────────────────────────────────────────────────────────────
|
||||
|
||||
export function ReadCoordinatesExplorer() {
|
||||
const [point, setPoint] = useState(() => ({ x: 3, y: 2 }));
|
||||
const [steps, setSteps] = useState<Step[] | null>(() => buildReadSteps(3, 2));
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const done = steps ? currentStep >= steps.length - 1 : false;
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setCurrentStep(0);
|
||||
setIsPlaying(false);
|
||||
}, []);
|
||||
|
||||
function newPoint() {
|
||||
let x = randInt(-9, 9);
|
||||
const y = randInt(-9, 9);
|
||||
if (x === point.x && y === point.y) x = -x || 1;
|
||||
setPoint({ x, y });
|
||||
setSteps(buildReadSteps(x, y));
|
||||
setCurrentStep(0);
|
||||
setIsPlaying(false);
|
||||
}
|
||||
|
||||
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 step = steps ? steps[currentStep] : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-7">
|
||||
Read the coordinates of the point
|
||||
</p>
|
||||
<button
|
||||
onClick={newPoint}
|
||||
className="rounded-lg bg-unit-7 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-7-dark"
|
||||
>
|
||||
New Point ↻
|
||||
</button>
|
||||
</Card>
|
||||
|
||||
{/* Display card */}
|
||||
<Card className="flex min-h-[320px] flex-col items-center justify-center gap-4 p-6">
|
||||
{step && (
|
||||
<>
|
||||
<p className="text-center text-sm font-medium text-muted">{step.label}</p>
|
||||
<MathDisplay math={step.math} className="text-2xl" />
|
||||
<div className="w-full max-w-md">
|
||||
<CartesianGrid
|
||||
points={step.points}
|
||||
highlight={
|
||||
step.guide && step.highlight
|
||||
? { ...step.highlight, axis: step.guide === "x" ? "x" : "both" }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Quick practice */}
|
||||
<ClientOnly fallback={<Card className="min-h-[220px]" />}>
|
||||
<ReadPractice />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user