Files
cabrits-math/components/explorers/binary-converter-explorer.tsx
2026-05-12 06:26:33 -04:00

318 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useMemo, useCallback } from "react";
import { Card } from "@/components/ui/card";
const PRESETS = ["101", "1011", "1101", "10101", "11000", "11111", "100000", "110010"];
interface Column {
exponent: number;
placeValue: number;
digit: string;
contrib: number;
isOne: boolean;
}
function powerOfTwo(n: number) {
let v = 1;
for (let i = 0; i < n; i++) v *= 2;
return v;
}
function buildColumnsFor(binaryStr: string): Column[] {
const n = binaryStr.length;
const cols: Column[] = [];
for (let i = 0; i < n; i++) {
const exponent = n - 1 - i;
const digit = binaryStr[i];
const placeValue = powerOfTwo(exponent);
const isOne = digit === "1";
cols.push({
exponent,
placeValue,
digit,
contrib: parseInt(digit, 10) * placeValue,
isOne,
});
}
return cols;
}
function emptyColumns(): Column[] {
const cols: Column[] = [];
for (let exp = 5; exp >= 0; exp--) {
cols.push({
exponent: exp,
placeValue: powerOfTwo(exp),
digit: "_",
contrib: 0,
isOne: false,
});
}
return cols;
}
export function BinaryConverterExplorer() {
const [input, setInput] = useState("");
const [submitted, setSubmitted] = useState<string | null>(null);
const [error, setError] = useState("");
const [answerHidden, setAnswerHidden] = useState(false);
const [chartHidden, setChartHidden] = useState(false);
const handleInputChange = (value: string) => {
setInput(value.replace(/[^01]/g, ""));
setError("");
};
const convert = useCallback((raw?: string) => {
const source = (raw ?? input).trim();
if (source.length === 0) {
setError("Type a binary number first (only 0s and 1s).");
setSubmitted(null);
return;
}
if (!/^[01]+$/.test(source)) {
setError("Binary numbers use only 0 and 1. Found another digit.");
setSubmitted(null);
return;
}
if (source.length > 8) {
setError("Keep it to 8 digits or fewer for this lesson.");
return;
}
setError("");
setSubmitted(source);
}, [input]);
const applyPreset = (value: string) => {
setInput(value);
convert(value);
};
const handleClear = () => {
setInput("");
setSubmitted(null);
setError("");
};
const handleRandom = () => {
const len = 3 + Math.floor(Math.random() * 4);
let s = "1";
for (let i = 1; i < len; i++) {
s += Math.random() < 0.5 ? "0" : "1";
}
setInput(s);
convert(s);
};
const columns = useMemo(() => {
if (!submitted) return emptyColumns();
return buildColumnsFor(submitted);
}, [submitted]);
const total = useMemo(() => {
if (!submitted) return 0;
return columns.reduce((acc, c) => acc + c.contrib, 0);
}, [columns, submitted]);
const fullExpansion = submitted
? columns.map((c) => `${c.digit}×${c.placeValue}`).join(" + ")
: "";
const onesOnly = submitted
? columns.filter((c) => c.isOne).length > 0
? columns.filter((c) => c.isOne).map((c) => c.placeValue).join(" + ") + " = " + total
: "0"
: "";
return (
<div className="space-y-4">
{/* Input Card */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-6">
Type a Binary Number
</p>
<div className="flex flex-wrap items-center gap-3">
<label
htmlFor="binaryInput"
className="text-sm font-bold text-unit-6-dark"
>
Binary:
</label>
<input
id="binaryInput"
type="text"
inputMode="numeric"
maxLength={8}
autoComplete="off"
spellCheck={false}
placeholder="e.g. 1011"
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") convert();
}}
className="w-44 rounded-lg border-2 border-unit-6 bg-surface px-3 py-2 text-center font-mono text-2xl font-bold tracking-[0.3em] text-unit-6 outline-none focus:border-unit-6-dark"
aria-label="Binary number input"
/>
<button
onClick={() => convert()}
className="rounded-lg bg-unit-6 px-5 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-6-dark"
>
Convert
</button>
<button
onClick={handleClear}
className="rounded-lg border-2 border-border bg-surface px-4 py-2 text-sm font-bold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
Clear
</button>
<button
onClick={handleRandom}
className="rounded-lg border-2 border-border bg-surface px-4 py-2 text-sm font-bold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
Random
</button>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{PRESETS.map((p) => (
<button
key={p}
onClick={() => applyPreset(p)}
className="rounded-lg border-2 border-unit-6/30 bg-unit-6-light px-3 py-1 font-mono text-sm font-bold tracking-widest text-unit-6-dark transition-colors hover:border-unit-6 hover:bg-unit-6 hover:text-white"
>
{p}
</button>
))}
</div>
{error && (
<p className="mt-3 rounded-lg border-2 border-incorrect bg-incorrect-light px-3 py-2 text-center text-sm font-bold text-incorrect">
{error}
</p>
)}
</Card>
{/* Place value chart */}
<Card>
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<p className="text-xs font-bold uppercase tracking-wider text-unit-6">
Place Value Chart (powers of 2)
</p>
<button
onClick={() => setChartHidden((v) => !v)}
className="rounded-md border border-border bg-surface px-2.5 py-1 text-xs font-semibold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
{chartHidden ? "Show Chart" : "Hide Chart"}
</button>
</div>
{!chartHidden && (
<div className="overflow-x-auto rounded-xl border-2 border-unit-6/30 bg-unit-6-light/40 p-3">
<table className="mx-auto border-collapse font-mono">
<tbody>
<tr>
{columns.map((c) => (
<td
key={`p-${c.exponent}`}
className="min-w-[64px] border-2 border-unit-6/40 bg-unit-6-light px-4 py-2 text-center text-sm font-bold text-unit-6-dark"
>
2<sup>{c.exponent}</sup>
</td>
))}
</tr>
<tr>
{columns.map((c) => (
<td
key={`v-${c.exponent}`}
className="min-w-[64px] border-2 border-unit-6/40 bg-surface px-4 py-2 text-center text-base font-bold text-unit-6"
>
{c.placeValue}
</td>
))}
</tr>
<tr>
{columns.map((c, i) => (
<td
key={`d-${i}`}
className={`min-w-[64px] border-2 border-unit-6/40 px-4 py-2 text-center text-2xl font-extrabold ${
c.digit === "1"
? "bg-correct-light text-correct"
: c.digit === "_"
? "bg-surface text-muted/60"
: "bg-surface text-muted"
}`}
>
{c.digit}
</td>
))}
</tr>
<tr>
{columns.map((c, i) => (
<td
key={`c-${i}`}
className={`min-w-[64px] border-2 border-unit-6/40 px-4 py-2 text-center text-sm font-bold ${
c.isOne
? "bg-correct-light text-correct"
: c.digit === "_"
? "bg-[#fafbfc] text-muted/60"
: "bg-[#fafbfc] text-muted"
}`}
>
{submitted ? c.contrib : "_"}
</td>
))}
</tr>
</tbody>
</table>
</div>
)}
{submitted && (
<div className="mt-4 text-center font-mono text-base font-bold text-unit-6">
<div>{fullExpansion}</div>
<div>= {onesOnly}</div>
</div>
)}
</Card>
{/* Answer banner */}
<Card
className={`flex min-h-[90px] flex-col items-center justify-center gap-2 text-center ${
submitted && !answerHidden
? "border-correct bg-correct-light"
: "border-unit-6/30 bg-unit-6-light/60"
}`}
>
{!submitted ? (
<p className="font-mono text-base font-bold text-unit-6">
Type a binary number above and press Convert.
</p>
) : answerHidden ? (
<p className="font-mono text-lg font-bold text-unit-6">
{submitted}<sub>2</sub> = ? (press Show Answer)
</p>
) : (
<p className="font-mono text-2xl font-extrabold text-correct">
{submitted}<sub>2</sub> = {total} in denary
</p>
)}
<button
onClick={() => setAnswerHidden((v) => !v)}
className="mt-1 rounded-md border border-border bg-surface px-3 py-1 text-xs font-semibold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
{answerHidden ? "Show Answer" : "Hide Answer"}
</button>
</Card>
{/* Footer note */}
<Card className="border-l-4 border-l-unit-6 bg-unit-6-light/40">
<p className="text-sm text-unit-6-dark">
<strong>For students:</strong> solve on paper <em>first</em> using the chart in your worksheet. Type the binary number here only to <em>check</em>. The lesson is in the chart, not the click.
</p>
</Card>
</div>
);
}