diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..de7759c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(cd \"C:/Users/earl_/source/repos/CabritsMath/cabrits-math\" && npx next build 2>&1 | tail -80)" + ] + } +} diff --git a/app/globals.css b/app/globals.css index 4a4f9fc..3384db0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -25,6 +25,10 @@ --unit-4-light: #ffe8ef; --unit-4-dark: #be123c; + --unit-5: #7c3aed; + --unit-5-light: #f0e7ff; + --unit-5-dark: #5b21b6; + --correct: #16a34a; --correct-light: #dcfce7; --incorrect: #dc2626; @@ -59,6 +63,9 @@ --color-unit-4: var(--unit-4); --color-unit-4-light: var(--unit-4-light); --color-unit-4-dark: var(--unit-4-dark); + --color-unit-5: var(--unit-5); + --color-unit-5-light: var(--unit-5-light); + --color-unit-5-dark: var(--unit-5-dark); --color-correct: var(--correct); --color-correct-light: var(--correct-light); diff --git a/app/lessons/page.tsx b/app/lessons/page.tsx index 15f8008..afb0fdc 100644 --- a/app/lessons/page.tsx +++ b/app/lessons/page.tsx @@ -7,7 +7,7 @@ export default function LessonsOverview() { return (

All Topics

-

Form 1, Term 2 — Select a topic to explore

+

Form 1 — Select a topic to explore

