+ {/* Mode toggle */}
+
+ {(["number-line", "rules"] as Mode[]).map((m) => (
+ {
+ setMode(m);
+ reset();
+ }}
+ className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
+ mode === m
+ ? "border-unit-5 bg-unit-5 text-white"
+ : "border-unit-5/40 text-unit-5 hover:bg-unit-5-light"
+ }`}
+ >
+ {m === "number-line" ? "Number Line" : "Sign Rules"}
+
+ ))}
+
+
+ {/* Operation tabs */}
+
+ {(["add", "subtract"] as const).map((o) => (
+ {
+ setOp(o);
+ reset();
+ }}
+ className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
+ op === o
+ ? "border-unit-5-dark bg-unit-5-dark text-white"
+ : "border-unit-5/30 text-unit-5-dark hover:bg-unit-5-light"
+ }`}
+ >
+ {o === "add" ? "Add (+)" : "Subtract (−)"}
+
+ ))}
+
+
+ {/* 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"
+ />
+
+ Go →
+
+
+ {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 (
+
+ {/* 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 ? (
+
+ Check
+
+ ) : (
+
+ Next City →
+
+ )}
+
+ {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 ? (
+
+ Check
+
+ ) : (
+
+ Next →
+
+ )}
+
+ {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) => (
+ {
+ setTab(t);
+ reset();
+ }}
+ className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
+ tab === t
+ ? "border-unit-5 bg-unit-5 text-white"
+ : "border-unit-5/40 text-unit-5 hover:bg-unit-5-light"
+ }`}
+ >
+ {t === "multiply"
+ ? "Multiply (×)"
+ : t === "divide"
+ ? "Divide (÷)"
+ : "Thermometer"}
+
+ ))}
+
+
+ {/* Input Card */}
+
+
+ {tab === "thermometer" ? "Enter temperature scenario" : "Enter two integers"}
+
+
+ {tab === "thermometer" ? (
+
+
+ Start temp (°C)
+ 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"
+ />
+
+
+ Change (°C)
+ 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"
+ />
+
+
+ Go →
+
+
+ ) : (
+
+ 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"
+ />
+
+ Go →
+
+
+ )}
+ {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": {