{curriculum.map((unit) => (
diff --git a/app/lessons/unit-5-integers/add-subtract/page.tsx b/app/lessons/unit-5-integers/add-subtract/page.tsx new file mode 100644 index 0000000..93eb0cb --- /dev/null +++ b/app/lessons/unit-5-integers/add-subtract/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Breadcrumbs } from "@/components/layout/breadcrumbs"; +import { IntegerAddSubtractExplorer } from "@/components/explorers/integer-add-subtract-explorer"; + +export default function IntegerAddSubtractPage() { + return ( +
+ + +

Add and Subtract Integers

+ + +
+ ); +} diff --git a/app/lessons/unit-5-integers/multiply-divide/page.tsx b/app/lessons/unit-5-integers/multiply-divide/page.tsx new file mode 100644 index 0000000..3696be7 --- /dev/null +++ b/app/lessons/unit-5-integers/multiply-divide/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Breadcrumbs } from "@/components/layout/breadcrumbs"; +import { IntegerMultiplyDivideExplorer } from "@/components/explorers/integer-multiply-divide-explorer"; + +export default function IntegerMultiplyDividePage() { + return ( +
+ + +

Multiply and Divide Integers

+ + +
+ ); +} diff --git a/app/lessons/unit-5-integers/page.tsx b/app/lessons/unit-5-integers/page.tsx new file mode 100644 index 0000000..e4bc4e5 --- /dev/null +++ b/app/lessons/unit-5-integers/page.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; +import { curriculum } from "@/lib/curriculum"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Breadcrumbs } from "@/components/layout/breadcrumbs"; + +export default function Unit5Overview() { + const unit = curriculum[4]; + + return ( +
+ +
+ Unit 5 — {unit.weeks} +

{unit.title}

+

{unit.description}

+
+
+ {unit.topics.map((topic, i) => ( + + +
+ + {i + 1} + + Week {topic.week} +
+

{topic.title}

+

{topic.description}

+
+ + ))} +
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 5ba59db..ee2c679 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -24,6 +24,11 @@ const unitStyles = { unitCard: "border-[#ffb2c7] bg-[#fff0f4]", chip: "bg-[#c6174e]", }, + "unit-5": { + tile: "from-[#a78bfa] to-[#6d28d9]", + unitCard: "border-[#c4b5fd] bg-[#f5f0ff]", + chip: "bg-[#6d28d9]", + }, }; const topicTiles = [ diff --git a/components/explorers/integer-add-subtract-explorer.tsx b/components/explorers/integer-add-subtract-explorer.tsx new file mode 100644 index 0000000..06ccb2e --- /dev/null +++ b/components/explorers/integer-add-subtract-explorer.tsx @@ -0,0 +1,593 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { Card } from "@/components/ui/card"; +import { StepControls } from "./step-controls"; +import { MathDisplay } from "@/components/math/math-display"; + +type Mode = "number-line" | "rules"; + +interface Step { + label: string; + math: string; + numberLineHighlight?: { + start: number; + end: number; + direction: "left" | "right"; + moves: number; + }; +} + +function buildAddSubtractSteps(a: number, b: number, op: "add" | "subtract"): Step[] { + const steps: Step[] = []; + const opSymbol = op === "add" ? "+" : "-"; + const expression = `${a} ${opSymbol} ${b < 0 ? `(${b})` : b}`; + + // Step 0: Show the original expression + steps.push({ + label: "Start with the expression", + math: expression, + numberLineHighlight: { start: a, end: a, direction: "right", moves: 0 }, + }); + + if (op === "add") { + // Adding + steps.push({ + label: `Place your finger on ${a} on the number line`, + math: `\\text{Start at } ${a}`, + numberLineHighlight: { start: a, end: a, direction: "right", moves: 0 }, + }); + + if (b >= 0) { + steps.push({ + label: `Adding a positive number: move ${b} steps to the RIGHT`, + math: `${a} + ${b} \\longrightarrow \\text{move right } ${b}`, + numberLineHighlight: { start: a, end: a + b, direction: "right", moves: b }, + }); + } else { + steps.push({ + label: `Adding a negative number: move ${Math.abs(b)} steps to the LEFT`, + math: `${a} + (${b}) \\longrightarrow \\text{move left } ${Math.abs(b)}`, + numberLineHighlight: { start: a, end: a + b, direction: "left", moves: Math.abs(b) }, + }); + } + + const result = a + b; + steps.push({ + label: `You land on ${result}`, + math: `${expression} = ${result}`, + numberLineHighlight: { start: a, end: result, direction: b >= 0 ? "right" : "left", moves: Math.abs(b) }, + }); + + // Sign rule explanation + if (a >= 0 && b >= 0) { + steps.push({ + label: "Rule: Positive + Positive = Positive", + math: `(+) + (+) = (+) \\quad \\Rightarrow \\quad ${expression} = ${result}`, + numberLineHighlight: { start: a, end: result, direction: "right", moves: Math.abs(b) }, + }); + } else if (a < 0 && b < 0) { + steps.push({ + label: "Rule: Negative + Negative = Negative (add magnitudes, keep negative sign)", + math: `(-) + (-) = (-) \\quad \\Rightarrow \\quad ${expression} = ${result}`, + numberLineHighlight: { start: a, end: result, direction: "left", moves: Math.abs(b) }, + }); + } else { + steps.push({ + label: "Rule: Different signs? Subtract the smaller from the larger, keep the sign of the larger", + math: `|${Math.abs(a)}| - |${Math.abs(b)}| = ${Math.abs(Math.abs(a) - Math.abs(b))} \\quad \\Rightarrow \\quad ${expression} = ${result}`, + numberLineHighlight: { start: a, end: result, direction: result >= a ? "right" : "left", moves: Math.abs(b) }, + }); + } + } else { + // Subtracting + steps.push({ + label: `Place your finger on ${a} on the number line`, + math: `\\text{Start at } ${a}`, + numberLineHighlight: { start: a, end: a, direction: "right", moves: 0 }, + }); + + if (b >= 0) { + steps.push({ + label: `Subtracting a positive number: move ${b} steps to the LEFT`, + math: `${a} - ${b} \\longrightarrow \\text{move left } ${b}`, + numberLineHighlight: { start: a, end: a - b, direction: "left", moves: b }, + }); + } else { + steps.push({ + label: `Subtracting a negative number is the same as ADDING a positive! Move ${Math.abs(b)} steps RIGHT`, + math: `${a} - (${b}) = ${a} + ${Math.abs(b)} \\longrightarrow \\text{move right } ${Math.abs(b)}`, + numberLineHighlight: { start: a, end: a - b, direction: "right", moves: Math.abs(b) }, + }); + } + + const result = a - b; + steps.push({ + label: `You land on ${result}`, + math: `${expression} = ${result}`, + numberLineHighlight: { start: a, end: result, direction: result >= a ? "right" : "left", moves: Math.abs(b) }, + }); + + // Two like/unlike signs rule + if (b >= 0) { + steps.push({ + label: "Two unlike signs (+ -) becomes negative: subtract means move left", + math: `+(-)\\text{ becomes } - \\quad \\Rightarrow \\quad ${expression} = ${result}`, + numberLineHighlight: { start: a, end: result, direction: "left", moves: Math.abs(b) }, + }); + } else { + steps.push({ + label: "Two like signs (- -) becomes positive: subtracting a negative means move right!", + math: `-(-) \\text{ becomes } + \\quad \\Rightarrow \\quad ${expression} = ${result}`, + numberLineHighlight: { start: a, end: result, direction: "right", moves: Math.abs(b) }, + }); + } + } + + return steps; +} + +function NumberLine({ + highlight, + animate, +}: { + highlight?: Step["numberLineHighlight"]; + animate: boolean; +}) { + const minVal = -15; + const maxVal = 15; + const range = maxVal - minVal; + + const start = highlight?.start ?? 0; + const end = highlight?.end ?? 0; + + const startX = ((start - minVal) / range) * 100; + const endX = ((end - minVal) / range) * 100; + + const leftX = Math.min(startX, endX); + const width = Math.abs(endX - startX); + + return ( +
+
+ {/* Arrow path highlight */} + {highlight && highlight.moves > 0 && ( +
+ )} + + {/* Direction arrow */} + {highlight && highlight.moves > 0 && ( +
+ {highlight.direction === "right" ? "→ right →" : "← left ←"} +
+ {highlight.moves} step{highlight.moves !== 1 ? "s" : ""} +
+
+ )} + + {/* Number line */} +
+
+ {/* Tick marks */} + {Array.from({ length: range + 1 }, (_, i) => { + const val = minVal + i; + const x = (i / range) * 100; + const isZero = val === 0; + const isHighlighted = highlight && (val === start || val === end); + const isInRange = highlight && highlight.moves > 0 && + val >= Math.min(start, end) && val <= Math.max(start, end); + + return ( +
+
+ {(val % 5 === 0 || isHighlighted) && ( + + {val} + + )} +
+ ); + })} +
+ + {/* Start marker */} + {highlight && ( +
+
+ + Start + +
+
+
+ )} + + {/* End marker */} + {highlight && highlight.moves > 0 && animate && ( +
+
+ + End: {end} + +
+
+
+ )} +
+ + {/* Arrows on edges */} +
+ ← negative + positive → +
+
+ ); +} + +function SignRuleCard({ rule, active }: { rule: string; active: boolean }) { + return ( +
+ {rule} +
+ ); +} + +function QuickPractice() { + const [problem, setProblem] = useState(() => generateProblem()); + const [userAnswer, setUserAnswer] = useState(""); + const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null); + const [score, setScore] = useState({ correct: 0, total: 0 }); + + function generateProblem() { + const ops = ["+", "-"] as const; + const op = ops[Math.floor(Math.random() * ops.length)]; + const a = Math.floor(Math.random() * 21) - 10; + const b = Math.floor(Math.random() * 21) - 10; + const answer = op === "+" ? a + b : a - b; + return { a, b, op, answer }; + } + + function checkAnswer() { + const parsed = parseInt(userAnswer); + if (isNaN(parsed)) return; + const isCorrect = parsed === problem.answer; + setFeedback(isCorrect ? "correct" : "incorrect"); + setScore((prev) => ({ + correct: prev.correct + (isCorrect ? 1 : 0), + total: prev.total + 1, + })); + } + + function nextProblem() { + setProblem(generateProblem()); + setUserAnswer(""); + setFeedback(null); + } + + const displayB = problem.b < 0 ? `(${problem.b})` : `${problem.b}`; + + return ( + +
+

Quick Practice

+ + {score.correct}/{score.total} + +
+ +
+ + {problem.a} {problem.op} {displayB} = + + { + setUserAnswer(e.target.value); + setFeedback(null); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + if (feedback !== null) nextProblem(); + else checkAnswer(); + } + }} + className={`w-20 rounded-lg border-2 px-3 py-2 text-center text-xl font-bold outline-none transition-colors ${ + feedback === "correct" + ? "border-correct bg-correct-light" + : feedback === "incorrect" + ? "border-incorrect bg-incorrect-light" + : "border-border focus:border-unit-5" + }`} + placeholder="?" + aria-label="Your answer" + /> + {feedback === null ? ( + + ) : ( + + )} +
+ + {feedback === "correct" && ( +

Correct! Well done!

+ )} + {feedback === "incorrect" && ( +

+ Not quite. The answer is {problem.answer}. Try the next one! +

+ )} +
+ ); +} + +export function IntegerAddSubtractExplorer() { + const [mode, setMode] = useState("number-line"); + const [op, setOp] = useState<"add" | "subtract">("add"); + const [inputA, setInputA] = useState("-3"); + const [inputB, setInputB] = useState("5"); + const [error, setError] = useState(""); + + const [steps, setSteps] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + + const done = steps ? currentStep >= steps.length - 1 : false; + + function handleGo() { + setError(""); + const a = parseInt(inputA); + const b = parseInt(inputB); + if (isNaN(a) || isNaN(b)) { + setError("Enter valid integers."); + return; + } + if (Math.abs(a) > 15 || Math.abs(b) > 15) { + setError("Keep numbers between -15 and 15 for the number line."); + return; + } + const result = op === "add" ? a + b : a - b; + if (Math.abs(result) > 15) { + setError("Result goes off the number line. Try smaller numbers."); + return; + } + try { + const s = buildAddSubtractSteps(a, b, op); + setSteps(s); + setCurrentStep(0); + setIsPlaying(false); + } catch { + setError("Could not compute. Check your inputs."); + } + } + + 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 reset = useCallback(() => { + setSteps(null); + setCurrentStep(0); + setIsPlaying(false); + }, []); + + const step = steps ? steps[currentStep] : null; + + const addRules = [ + { rule: "(+) + (+) = (+)", active: step?.label.includes("Positive + Positive") ?? false }, + { rule: "(-) + (-) = (-)", active: step?.label.includes("Negative + Negative") ?? false }, + { rule: "Different signs? Subtract, keep sign of larger", active: step?.label.includes("Different signs") ?? false }, + ]; + + const subRules = [ + { rule: "+(−) becomes −", active: step?.label.includes("unlike signs") ?? false }, + { rule: "−(−) becomes +", active: step?.label.includes("like signs") ?? false }, + ]; + + return ( +
+ {/* Mode toggle */} +
+ {(["number-line", "rules"] as Mode[]).map((m) => ( + + ))} +
+ + {/* Operation tabs */} +
+ {(["add", "subtract"] as const).map((o) => ( + + ))} +
+ + {/* Input Card */} + +

+ Enter two integers +

+
+ setInputA(e.target.value)} + 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-5" + aria-label="First integer" + /> + + {op === "add" ? "+" : "−"} + + setInputB(e.target.value)} + 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-5" + aria-label="Second integer" + /> + +
+ {error &&

{error}

} +
+ + {/* Sign Rules Reference */} + {mode === "rules" && ( +
+ {(op === "add" ? addRules : subRules).map((r) => ( + + ))} +
+ )} + + {/* Display Card with Number Line */} + + {!step ? ( +

+ Enter integers above and click Go +

+ ) : ( + <> +

{step.label}

+ + {mode === "number-line" && ( + 0} + /> + )} + + )} +
+ + {/* Controls */} + + 0} + /> + + + {/* Result */} + + {!done || !steps ? ( +

Result will appear here when steps are complete

+ ) : ( + <> +

Answer

+
+ +
+ + )} +
+ + {/* Quick Practice */} + +
+ ); +} diff --git a/components/explorers/integer-multiply-divide-explorer.tsx b/components/explorers/integer-multiply-divide-explorer.tsx new file mode 100644 index 0000000..01e28fa --- /dev/null +++ b/components/explorers/integer-multiply-divide-explorer.tsx @@ -0,0 +1,708 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { Card } from "@/components/ui/card"; +import { StepControls } from "./step-controls"; +import { MathDisplay } from "@/components/math/math-display"; + +type Tab = "multiply" | "divide" | "thermometer"; + +interface Step { + label: string; + math: string; + signVisual?: { signA: "+" | "-"; signB: "+" | "-"; resultSign: "+" | "-" }; + thermometer?: { startTemp: number; change: number; endTemp: number }; +} + +function getSign(n: number): "+" | "-" { + return n >= 0 ? "+" : "-"; +} + +function buildMultiplySteps(a: number, b: number): Step[] { + const steps: Step[] = []; + const result = a * b; + const signA = getSign(a); + const signB = getSign(b); + const signR = getSign(result); + + steps.push({ + label: "Start with the multiplication", + math: `${a < 0 ? `(${a})` : a} \\times ${b < 0 ? `(${b})` : b}`, + }); + + // Step: Multiply the absolute values + steps.push({ + label: `First, multiply the absolute values: ${Math.abs(a)} × ${Math.abs(b)}`, + math: `|${a}| \\times |${b}| = ${Math.abs(a)} \\times ${Math.abs(b)} = ${Math.abs(result)}`, + }); + + // Step: Determine the sign + const sameSign = (a >= 0 && b >= 0) || (a < 0 && b < 0); + if (sameSign) { + steps.push({ + label: `Same signs (${signA} × ${signB}): the result is POSITIVE`, + math: `(${signA}) \\times (${signB}) = (+)`, + signVisual: { signA, signB, resultSign: "+" }, + }); + } else { + steps.push({ + label: `Different signs (${signA} × ${signB}): the result is NEGATIVE`, + math: `(${signA}) \\times (${signB}) = (-)`, + signVisual: { signA, signB, resultSign: "-" }, + }); + } + + // Step: Combine + steps.push({ + label: "Combine the magnitude and sign", + math: `${a < 0 ? `(${a})` : a} \\times ${b < 0 ? `(${b})` : b} = ${result}`, + signVisual: { signA, signB, resultSign: signR }, + }); + + // Story time for negative × negative + if (a < 0 && b < 0) { + steps.push({ + label: "Why does negative × negative = positive? Think of it like a double-negative in English!", + math: `\\text{\"Do NOT not eat!\"} = \\text{\"Eat!\"} \\quad (-) \\times (-) = (+)`, + signVisual: { signA: "-", signB: "-", resultSign: "+" }, + }); + } + + return steps; +} + +function buildDivideSteps(a: number, b: number): Step[] { + const steps: Step[] = []; + const result = a / b; + const signA = getSign(a); + const signB = getSign(b); + const signR = getSign(result); + const isWhole = Number.isInteger(result); + const displayResult = isWhole ? result.toString() : result.toFixed(2); + + steps.push({ + label: "Start with the division", + math: `${a < 0 ? `(${a})` : a} \\div ${b < 0 ? `(${b})` : b}`, + }); + + steps.push({ + label: `First, divide the absolute values: ${Math.abs(a)} ÷ ${Math.abs(b)}`, + math: `|${a}| \\div |${b}| = ${Math.abs(a)} \\div ${Math.abs(b)} = ${isWhole ? Math.abs(result) : Math.abs(result).toFixed(2)}`, + }); + + const sameSign = (a >= 0 && b >= 0) || (a < 0 && b < 0); + if (sameSign) { + steps.push({ + label: `Same signs (${signA} ÷ ${signB}): the result is POSITIVE`, + math: `(${signA}) \\div (${signB}) = (+)`, + signVisual: { signA, signB, resultSign: "+" }, + }); + } else { + steps.push({ + label: `Different signs (${signA} ÷ ${signB}): the result is NEGATIVE`, + math: `(${signA}) \\div (${signB}) = (-)`, + signVisual: { signA, signB, resultSign: "-" }, + }); + } + + steps.push({ + label: "Combine the magnitude and sign", + math: `${a < 0 ? `(${a})` : a} \\div ${b < 0 ? `(${b})` : b} = ${displayResult}`, + signVisual: { signA, signB, resultSign: signR }, + }); + + return steps; +} + +function buildThermometerSteps(startTemp: number, change: number): Step[] { + const steps: Step[] = []; + const endTemp = startTemp + change; + const direction = change >= 0 ? "rose" : "dropped"; + const absChange = Math.abs(change); + + steps.push({ + label: "Read the starting temperature on the thermometer", + math: `\\text{Starting temperature: } ${startTemp}°C`, + thermometer: { startTemp, change: 0, endTemp: startTemp }, + }); + + steps.push({ + label: `The temperature ${direction} by ${absChange}°C`, + math: change >= 0 + ? `${startTemp}°C + ${absChange}°C` + : `${startTemp}°C - ${absChange}°C`, + thermometer: { startTemp, change: 0, endTemp: startTemp }, + }); + + steps.push({ + label: change >= 0 ? "Rising temperature means we ADD" : "Dropping temperature means we SUBTRACT", + math: `${startTemp} + (${change}) = ${endTemp}`, + thermometer: { startTemp, change, endTemp }, + }); + + steps.push({ + label: `The new temperature is ${endTemp}°C`, + math: `\\text{New temperature: } ${endTemp}°C`, + thermometer: { startTemp, change, endTemp }, + }); + + return steps; +} + +function SignRuleGrid({ visual, operation }: { visual?: Step["signVisual"]; operation: "×" | "÷" }) { + const rules = [ + { a: "+", b: "+", result: "+", example: operation === "×" ? "3 × 2 = 6" : "6 ÷ 2 = 3" }, + { a: "-", b: "-", result: "+", example: operation === "×" ? "(-3) × (-2) = 6" : "(-6) ÷ (-2) = 3" }, + { a: "+", b: "-", result: "-", example: operation === "×" ? "3 × (-2) = -6" : "6 ÷ (-2) = -3" }, + { a: "-", b: "+", result: "-", example: operation === "×" ? "(-3) × 2 = -6" : "(-6) ÷ 2 = -3" }, + ]; + + return ( +
+ {rules.map((r) => { + const isActive = visual && visual.signA === r.a && visual.signB === r.b; + return ( +
+

+ ({r.a}) {operation} ({r.b}) = ({r.result}) +

+

+ {r.example} +

+
+ ); + })} +
+ ); +} + +function Thermometer({ data }: { data?: Step["thermometer"] }) { + const minTemp = -20; + const maxTemp = 30; + const range = maxTemp - minTemp; + + const startTemp = data?.startTemp ?? 0; + const endTemp = data?.endTemp ?? 0; + + const startY = ((startTemp - minTemp) / range) * 100; + const endY = ((endTemp - minTemp) / range) * 100; + const fillY = ((endTemp - minTemp) / range) * 100; + + const ticks = []; + for (let t = minTemp; t <= maxTemp; t += 5) { + ticks.push(t); + } + + return ( +
+ {/* Thermometer */} +
+
+ {/* Mercury fill */} +
= 0 + ? `linear-gradient(to top, #dc2626, #f97316)` + : `linear-gradient(to top, #2563eb, #60a5fa)`, + }} + /> + {/* Tick marks */} + {ticks.map((t) => { + const y = ((t - minTemp) / range) * 100; + return ( +
+
+
+ ); + })} +
+ {/* Bulb */} +
+ °C +
+
+ + {/* Scale labels */} +
+ {ticks.map((t) => { + const y = ((t - minTemp) / range) * 100; + const isStart = t === startTemp; + const isEnd = data && data.change !== 0 && t === endTemp; + return ( +
+ + {t}°C + + {isStart && ( + + START + + )} + {isEnd && ( + + END + + )} +
+ ); + })} +
+
+ ); +} + +function ThermometerPractice() { + const scenarios = [ + { city: "London", startTemp: -3, change: 7, question: "The temperature rose by 7°C. What is the new temperature?" }, + { city: "Moscow", startTemp: -12, change: -5, question: "The temperature dropped by 5°C. What is the new temperature?" }, + { city: "Kingston", startTemp: 28, change: -10, question: "The temperature dropped by 10°C. What is the new temperature?" }, + { city: "Ottawa", startTemp: -8, change: 15, question: "The temperature rose by 15°C. What is the new temperature?" }, + { city: "Reykjavik", startTemp: -1, change: -6, question: "The temperature dropped by 6°C. What is the new temperature?" }, + ]; + + const [scenarioIdx, setScenarioIdx] = useState(0); + const [userAnswer, setUserAnswer] = useState(""); + const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null); + + const scenario = scenarios[scenarioIdx]; + const correctAnswer = scenario.startTemp + scenario.change; + + function check() { + const parsed = parseInt(userAnswer); + if (isNaN(parsed)) return; + setFeedback(parsed === correctAnswer ? "correct" : "incorrect"); + } + + function next() { + setScenarioIdx((i) => (i + 1) % scenarios.length); + setUserAnswer(""); + setFeedback(null); + } + + return ( + +

+ Real-World Challenge +

+
+

+ In {scenario.city}, the temperature is {scenario.startTemp}°C. +

+

{scenario.question}

+
+
+ { + setUserAnswer(e.target.value); + setFeedback(null); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + if (feedback !== null) next(); + else check(); + } + }} + className={`w-24 rounded-lg border-2 px-3 py-2 text-center text-lg font-bold outline-none transition-colors ${ + feedback === "correct" + ? "border-correct bg-correct-light" + : feedback === "incorrect" + ? "border-incorrect bg-incorrect-light" + : "border-border focus:border-unit-5" + }`} + placeholder="°C" + aria-label="Temperature answer" + /> + °C + {feedback === null ? ( + + ) : ( + + )} +
+ {feedback === "correct" && ( +

+ Correct! {scenario.startTemp} + ({scenario.change}) = {correctAnswer}°C +

+ )} + {feedback === "incorrect" && ( +

+ Not quite. {scenario.startTemp} + ({scenario.change}) = {correctAnswer}°C +

+ )} +
+ ); +} + +function MultiplyDividePractice() { + const [problem, setProblem] = useState(() => generateProblem()); + const [userAnswer, setUserAnswer] = useState(""); + const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null); + const [score, setScore] = useState({ correct: 0, total: 0 }); + + function generateProblem() { + const ops = ["×", "÷"] as const; + const op = ops[Math.floor(Math.random() * ops.length)]; + + if (op === "×") { + const a = Math.floor(Math.random() * 25) - 12; + const b = Math.floor(Math.random() * 25) - 12; + return { a, b, op, answer: a * b }; + } else { + // Generate division with clean integer results + const b = Math.floor(Math.random() * 11) - 5; + const nonZeroB = b === 0 ? 3 : b; + const answer = Math.floor(Math.random() * 21) - 10; + const a = answer * nonZeroB; + return { a, b: nonZeroB, op, answer }; + } + } + + function check() { + const parsed = parseInt(userAnswer); + if (isNaN(parsed)) return; + const isCorrect = parsed === problem.answer; + setFeedback(isCorrect ? "correct" : "incorrect"); + setScore((prev) => ({ + correct: prev.correct + (isCorrect ? 1 : 0), + total: prev.total + 1, + })); + } + + function next() { + setProblem(generateProblem()); + setUserAnswer(""); + setFeedback(null); + } + + const displayA = problem.a < 0 ? `(${problem.a})` : `${problem.a}`; + const displayB = problem.b < 0 ? `(${problem.b})` : `${problem.b}`; + + return ( + +
+

Quick Practice

+ + {score.correct}/{score.total} + +
+
+ + {displayA} {problem.op} {displayB} = + + { + setUserAnswer(e.target.value); + setFeedback(null); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + if (feedback !== null) next(); + else check(); + } + }} + className={`w-20 rounded-lg border-2 px-3 py-2 text-center text-xl font-bold outline-none transition-colors ${ + feedback === "correct" + ? "border-correct bg-correct-light" + : feedback === "incorrect" + ? "border-incorrect bg-incorrect-light" + : "border-border focus:border-unit-5" + }`} + placeholder="?" + aria-label="Your answer" + /> + {feedback === null ? ( + + ) : ( + + )} +
+ {feedback === "correct" && ( +

Correct!

+ )} + {feedback === "incorrect" && ( +

+ Not quite. The answer is {problem.answer}. +

+ )} +
+ ); +} + +export function IntegerMultiplyDivideExplorer() { + const [tab, setTab] = useState("multiply"); + const [inputA, setInputA] = useState("-3"); + const [inputB, setInputB] = useState("5"); + const [startTemp, setStartTemp] = useState("-3"); + const [tempChange, setTempChange] = useState("7"); + const [error, setError] = useState(""); + + const [steps, setSteps] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + + const done = steps ? currentStep >= steps.length - 1 : false; + + function handleGo() { + setError(""); + + if (tab === "thermometer") { + const st = parseInt(startTemp); + const ch = parseInt(tempChange); + if (isNaN(st) || isNaN(ch)) { + setError("Enter valid integers."); + return; + } + if (Math.abs(st) > 20 || Math.abs(ch) > 30) { + setError("Keep temperatures reasonable (-20 to 30)."); + return; + } + const s = buildThermometerSteps(st, ch); + setSteps(s); + setCurrentStep(0); + setIsPlaying(false); + return; + } + + const a = parseInt(inputA); + const b = parseInt(inputB); + if (isNaN(a) || isNaN(b)) { + setError("Enter valid integers."); + return; + } + if (tab === "divide" && b === 0) { + setError("Cannot divide by zero!"); + return; + } + if (Math.abs(a) > 99 || Math.abs(b) > 99) { + setError("Keep numbers under 100."); + return; + } + if (tab === "divide" && !Number.isInteger(a / b)) { + setError("Choose numbers that divide evenly for now."); + return; + } + + try { + const s = tab === "multiply" ? buildMultiplySteps(a, b) : buildDivideSteps(a, b); + setSteps(s); + setCurrentStep(0); + setIsPlaying(false); + } catch { + setError("Could not compute. Check your inputs."); + } + } + + 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 reset = useCallback(() => { + setSteps(null); + setCurrentStep(0); + setIsPlaying(false); + }, []); + + const step = steps ? steps[currentStep] : null; + + return ( +
+ {/* Tab selector */} +
+ {(["multiply", "divide", "thermometer"] as Tab[]).map((t) => ( + + ))} +
+ + {/* Input Card */} + +

+ {tab === "thermometer" ? "Enter temperature scenario" : "Enter two integers"} +

+ + {tab === "thermometer" ? ( +
+
+ + setStartTemp(e.target.value)} + className="w-24 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-5" + aria-label="Starting temperature" + /> +
+
+ + setTempChange(e.target.value)} + className="w-24 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-5" + aria-label="Temperature change" + /> +
+ +
+ ) : ( +
+ setInputA(e.target.value)} + 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-5" + aria-label="First integer" + /> + + {tab === "multiply" ? "×" : "÷"} + + setInputB(e.target.value)} + 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-5" + aria-label="Second integer" + /> + +
+ )} + {error &&

{error}

} +
+ + {/* Sign Rules Grid (for multiply/divide) */} + {tab !== "thermometer" && ( + + )} + + {/* Display Card */} + + {!step ? ( +

+ Enter values above and click Go +

+ ) : ( + <> +

{step.label}

+ + {step.thermometer && } + + )} +
+ + {/* Controls */} + + 0} + /> + + + {/* Result */} + + {!done || !steps ? ( +

Result will appear here when steps are complete

+ ) : ( + <> +

Answer

+
+ +
+ + )} +
+ + {/* Practice sections */} + {tab === "thermometer" ? : } +
+ ); +} diff --git a/components/layout/mobile-nav.tsx b/components/layout/mobile-nav.tsx index 404c50b..d52854d 100644 --- a/components/layout/mobile-nav.tsx +++ b/components/layout/mobile-nav.tsx @@ -11,6 +11,7 @@ const unitColorMap = { "unit-2": "text-unit-2-dark", "unit-3": "text-unit-3-dark", "unit-4": "text-unit-4-dark", + "unit-5": "text-unit-5-dark", }; const unitDotColor = { @@ -18,6 +19,7 @@ const unitDotColor = { "unit-2": "bg-unit-2", "unit-3": "bg-unit-3", "unit-4": "bg-unit-4", + "unit-5": "bg-unit-5", }; export function MobileNav() { diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 99fa031..5d5c49b 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -31,6 +31,12 @@ const unitColorMap = { heading: "text-unit-4-dark", hoverBg: "hover:bg-unit-4-light/50", }, + "unit-5": { + active: "bg-unit-5-light text-unit-5-dark border-unit-5/20", + dot: "bg-unit-5", + heading: "text-unit-5-dark", + hoverBg: "hover:bg-unit-5-light/50", + }, }; export function Sidebar() { diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index 94bd4fc..a88ec0c 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -1,7 +1,7 @@ import { cn } from "@/lib/utils"; import { type HTMLAttributes } from "react"; -type BadgeVariant = "default" | "unit-1" | "unit-2" | "unit-3" | "unit-4"; +type BadgeVariant = "default" | "unit-1" | "unit-2" | "unit-3" | "unit-4" | "unit-5"; interface BadgeProps extends HTMLAttributes { variant?: BadgeVariant; @@ -13,6 +13,7 @@ const variantStyles: Record = { "unit-2": "border border-unit-2/20 bg-unit-2-light text-unit-2-dark", "unit-3": "border border-unit-3/20 bg-unit-3-light text-unit-3-dark", "unit-4": "border border-unit-4/20 bg-unit-4-light text-unit-4-dark", + "unit-5": "border border-unit-5/20 bg-unit-5-light text-unit-5-dark", }; export function Badge({ variant = "default", className, children, ...props }: BadgeProps) { diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 3b798ac..59e3a11 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,7 +1,7 @@ import { cn } from "@/lib/utils"; import { type ButtonHTMLAttributes } from "react"; -type ButtonVariant = "primary" | "secondary" | "ghost" | "unit-1" | "unit-2" | "unit-3" | "unit-4"; +type ButtonVariant = "primary" | "secondary" | "ghost" | "unit-1" | "unit-2" | "unit-3" | "unit-4" | "unit-5"; type ButtonSize = "sm" | "md" | "lg"; interface ButtonProps extends ButtonHTMLAttributes { @@ -17,6 +17,7 @@ const variantStyles: Record = { "unit-2": "bg-unit-2 text-white shadow-[var(--shadow-sm)] hover:bg-unit-2-dark hover:shadow-[var(--shadow-lg)]", "unit-3": "bg-unit-3 text-white shadow-[var(--shadow-sm)] hover:bg-unit-3-dark hover:shadow-[var(--shadow-lg)]", "unit-4": "bg-unit-4 text-white shadow-[var(--shadow-sm)] hover:bg-unit-4-dark hover:shadow-[var(--shadow-lg)]", + "unit-5": "bg-unit-5 text-white shadow-[var(--shadow-sm)] hover:bg-unit-5-dark hover:shadow-[var(--shadow-lg)]", }; const sizeStyles: Record = { diff --git a/components/ui/card.tsx b/components/ui/card.tsx index c7ae94a..366faac 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -2,7 +2,7 @@ import { cn } from "@/lib/utils"; import { type HTMLAttributes } from "react"; interface CardProps extends HTMLAttributes { - accent?: "unit-1" | "unit-2" | "unit-3" | "unit-4"; + accent?: "unit-1" | "unit-2" | "unit-3" | "unit-4" | "unit-5"; hover?: boolean; } @@ -11,6 +11,7 @@ const accentStyles = { "unit-2": "border-l-4 border-l-unit-2", "unit-3": "border-l-4 border-l-unit-3", "unit-4": "border-l-4 border-l-unit-4", + "unit-5": "border-l-4 border-l-unit-5", }; export function Card({ diff --git a/lib/curriculum.ts b/lib/curriculum.ts index f23e888..84e1e2e 100644 --- a/lib/curriculum.ts +++ b/lib/curriculum.ts @@ -7,12 +7,12 @@ export interface Topic { } export interface Unit { - number: 1 | 2 | 3 | 4; + number: 1 | 2 | 3 | 4 | 5; slug: string; title: string; description: string; weeks: string; - color: "unit-1" | "unit-2" | "unit-3" | "unit-4"; + color: "unit-1" | "unit-2" | "unit-3" | "unit-4" | "unit-5"; topics: Topic[]; } @@ -176,6 +176,30 @@ export const curriculum: Unit[] = [ }, ], }, + { + number: 5, + slug: "unit-5-integers", + title: "Integers", + description: "Add, subtract, multiply, divide integers and apply to real-world problems", + weeks: "Weeks 2-4", + color: "unit-5", + topics: [ + { + slug: "add-subtract", + title: "Add and Subtract Integers", + shortTitle: "Add & Subtract", + week: 2, + description: "Use number lines and sign rules to add and subtract positive and negative numbers", + }, + { + slug: "multiply-divide", + title: "Multiply and Divide Integers", + shortTitle: "Multiply & Divide", + week: 3, + description: "Sign rules for multiplication and division with real-world thermometer problems", + }, + ], + }, ]; export function getUnit(slug: string): Unit | undefined { @@ -187,12 +211,13 @@ export function getTopic(unitSlug: string, topicSlug: string): Topic | undefined return unit?.topics.find((t) => t.slug === topicSlug); } -export function getUnitColor(unitNumber: 1 | 2 | 3 | 4): string { +export function getUnitColor(unitNumber: 1 | 2 | 3 | 4 | 5): string { const colors = { 1: "unit-1", 2: "unit-2", 3: "unit-3", 4: "unit-4", + 5: "unit-5", }; return colors[unitNumber]; } diff --git a/package-lock.json b/package-lock.json index 908cd38..abc8d60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "clsx": "^2.1.1", "framer-motion": "^12.34.3", "katex": "^0.16.33", - "next": "16.1.6", - "react": "19.2.3", - "react-dom": "19.2.3", + "next": "^16.2.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "tailwind-merge": "^3.5.0" }, "devDependencies": { @@ -975,9 +975,10 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==" + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", + "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { "version": "16.1.6", @@ -989,12 +990,13 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", + "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1004,12 +1006,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", + "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1019,12 +1022,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", + "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1034,12 +1038,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", + "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1049,12 +1054,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", + "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1064,12 +1070,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", + "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1079,12 +1086,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", + "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1094,12 +1102,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", + "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -3395,10 +3404,11 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.5", @@ -4737,13 +4747,14 @@ "dev": true }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", + "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.2.1", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -4755,15 +4766,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.1", + "@next/swc-darwin-x64": "16.2.1", + "@next/swc-linux-arm64-gnu": "16.2.1", + "@next/swc-linux-arm64-musl": "16.2.1", + "@next/swc-linux-x64-gnu": "16.2.1", + "@next/swc-linux-x64-musl": "16.2.1", + "@next/swc-win32-arm64-msvc": "16.2.1", + "@next/swc-win32-x64-msvc": "16.2.1", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -5060,10 +5071,11 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5161,6 +5173,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5169,6 +5182,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, @@ -5819,10 +5833,11 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 8cb521f..25b7e96 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "clsx": "^2.1.1", "framer-motion": "^12.34.3", "katex": "^0.16.33", - "next": "16.1.6", - "react": "19.2.3", - "react-dom": "19.2.3", + "next": "^16.2.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "tailwind-merge": "^3.5.0" }, "devDependencies": {