+ {/* @ts-ignore */}
{label}
diff --git a/src/components/QuestNodeModal.tsx b/src/components/QuestNodeModal.tsx
index 85001a0..87c4cae 100644
--- a/src/components/QuestNodeModal.tsx
+++ b/src/components/QuestNodeModal.tsx
@@ -846,7 +846,6 @@ export const QuestNodeModal = ({
node,
arc,
arcAccent,
- arcDark,
arcId = "east_blue",
nodeIndex = 0,
onClose,
diff --git a/src/components/QuestProgressCard.tsx b/src/components/QuestProgressCard.tsx
deleted file mode 100644
index 3cbf784..0000000
--- a/src/components/QuestProgressCard.tsx
+++ /dev/null
@@ -1,507 +0,0 @@
-import { useState } from "react";
-import { ChevronDown, ChevronRight } from "lucide-react";
-import { useNavigate } from "react-router-dom";
-import type { QuestNode, QuestArc } from "../types/quest";
-import { CREW_RANKS } from "../types/quest";
-import {
- useQuestStore,
- getQuestSummary,
- getCrewRank,
-} from "../stores/useQuestStore";
-import { ChestOpenModal } from "./ChestOpenModal";
-
-// ─── Styles ───────────────────────────────────────────────────────────────────
-const STYLES = `
- @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@600;700;900&family=Sorts+Mill+Goudy:ital@0;1&family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
-
- /* ══ CARD SHELL ══ */
- .qpc2-card {
- position: relative; overflow: hidden;
- border-radius: 24px;
- background: linear-gradient(160deg, #0b1a35 0%, #060e1f 55%, #0d1530 100%);
- border: 1.5px solid rgba(251,191,36,0.2);
- box-shadow:
- 0 8px 32px rgba(0,0,0,0.35),
- 0 0 0 1px rgba(255,255,255,0.04) inset,
- 0 1px 0 rgba(255,255,255,0.08) inset;
- }
-
- /* Animated sea shimmer behind everything */
- .qpc2-sea {
- position: absolute; inset: 0; pointer-events: none; z-index: 0;
- background:
- repeating-linear-gradient(105deg, transparent 0%, transparent 55%,
- rgba(56,189,248,0.022) 56%, transparent 57%),
- repeating-linear-gradient(75deg, transparent 0%, transparent 70%,
- rgba(56,189,248,0.014) 71%, transparent 72%);
- background-size: 300% 300%, 250% 250%;
- animation: qpc2Sea 12s ease-in-out infinite alternate;
- }
- @keyframes qpc2Sea {
- 0% { background-position: 0% 0%, 100% 0%; }
- 100% { background-position: 100% 100%, 0% 100%; }
- }
-
- /* Faint gold orb top-right */
- .qpc2-orb {
- position: absolute; top: -40px; right: -30px;
- width: 160px; height: 160px; border-radius: 50%;
- background: radial-gradient(circle, rgba(251,191,36,0.14) 0%, transparent 70%);
- pointer-events: none; z-index: 0;
- }
-
- /* ══ RANK HERO (always visible) ══ */
- .qpc2-hero {
- position: relative; z-index: 2;
- padding: 1rem 1.1rem 0.9rem;
- cursor: pointer;
- transition: background 0.18s ease;
- }
- .qpc2-hero:hover { background: rgba(255,255,255,0.025); }
-
- .qpc2-hero-row {
- display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
- }
- .qpc2-hero-left { display: flex; align-items: center; gap: 0.75rem; flex: 1; min-width: 0; }
- .qpc2-hero-right { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
-
- /* Rank badge icon */
- .qpc2-rank-icon {
- width: 44px; height: 44px; border-radius: 14px; flex-shrink: 0;
- background: linear-gradient(135deg, #1e0e4a, #3730a3);
- border: 1.5px solid rgba(251,191,36,0.35);
- display: flex; align-items: center; justify-content: center;
- font-size: 1.35rem;
- box-shadow: 0 4px 0 rgba(30,14,74,0.7), 0 0 16px rgba(251,191,36,0.1);
- }
-
- .qpc2-rank-label {
- font-family: 'Cinzel', serif;
- font-size: 0.78rem; font-weight: 700;
- color: rgba(255,255,255,0.45); letter-spacing: 0.12em;
- text-transform: uppercase; margin-bottom: 0.1rem;
- }
- .qpc2-rank-name {
- font-family: 'Sorts Mill Goudy', serif;
- font-size: 1.05rem; font-weight: 700;
- color: #fbbf24;
- text-shadow: 0 0 18px rgba(251,191,36,0.45);
- line-height: 1.1;
- }
-
- /* Rank progress bar */
- .qpc2-rank-bar-wrap {
- margin-top: 0.55rem;
- display: flex; align-items: center; gap: 0.6rem;
- }
- .qpc2-rank-bar-track {
- flex: 1; height: 5px; border-radius: 100px;
- background: rgba(255,255,255,0.1); overflow: hidden;
- }
- .qpc2-rank-bar-fill {
- height: 100%; border-radius: 100px;
- background: linear-gradient(90deg, #fbbf24, #f59e0b);
- box-shadow: 0 0 8px rgba(251,191,36,0.5);
- transition: width 0.7s cubic-bezier(0.34,1.56,0.64,1);
- }
- .qpc2-rank-bar-label {
- font-family: 'Nunito Sans', sans-serif;
- font-size: 0.6rem; font-weight: 700;
- color: rgba(255,255,255,0.35); white-space: nowrap;
- }
-
- /* Stats row */
- .qpc2-stats {
- display: flex; gap: 0.5rem; margin-top: 0.75rem;
- padding-top: 0.7rem;
- border-top: 1px solid rgba(255,255,255,0.07);
- }
- .qpc2-stat {
- flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.1rem;
- }
- .qpc2-stat-val {
- font-family: 'Nunito', sans-serif;
- font-size: 0.95rem; font-weight: 900; color: #fbbf24;
- }
- .qpc2-stat-lbl {
- font-family: 'Nunito Sans', sans-serif;
- font-size: 0.56rem; font-weight: 700;
- color: rgba(255,255,255,0.35); text-align: center;
- letter-spacing: 0.06em; text-transform: uppercase;
- }
- .qpc2-stat-div {
- width: 1px; background: rgba(255,255,255,0.08); margin: 0.1rem 0;
- }
-
- /* Chest badge */
- .qpc2-chest-badge {
- display: flex; align-items: center; gap: 0.22rem;
- padding: 0.22rem 0.6rem;
- background: linear-gradient(135deg, #fbbf24, #f59e0b);
- border-radius: 100px;
- font-family: 'Nunito', sans-serif;
- font-size: 0.65rem; font-weight: 900; color: #1a0800;
- box-shadow: 0 2px 0 #d97706, 0 0 10px rgba(251,191,36,0.35);
- animation: qpc2ChestPop 1.8s ease-in-out infinite;
- }
- @keyframes qpc2ChestPop {
- 0%,100%{ transform: scale(1); }
- 50% { transform: scale(1.07); }
- }
-
- /* Expand chevron */
- .qpc2-chevron {
- color: rgba(255,255,255,0.35);
- transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), color 0.2s;
- }
- .qpc2-chevron.open { transform: rotate(180deg); color: #fbbf24; }
-
- /* ══ COLLAPSIBLE BODY ══ */
- .qpc2-body {
- position: relative; z-index: 2;
- overflow: hidden;
- max-height: 0;
- transition: max-height 0.4s cubic-bezier(0.4,0,0.2,1);
- }
- .qpc2-body.open { max-height: 600px; }
-
- .qpc2-divider {
- height: 1px; background: rgba(255,255,255,0.07); margin: 0 1.1rem;
- }
-
- /* ══ QUEST ROWS ══ */
- .qpc2-quest-list { display: flex; flex-direction: column; padding: 0.5rem 0; }
-
- .qpc2-quest-row {
- display: flex; align-items: center; gap: 0.7rem;
- padding: 0.75rem 1.1rem;
- cursor: pointer;
- transition: background 0.15s ease;
- position: relative;
- }
- .qpc2-quest-row:hover { background: rgba(255,255,255,0.03); }
-
- /* Left accent line = arc colour */
- .qpc2-quest-row::before {
- content: ''; position: absolute; left: 0; top: 16%; bottom: 16%;
- width: 3px; border-radius: 0 3px 3px 0;
- background: var(--ac);
- opacity: 0.7;
- }
-
- .qpc2-quest-icon {
- width: 38px; height: 38px; border-radius: 12px; flex-shrink: 0;
- display: flex; align-items: center; justify-content: center;
- font-size: 1.2rem;
- background: rgba(255,255,255,0.05);
- border: 1.5px solid rgba(255,255,255,0.08);
- transition: transform 0.2s ease;
- }
- .qpc2-quest-row:hover .qpc2-quest-icon { transform: scale(1.1) rotate(-5deg); }
- .qpc2-quest-icon.claimable {
- background: rgba(251,191,36,0.12);
- border-color: rgba(251,191,36,0.4);
- animation: qpc2Wiggle 2s ease-in-out infinite;
- }
- @keyframes qpc2Wiggle {
- 0%,100%{ transform: rotate(0deg); }
- 25% { transform: rotate(-8deg) scale(1.06); }
- 75% { transform: rotate(8deg) scale(1.06); }
- }
-
- .qpc2-quest-body { flex: 1; min-width: 0; }
- .qpc2-quest-arc {
- font-size: 0.57rem; font-weight: 800; letter-spacing: 0.12em;
- text-transform: uppercase; color: var(--ac);
- margin-bottom: 0.08rem;
- }
- .qpc2-quest-title {
- font-family: 'Sorts Mill Goudy', serif;
- font-size: 0.82rem; font-weight: 700; color: rgba(255,255,255,0.9);
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
- margin-bottom: 0.28rem;
- }
- .qpc2-mini-track {
- height: 4px; background: rgba(255,255,255,0.08);
- border-radius: 100px; overflow: hidden; margin-bottom: 0.18rem;
- }
- .qpc2-mini-fill {
- height: 100%; border-radius: 100px;
- background: var(--ac);
- box-shadow: 0 0 5px color-mix(in srgb, var(--ac) 55%, transparent);
- transition: width 0.5s cubic-bezier(0.34,1.56,0.64,1);
- }
- .qpc2-mini-label {
- font-family: 'Nunito Sans', sans-serif;
- font-size: 0.58rem; font-weight: 700; color: rgba(255,255,255,0.3);
- }
- .qpc2-claimable-label {
- font-family: 'Nunito Sans', sans-serif;
- font-size: 0.62rem; font-weight: 700; color: #fbbf24;
- }
-
- /* Claim button */
- .qpc2-claim-btn {
- padding: 0.32rem 0.7rem; border: none; border-radius: 100px; cursor: pointer;
- background: linear-gradient(135deg, #fbbf24, #f59e0b);
- font-family: 'Nunito', sans-serif;
- font-size: 0.65rem; font-weight: 900; color: #1a0800;
- box-shadow: 0 2px 0 #d97706, 0 3px 8px rgba(251,191,36,0.25);
- flex-shrink: 0; white-space: nowrap;
- transition: all 0.12s ease;
- }
- .qpc2-claim-btn:hover { transform: translateY(-1px); box-shadow: 0 3px 0 #d97706; }
- .qpc2-claim-btn:active { transform: translateY(1px); }
-
- /* ══ FOOTER LINK ══ */
- .qpc2-footer {
- position: relative; z-index: 2;
- display: flex; align-items: center; justify-content: center; gap: 0.3rem;
- padding: 0.65rem 1.1rem;
- border-top: 1px solid rgba(255,255,255,0.07);
- cursor: pointer;
- transition: background 0.15s ease;
- }
- .qpc2-footer:hover { background: rgba(255,255,255,0.03); }
- .qpc2-footer-label {
- font-family: 'Nunito', sans-serif;
- font-size: 0.72rem; font-weight: 800;
- color: rgba(251,191,36,0.7);
- letter-spacing: 0.04em;
- }
- .qpc2-footer:hover .qpc2-footer-label { color: #fbbf24; }
-
- /* ══ EMPTY STATE ══ */
- .qpc2-empty {
- padding: 1.25rem 1.1rem; text-align: center;
- display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
- }
- .qpc2-empty-title {
- font-family: 'Sorts Mill Goudy', serif;
- font-size: 0.88rem; font-weight: 700; color: rgba(255,255,255,0.55);
- }
- .qpc2-empty-sub {
- font-family: 'Nunito Sans', sans-serif;
- font-size: 0.68rem; font-weight: 600; color: rgba(255,255,255,0.25);
- }
-`;
-
-// ─── Helpers ──────────────────────────────────────────────────────────────────
-function getActiveQuests(arcs: QuestArc[]) {
- const results: { node: QuestNode; arc: QuestArc }[] = [];
- for (const arc of arcs) {
- for (const node of arc.nodes) {
- if (node.status === "claimable" || node.status === "active") {
- results.push({ node, arc });
- }
- }
- }
- // Claimable first, then active; max 2 shown
- results.sort((a, b) => {
- if (a.node.status === "claimable" && b.node.status !== "claimable")
- return -1;
- if (b.node.status === "claimable" && a.node.status !== "claimable")
- return 1;
- return 0;
- });
- return results.slice(0, 2);
-}
-
-// ─── Component ────────────────────────────────────────────────────────────────
-interface Props {
- onViewAll?: () => void;
-}
-
-export const QuestProgressCard = ({ onViewAll }: Props) => {
- const navigate = useNavigate();
- const arcs = useQuestStore((s) => s.arcs);
- const claimNode = useQuestStore((s) => s.claimNode);
-
- const summary = getQuestSummary(arcs);
- const rank = getCrewRank(arcs);
- const activeQuests = getActiveQuests(arcs);
-
- const [open, setOpen] = useState(false);
- const [claimingNode, setClaimingNode] = useState<{
- node: QuestNode;
- arcId: string;
- } | null>(null);
-
- const handleViewAll = () => {
- if (onViewAll) onViewAll();
- else navigate("/student/quests");
- };
-
- const handleClaim = (node: QuestNode, arcId: string) => {
- setClaimingNode({ node, arcId });
- };
-
- const handleChestClose = () => {
- if (!claimingNode) return;
- claimNode(claimingNode.arcId, claimingNode.node.id);
- setClaimingNode(null);
- };
-
- // Next rank label
- const nextRankLabel = rank.next
- ? `${Math.round(rank.progressToNext * 100)}% to ${rank.next.label}`
- : "Max rank reached";
-
- return (
- <>
-
-
-
- {/* Atmosphere layers */}
-
-
-
- {/* ── Rank hero (always visible, tap to expand) ── */}
-
setOpen((o) => !o)}>
-
-
-
{rank.emoji}
-
-
Crew Rank
-
{rank.label}
-
-
-
- {summary.claimableNodes > 0 && (
-
- 📦 {summary.claimableNodes}
-
- )}
-
-
-
-
- {/* Rank progress bar */}
-
-
- {/* Stats strip */}
-
- {[
- { val: `${summary.earnedXP}`, lbl: "XP Earned" },
- null,
- {
- val: `${summary.completedNodes}/${summary.totalNodes}`,
- lbl: "Quests Done",
- },
- null,
- {
- val: `${summary.arcsCompleted}/${summary.totalArcs}`,
- lbl: "Arcs",
- },
- ].map((item, i) =>
- item === null ? (
-
- ) : (
-
- {item.val}
- {item.lbl}
-
- ),
- )}
-
-
-
- {/* ── Collapsible quest list ── */}
-
-
-
- {activeQuests.length === 0 ? (
-
-
⚓
-
All caught up, Captain!
-
- No active quests — keep sailing
-
-
- ) : (
- activeQuests.map(({ node, arc }) => {
- const pct = Math.min(
- 100,
- Math.round((node.progress / node.requirement.target) * 100),
- );
- const isClaimable = node.status === "claimable";
- return (
-
!isClaimable && handleViewAll()}
- >
-
- {isClaimable ? "📦" : node.emoji}
-
-
-
- {arc.emoji} {arc.name}
-
-
{node.title}
- {isClaimable ? (
-
- ✨ Chest ready to open!
-
- ) : (
- <>
-
-
- {node.progress} / {node.requirement.target}{" "}
- {node.requirement.label}
-
- >
- )}
-
- {isClaimable ? (
-
{
- e.stopPropagation();
- handleClaim(node, arc.id);
- }}
- >
- Open 📦
-
- ) : (
-
- )}
-
- );
- })
- )}
-
-
- {/* Footer — navigate to full map */}
-
- View full quest map
-
-
-
-
-
- {claimingNode && (
-
- )}
- >
- );
-};
diff --git a/src/components/RenderQuestionText.tsx b/src/components/RenderQuestionText.tsx
index 35f1049..de1c686 100644
--- a/src/components/RenderQuestionText.tsx
+++ b/src/components/RenderQuestionText.tsx
@@ -1,4 +1,5 @@
import { Component, type ReactNode } from "react";
+// @ts-ignore
import { BlockMath, InlineMath } from "react-katex";
// ─── Error boundary ───────────────────────────────────────────────────────────
diff --git a/src/components/SearchOverlay.tsx b/src/components/SearchOverlay.tsx
index 8b2963c..8b8d1b1 100644
--- a/src/components/SearchOverlay.tsx
+++ b/src/components/SearchOverlay.tsx
@@ -28,7 +28,7 @@ interface Props {
// ─── Nav items ────────────────────────────────────────────────────────────────
const NAV_ITEMS: (SearchItem & {
- icon: React.ElementType;
+ icon: React.ComponentType
;
color: string;
bg: string;
})[] = [
@@ -490,6 +490,7 @@ export const SearchOverlay = ({
className="so-item-icon"
style={{ background: bg }}
>
+ {/* @ts-ignore */}
@@ -517,6 +518,7 @@ export const SearchOverlay = ({
className="so-quick-chip"
onClick={() => handleSelect(item)}
>
+ {/* @ts-ignore */}
{item.title}
@@ -533,6 +535,7 @@ export const SearchOverlay = ({
.filter((s) => s.user_status === "IN_PROGRESS")
.slice(0, 3)
.map((sheet) => {
+ // @ts-ignore
const item: SearchItem = {
type: "sheet",
title: sheet.title,
@@ -602,8 +605,9 @@ export const SearchOverlay = ({
const Icon = navMeta?.icon ?? BookOpen;
const iconColor = navMeta?.color ?? "#a855f7";
const iconBg = navMeta?.bg ?? "#fdf4ff";
+
const statusMeta = item.status
- ? STATUS_META[item.status as keyof typeof STATUS_META]
+ ? STATUS_META[item?.status as keyof typeof STATUS_META]
: null;
return (
diff --git a/src/components/lessons/BoxPlotComparisonWidget.tsx b/src/components/lessons/BoxPlotComparisonWidget.tsx
index 159f6ff..c98cc72 100644
--- a/src/components/lessons/BoxPlotComparisonWidget.tsx
+++ b/src/components/lessons/BoxPlotComparisonWidget.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
const BoxPlotComparisonWidget: React.FC = () => {
// Box Plot A is fixed
@@ -9,16 +9,24 @@ const BoxPlotComparisonWidget: React.FC = () => {
const [spread, setSpread] = useState(1); // Scale spread
const statsB = {
- min: 10 + shift - (5 * (spread - 1)), // Just approximating visual expansion
- q1: 16 + shift - (2 * (spread - 1)),
+ min: 10 + shift - 5 * (spread - 1), // Just approximating visual expansion
+ q1: 16 + shift - 2 * (spread - 1),
med: 26 + shift,
- q3: 34 + shift + (2 * (spread - 1)),
- max: 38 + shift + (4 * (spread - 1))
+ q3: 34 + shift + 2 * (spread - 1),
+ max: 38 + shift + 4 * (spread - 1),
};
const scaleX = (val: number) => (val / 60) * 100; // 0 to 60 range mapping to %
- const BoxPlot = ({ stats, color, label }: { stats: any, color: string, label: string }) => {
+ const BoxPlot = ({
+ stats,
+ color,
+ label,
+ }: {
+ stats: any;
+ color: string;
+ label: string;
+ }) => {
const leftW = scaleX(stats.min);
const rightW = scaleX(stats.max);
const boxL = scaleX(stats.q1);
@@ -27,85 +35,151 @@ const BoxPlotComparisonWidget: React.FC = () => {
return (
-
{label}
-
- {/* Main Line (Whisker to Whisker) */}
-
-
- {/* Whiskers */}
-
-
+
+ {label}
+
- {/* Box */}
-
-
+ {/* Main Line (Whisker to Whisker) */}
+
- {/* Median Line */}
-
+ {/* Whiskers */}
+
+
- {/* Labels on Hover */}
-
- Min:{stats.min.toFixed(0)} Q1:{stats.q1.toFixed(0)} Med:{stats.med.toFixed(0)} Q3:{stats.q3.toFixed(0)} Max:{stats.max.toFixed(0)}
-
+ {/* Box */}
+
+
+ {/* Median Line */}
+
+
+ {/* Labels on Hover */}
+
+ Min:{stats.min.toFixed(0)} Q1:{stats.q1.toFixed(0)} Med:
+ {stats.med.toFixed(0)} Q3:{stats.q3.toFixed(0)} Max:
+ {stats.max.toFixed(0)}
+
);
};
const iqrA = statsA.q3 - statsA.q1;
const iqrB = statsB.q3 - statsB.q1;
- const rangeA = statsA.max - statsA.min;
- const rangeB = statsB.max - statsB.min;
return (
-
-
-
-
- {/* Axis */}
-
- 0 10 20 30 40 50 60
-
+
+
+
+
+ {/* Axis */}
+
+ 0
+ 10
+ 20
+ 30
+ 40
+ 50
+ 60
+
+
+
+
+
-
-
-
- Shift Center (Median B)
- setShift(parseInt(e.target.value))} className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600"/>
-
-
- Adjust Spread (IQR B)
- setSpread(parseFloat(e.target.value))} className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600"/>
-
+
+
+
+ Median Comparison
-
-
-
-
Median Comparison
-
- {statsA.med}
- {statsA.med > statsB.med ? '>' : statsA.med < statsB.med ? '<' : '='}
- {statsB.med}
-
-
-
-
IQR Comparison
-
- {iqrA.toFixed(0)}
- {iqrA > iqrB ? '>' : iqrA < iqrB ? '<' : '='}
- {iqrB.toFixed(0)}
-
-
-
- The box length represents the IQR (Middle 50%). The whiskers represent the full Range.
-
+
+ {statsA.med}
+
+ {statsA.med > statsB.med
+ ? ">"
+ : statsA.med < statsB.med
+ ? "<"
+ : "="}
+
+ {statsB.med}
+
+
+
+ IQR Comparison
+
+
+
+ {iqrA.toFixed(0)}
+
+
+ {iqrA > iqrB ? ">" : iqrA < iqrB ? "<" : "="}
+
+ {iqrB.toFixed(0)}
+
+
+
+ The box length represents the IQR (Middle 50%). The whiskers
+ represent the full Range.
+
+
);
};
-export default BoxPlotComparisonWidget;
\ No newline at end of file
+export default BoxPlotComparisonWidget;
diff --git a/src/components/lessons/CircleTheoremsWidget.tsx b/src/components/lessons/CircleTheoremsWidget.tsx
index d11648a..eb15b78 100644
--- a/src/components/lessons/CircleTheoremsWidget.tsx
+++ b/src/components/lessons/CircleTheoremsWidget.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useRef } from "react";
const CircleTheoremsWidget: React.FC = () => {
// C is the point on the major arc
@@ -8,24 +8,10 @@ const CircleTheoremsWidget: React.FC = () => {
const R = 120;
const center = { x: 200, y: 180 };
-
- // Fixed points A and B at the bottom
- const angleA = 330; // 30 deg below x axis
- const angleB = 210; // 150 deg below x axis? No, let's place them symmetrically
-
- // Let's place A and B to define a nice arc
- // A at -30 deg (330), B at 210 is too far.
- // Let's put A at 320 (-40) and B at 220 (-140).
- // Wait, standard unit circle angles.
- // A at 340 (-20), B at 200. Arc is 140 deg at bottom.
- // Major arc is top. C moves on top.
-
- const posA = { x: center.x + R * Math.cos(340 * Math.PI/180), y: center.y - R * Math.sin(340 * Math.PI/180) }; // SVG y inverted logic?
- // Let's just use standard math cos/sin and add to center.y
- // SVG y is positive down.
+
const getPos = (deg: number) => ({
- x: center.x + R * Math.cos(deg * Math.PI / 180),
- y: center.y + R * Math.sin(deg * Math.PI / 180)
+ x: center.x + R * Math.cos((deg * Math.PI) / 180),
+ y: center.y + R * Math.sin((deg * Math.PI) / 180),
});
const A = getPos(30); // Bottom Right
@@ -38,9 +24,9 @@ const CircleTheoremsWidget: React.FC = () => {
const rect = svgRef.current.getBoundingClientRect();
const dx = e.clientX - rect.left - center.x;
const dy = e.clientY - rect.top - center.y;
- let deg = Math.atan2(dy, dx) * 180 / Math.PI;
+ let deg = (Math.atan2(dy, dx) * 180) / Math.PI;
if (deg < 0) deg += 360;
-
+
// Constrain C to the major arc (approx 160 to 350 is the "bad" zone? No, A=30, B=150.
// Bad zone is between 30 and 150 (the minor arc).
// Let's allow C anywhere except the minor arc to avoid crossing lines weirdly.
@@ -53,66 +39,130 @@ const CircleTheoremsWidget: React.FC = () => {
return (
-
Central vs. Inscribed Angle
+
+ Central vs. Inscribed Angle
+
- Drag point C along the top arc. Notice that the inscribed angle stays constant!
+ Drag point C along the top
+ arc. Notice that the inscribed angle stays constant!
-
isDragging.current = false}
- onMouseLeave={() => isDragging.current = false}
+ onMouseUp={() => (isDragging.current = false)}
+ onMouseLeave={() => (isDragging.current = false)}
className="select-none"
>
{/* Circle */}
-
-
+
{/* Central Angle Lines */}
-
-
+
{/* Central Angle Wedge */}
{/* 30 to 150 */}
-
- {centralAngleValue}°
- Central
-
+
+
+ {centralAngleValue}°
+
+
+ Central
+
{/* Inscribed Angle Lines */}
-
-
+
{/* Points */}
- {/* Center */}
- O
-
+ {" "}
+ {/* Center */}
+
+ O
+
- A
-
+
+ A
+
- B
-
+
+ B
+
{/* Draggable C */}
- isDragging.current = true} className="cursor-grab active:cursor-grabbing">
- {/* Hit area */}
-
- C
+ (isDragging.current = true)}
+ className="cursor-grab active:cursor-grabbing"
+ >
+ {" "}
+ {/* Hit area */}
+
+
+ C
+
-
{/* Inscribed Angle Label */}
{/* Simple approximation for label placement: slightly "in" from C towards center */}
-
+
{centralAngleValue / 2}°
-
- Inscribed Angle = ½ × Central Angle
-
-
- {centralAngleValue / 2}° = ½ × {centralAngleValue}°
-
+
+ Inscribed Angle = ½ ×
+ Central Angle
+
+
+ {centralAngleValue / 2}° = ½ × {centralAngleValue}°
+
);
diff --git a/src/components/lessons/ClauseBreakdownWidget.tsx b/src/components/lessons/ClauseBreakdownWidget.tsx
index dc54ec3..3fe0b51 100644
--- a/src/components/lessons/ClauseBreakdownWidget.tsx
+++ b/src/components/lessons/ClauseBreakdownWidget.tsx
@@ -1,7 +1,14 @@
-import React, { useState } from 'react';
-import { MousePointerClick } from 'lucide-react';
+import { useState } from "react";
+import { MousePointerClick } from "lucide-react";
-export type SegmentType = 'ic' | 'dc' | 'modifier' | 'conjunction' | 'punct' | 'subject' | 'verb';
+export type SegmentType =
+ | "ic"
+ | "dc"
+ | "modifier"
+ | "conjunction"
+ | "punct"
+ | "subject"
+ | "verb";
export interface Segment {
text: string;
@@ -19,52 +26,95 @@ interface ClauseBreakdownWidgetProps {
accentColor?: string;
}
-const TYPE_STYLES: Record
= {
- ic: { bg: 'bg-blue-100', text: 'text-blue-800', border: 'border-blue-300', ring: '#93c5fd' },
- dc: { bg: 'bg-green-100', text: 'text-green-800', border: 'border-green-300', ring: '#86efac' },
- modifier: { bg: 'bg-orange-100', text: 'text-orange-800', border: 'border-orange-300', ring: '#fdba74' },
- conjunction: { bg: 'bg-purple-100', text: 'text-purple-800', border: 'border-purple-300', ring: '#c4b5fd' },
- subject: { bg: 'bg-sky-100', text: 'text-sky-800', border: 'border-sky-300', ring: '#7dd3fc' },
- verb: { bg: 'bg-rose-100', text: 'text-rose-800', border: 'border-rose-300', ring: '#fda4af' },
- punct: { bg: 'bg-gray-100', text: 'text-gray-600', border: 'border-gray-300', ring: '#d1d5db' },
+const TYPE_STYLES: Record<
+ SegmentType,
+ { bg: string; text: string; border: string; ring: string }
+> = {
+ ic: {
+ bg: "bg-blue-100",
+ text: "text-blue-800",
+ border: "border-blue-300",
+ ring: "#93c5fd",
+ },
+ dc: {
+ bg: "bg-green-100",
+ text: "text-green-800",
+ border: "border-green-300",
+ ring: "#86efac",
+ },
+ modifier: {
+ bg: "bg-orange-100",
+ text: "text-orange-800",
+ border: "border-orange-300",
+ ring: "#fdba74",
+ },
+ conjunction: {
+ bg: "bg-purple-100",
+ text: "text-purple-800",
+ border: "border-purple-300",
+ ring: "#c4b5fd",
+ },
+ subject: {
+ bg: "bg-sky-100",
+ text: "text-sky-800",
+ border: "border-sky-300",
+ ring: "#7dd3fc",
+ },
+ verb: {
+ bg: "bg-rose-100",
+ text: "text-rose-800",
+ border: "border-rose-300",
+ ring: "#fda4af",
+ },
+ punct: {
+ bg: "bg-gray-100",
+ text: "text-gray-600",
+ border: "border-gray-300",
+ ring: "#d1d5db",
+ },
};
const TYPE_LABELS: Record = {
- ic: 'Independent Clause',
- dc: 'Dependent Clause',
- modifier: 'Modifier',
- conjunction: 'Conjunction',
- subject: 'Subject',
- verb: 'Verb / Predicate',
- punct: 'Punctuation',
+ ic: "Independent Clause",
+ dc: "Dependent Clause",
+ modifier: "Modifier",
+ conjunction: "Conjunction",
+ subject: "Subject",
+ verb: "Verb / Predicate",
+ punct: "Punctuation",
};
// Pre-resolved tab accent classes (avoids Tailwind purge issues with dynamic strings)
const TAB_ACTIVE: Record = {
- purple: 'border-b-2 border-purple-600 text-purple-700 bg-white',
- teal: 'border-b-2 border-teal-600 text-teal-700 bg-white',
- fuchsia: 'border-b-2 border-fuchsia-600 text-fuchsia-700 bg-white',
- amber: 'border-b-2 border-amber-600 text-amber-700 bg-white',
+ purple: "border-b-2 border-purple-600 text-purple-700 bg-white",
+ teal: "border-b-2 border-teal-600 text-teal-700 bg-white",
+ fuchsia: "border-b-2 border-fuchsia-600 text-fuchsia-700 bg-white",
+ amber: "border-b-2 border-amber-600 text-amber-700 bg-white",
};
-export default function ClauseBreakdownWidget({ examples, accentColor = 'purple' }: ClauseBreakdownWidgetProps) {
+export default function ClauseBreakdownWidget({
+ examples,
+ accentColor = "purple",
+}: ClauseBreakdownWidgetProps) {
const [activeTab, setActiveTab] = useState(0);
const [selected, setSelected] = useState(null);
const example = examples[activeTab];
- const switchTab = (i: number) => { setActiveTab(i); setSelected(null); };
+ const switchTab = (i: number) => {
+ setActiveTab(i);
+ setSelected(null);
+ };
const selectedSeg = selected !== null ? example.segments[selected] : null;
const tabActive = TAB_ACTIVE[accentColor] ?? TAB_ACTIVE.purple;
// Unique labeled segment types for the legend
const legendTypes = Array.from(
- new Set(example.segments.filter(s => s.label).map(s => s.type))
+ new Set(example.segments.filter((s) => s.label).map((s) => s.type)),
);
return (
-
{/* Tab strip */}
{examples.length > 1 && (
@@ -73,7 +123,9 @@ export default function ClauseBreakdownWidget({ examples, accentColor = 'purple'
key={i}
onClick={() => switchTab(i)}
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors ${
- i === activeTab ? tabActive : 'text-gray-500 hover:text-gray-700'
+ i === activeTab
+ ? tabActive
+ : "text-gray-500 hover:text-gray-700"
}`}
>
{ex.title}
@@ -83,14 +135,18 @@ export default function ClauseBreakdownWidget({ examples, accentColor = 'purple'
)}
{examples.length === 1 && (
-
{example.title}
+
+ {example.title}
+
)}
{/* Instruction */}
-
Click any colored part to see its grammatical role
+
+ Click any colored part to see its grammatical role
+
{/* Sentence display */}
@@ -99,7 +155,11 @@ export default function ClauseBreakdownWidget({ examples, accentColor = 'purple'
{example.segments.map((seg, i) => {
if (!seg.label) {
// Punctuation / unlabeled — plain unstyled text, not clickable
- return
{seg.text} ;
+ return (
+
+ {seg.text}
+
+ );
}
const style = TYPE_STYLES[seg.type];
const isSelected = selected === i;
@@ -112,7 +172,14 @@ export default function ClauseBreakdownWidget({ examples, accentColor = 'purple'
? `border-2 ${style.border} font-semibold`
: `border ${style.border} hover:opacity-80`
}`}
- style={isSelected ? { outline: `2.5px solid ${style.ring}`, outlineOffset: '1px' } : {}}
+ style={
+ isSelected
+ ? {
+ outline: `2.5px solid ${style.ring}`,
+ outlineOffset: "1px",
+ }
+ : {}
+ }
>
{seg.text}
@@ -130,29 +197,38 @@ export default function ClauseBreakdownWidget({ examples, accentColor = 'purple'
style={{ backgroundColor: TYPE_STYLES[selectedSeg.type].ring }}
/>
-
+
{selectedSeg.label ?? TYPE_LABELS[selectedSeg.type]}
-
+
"{selectedSeg.text.trim()}"
) : (
-
No element selected — click a colored span above.
+
+ No element selected — click a colored span above.
+
)}
{/* Legend */}
- {legendTypes.map(type => {
+ {legendTypes.map((type) => {
const style = TYPE_STYLES[type];
return (
-
+
{TYPE_LABELS[type]}
);
diff --git a/src/components/lessons/ContextEliminationWidget.tsx b/src/components/lessons/ContextEliminationWidget.tsx
index 0c302b5..ed65b22 100644
--- a/src/components/lessons/ContextEliminationWidget.tsx
+++ b/src/components/lessons/ContextEliminationWidget.tsx
@@ -1,5 +1,5 @@
-import React, { useState } from 'react';
-import { CheckCircle2, RotateCcw, ChevronRight } from 'lucide-react';
+import { useState } from "react";
+import { CheckCircle2, RotateCcw, ChevronRight } from "lucide-react";
export interface VocabOption {
id: string;
@@ -10,7 +10,7 @@ export interface VocabOption {
export interface VocabExercise {
sentence: string;
- word: string; // the target word — will be highlighted
+ word: string; // the target word — will be highlighted
question: string;
options: VocabOption[];
}
@@ -20,41 +20,58 @@ interface ContextEliminationWidgetProps {
accentColor?: string;
}
-export default function ContextEliminationWidget({ exercises, accentColor = 'rose' }: ContextEliminationWidgetProps) {
+export default function ContextEliminationWidget({
+ exercises,
+ accentColor = "rose",
+}: ContextEliminationWidgetProps) {
const [activeEx, setActiveEx] = useState(0);
const [eliminated, setEliminated] = useState
>(new Set());
const [revealed, setRevealed] = useState(false);
const [triedCorrect, setTriedCorrect] = useState(false);
const exercise = exercises[activeEx];
- const wrongIds = exercise.options.filter(o => !o.isCorrect).map(o => o.id);
- const allWrongEliminated = wrongIds.every(id => eliminated.has(id));
+ const wrongIds = exercise.options
+ .filter((o) => !o.isCorrect)
+ .map((o) => o.id);
const eliminate = (id: string) => {
- const opt = exercise.options.find(o => o.id === id)!;
+ const opt = exercise.options.find((o) => o.id === id)!;
if (opt.isCorrect) {
setTriedCorrect(true);
setTimeout(() => setTriedCorrect(false), 1500);
} else {
const newElim = new Set([...eliminated, id]);
setEliminated(newElim);
- if (wrongIds.every(wid => newElim.has(wid))) {
+ if (wrongIds.every((wid) => newElim.has(wid))) {
setRevealed(true);
}
}
};
- const reset = () => { setEliminated(new Set()); setRevealed(false); setTriedCorrect(false); };
- const switchEx = (i: number) => { setActiveEx(i); setEliminated(new Set()); setRevealed(false); setTriedCorrect(false); };
+ const reset = () => {
+ setEliminated(new Set());
+ setRevealed(false);
+ setTriedCorrect(false);
+ };
+ const switchEx = (i: number) => {
+ setActiveEx(i);
+ setEliminated(new Set());
+ setRevealed(false);
+ setTriedCorrect(false);
+ };
// Highlight the target word in the sentence
const renderSentence = () => {
- const idx = exercise.sentence.toLowerCase().indexOf(exercise.word.toLowerCase());
+ const idx = exercise.sentence
+ .toLowerCase()
+ .indexOf(exercise.word.toLowerCase());
if (idx === -1) return <>{exercise.sentence}>;
return (
<>
{exercise.sentence.slice(0, idx)}
-
+
{exercise.sentence.slice(idx, idx + exercise.word.length)}
{exercise.sentence.slice(idx + exercise.word.length)}
@@ -74,7 +91,7 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors ${
i === activeEx
? `bg-white border-b-2 border-${accentColor}-600 text-${accentColor}-700`
- : 'text-gray-500 hover:text-gray-700'
+ : "text-gray-500 hover:text-gray-700"
}`}
>
Word {i + 1}
@@ -84,17 +101,27 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
)}
{/* Sentence in context */}
-
-
Sentence in Context
-
{renderSentence()}
+
+
+ Sentence in Context
+
+
+ {renderSentence()}
+
{/* Question + instruction */}
-
{exercise.question}
+
+ {exercise.question}
+
{revealed
- ? 'You found it! The correct definition is highlighted.'
+ ? "You found it! The correct definition is highlighted."
: 'Click "Eliminate" on definitions that don\'t fit the context. Work by elimination.'}
@@ -108,40 +135,52 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
{/* Options */}
- {exercise.options.map(opt => {
+ {exercise.options.map((opt) => {
const isElim = eliminated.has(opt.id);
const isAnswer = opt.isCorrect && revealed;
- let wrapCls = 'border-gray-200 bg-white';
- if (isAnswer) wrapCls = 'border-green-400 bg-green-50';
- else if (isElim) wrapCls = 'border-gray-100 bg-gray-50';
+ let wrapCls = "border-gray-200 bg-white";
+ if (isAnswer) wrapCls = "border-green-400 bg-green-50";
+ else if (isElim) wrapCls = "border-gray-100 bg-gray-50";
return (
-
+
{opt.id}.
-
+
{opt.definition}
{isElim && (
-
{opt.elimReason}
+
+ {opt.elimReason}
+
)}
{isAnswer && (
-
✓ {opt.elimReason}
+
+ ✓ {opt.elimReason}
+
)}
- {isAnswer && }
+ {isAnswer && (
+
+ )}
{!isElim && !isAnswer && !revealed && (
eliminate(opt.id)}
@@ -158,7 +197,10 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
-
+
Reset
{revealed && activeEx < exercises.length - 1 && (
diff --git a/src/components/lessons/CoordinatePlane.tsx b/src/components/lessons/CoordinatePlane.tsx
index 4fc051c..40a7312 100644
--- a/src/components/lessons/CoordinatePlane.tsx
+++ b/src/components/lessons/CoordinatePlane.tsx
@@ -1,6 +1,11 @@
-import React, { useRef, useState, useEffect } from 'react';
-import { scaleToSvg, scaleFromSvg, round, calculateDistanceSquared } from '../utils/math';
-import { CircleState, Point } from '../types';
+import React, { useRef, useState } from "react";
+import {
+ scaleToSvg,
+ scaleFromSvg,
+ round,
+ calculateDistanceSquared,
+} from "../../utils/math";
+import { type CircleState, type Point } from "../../types/lesson";
interface CoordinatePlaneProps {
circle: CircleState;
@@ -8,15 +13,15 @@ interface CoordinatePlaneProps {
onPointClick?: (p: Point) => void;
interactive?: boolean;
showDistance?: boolean;
- mode?: 'view' | 'place_point';
+ mode?: "view" | "place_point";
}
-const CoordinatePlane: React.FC = ({
- circle,
- point,
- onPointClick,
+const CoordinatePlane: React.FC = ({
+ circle,
+ point,
+ onPointClick,
showDistance = false,
- mode = 'view'
+ mode = "view",
}) => {
const svgRef = useRef(null);
const [hoverPoint, setHoverPoint] = useState(null);
@@ -39,20 +44,20 @@ const CoordinatePlane: React.FC = ({
const rPx = toX(circle.r) - toX(0);
const handleMouseMove = (e: React.MouseEvent) => {
- if (mode !== 'place_point' || !svgRef.current) return;
+ if (mode !== "place_point" || !svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
const rawX = e.clientX - rect.left;
const rawY = e.clientY - rect.top;
-
+
// Snap to nearest 0.5 for cleaner UX
const graphX = Math.round(fromX(rawX) * 2) / 2;
const graphY = Math.round(fromY(rawY) * 2) / 2;
-
+
setHoverPoint({ x: graphX, y: graphY });
};
const handleClick = () => {
- if (mode === 'place_point' && hoverPoint && onPointClick) {
+ if (mode === "place_point" && hoverPoint && onPointClick) {
onPointClick(hoverPoint);
}
};
@@ -64,11 +69,13 @@ const CoordinatePlane: React.FC = ({
ticks.push(i);
}
- const dSquared = point ? calculateDistanceSquared(point.x, point.y, circle.h, circle.k) : 0;
+ const dSquared = point
+ ? calculateDistanceSquared(point.x, point.y, circle.h, circle.k)
+ : 0;
const isInside = dSquared < circle.r * circle.r;
const isOn = Math.abs(dSquared - circle.r * circle.r) < 0.01;
- const pointColor = isOn ? 'text-yellow-600' : isInside ? 'text-green-600' : 'text-red-600';
- const pointFill = isOn ? '#ca8a04' : isInside ? '#16a34a' : '#dc2626';
+
+ const pointFill = isOn ? "#ca8a04" : isInside ? "#16a34a" : "#dc2626";
return (
@@ -80,73 +87,129 @@ const CoordinatePlane: React.FC
= ({
onMouseMove={handleMouseMove}
onMouseLeave={() => setHoverPoint(null)}
onClick={handleClick}
- className={`${mode === 'place_point' ? 'cursor-crosshair' : 'cursor-default'}`}
+ className={`${mode === "place_point" ? "cursor-crosshair" : "cursor-default"}`}
>
{/* Grid Background */}
- {ticks.map(t => (
+ {ticks.map((t) => (
-
-
+
+
))}
{/* Axes */}
-
-
+
+
{/* Circle */}
-
-
+
{/* Center Point */}
- Center ({circle.h}, {circle.k})
+
+ Center ({circle.h}, {circle.k})
+
{/* Radius Line (only if distance line is not active to avoid clutter) */}
{!point && (
-
)}
{!point && (
- r = {circle.r}
+
+ r = {circle.r}
+
)}
{/* Placed Point */}
{point && (
<>
-
-
-
+
+
({point.x}, {point.y})
>
)}
{/* Hover Ghost Point */}
- {mode === 'place_point' && hoverPoint && !point && (
-
+ {mode === "place_point" && hoverPoint && !point && (
+
)}
@@ -157,25 +220,42 @@ const CoordinatePlane: React.FC = ({
{/* Info Panel below graph */}
{point && showDistance && (
-
+
Distance Check:
-
- {isOn ? 'On Circle' : isInside ? 'Inside' : 'Outside'}
+
+ {isOn ? "On Circle" : isInside ? "Inside" : "Outside"}
d² = (x - h)² + (y - k)²
-
d² = ({point.x} - {circle.h})² + ({point.y} - {circle.k})²
-
d² = {round(calculateDistanceSquared(point.x, point.y, circle.h, circle.k))} vs r² = {circle.r * circle.r}
+
+ d² = ({point.x} - {circle.h})² + ({point.y} - {circle.k})²
+
+
+ d² ={" "}
+ {round(
+ calculateDistanceSquared(point.x, point.y, circle.h, circle.k),
+ )}{" "}
+ vs r² ={" "}
+ {circle.r * circle.r}
+
)}
diff --git a/src/components/lessons/DataClaimWidget.tsx b/src/components/lessons/DataClaimWidget.tsx
index b116e97..f49e14a 100644
--- a/src/components/lessons/DataClaimWidget.tsx
+++ b/src/components/lessons/DataClaimWidget.tsx
@@ -1,9 +1,9 @@
-import React, { useState } from 'react';
-import { CheckCircle2, XCircle, RotateCcw } from 'lucide-react';
+import { useState } from "react";
+import { CheckCircle2, XCircle, RotateCcw } from "lucide-react";
// ── Types ──────────────────────────────────────────────────────────────────
-export type Verdict = 'supported' | 'contradicted' | 'neither';
+export type Verdict = "supported" | "contradicted" | "neither";
export interface ChartSeries {
name: string;
@@ -11,12 +11,12 @@ export interface ChartSeries {
}
export interface ChartData {
- type: 'bar' | 'line';
+ type: "bar" | "line";
title: string;
yLabel?: string;
xLabel?: string;
source?: string;
- unit?: string; // e.g. '%', '°C', 'min'
+ unit?: string; // e.g. '%', '°C', 'min'
series: ChartSeries[];
}
@@ -34,15 +34,24 @@ export interface DataExercise {
// ── Chart palette ──────────────────────────────────────────────────────────
-const PALETTE = ['#3b82f6', '#8b5cf6', '#f97316', '#10b981', '#ef4444', '#ec4899'];
+const PALETTE = [
+ "#3b82f6",
+ "#8b5cf6",
+ "#f97316",
+ "#10b981",
+ "#ef4444",
+ "#ec4899",
+];
// ── BarChart ───────────────────────────────────────────────────────────────
function BarChart({ chart }: { chart: ChartData }) {
- const [hovered, setHovered] = useState<{ si: number; pi: number } | null>(null);
+ const [hovered, setHovered] = useState<{ si: number; pi: number } | null>(
+ null,
+ );
- const labels = chart.series[0].data.map(d => d.label);
- const allValues = chart.series.flatMap(s => s.data.map(d => d.value));
+ const labels = chart.series[0].data.map((d) => d.label);
+ const allValues = chart.series.flatMap((s) => s.data.map((d) => d.value));
const maxVal = Math.max(...allValues);
// Round up max to nearest 10 for cleaner y-axis
const yMax = Math.ceil(maxVal / 10) * 10;
@@ -52,22 +61,36 @@ function BarChart({ chart }: { chart: ChartData }) {
return (
-
{chart.title}
+
+ {chart.title}
+
{/* Y-axis */}
-
- {yTicks.map(t => (
-
{t}{chart.unit ?? ''}
+
+ {yTicks.map((t) => (
+
+ {t}
+ {chart.unit ?? ""}
+
))}
{/* Bar groups */}
-
- {labels.map((label, pi) => (
+
+ {labels.map((_, pi) => (
{/* Bar group */}
-
+
{chart.series.map((s, si) => {
const val = s.data[pi].value;
const heightPct = (val / yMax) * 100;
@@ -79,9 +102,11 @@ function BarChart({ chart }: { chart: ChartData }) {
style={{
height: `${heightPct}%`,
backgroundColor: isHov
- ? PALETTE[si % PALETTE.length] + 'dd'
- : PALETTE[si % PALETTE.length] + 'cc',
- outline: isHov ? `2px solid ${PALETTE[si % PALETTE.length]}` : 'none',
+ ? PALETTE[si % PALETTE.length] + "dd"
+ : PALETTE[si % PALETTE.length] + "cc",
+ outline: isHov
+ ? `2px solid ${PALETTE[si % PALETTE.length]}`
+ : "none",
}}
onMouseEnter={() => setHovered({ si, pi })}
onMouseLeave={() => setHovered(null)}
@@ -90,9 +115,12 @@ function BarChart({ chart }: { chart: ChartData }) {
{isHov && (
- {val}{chart.unit ?? ''}
+ {val}
+ {chart.unit ?? ""}
)}
@@ -107,17 +135,32 @@ function BarChart({ chart }: { chart: ChartData }) {
{/* X-axis labels */}
{labels.map((label, i) => (
-
{label}
+
+ {label}
+
))}
- {chart.xLabel &&
{chart.xLabel}
}
+ {chart.xLabel && (
+
+ {chart.xLabel}
+
+ )}
{/* Legend */}
{chart.series.length > 1 && (
{chart.series.map((s, si) => (
-
-
+
))}
@@ -127,17 +170,26 @@ function BarChart({ chart }: { chart: ChartData }) {
{/* Hover info bar */}
{hovered && (
-
+
{chart.series[hovered.si].name}
- {' — '}
- {chart.series[0].data[hovered.pi].label}:
- {chart.series[hovered.si].data[hovered.pi].value}{chart.unit ?? ''}
+ {" — "}
+ {chart.series[0].data[hovered.pi].label}:{" "}
+
+ {chart.series[hovered.si].data[hovered.pi].value}
+ {chart.unit ?? ""}
)}
- {chart.source &&
Source: {chart.source}
}
+ {chart.source && (
+
+ Source: {chart.source}
+
+ )}
);
}
@@ -145,14 +197,17 @@ function BarChart({ chart }: { chart: ChartData }) {
// ── LineChart ──────────────────────────────────────────────────────────────
function LineChart({ chart }: { chart: ChartData }) {
- const [hovered, setHovered] = useState<{ si: number; pi: number } | null>(null);
+ const [hovered, setHovered] = useState<{ si: number; pi: number } | null>(
+ null,
+ );
- const W = 480, H = 200;
+ const W = 480,
+ H = 200;
const PAD = { top: 20, right: 20, bottom: 36, left: 48 };
const cW = W - PAD.left - PAD.right;
const cH = H - PAD.top - PAD.bottom;
- const allValues = chart.series.flatMap(s => s.data.map(d => d.value));
+ const allValues = chart.series.flatMap((s) => s.data.map((d) => d.value));
const minVal = Math.min(...allValues);
const maxVal = Math.max(...allValues);
const spread = maxVal - minVal || 1;
@@ -163,28 +218,51 @@ function LineChart({ chart }: { chart: ChartData }) {
const yMax = maxVal + yPad;
const yRange = yMax - yMin;
- const labels = chart.series[0].data.map(d => d.label);
+ const labels = chart.series[0].data.map((d) => d.label);
const xStep = cW / (labels.length - 1);
const xPos = (i: number) => PAD.left + i * xStep;
const yPos = (v: number) => PAD.top + cH - ((v - yMin) / yRange) * cH;
// Y-axis ticks: 5 evenly spaced
- const yTicks = Array.from({ length: 5 }, (_, i) => minVal + ((maxVal - minVal) / 4) * i);
+ const yTicks = Array.from(
+ { length: 5 },
+ (_, i) => minVal + ((maxVal - minVal) / 4) * i,
+ );
return (
-
{chart.title}
+
+ {chart.title}
+
-
+
{/* Grid lines */}
{yTicks.map((t, i) => {
const y = yPos(t);
return (
-
-
- {t % 1 === 0 ? t : t.toFixed(2)}{chart.unit ?? ''}
+
+
+ {t % 1 === 0 ? t : t.toFixed(2)}
+ {chart.unit ?? ""}
);
@@ -193,10 +271,18 @@ function LineChart({ chart }: { chart: ChartData }) {
{/* Lines + dots */}
{chart.series.map((s, si) => {
const color = PALETTE[si % PALETTE.length];
- const pts = s.data.map((d, i) => `${xPos(i)},${yPos(d.value)}`).join(' ');
+ const pts = s.data
+ .map((d, i) => `${xPos(i)},${yPos(d.value)}`)
+ .join(" ");
return (
-
+
{s.data.map((d, pi) => {
const isHov = hovered?.si === si && hovered?.pi === pi;
const cx = xPos(pi);
@@ -204,20 +290,36 @@ function LineChart({ chart }: { chart: ChartData }) {
return (
setHovered({ si, pi })}
onMouseLeave={() => setHovered(null)}
/>
{isHov && (
<>
-
- {d.value}{chart.unit ?? ''}
+
+ {d.value}
+ {chart.unit ?? ""}
>
)}
@@ -230,21 +332,45 @@ function LineChart({ chart }: { chart: ChartData }) {
{/* X-axis labels */}
{labels.map((label, i) => (
-
+
{label}
))}
{/* Axes */}
-
-
+
+
{/* Y-axis label */}
{chart.yLabel && (
{chart.yLabel}
@@ -256,8 +382,14 @@ function LineChart({ chart }: { chart: ChartData }) {
{chart.series.length > 1 && (
{chart.series.map((s, si) => (
-
-
+
))}
@@ -267,17 +399,26 @@ function LineChart({ chart }: { chart: ChartData }) {
{/* Hover tooltip */}
{hovered && (
-
+
{chart.series[hovered.si].name}
- {' · '}
- {chart.series[0].data[hovered.pi].label}:
- {chart.series[hovered.si].data[hovered.pi].value}{chart.unit ?? ''}
+ {" · "}
+ {chart.series[0].data[hovered.pi].label}:{" "}
+
+ {chart.series[hovered.si].data[hovered.pi].value}
+ {chart.unit ?? ""}
)}
- {chart.source &&
Source: {chart.source}
}
+ {chart.source && (
+
+ Source: {chart.source}
+
+ )}
);
}
@@ -285,9 +426,9 @@ function LineChart({ chart }: { chart: ChartData }) {
// ── Main widget ────────────────────────────────────────────────────────────
const VERDICT_LABELS: Record
= {
- supported: 'Supported by data',
- contradicted: 'Contradicted by data',
- neither: 'Neither proven nor disproven',
+ supported: "Supported by data",
+ contradicted: "Contradicted by data",
+ neither: "Neither proven nor disproven",
};
interface DataClaimWidgetProps {
@@ -296,25 +437,60 @@ interface DataClaimWidgetProps {
}
// Pre-resolved accent classes to avoid Tailwind purge issues
-const ACCENT_CLASSES: Record = {
- amber: { tab: 'border-b-2 border-amber-600 text-amber-700', header: 'bg-amber-50', label: 'text-amber-600', btn: 'bg-amber-600 hover:bg-amber-700' },
- teal: { tab: 'border-b-2 border-teal-600 text-teal-700', header: 'bg-teal-50', label: 'text-teal-600', btn: 'bg-teal-600 hover:bg-teal-700' },
- purple: { tab: 'border-b-2 border-purple-600 text-purple-700', header: 'bg-purple-50', label: 'text-purple-600', btn: 'bg-purple-600 hover:bg-purple-700' },
- fuchsia: { tab: 'border-b-2 border-fuchsia-600 text-fuchsia-700', header: 'bg-fuchsia-50', label: 'text-fuchsia-600', btn: 'bg-fuchsia-600 hover:bg-fuchsia-700' },
+const ACCENT_CLASSES: Record<
+ string,
+ { tab: string; header: string; label: string; btn: string }
+> = {
+ amber: {
+ tab: "border-b-2 border-amber-600 text-amber-700",
+ header: "bg-amber-50",
+ label: "text-amber-600",
+ btn: "bg-amber-600 hover:bg-amber-700",
+ },
+ teal: {
+ tab: "border-b-2 border-teal-600 text-teal-700",
+ header: "bg-teal-50",
+ label: "text-teal-600",
+ btn: "bg-teal-600 hover:bg-teal-700",
+ },
+ purple: {
+ tab: "border-b-2 border-purple-600 text-purple-700",
+ header: "bg-purple-50",
+ label: "text-purple-600",
+ btn: "bg-purple-600 hover:bg-purple-700",
+ },
+ fuchsia: {
+ tab: "border-b-2 border-fuchsia-600 text-fuchsia-700",
+ header: "bg-fuchsia-50",
+ label: "text-fuchsia-600",
+ btn: "bg-fuchsia-600 hover:bg-fuchsia-700",
+ },
};
-export default function DataClaimWidget({ exercises, accentColor = 'amber' }: DataClaimWidgetProps) {
+export default function DataClaimWidget({
+ exercises,
+ accentColor = "amber",
+}: DataClaimWidgetProps) {
const [activeEx, setActiveEx] = useState(0);
const [answers, setAnswers] = useState>({});
const [submitted, setSubmitted] = useState(false);
const exercise = exercises[activeEx];
const allAnswered = exercise.claims.every((_, i) => answers[i] !== undefined);
- const score = submitted ? exercise.claims.filter((c, i) => answers[i] === c.verdict).length : 0;
+ const score = submitted
+ ? exercise.claims.filter((c, i) => answers[i] === c.verdict).length
+ : 0;
const c = ACCENT_CLASSES[accentColor] ?? ACCENT_CLASSES.amber;
- const reset = () => { setAnswers({}); setSubmitted(false); };
- const switchEx = (i: number) => { setActiveEx(i); setAnswers({}); setSubmitted(false); };
+ const reset = () => {
+ setAnswers({});
+ setSubmitted(false);
+ };
+ const switchEx = (i: number) => {
+ setActiveEx(i);
+ setAnswers({});
+ setSubmitted(false);
+ };
return (
@@ -326,7 +502,7 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
key={i}
onClick={() => switchEx(i)}
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors bg-white ${
- i === activeEx ? c.tab : 'text-gray-500 hover:text-gray-700'
+ i === activeEx ? c.tab : "text-gray-500 hover:text-gray-700"
}`}
>
{ex.title}
@@ -337,73 +513,100 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
{/* Chart */}
-
Data Source
- {exercise.chart.type === 'bar'
- ?
- :
- }
+
+ Data Source
+
+ {exercise.chart.type === "bar" ? (
+
+ ) : (
+
+ )}
{/* Claims */}
- For each claim, decide if the data{' '}
- supports ,{' '}
- contradicts , or{' '}
- neither proves nor disproves it:
+ For each claim, decide if the data{" "}
+ supports ,{" "}
+ contradicts , or{" "}
+
+ neither proves nor disproves
+ {" "}
+ it:
{exercise.claims.map((claim, i) => {
const userAnswer = answers[i];
const isCorrect = submitted && userAnswer === claim.verdict;
- const isWrong = submitted && userAnswer !== undefined && userAnswer !== claim.verdict;
+ const isWrong =
+ submitted &&
+ userAnswer !== undefined &&
+ userAnswer !== claim.verdict;
return (
- Claim {i + 1}:
+
+ Claim {i + 1}:
+
{claim.text}
- {(['supported', 'contradicted', 'neither'] as Verdict[]).map(v => {
- const isSelected = userAnswer === v;
- const isCorrectOpt = submitted && v === claim.verdict;
- let cls = 'border-gray-200 text-gray-600 hover:border-gray-400 hover:bg-gray-50';
- if (isSelected && !submitted) cls = `border-amber-500 bg-amber-50 text-amber-800 font-semibold`;
- if (submitted) {
- if (isCorrectOpt) cls = 'border-green-400 bg-green-100 text-green-800 font-semibold';
- else if (isSelected) cls = 'border-red-300 bg-red-100 text-red-700';
- else cls = 'border-gray-100 text-gray-400';
- }
- return (
- setAnswers(prev => ({ ...prev, [i]: v }))}
- className={`px-3 py-1.5 rounded-full border text-xs transition-all ${cls}`}
- >
- {VERDICT_LABELS[v]}
-
- );
- })}
+ {(["supported", "contradicted", "neither"] as Verdict[]).map(
+ (v) => {
+ const isSelected = userAnswer === v;
+ const isCorrectOpt = submitted && v === claim.verdict;
+ let cls =
+ "border-gray-200 text-gray-600 hover:border-gray-400 hover:bg-gray-50";
+ if (isSelected && !submitted)
+ cls = `border-amber-500 bg-amber-50 text-amber-800 font-semibold`;
+ if (submitted) {
+ if (isCorrectOpt)
+ cls =
+ "border-green-400 bg-green-100 text-green-800 font-semibold";
+ else if (isSelected)
+ cls = "border-red-300 bg-red-100 text-red-700";
+ else cls = "border-gray-100 text-gray-400";
+ }
+ return (
+
+ setAnswers((prev) => ({ ...prev, [i]: v }))
+ }
+ className={`px-3 py-1.5 rounded-full border text-xs transition-all ${cls}`}
+ >
+ {VERDICT_LABELS[v]}
+
+ );
+ },
+ )}
{submitted && (
- {isCorrect
- ?
- :
- }
+ {isCorrect ? (
+
+ ) : (
+
+ )}
{!isCorrect && (
- Answer: {VERDICT_LABELS[claim.verdict]}.
+
+ Answer: {VERDICT_LABELS[claim.verdict]}.{" "}
+
)}
{claim.explanation}
@@ -422,7 +625,9 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
disabled={!allAnswered}
onClick={() => setSubmitted(true)}
className={`px-5 py-2.5 rounded-full text-sm font-bold text-white transition-colors ${
- allAnswered ? c.btn : 'bg-gray-200 text-gray-400 cursor-not-allowed'
+ allAnswered
+ ? c.btn
+ : "bg-gray-200 text-gray-400 cursor-not-allowed"
}`}
>
Check all answers
@@ -432,7 +637,10 @@ export default function DataClaimWidget({ exercises, accentColor = 'amber' }: Da
{score}/{exercise.claims.length} correct
-
+
Try again
diff --git a/src/components/lessons/DecisionTreeWidget.tsx b/src/components/lessons/DecisionTreeWidget.tsx
index 1c933b6..085a9be 100644
--- a/src/components/lessons/DecisionTreeWidget.tsx
+++ b/src/components/lessons/DecisionTreeWidget.tsx
@@ -1,22 +1,28 @@
-import React, { useState } from 'react';
-import { ChevronRight, RotateCcw, CheckCircle2, AlertTriangle, Info } from 'lucide-react';
+import React, { useState } from "react";
+import {
+ ChevronRight,
+ RotateCcw,
+ CheckCircle2,
+ AlertTriangle,
+ Info,
+} from "lucide-react";
export interface TreeNode {
id: string;
- question: string;
+ question?: string;
hint?: string;
yesLabel?: string;
noLabel?: string;
yes?: TreeNode;
no?: TreeNode;
result?: string;
- resultType?: 'correct' | 'warning' | 'info';
+ resultType?: "correct" | "warning" | "info";
ruleRef?: string;
}
export interface TreeScenario {
- label: string; // Short tab label, e.g. "Sentence 1"
- sentence: string; // The sentence to analyze
+ label: string; // Short tab label, e.g. "Sentence 1"
+ sentence: string; // The sentence to analyze
tree: TreeNode;
}
@@ -25,59 +31,66 @@ interface DecisionTreeWidgetProps {
accentColor?: string;
}
-type Answers = Record
;
+type Answers = Record;
/** Walk the tree following answers, return ordered list of [node, answer|null] pairs traversed */
-function getPath(root: TreeNode, answers: Answers): Array<{ node: TreeNode; answer: 'yes' | 'no' | null }> {
- const path: Array<{ node: TreeNode; answer: 'yes' | 'no' | null }> = [];
+function getPath(
+ root: TreeNode,
+ answers: Answers,
+): Array<{ node: TreeNode; answer: "yes" | "no" | null }> {
+ const path: Array<{ node: TreeNode; answer: "yes" | "no" | null }> = [];
let current: TreeNode | undefined = root;
while (current) {
+ // @ts-ignore
const ans = answers[current.id] ?? null;
path.push({ node: current, answer: ans });
if (ans === null) break; // not answered yet — this is the active node
if (current.result !== undefined) break; // leaf
- current = ans === 'yes' ? current.yes : current.no;
+ current = ans === "yes" ? current.yes : current.no;
}
return path;
}
const RESULT_STYLES = {
correct: {
- bg: 'bg-green-50',
- border: 'border-green-300',
- text: 'text-green-800',
+ bg: "bg-green-50",
+ border: "border-green-300",
+ text: "text-green-800",
icon: ,
},
warning: {
- bg: 'bg-amber-50',
- border: 'border-amber-300',
- text: 'text-amber-800',
+ bg: "bg-amber-50",
+ border: "border-amber-300",
+ text: "text-amber-800",
icon: ,
},
info: {
- bg: 'bg-blue-50',
- border: 'border-blue-300',
- text: 'text-blue-800',
+ bg: "bg-blue-50",
+ border: "border-blue-300",
+ text: "text-blue-800",
icon: ,
},
};
-export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }: DecisionTreeWidgetProps) {
+export default function DecisionTreeWidget({
+ scenarios,
+ accentColor = "purple",
+}: DecisionTreeWidgetProps) {
const [activeScenario, setActiveScenario] = useState(0);
const [answers, setAnswers] = useState({});
const scenario = scenarios[activeScenario];
const path = getPath(scenario.tree, answers);
const lastStep = path[path.length - 1];
- const isLeaf = lastStep.node.result !== undefined;
- const isComplete = isLeaf && lastStep.answer === null; // reached leaf, no more choices needed
+
+ // reached leaf, no more choices needed
// Actually leaf nodes don't have yes/no — they just show result when we arrive
const atLeaf = lastStep.node.result !== undefined;
- const handleAnswer = (nodeId: string, ans: 'yes' | 'no') => {
- setAnswers(prev => {
+ const handleAnswer = (nodeId: string, ans: "yes" | "no") => {
+ setAnswers((prev) => {
// Remove all answers for nodes that come AFTER this one in the current path
- const pathIds = path.map(p => p.node.id);
+ const pathIds = path.map((p) => p.node.id);
const idx = pathIds.indexOf(nodeId);
const newAnswers: Answers = {};
for (let i = 0; i < idx; i++) {
@@ -107,7 +120,7 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors ${
i === activeScenario
? `bg-white border-b-2 border-${accentColor}-600 text-${accentColor}-700`
- : 'text-gray-500 hover:text-gray-700'
+ : "text-gray-500 hover:text-gray-700"
}`}
>
{sc.label}
@@ -117,9 +130,17 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
)}
{/* Sentence under analysis */}
-
-
Analyze this sentence
-
"{scenario.sentence}"
+
+
+ Analyze this sentence
+
+
+ "{scenario.sentence}"
+
{/* Breadcrumb path */}
@@ -133,22 +154,36 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
{
// Reset from this node forward
- const pathIds = path.map(p => p.node.id);
+ const pathIds = path.map((p) => p.node.id);
const idx = pathIds.indexOf(step.node.id);
- setAnswers(prev => {
+ setAnswers((prev) => {
const newAnswers: Answers = {};
- for (let j = 0; j < idx; j++) newAnswers[pathIds[j]] = prev[pathIds[j]]!;
+ for (let j = 0; j < idx; j++)
+ newAnswers[pathIds[j]] = prev[pathIds[j]]!;
return newAnswers;
});
}}
className={`px-2 py-0.5 rounded transition-colors ${
- isAnswered ? 'text-gray-600 hover:text-gray-900 hover:bg-gray-200' : 'text-gray-400'
+ isAnswered
+ ? "text-gray-600 hover:text-gray-900 hover:bg-gray-200"
+ : "text-gray-400"
}`}
>
- {step.node.question.length > 40 ? step.node.question.slice(0, 40) + '…' : step.node.question}
+ {
+ // @ts-ignore
+ step.node.question.length > 40
+ ? // @ts-ignore
+ step.node.question.slice(0, 40) + "…"
+ : step.node.question
+ }
{step.answer && (
-
- → {step.answer === 'yes' ? (step.node.yesLabel ?? 'Yes') : (step.node.noLabel ?? 'No')}
+
+ →{" "}
+ {step.answer === "yes"
+ ? (step.node.yesLabel ?? "Yes")
+ : (step.node.noLabel ?? "No")}
)}
@@ -161,55 +196,61 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
{/* Active node */}
- {atLeaf ? (
- /* Leaf result */
- (() => {
- const node = lastStep.node;
- const rType = node.resultType ?? 'correct';
- const s = RESULT_STYLES[rType];
- return (
-
-
- {s.icon}
-
-
{node.result}
- {node.ruleRef && (
-
- {node.ruleRef}
+ {atLeaf
+ ? /* Leaf result */
+ (() => {
+ const node = lastStep.node;
+ const rType = node.resultType ?? "correct";
+ const s = RESULT_STYLES[rType];
+ return (
+
+
+ {s.icon}
+
+
+ {node.result}
- )}
+ {node.ruleRef && (
+
+ {node.ruleRef}
+
+ )}
+
-
- );
- })()
- ) : (
- /* Decision question */
- (() => {
- const node = lastStep.node;
- return (
-
-
{node.question}
- {node.hint &&
{node.hint}
}
- {!node.hint &&
}
-
-
handleAnswer(node.id, 'yes')}
- className="flex-1 min-w-[140px] px-4 py-3 rounded-xl border-2 border-green-300 bg-green-50 text-green-800 font-semibold text-sm hover:bg-green-100 transition-colors"
- >
- ✓ {node.yesLabel ?? 'Yes'}
-
-
handleAnswer(node.id, 'no')}
- className="flex-1 min-w-[140px] px-4 py-3 rounded-xl border-2 border-red-200 bg-red-50 text-red-700 font-semibold text-sm hover:bg-red-100 transition-colors"
- >
- ✗ {node.noLabel ?? 'No'}
-
+ );
+ })()
+ : /* Decision question */
+ (() => {
+ const node = lastStep.node;
+ return (
+
+
+ {node.question}
+
+ {node.hint && (
+
{node.hint}
+ )}
+ {!node.hint &&
}
+
+ handleAnswer(node.id, "yes")}
+ className="flex-1 min-w-35 px-4 py-3 rounded-xl border-2 border-green-300 bg-green-50 text-green-800 font-semibold text-sm hover:bg-green-100 transition-colors"
+ >
+ ✓ {node.yesLabel ?? "Yes"}
+
+ handleAnswer(node.id, "no")}
+ className="flex-1 min-w-35 px-4 py-3 rounded-xl border-2 border-red-200 bg-red-50 text-red-700 font-semibold text-sm hover:bg-red-100 transition-colors"
+ >
+ ✗ {node.noLabel ?? "No"}
+
+
-
- );
- })()
- )}
+ );
+ })()}
{/* Footer */}
@@ -221,14 +262,16 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
Try again
- {atLeaf && scenarios.length > 1 && activeScenario < scenarios.length - 1 && (
-
switchScenario(activeScenario + 1)}
- className={`ml-auto flex items-center gap-1.5 text-sm font-semibold text-${accentColor}-700 hover:text-${accentColor}-900 transition-colors`}
- >
- Next sentence
-
- )}
+ {atLeaf &&
+ scenarios.length > 1 &&
+ activeScenario < scenarios.length - 1 && (
+
switchScenario(activeScenario + 1)}
+ className={`ml-auto flex items-center gap-1.5 text-sm font-semibold text-${accentColor}-700 hover:text-${accentColor}-900 transition-colors`}
+ >
+ Next sentence
+
+ )}
);
diff --git a/src/components/lessons/ExponentialExplorer.tsx b/src/components/lessons/ExponentialExplorer.tsx
index 8360e82..3fd3168 100644
--- a/src/components/lessons/ExponentialExplorer.tsx
+++ b/src/components/lessons/ExponentialExplorer.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
const ExponentialExplorer: React.FC = () => {
const [a, setA] = useState(2); // Initial Value
@@ -6,83 +6,153 @@ const ExponentialExplorer: React.FC = () => {
const [k, setK] = useState(0); // Horizontal Asymptote shift
const width = 300;
- const height = 300;
const range = 5; // x range -5 to 5
-
+
// Mapping
const toPx = (v: number, isY = false) => {
- const scale = width / (range * 2);
- const center = width / 2;
- return isY ? center - v * scale : center + v * scale;
+ const scale = width / (range * 2);
+ const center = width / 2;
+ return isY ? center - v * scale : center + v * scale;
};
const generatePath = () => {
- let d = "";
- for (let x = -range; x <= range; x += 0.1) {
- const y = a * Math.pow(b, x) + k;
- if (y > range * 2 || y < -range * 2) continue; // Clip
- const px = toPx(x);
- const py = toPx(y, true);
- d += d ? ` L ${px} ${py}` : `M ${px} ${py}`;
- }
- return d;
+ let d = "";
+ for (let x = -range; x <= range; x += 0.1) {
+ const y = a * Math.pow(b, x) + k;
+ if (y > range * 2 || y < -range * 2) continue; // Clip
+ const px = toPx(x);
+ const py = toPx(y, true);
+ d += d ? ` L ${px} ${py}` : `M ${px} ${py}`;
+ }
+ return d;
};
return (
-
-
-
-
Standard Form
-
- y = {a} · {b} x {k >= 0 ? '+' : ''} {k}
-
-
+
+
+
+
+ Standard Form
+
+
+ y = {a} ·{" "}
+ {b}
+ x {k >= 0 ? "+" : ""}{" "}
+ {k}
+
+
-
-
-
- Initial Value (a) {a}
-
- setA(parseFloat(e.target.value))} className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"/>
-
-
-
- Growth Factor (b) {b}
-
-
setB(parseFloat(e.target.value))} className="w-full h-2 bg-emerald-100 rounded-lg appearance-none cursor-pointer accent-emerald-600"/>
-
{b > 1 ? "Growth (b > 1)" : "Decay (0 < b < 1)"}
-
-
-
- Vertical Shift (k) {k}
-
- setK(parseFloat(e.target.value))} className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600"/>
-
-
-
+
+
+
+ Initial Value (a) {a}
+
+ setA(parseFloat(e.target.value))}
+ className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
+ />
+
+
+
+ Growth Factor (b) {b}
+
+
setB(parseFloat(e.target.value))}
+ className="w-full h-2 bg-emerald-100 rounded-lg appearance-none cursor-pointer accent-emerald-600"
+ />
+
+ {b > 1 ? "Growth (b > 1)" : "Decay (0 < b < 1)"}
+
+
+
+
+ Vertical Shift (k) {k}
+
+ setK(parseFloat(e.target.value))}
+ className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600"
+ />
+
+
+
-
-
-
-
-
-
- {/* Asymptote */}
-
- y = {k}
+
+
+
+
+
- {/* Function */}
-
-
- {/* Intercept */}
-
-
-
-
-
+ {/* Asymptote */}
+
+
+ y = {k}
+
+
+ {/* Function */}
+
+
+ {/* Intercept */}
+
+
+
+
+
);
};
-export default ExponentialExplorer;
\ No newline at end of file
+export default ExponentialExplorer;
diff --git a/src/components/lessons/HistogramBuilderWidget.tsx b/src/components/lessons/HistogramBuilderWidget.tsx
index 0ba4a5c..5a25895 100644
--- a/src/components/lessons/HistogramBuilderWidget.tsx
+++ b/src/components/lessons/HistogramBuilderWidget.tsx
@@ -1,86 +1,103 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
const HistogramBuilderWidget: React.FC = () => {
- const [mode, setMode] = useState<'count' | 'percent'>('count');
-
+ const [mode, setMode] = useState<"count" | "percent">("count");
+
// Data: [60, 70), [70, 80), [80, 90), [90, 100)
const data = [
- { bin: '60-70', count: 4, label: '60s' },
- { bin: '70-80', count: 9, label: '70s' },
- { bin: '80-90', count: 6, label: '80s' },
- { bin: '90-100', count: 1, label: '90s' },
+ { bin: "60-70", count: 4, label: "60s" },
+ { bin: "70-80", count: 9, label: "70s" },
+ { bin: "80-90", count: 6, label: "80s" },
+ { bin: "90-100", count: 1, label: "90s" },
];
const total = data.reduce((acc, curr) => acc + curr.count, 0); // 20
-
- const maxCount = Math.max(...data.map(d => d.count));
+
+ const maxCount = Math.max(...data.map((d) => d.count));
const maxPercent = maxCount / total; // 0.45
return (
-
-
Test Scores Distribution
-
- setMode('count')}
- className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all ${mode === 'count' ? 'bg-white shadow-sm text-indigo-600' : 'text-slate-500 hover:text-slate-700'}`}
- >
- Frequency (Count)
-
- setMode('percent')}
- className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all ${mode === 'percent' ? 'bg-white shadow-sm text-rose-600' : 'text-slate-500 hover:text-slate-700'}`}
- >
- Relative Freq (%)
-
-
-
+
+
Test Scores Distribution
+
+ setMode("count")}
+ className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all ${mode === "count" ? "bg-white shadow-sm text-indigo-600" : "text-slate-500 hover:text-slate-700"}`}
+ >
+ Frequency (Count)
+
+ setMode("percent")}
+ className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all ${mode === "percent" ? "bg-white shadow-sm text-rose-600" : "text-slate-500 hover:text-slate-700"}`}
+ >
+ Relative Freq (%)
+
+
+
-
- {/* Y Axis Labels */}
-
- {mode === 'count' ? maxCount + 1 : ((maxPercent + 0.05)*100).toFixed(0) + '%'}
- {mode === 'count' ? Math.round((maxCount+1)/2) : (((maxPercent + 0.05)/2)*100).toFixed(0) + '%'}
- 0
-
+
+ {/* Y Axis Labels */}
+
+
+ {mode === "count"
+ ? maxCount + 1
+ : ((maxPercent + 0.05) * 100).toFixed(0) + "%"}
+
+
+ {mode === "count"
+ ? Math.round((maxCount + 1) / 2)
+ : (((maxPercent + 0.05) / 2) * 100).toFixed(0) + "%"}
+
+ 0
+
- {data.map((d, i) => {
- const heightRatio = d.count / maxCount; // Normalize to max height of graph area roughly
- // Actually map 0 to maxScale
- const maxScale = mode === 'count' ? maxCount + 1 : (maxPercent + 0.05);
- const val = mode === 'count' ? d.count : d.count / total;
- const hPercent = (val / maxScale) * 100;
+ {data.map((d, i) => {
+ // Normalize to max height of graph area roughly
+ // Actually map 0 to maxScale
+ const maxScale = mode === "count" ? maxCount + 1 : maxPercent + 0.05;
+ const val = mode === "count" ? d.count : d.count / total;
+ const hPercent = (val / maxScale) * 100;
- return (
-
- {/* Tooltip */}
-
- {d.bin}: {mode === 'count' ? d.count : `${(d.count/total*100).toFixed(0)}%`}
-
-
- {/* Bar */}
-
-
- {/* Bin Label */}
-
- {d.label}
-
-
- );
- })}
-
-
-
-
- Key Takeaway: Notice that the shape of the distribution stays exactly the same.
- Only the Y-axis scale changes.
-
-
+ return (
+
+ {/* Tooltip */}
+
+ {d.bin}:{" "}
+ {mode === "count"
+ ? d.count
+ : `${((d.count / total) * 100).toFixed(0)}%`}
+
+
+ {/* Bar */}
+
+
+ {/* Bin Label */}
+
+ {d.label}
+
+
+ );
+ })}
+
+
+
+
+ Key Takeaway: Notice that the{" "}
+ shape of the
+ distribution stays exactly the same. Only the{" "}
+ Y-axis scale {" "}
+ changes.
+
+
);
};
-export default HistogramBuilderWidget;
\ No newline at end of file
+export default HistogramBuilderWidget;
diff --git a/src/components/lessons/LessonShell.tsx b/src/components/lessons/LessonShell.tsx
index b06d373..21af3e6 100644
--- a/src/components/lessons/LessonShell.tsx
+++ b/src/components/lessons/LessonShell.tsx
@@ -64,7 +64,6 @@ const PALETTES = {
};
export default function LessonShell({
- title,
sections,
color,
onFinish,
diff --git a/src/components/lessons/LinearTransformationWidget.tsx b/src/components/lessons/LinearTransformationWidget.tsx
index 07c22f0..02833c6 100644
--- a/src/components/lessons/LinearTransformationWidget.tsx
+++ b/src/components/lessons/LinearTransformationWidget.tsx
@@ -1,19 +1,19 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
const LinearTransformationWidget: React.FC = () => {
const [h, setH] = useState(0); // Horizontal shift (x - h)
const [k, setK] = useState(0); // Vertical shift + k
const [reflectX, setReflectX] = useState(false); // -f(x)
- const [stretch, setStretch] = useState(1); // a * f(x)
+ const stretch = 1; // a * f(x)
// Base function f(x) = 0.5x
- // Transformed g(x) = a * f(x - h) + k
+ // Transformed g(x) = a * f(x - h) + k
// g(x) = a * (0.5 * (x - h)) + k
-
- // Actually, let's use f(x) = x for simplicity, or 0.5x to show slope changes easier?
+
+ // Actually, let's use f(x) = x for simplicity, or 0.5x to show slope changes easier?
// PDF examples use general f(x). Let's use f(x) = x as base.
// g(x) = stretch * (x - h) + k. If reflectX is true, stretch becomes -stretch.
-
+
const effectiveStretch = reflectX ? -stretch : stretch;
const range = 10;
@@ -21,100 +21,158 @@ const LinearTransformationWidget: React.FC = () => {
const size = 300;
const center = size / 2;
- const toPx = (v: number, isY = false) => isY ? center - v * scale : center + v * scale;
+ const toPx = (v: number, isY = false) =>
+ isY ? center - v * scale : center + v * scale;
// Base: y = 0.5x (to make it distinct from diagonals)
const getBasePath = () => {
- const m = 0.5;
- const x1 = -range, x2 = range;
- const y1 = m * x1;
- const y2 = m * x2;
- return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
+ const m = 0.5;
+ const x1 = -range,
+ x2 = range;
+ const y1 = m * x1;
+ const y2 = m * x2;
+ return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
};
const getTransformedPath = () => {
- // f(x) = 0.5x
- // g(x) = effectiveStretch * (0.5 * (x - h)) + k
- const x1 = -range, x2 = range;
- const y1 = effectiveStretch * (0.5 * (x1 - h)) + k;
- const y2 = effectiveStretch * (0.5 * (x2 - h)) + k;
- return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
+ // f(x) = 0.5x
+ // g(x) = effectiveStretch * (0.5 * (x - h)) + k
+ const x1 = -range,
+ x2 = range;
+ const y1 = effectiveStretch * (0.5 * (x1 - h)) + k;
+ const y2 = effectiveStretch * (0.5 * (x2 - h)) + k;
+ return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
};
return (
-
-
-
Base: f(x) = 0.5x
-
- g(x) = {reflectX ? '-' : ''}{stretch !== 1 ? stretch : ''}f(x {h > 0 ? '-' : '+'} {Math.abs(h)}) {k >= 0 ? '+' : '-'} {Math.abs(k)}
-
-
+
+
+
+ Base:{" "}
+ f(x) = 0.5x
+
+
+ g(x) = {reflectX ? "-" : ""}
+ {stretch !== 1 ? stretch : ""}f(x {h > 0 ? "-" : "+"}{" "}
+ {Math.abs(h)}) {k >= 0 ? "+" : "-"} {Math.abs(k)}
+
+
-
-
-
- Horizontal Shift (h) {h}
-
-
setH(parseInt(e.target.value))}
- className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600 mt-1"
- />
-
- Left (x+h)
- Right (x-h)
-
-
+
+
+
+ Horizontal Shift (h) {h}
+
+
setH(parseInt(e.target.value))}
+ className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600 mt-1"
+ />
+
+ Left (x+h)
+ Right (x-h)
+
+
-
-
- Vertical Shift (k) {k}
-
- setK(parseInt(e.target.value))}
- className="w-full h-2 bg-emerald-100 rounded-lg appearance-none cursor-pointer accent-emerald-600 mt-1"
- />
-
+
+
+ Vertical Shift (k) {k}
+
+ setK(parseInt(e.target.value))}
+ className="w-full h-2 bg-emerald-100 rounded-lg appearance-none cursor-pointer accent-emerald-600 mt-1"
+ />
+
-
-
- setReflectX(e.target.checked)} className="accent-rose-600 w-4 h-4"/>
- Reflect (-f(x))
-
-
-
-
+
+
+ setReflectX(e.target.checked)}
+ className="accent-rose-600 w-4 h-4"
+ />
+ Reflect (-f(x))
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- {/* Axes */}
-
-
+
+
+
+
+
+
+
+
+
- {/* Base Function (Ghost) */}
-
- f(x)
+ {/* Axes */}
+
+
- {/* Transformed Function */}
-
- g(x)
-
-
-
+ {/* Base Function (Ghost) */}
+
+
+ f(x)
+
+
+ {/* Transformed Function */}
+
+
+ g(x)
+
+
+
+
);
};
-export default LinearTransformationWidget;
\ No newline at end of file
+export default LinearTransformationWidget;
diff --git a/src/components/lessons/MultiStepPercentWidget.tsx b/src/components/lessons/MultiStepPercentWidget.tsx
index adee7d9..e8237f9 100644
--- a/src/components/lessons/MultiStepPercentWidget.tsx
+++ b/src/components/lessons/MultiStepPercentWidget.tsx
@@ -1,14 +1,13 @@
-import React, { useState } from 'react';
-import { ArrowRight } from 'lucide-react';
+import React, { useState } from "react";
const MultiStepPercentWidget: React.FC = () => {
- const [start, setStart] = useState(100);
+ const start = 100;
const [change1, setChange1] = useState(40); // +40%
const [change2, setChange2] = useState(-25); // -25%
- const step1Val = start * (1 + change1/100);
- const finalVal = step1Val * (1 + change2/100);
-
+ const step1Val = start * (1 + change1 / 100);
+ const finalVal = step1Val * (1 + change2 / 100);
+
const overallChange = ((finalVal - start) / start) * 100;
const naiveChange = change1 + change2;
@@ -18,86 +17,134 @@ const MultiStepPercentWidget: React.FC = () => {
return (
-
-
-
-
Change 1 (Markup)
-
- setChange1(parseInt(e.target.value))}
- className="flex-1 accent-indigo-600"
- />
- {change1 > 0 ? '+' : ''}{change1}%
-
-
-
-
Change 2 (Discount)
-
- setChange2(parseInt(e.target.value))}
- className="flex-1 accent-rose-600"
- />
- {change2 > 0 ? '+' : ''}{change2}%
-
-
-
+
+
+
+
+ Change 1 (Markup)
+
+
+ setChange1(parseInt(e.target.value))}
+ className="flex-1 accent-indigo-600"
+ />
+
+ {change1 > 0 ? "+" : ""}
+ {change1}%
+
+
+
+
+
+ Change 2 (Discount)
+
+
+ setChange2(parseInt(e.target.value))}
+ className="flex-1 accent-rose-600"
+ />
+
+ {change2 > 0 ? "+" : ""}
+ {change2}%
+
+
+
+
-
- {/* Step 0 */}
-
-
- Start
- ${start}
-
-
-
+
+ {/* Step 0 */}
+
+
+ Start
+ ${start}
+
+
+
- {/* Step 1 */}
-
-
- After {change1 > 0 ? '+' : ''}{change1}%
- ${step1Val.toFixed(2)}
-
-
-
+ {/* Step 1 */}
+
+
+
+ After {change1 > 0 ? "+" : ""}
+ {change1}%
+
+ ${step1Val.toFixed(2)}
+
+
+
- {/* Step 2 */}
-
-
- After {change2 > 0 ? '+' : ''}{change2}%
- ${finalVal.toFixed(2)}
-
-
-
-
-
+ {/* Step 2 */}
+
+
+
+ After {change2 > 0 ? "+" : ""}
+ {change2}%
+
+ ${finalVal.toFixed(2)}
+
+
+
+
+
-
-
-
The Trap (Additive)
-
- {naiveChange > 0 ? '+' : ''}{naiveChange}%
-
-
({change1} + {change2})
-
-
-
Actual Change
-
- {overallChange > 0 ? '+' : ''}{overallChange.toFixed(2)}%
-
-
- 1.{change1} × {1 + change2/100} = {(1 + change1/100) * (1 + change2/100)}
-
-
-
+
+
+
+ The Trap (Additive)
+
+
+ {naiveChange > 0 ? "+" : ""}
+ {naiveChange}%
+
+
+ ({change1} + {change2})
+
+
+
+
+ Actual Change
+
+
+ {overallChange > 0 ? "+" : ""}
+ {overallChange.toFixed(2)}%
+
+
+ 1.{change1} × {1 + change2 / 100} ={" "}
+ {(1 + change1 / 100) * (1 + change2 / 100)}
+
+
+
);
};
-export default MultiStepPercentWidget;
\ No newline at end of file
+export default MultiStepPercentWidget;
diff --git a/src/components/lessons/PolygonWidget.tsx b/src/components/lessons/PolygonWidget.tsx
index a0f9290..1fbb09f 100644
--- a/src/components/lessons/PolygonWidget.tsx
+++ b/src/components/lessons/PolygonWidget.tsx
@@ -1,8 +1,8 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from "react";
const PolygonWidget: React.FC = () => {
const [n, setN] = useState(5);
-
+
// Math
const interiorSum = (n - 2) * 180;
const eachInterior = Math.round((interiorSum / n) * 100) / 100;
@@ -15,79 +15,128 @@ const PolygonWidget: React.FC = () => {
const cy = height / 2;
const r = 80;
- // Generate points
+ // @ts-ignore
const points = [];
for (let i = 0; i < n; i++) {
const angle = (i * 2 * Math.PI) / n - Math.PI / 2; // Start at top
points.push({
x: cx + r * Math.cos(angle),
- y: cy + r * Math.sin(angle)
+ y: cy + r * Math.sin(angle),
});
}
// Generate path string
- const pathD = points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ') + ' Z';
+ const pathD =
+ points
+ .map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`))
+ .join(" ") + " Z";
// Generate exterior lines (extensions)
const exteriorLines = points.map((p, i) => {
+ // @ts-ignore
const nextP = points[(i + 1) % n];
// Vector from p to nextP
const dx = nextP.x - p.x;
const dy = nextP.y - p.y;
// Normalize and extend
- const len = Math.sqrt(dx*dx + dy*dy);
+ const len = Math.sqrt(dx * dx + dy * dy);
const exLen = 40;
- const exX = nextP.x + (dx/len) * exLen;
- const exY = nextP.y + (dy/len) * exLen;
+ const exX = nextP.x + (dx / len) * exLen;
+ const exY = nextP.y + (dy / len) * exLen;
return { x1: nextP.x, y1: nextP.y, x2: exX, y2: exY };
});
return (
-
Number of Sides (n): {n}
-
setN(parseInt(e.target.value))}
- className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-emerald-600 mb-6"
- />
-
-
-
-
Interior Sum
-
(n - 2) × 180° = {interiorSum}°
-
-
-
-
Each Interior Angle
-
{interiorSum} / {n} = {eachInterior}°
-
+
+ Number of Sides (n):{" "}
+ {n}
+
+
setN(parseInt(e.target.value))}
+ className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-emerald-600 mb-6"
+ />
-
-
Each Exterior Angle
-
360 / {n} = {eachExterior}°
-
-
+
+
+
+ Interior Sum
+
+
+ (n - 2) × 180° ={" "}
+ {interiorSum}°
+
+
+
+
+
+ Each Interior Angle
+
+
+ {interiorSum} / {n} ={" "}
+ {eachInterior}°
+
+
+
+
+
+ Each Exterior Angle
+
+
+ 360 / {n} ={" "}
+ {eachExterior}°
+
+
+
{/* Extensions for exterior angles */}
{exteriorLines.map((line, i) => (
-
+
))}
{/* Polygon */}
-
-
+
+
{/* Vertices */}
{points.map((p, i) => (
))}
{/* Center text */}
-
- {n}-gon
+
+ {n}-gon
diff --git a/src/components/lessons/PolynomialBehaviorWidget.tsx b/src/components/lessons/PolynomialBehaviorWidget.tsx
index c00699f..cf98542 100644
--- a/src/components/lessons/PolynomialBehaviorWidget.tsx
+++ b/src/components/lessons/PolynomialBehaviorWidget.tsx
@@ -1,77 +1,146 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
const PolynomialBehaviorWidget: React.FC = () => {
- const [degreeType, setDegreeType] = useState<'even' | 'odd'>('odd');
- const [lcSign, setLcSign] = useState<'pos' | 'neg'>('pos');
+ const [degreeType, setDegreeType] = useState<"even" | "odd">("odd");
+ const [lcSign, setLcSign] = useState<"pos" | "neg">("pos");
- // Visualization
- const width = 300;
- const height = 200;
-
const getPath = () => {
- // Create schematic shapes
- // Odd +: Low Left -> High Right
- // Odd -: High Left -> Low Right
- // Even +: High Left -> High Right
- // Even -: Low Left -> Low Right
-
- const startY = (degreeType === 'odd' && lcSign === 'pos') || (degreeType === 'even' && lcSign === 'neg') ? 180 : 20;
- const endY = (lcSign === 'pos') ? 20 : 180;
-
- // Control points for curvy polynomial look
- const cp1Y = startY === 20 ? 150 : 50;
- const cp2Y = endY === 20 ? 150 : 50;
+ // Create schematic shapes
+ // Odd +: Low Left -> High Right
+ // Odd -: High Left -> Low Right
+ // Even +: High Left -> High Right
+ // Even -: Low Left -> Low Right
- return `M 20 ${startY} C 100 ${cp1Y}, 200 ${cp2Y}, 280 ${endY}`;
+ const startY =
+ (degreeType === "odd" && lcSign === "pos") ||
+ (degreeType === "even" && lcSign === "neg")
+ ? 180
+ : 20;
+ const endY = lcSign === "pos" ? 20 : 180;
+
+ // Control points for curvy polynomial look
+ const cp1Y = startY === 20 ? 150 : 50;
+ const cp2Y = endY === 20 ? 150 : 50;
+
+ return `M 20 ${startY} C 100 ${cp1Y}, 200 ${cp2Y}, 280 ${endY}`;
};
return (
-
-
-
Degree (Highest Power)
-
- setDegreeType('even')} className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${degreeType === 'even' ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-slate-600 border-slate-200'}`}>Even (x², x⁴)
- setDegreeType('odd')} className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${degreeType === 'odd' ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-slate-600 border-slate-200'}`}>Odd (x, x³)
-
-
-
-
Leading Coefficient
-
- setLcSign('pos')} className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${lcSign === 'pos' ? 'bg-emerald-600 text-white border-emerald-600' : 'bg-white text-slate-600 border-slate-200'}`}>Positive (+)
- setLcSign('neg')} className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${lcSign === 'neg' ? 'bg-rose-600 text-white border-rose-600' : 'bg-white text-slate-600 border-slate-200'}`}>Negative (-)
-
-
+
+
+
+ Degree (Highest Power)
+
+
+ setDegreeType("even")}
+ className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${degreeType === "even" ? "bg-indigo-600 text-white border-indigo-600" : "bg-white text-slate-600 border-slate-200"}`}
+ >
+ Even (x², x⁴)
+
+ setDegreeType("odd")}
+ className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${degreeType === "odd" ? "bg-indigo-600 text-white border-indigo-600" : "bg-white text-slate-600 border-slate-200"}`}
+ >
+ Odd (x, x³)
+
+
+
+
+ Leading Coefficient
+
+
+ setLcSign("pos")}
+ className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${lcSign === "pos" ? "bg-emerald-600 text-white border-emerald-600" : "bg-white text-slate-600 border-slate-200"}`}
+ >
+ Positive (+)
+
+ setLcSign("neg")}
+ className={`px-4 py-2 rounded-lg font-bold text-sm border transition-all ${lcSign === "neg" ? "bg-rose-600 text-white border-rose-600" : "bg-white text-slate-600 border-slate-200"}`}
+ >
+ Negative (-)
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
End Behavior
-
+
+
+
+
-
- {degreeType === 'even' && lcSign === 'pos' && "Ends go in the SAME direction (UP)."}
- {degreeType === 'even' && lcSign === 'neg' && "Ends go in the SAME direction (DOWN)."}
- {degreeType === 'odd' && lcSign === 'pos' && "Ends go in OPPOSITE directions (Down Left, Up Right)."}
- {degreeType === 'odd' && lcSign === 'neg' && "Ends go in OPPOSITE directions (Up Left, Down Right)."}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ End Behavior
+
+
+
+ {degreeType === "even" &&
+ lcSign === "pos" &&
+ "Ends go in the SAME direction (UP)."}
+ {degreeType === "even" &&
+ lcSign === "neg" &&
+ "Ends go in the SAME direction (DOWN)."}
+ {degreeType === "odd" &&
+ lcSign === "pos" &&
+ "Ends go in OPPOSITE directions (Down Left, Up Right)."}
+ {degreeType === "odd" &&
+ lcSign === "neg" &&
+ "Ends go in OPPOSITE directions (Up Left, Down Right)."}
+
);
};
-export default PolynomialBehaviorWidget;
\ No newline at end of file
+export default PolynomialBehaviorWidget;
diff --git a/src/components/lessons/ProbabilityTreeWidget.tsx b/src/components/lessons/ProbabilityTreeWidget.tsx
index 78d2a21..17a855d 100644
--- a/src/components/lessons/ProbabilityTreeWidget.tsx
+++ b/src/components/lessons/ProbabilityTreeWidget.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
const ProbabilityTreeWidget: React.FC = () => {
const [replacement, setReplacement] = useState(false);
@@ -31,223 +31,402 @@ const ProbabilityTreeWidget: React.FC = () => {
const pBB = pB * pB_B;
const fraction = (num: number, den: number) => {
- if (den === 0) return "0";
- return (
-
- {num}/{den}
-
- );
+ if (den === 0) return "0";
+ return (
+
+ {num}/{den}
+
+ );
};
- const getPathColor = (path: string, segment: 'top' | 'bottom' | 'top-top' | 'top-bottom' | 'bottom-top' | 'bottom-bottom') => {
- const defaultColor = "#cbd5e1"; // Slate 300
-
- if (!hoverPath) {
- // Default coloring based on branch type
- if (segment.includes('top')) return "#f43f5e"; // Red branches
- if (segment.includes('bottom')) return "#3b82f6"; // Blue branches
- return defaultColor;
- }
-
- // Highlighting logic based on hoverPath
- if (segment === 'top') return (hoverPath === 'RR' || hoverPath === 'RB') ? "#f43f5e" : "#f1f5f9";
- if (segment === 'bottom') return (hoverPath === 'BR' || hoverPath === 'BB') ? "#3b82f6" : "#f1f5f9";
-
- if (segment === 'top-top') return hoverPath === 'RR' ? "#f43f5e" : "#f1f5f9";
- if (segment === 'top-bottom') return hoverPath === 'RB' ? "#3b82f6" : "#f1f5f9";
-
- if (segment === 'bottom-top') return hoverPath === 'BR' ? "#f43f5e" : "#f1f5f9";
- if (segment === 'bottom-bottom') return hoverPath === 'BB' ? "#3b82f6" : "#f1f5f9";
+ const getPathColor = (
+ segment:
+ | "top"
+ | "bottom"
+ | "top-top"
+ | "top-bottom"
+ | "bottom-top"
+ | "bottom-bottom",
+ ) => {
+ const defaultColor = "#cbd5e1"; // Slate 300
+ if (!hoverPath) {
+ // Default coloring based on branch type
+ if (segment.includes("top")) return "#f43f5e"; // Red branches
+ if (segment.includes("bottom")) return "#3b82f6"; // Blue branches
return defaultColor;
+ }
+
+ // Highlighting logic based on hoverPath
+ if (segment === "top")
+ return hoverPath === "RR" || hoverPath === "RB" ? "#f43f5e" : "#f1f5f9";
+ if (segment === "bottom")
+ return hoverPath === "BR" || hoverPath === "BB" ? "#3b82f6" : "#f1f5f9";
+
+ if (segment === "top-top")
+ return hoverPath === "RR" ? "#f43f5e" : "#f1f5f9";
+ if (segment === "top-bottom")
+ return hoverPath === "RB" ? "#3b82f6" : "#f1f5f9";
+
+ if (segment === "bottom-top")
+ return hoverPath === "BR" ? "#f43f5e" : "#f1f5f9";
+ if (segment === "bottom-bottom")
+ return hoverPath === "BB" ? "#3b82f6" : "#f1f5f9";
+
+ return defaultColor;
};
-
+
const getStrokeWidth = (segment: string) => {
- if (!hoverPath) return 2;
-
- if (segment === 'top') return (hoverPath === 'RR' || hoverPath === 'RB') ? 4 : 1;
- if (segment === 'bottom') return (hoverPath === 'BR' || hoverPath === 'BB') ? 4 : 1;
-
- if (segment === 'top-top') return hoverPath === 'RR' ? 4 : 1;
- if (segment === 'top-bottom') return hoverPath === 'RB' ? 4 : 1;
-
- if (segment === 'bottom-top') return hoverPath === 'BR' ? 4 : 1;
- if (segment === 'bottom-bottom') return hoverPath === 'BB' ? 4 : 1;
-
- return 2;
- }
+ if (!hoverPath) return 2;
+
+ if (segment === "top")
+ return hoverPath === "RR" || hoverPath === "RB" ? 4 : 1;
+ if (segment === "bottom")
+ return hoverPath === "BR" || hoverPath === "BB" ? 4 : 1;
+
+ if (segment === "top-top") return hoverPath === "RR" ? 4 : 1;
+ if (segment === "top-bottom") return hoverPath === "RB" ? 4 : 1;
+
+ if (segment === "bottom-top") return hoverPath === "BR" ? 4 : 1;
+ if (segment === "bottom-bottom") return hoverPath === "BB" ? 4 : 1;
+
+ return 2;
+ };
return (
-
- {/* Controls */}
-
-
-
-
Red Items
-
- setInitR(Math.max(1, initR-1))} className="w-6 h-6 bg-rose-100 text-rose-700 rounded hover:bg-rose-200">-
- {initR}
- setInitR(Math.min(10, initR+1))} className="w-6 h-6 bg-rose-100 text-rose-700 rounded hover:bg-rose-200">+
-
-
-
-
Blue Items
-
- setInitB(Math.max(1, initB-1))} className="w-6 h-6 bg-blue-100 text-blue-700 rounded hover:bg-blue-200">-
- {initB}
- setInitB(Math.min(10, initB+1))} className="w-6 h-6 bg-blue-100 text-blue-700 rounded hover:bg-blue-200">+
-
-
-
+ {/* Controls */}
+
+
+
+
+ Red Items
+
+
+ setInitR(Math.max(1, initR - 1))}
+ className="w-6 h-6 bg-rose-100 text-rose-700 rounded hover:bg-rose-200"
+ >
+ -
+
+ {initR}
+ setInitR(Math.min(10, initR + 1))}
+ className="w-6 h-6 bg-rose-100 text-rose-700 rounded hover:bg-rose-200"
+ >
+ +
+
+
+
+
+
+ Blue Items
+
+
+ setInitB(Math.max(1, initB - 1))}
+ className="w-6 h-6 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
+ >
+ -
+
+ {initB}
+ setInitB(Math.min(10, initB + 1))}
+ className="w-6 h-6 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
+ >
+ +
+
+
+
+
-
- setReplacement(true)}
- className={`px-3 py-1 text-xs font-bold rounded transition-all ${replacement ? 'bg-white shadow text-indigo-600' : 'text-slate-500'}`}
- >
- With Replacement
-
- setReplacement(false)}
- className={`px-3 py-1 text-xs font-bold rounded transition-all ${!replacement ? 'bg-white shadow text-indigo-600' : 'text-slate-500'}`}
- >
- Without Replacement
-
-
-
+
+ setReplacement(true)}
+ className={`px-3 py-1 text-xs font-bold rounded transition-all ${replacement ? "bg-white shadow text-indigo-600" : "text-slate-500"}`}
+ >
+ With Replacement
+
+ setReplacement(false)}
+ className={`px-3 py-1 text-xs font-bold rounded transition-all ${!replacement ? "bg-white shadow text-indigo-600" : "text-slate-500"}`}
+ >
+ Without Replacement
+
+
+
-
-
- {/* Root */}
-
-
- {/* Level 1 Branches */}
-
-
-
- {/* Level 1 Labels */}
-
- {initR}/{total}
-
-
- {initB}/{total}
-
+
+
+ {/* Root */}
+
- {/* Level 1 Nodes */}
-
- R
+ {/* Level 1 Branches */}
+
+
-
- B
+ {/* Level 1 Labels */}
+
+
+ {initR}/{total}
+
+
+
+
+ {initB}/{total}
+
+
- {/* Level 2 Branches (Top) */}
-
-
-
- {/* Level 2 Top Labels */}
-
- {r_R}/{r_Total}
-
-
- {initB}/{r_Total}
-
+ {/* Level 1 Nodes */}
+
+
+ R
+
- {/* Level 2 Branches (Bottom) */}
-
-
+
+
+ B
+
- {/* Level 2 Bottom Labels */}
-
- {initR}/{b_Total}
-
-
- {b_B}/{b_Total}
-
+ {/* Level 2 Branches (Top) */}
+
+
- {/* Outcomes (Interactive Targets) */}
- setHoverPath('RR')}
- onMouseLeave={() => setHoverPath(null)}
- >
- RR: {(pRR * 100).toFixed(1)}%
-
-
+ {/* Level 2 Top Labels */}
+
+
+ {r_R}/{r_Total}
+
+
+
+
+ {initB}/{r_Total}
+
+
- setHoverPath('RB')}
- onMouseLeave={() => setHoverPath(null)}
- >
- RB: {(pRB * 100).toFixed(1)}%
-
-
+ {/* Level 2 Branches (Bottom) */}
+
+
- setHoverPath('BR')}
- onMouseLeave={() => setHoverPath(null)}
- >
- BR: {(pBR * 100).toFixed(1)}%
-
-
+ {/* Level 2 Bottom Labels */}
+
+
+ {initR}/{b_Total}
+
+
+
+
+ {b_B}/{b_Total}
+
+
- setHoverPath('BB')}
- onMouseLeave={() => setHoverPath(null)}
- >
- BB: {(pBB * 100).toFixed(1)}%
-
-
-
-
+ {/* Outcomes (Interactive Targets) */}
+ setHoverPath("RR")}
+ onMouseLeave={() => setHoverPath(null)}
+ >
+
+ RR: {(pRR * 100).toFixed(1)}%
+
+
+
- {/* Calculation Panel */}
-
- {!hoverPath ? (
-
Hover over an outcome (e.g., RR) to see the calculation.
- ) : (
- <>
-
- Calculation for {hoverPath}
- ({hoverPath[0] === 'R' ? 'Red' : 'Blue'} then {hoverPath[1] === 'R' ? 'Red' : 'Blue'}):
-
-
- {/* First Draw */}
- P({hoverPath[0]})
- ×
- P({hoverPath[1]} | {hoverPath[0]})
- =
-
- {/* Numbers */}
- {fraction(hoverPath[0] === 'R' ? initR : initB, total)}
- ×
- {fraction(
- hoverPath === 'RR' ? r_R : hoverPath === 'RB' ? initB : hoverPath === 'BR' ? initR : b_B,
- hoverPath[0] === 'R' ? r_Total : b_Total
- )}
- =
-
- {/* Result */}
-
- {fraction(
- (hoverPath[0] === 'R' ? initR : initB) * (hoverPath === 'RR' ? r_R : hoverPath === 'RB' ? initB : hoverPath === 'BR' ? initR : b_B),
- total * (hoverPath[0] === 'R' ? r_Total : b_Total)
- )}
-
-
- {!replacement && hoverPath[0] === hoverPath[1] && (
-
- ⚠ Notice: The numerator decreased because we kept the first {hoverPath[0] === 'R' ? 'Red' : 'Blue'} item!
-
- )}
- >
- )}
-
+ setHoverPath("RB")}
+ onMouseLeave={() => setHoverPath(null)}
+ >
+
+ RB: {(pRB * 100).toFixed(1)}%
+
+
+
+
+ setHoverPath("BR")}
+ onMouseLeave={() => setHoverPath(null)}
+ >
+
+ BR: {(pBR * 100).toFixed(1)}%
+
+
+
+
+ setHoverPath("BB")}
+ onMouseLeave={() => setHoverPath(null)}
+ >
+
+ BB: {(pBB * 100).toFixed(1)}%
+
+
+
+
+
+
+ {/* Calculation Panel */}
+
+ {!hoverPath ? (
+
+ Hover over an outcome (e.g., RR) to see the calculation.
+
+ ) : (
+ <>
+
+ Calculation for{" "}
+
+ {hoverPath}
+
+ ({hoverPath[0] === "R" ? "Red" : "Blue"} then{" "}
+ {hoverPath[1] === "R" ? "Red" : "Blue"}):
+
+
+ {/* First Draw */}
+ P({hoverPath[0]})
+ ×
+
+ P({hoverPath[1]} | {hoverPath[0]})
+
+ =
+
+ {/* Numbers */}
+ {fraction(hoverPath[0] === "R" ? initR : initB, total)}
+ ×
+ {fraction(
+ hoverPath === "RR"
+ ? r_R
+ : hoverPath === "RB"
+ ? initB
+ : hoverPath === "BR"
+ ? initR
+ : b_B,
+ hoverPath[0] === "R" ? r_Total : b_Total,
+ )}
+ =
+
+ {/* Result */}
+
+ {fraction(
+ (hoverPath[0] === "R" ? initR : initB) *
+ (hoverPath === "RR"
+ ? r_R
+ : hoverPath === "RB"
+ ? initB
+ : hoverPath === "BR"
+ ? initR
+ : b_B),
+ total * (hoverPath[0] === "R" ? r_Total : b_Total),
+ )}
+
+
+ {!replacement && hoverPath[0] === hoverPath[1] && (
+
+ ⚠ Notice: The numerator decreased because we kept the first{" "}
+ {hoverPath[0] === "R" ? "Red" : "Blue"} item!
+
+ )}
+ >
+ )}
+
);
};
-export default ProbabilityTreeWidget;
\ No newline at end of file
+export default ProbabilityTreeWidget;
diff --git a/src/components/lessons/RadicalSolutionWidget.tsx b/src/components/lessons/RadicalSolutionWidget.tsx
index 8a86de0..7948c75 100644
--- a/src/components/lessons/RadicalSolutionWidget.tsx
+++ b/src/components/lessons/RadicalSolutionWidget.tsx
@@ -1,135 +1,230 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
const RadicalSolutionWidget: React.FC = () => {
// Equation: sqrt(x) = x - k
- const [k, setK] = useState(2);
+ const [k, setK] = useState(2);
// Intersection logic
// x = (x-k)^2 => x = x^2 - 2kx + k^2 => x^2 - (2k+1)x + k^2 = 0
// Roots via quadratic formula
const a = 1;
- const b = -(2*k + 1);
- const c = k*k;
- const disc = b*b - 4*a*c;
-
+ const b = -(2 * k + 1);
+ const c = k * k;
+ const disc = b * b - 4 * a * c;
+
let solutions: number[] = [];
if (disc >= 0) {
- const x1 = (-b + Math.sqrt(disc)) / (2*a);
- const x2 = (-b - Math.sqrt(disc)) / (2*a);
- solutions = [x1, x2].filter(val => val >= 0); // Domain x>=0
+ const x1 = (-b + Math.sqrt(disc)) / (2 * a);
+ const x2 = (-b - Math.sqrt(disc)) / (2 * a);
+ solutions = [x1, x2].filter((val) => val >= 0); // Domain x>=0
}
// Check validity against original equation sqrt(x) = x - k
- const validSolutions = solutions.filter(x => Math.abs(Math.sqrt(x) - (x - k)) < 0.01);
- const extraneousSolutions = solutions.filter(x => Math.abs(Math.sqrt(x) - (x - k)) >= 0.01);
+ const validSolutions = solutions.filter(
+ (x) => Math.abs(Math.sqrt(x) - (x - k)) < 0.01,
+ );
+ const extraneousSolutions = solutions.filter(
+ (x) => Math.abs(Math.sqrt(x) - (x - k)) >= 0.01,
+ );
// Vis
- const width = 300;
+
const height = 300;
const range = 10;
const scale = 25;
- const toPx = (v: number, isY = false) => isY ? height - v * scale - 20 : v * scale + 20;
+ const toPx = (v: number, isY = false) =>
+ isY ? height - v * scale - 20 : v * scale + 20;
const pathSqrt = () => {
- let d = "";
- for(let x=0; x<=range; x+=0.1) {
- d += d ? ` L ${toPx(x)} ${toPx(Math.sqrt(x), true)}` : `M ${toPx(x)} ${toPx(Math.sqrt(x), true)}`;
- }
- return d;
+ let d = "";
+ for (let x = 0; x <= range; x += 0.1) {
+ d += d
+ ? ` L ${toPx(x)} ${toPx(Math.sqrt(x), true)}`
+ : `M ${toPx(x)} ${toPx(Math.sqrt(x), true)}`;
+ }
+ return d;
};
const pathLine = () => {
- // y = x - k
- const x1 = 0; const y1 = -k;
- const x2 = range; const y2 = range - k;
- return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
+ // y = x - k
+ const x1 = 0;
+ const y1 = -k;
+ const x2 = range;
+ const y2 = range - k;
+ return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
};
// Phantom parabola path (x = y^2) - representing the squared equation
// This includes y = -sqrt(x)
const pathPhantom = () => {
- let d = "";
- for(let x=0; x<=range; x+=0.1) {
- d += d ? ` L ${toPx(x)} ${toPx(-Math.sqrt(x), true)}` : `M ${toPx(x)} ${toPx(-Math.sqrt(x), true)}`;
- }
- return d;
+ let d = "";
+ for (let x = 0; x <= range; x += 0.1) {
+ d += d
+ ? ` L ${toPx(x)} ${toPx(-Math.sqrt(x), true)}`
+ : `M ${toPx(x)} ${toPx(-Math.sqrt(x), true)}`;
+ }
+ return d;
};
return (
-
-
-
-
Equation
-
- √x = x - {k}
-
-
-
-
- Shift Line (k) = {k}
- setK(parseFloat(e.target.value))} className="w-full h-2 bg-slate-200 rounded-lg accent-indigo-600 mt-2"/>
-
-
-
-
-
Valid Solutions
-
- {validSolutions.length > 0 ? validSolutions.map(n => `x = ${n.toFixed(2)}`).join(', ') : "None"}
-
-
-
-
Extraneous Solutions
-
- {extraneousSolutions.length > 0 ? extraneousSolutions.map(n => `x = ${n.toFixed(2)}`).join(', ') : "None"}
-
-
-
-
-
- The extraneous solution is a real intersection for the squared equation (the phantom curve), but not for the original radical.
-
+
+
+
+
+ Equation
-
-
-
-
- {/* Grid */}
-
-
-
-
-
-
-
- {/* Axes */}
-
-
-
- {/* Phantom -sqrt(x) */}
-
-
- {/* Real sqrt(x) */}
-
-
- {/* Line x-k */}
-
-
- {/* Points */}
- {validSolutions.map(x => (
-
- ))}
- {extraneousSolutions.map(x => (
-
- ))}
-
-
y = √x
-
y = x - {k}
-
+
+ √x = x - {k}
+
+
+
+
+ Shift Line (k) = {k}
+
+ setK(parseFloat(e.target.value))}
+ className="w-full h-2 bg-slate-200 rounded-lg accent-indigo-600 mt-2"
+ />
+
+
+
+
+
+ Valid Solutions
+
+
+ {validSolutions.length > 0
+ ? validSolutions.map((n) => `x = ${n.toFixed(2)}`).join(", ")
+ : "None"}
+
+
+
+
+ Extraneous Solutions
+
+
+ {extraneousSolutions.length > 0
+ ? extraneousSolutions
+ .map((n) => `x = ${n.toFixed(2)}`)
+ .join(", ")
+ : "None"}
+
+
+
+
+
+ The extraneous {" "}
+ solution is a real intersection for the squared equation
+ (the phantom curve), but not for the original radical.
+
+
+
+
+
+ {/* Grid */}
+
+
+
+
+
+
+
+ {/* Axes */}
+
+
+
+ {/* Phantom -sqrt(x) */}
+
+
+ {/* Real sqrt(x) */}
+
+
+ {/* Line x-k */}
+
+
+ {/* Points */}
+ {validSolutions.map((x) => (
+
+ ))}
+ {extraneousSolutions.map((x) => (
+
+ ))}
+
+
+ y = √x
+
+
+ y = x - {k}
+
+
+
+
);
};
-export default RadicalSolutionWidget;
\ No newline at end of file
+export default RadicalSolutionWidget;
diff --git a/src/components/lessons/SimilarityTestsWidget.tsx b/src/components/lessons/SimilarityTestsWidget.tsx
index 9bb7efe..f3f0be8 100644
--- a/src/components/lessons/SimilarityTestsWidget.tsx
+++ b/src/components/lessons/SimilarityTestsWidget.tsx
@@ -1,32 +1,35 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useRef } from "react";
-type Mode = 'AA' | 'SAS' | 'SSS';
+type Mode = "AA" | "SAS" | "SSS";
const SimilarityTestsWidget: React.FC = () => {
- const [mode, setMode] = useState
('AA');
+ const [mode, setMode] = useState("AA");
const [scale, setScale] = useState(1.5);
// Store Vertex B's position relative to A (x offset, y height)
// A is at (40, 220). SVG Y is down.
- const [vertexB, setVertexB] = useState({ x: 40, y: 100 });
+ const [vertexB, setVertexB] = useState({ x: 40, y: 100 });
const isDragging = useRef(false);
const svgRef = useRef(null);
// Triangle 1 (ABC) - Fixed base AC
const A = { x: 40, y: 220 };
const C = { x: 120, y: 220 }; // Base length = 80
-
+
// Calculate B in SVG coordinates based on state
// vertexB.y is the height (upwards), so we subtract from A.y
const B = { x: A.x + vertexB.x, y: A.y - vertexB.y };
// Calculate lengths and angles for T1
- const dist = (p1: {x:number, y:number}, p2: {x:number, y:number}) => Math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2);
+ const dist = (p1: { x: number; y: number }, p2: { x: number; y: number }) =>
+ Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
const c1 = dist(A, B); // side c (opp C) - Side AB
const a1 = dist(B, C); // side a (opp A) - Side BC
const b1 = dist(A, C); // side b (opp B) - Side AC (Base)
const getAngle = (a: number, b: number, c: number) => {
- return Math.acos((b**2 + c**2 - a**2) / (2 * b * c)) * (180 / Math.PI);
+ return (
+ Math.acos((b ** 2 + c ** 2 - a ** 2) / (2 * b * c)) * (180 / Math.PI)
+ );
};
const angleA = getAngle(a1, b1, c1);
@@ -34,18 +37,18 @@ const SimilarityTestsWidget: React.FC = () => {
// const angleC = getAngle(c1, a1, b1);
// Triangle 2 (DEF) - Scaled version of ABC
- // Start D with enough margin. Max width of T1 is ~100-140.
+ // Start D with enough margin. Max width of T1 is ~100-140.
// Let's place D at x=240.
const D = { x: 240, y: 220 };
-
+
// F is horizontal from D by scaled base length
const F = { x: D.x + b1 * scale, y: D.y };
-
+
// E is scaled vector AB from D
const vecAB = { x: B.x - A.x, y: B.y - A.y };
- const E = {
- x: D.x + vecAB.x * scale,
- y: D.y + vecAB.y * scale
+ const E = {
+ x: D.x + vecAB.x * scale,
+ y: D.y + vecAB.y * scale,
};
// Interaction
@@ -54,17 +57,17 @@ const SimilarityTestsWidget: React.FC = () => {
const rect = svgRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
-
+
// Constraints for B relative to A
// Keep B within reasonable bounds to prevent breaking the layout
// Base is 40 to 120. B.x can range from 0 to 140?
const newX = x - A.x;
const height = A.y - y;
-
+
// Clamp
const clampedX = Math.max(-20, Math.min(100, newX));
const clampedH = Math.max(40, Math.min(180, height));
-
+
setVertexB({ x: clampedX, y: clampedH });
};
@@ -72,18 +75,22 @@ const SimilarityTestsWidget: React.FC = () => {
const sideColor = "#059669"; // Emerald
// Helper: draw filled angle wedge + labelled badge at a vertex
- const angleC = 180 - angleA - angleB;
const renderAngle = (
- vx: number, vy: number,
- p1x: number, p1y: number,
- p2x: number, p2y: number,
+ vx: number,
+ vy: number,
+ p1x: number,
+ p1y: number,
+ p2x: number,
+ p2y: number,
deg: number,
- r = 28
+ r = 28,
) => {
const d1 = Math.atan2(p1y - vy, p1x - vx);
const d2 = Math.atan2(p2y - vy, p2x - vx);
- const sx = vx + r * Math.cos(d1), sy = vy + r * Math.sin(d1);
- const ex = vx + r * Math.cos(d2), ey = vy + r * Math.sin(d2);
+ const sx = vx + r * Math.cos(d1),
+ sy = vy + r * Math.sin(d1);
+ const ex = vx + r * Math.cos(d2),
+ ey = vy + r * Math.sin(d2);
const cross = (p1x - vx) * (p2y - vy) - (p1y - vy) * (p2x - vx);
const sweep = cross > 0 ? 1 : 0;
let diff = d2 - d1;
@@ -91,13 +98,40 @@ const SimilarityTestsWidget: React.FC = () => {
while (diff < -Math.PI) diff += 2 * Math.PI;
const mid = d1 + diff / 2;
const lr = r + 18;
- const lx = vx + lr * Math.cos(mid), ly = vy + lr * Math.sin(mid);
+ const lx = vx + lr * Math.cos(mid),
+ ly = vy + lr * Math.sin(mid);
const txt = `${Math.round(deg)}°`;
return (
-
-
- {txt}
+
+
+
+ {txt}
+
);
};
@@ -106,14 +140,14 @@ const SimilarityTestsWidget: React.FC = () => {
isDragging.current = false}
- onMouseLeave={() => isDragging.current = false}
+ ref={svgRef}
+ width="550"
+ height="280"
+ className="cursor-default select-none"
+ onMouseMove={handleMouseMove}
+ onMouseUp={() => (isDragging.current = false)}
+ onMouseLeave={() => (isDragging.current = false)}
>
-
-
-
-
-
-
+
+
+
+
+
+
- {/* Triangle 1 (ABC) */}
-
+ {/* Triangle 1 (ABC) */}
+
- {/* Vertices T1 */}
-
- A
-
- C
+ {/* Vertices T1 */}
+
+
+ A
+
+
+
+ C
+
- {/* Draggable B */}
- isDragging.current = true} className="cursor-grab active:cursor-grabbing">
- {/* Hit area */}
-
- B
-
+ {/* Draggable B */}
+ (isDragging.current = true)}
+ className="cursor-grab active:cursor-grabbing"
+ >
+ {" "}
+ {/* Hit area */}
+
+
+ B
+
+
- {/* Triangle 2 (DEF) */}
-
+ {/* Triangle 2 (DEF) */}
+
-
- D
-
- F
-
- E
+
+
+ D
+
+
+
+ F
+
+
+
+ E
+
- {/* Visual Overlays based on Mode */}
- {mode === 'AA' && (
- <>
- {/* Angle A and D (base-left) */}
- {renderAngle(A.x, A.y, C.x, C.y, B.x, B.y, angleA)}
- {renderAngle(D.x, D.y, F.x, F.y, E.x, E.y, angleA)}
- {/* Angle B and E (apex) */}
- {renderAngle(B.x, B.y, A.x, A.y, C.x, C.y, angleB)}
- {renderAngle(E.x, E.y, D.x, D.y, F.x, F.y, angleB)}
- >
- )}
+ {/* Visual Overlays based on Mode */}
+ {mode === "AA" && (
+ <>
+ {/* Angle A and D (base-left) */}
+ {renderAngle(A.x, A.y, C.x, C.y, B.x, B.y, angleA)}
+ {renderAngle(D.x, D.y, F.x, F.y, E.x, E.y, angleA)}
+ {/* Angle B and E (apex) */}
+ {renderAngle(B.x, B.y, A.x, A.y, C.x, C.y, angleB)}
+ {renderAngle(E.x, E.y, D.x, D.y, F.x, F.y, angleB)}
+ >
+ )}
- {mode === 'SAS' && (
- <>
- {/* Included Angle A and D */}
- {renderAngle(A.x, A.y, C.x, C.y, B.x, B.y, angleA)}
- {renderAngle(D.x, D.y, F.x, F.y, E.x, E.y, angleA)}
+ {mode === "SAS" && (
+ <>
+ {/* Included Angle A and D */}
+ {renderAngle(A.x, A.y, C.x, C.y, B.x, B.y, angleA)}
+ {renderAngle(D.x, D.y, F.x, F.y, E.x, E.y, angleA)}
- {/* Side labels with background badges */}
- {/* Side AB / DE */}
-
- {Math.round(c1)}
-
- {Math.round(c1 * scale)}
+ {/* Side labels with background badges */}
+ {/* Side AB / DE */}
+
+
+ {Math.round(c1)}
+
+
+
+ {Math.round(c1 * scale)}
+
- {/* Side AC / DF */}
-
- {Math.round(b1)}
-
- {Math.round(b1 * scale)}
- >
- )}
+ {/* Side AC / DF */}
+
+
+ {Math.round(b1)}
+
+
+
+ {Math.round(b1 * scale)}
+
+ >
+ )}
- {mode === 'SSS' && (
- <>
- {/* Side AB / DE */}
-
- {Math.round(c1)}
-
- {Math.round(c1 * scale)}
+ {mode === "SSS" && (
+ <>
+ {/* Side AB / DE */}
+
+
+ {Math.round(c1)}
+
+
+
+ {Math.round(c1 * scale)}
+
- {/* Side AC / DF */}
-
- {Math.round(b1)}
-
- {Math.round(b1 * scale)}
+ {/* Side AC / DF */}
+
+
+ {Math.round(b1)}
+
+
+
+ {Math.round(b1 * scale)}
+
- {/* Side BC / EF */}
-
- {Math.round(a1)}
-
- {Math.round(a1 * scale)}
- >
- )}
+ {/* Side BC / EF */}
+
+
+ {Math.round(a1)}
+
+
+
+ {Math.round(a1 * scale)}
+
+ >
+ )}
-
-
- {mode === 'AA' && "Angle-Angle (AA) Similarity"}
- {mode === 'SAS' && "Side-Angle-Side (SAS) Similarity"}
- {mode === 'SSS' && "Side-Side-Side (SSS) Similarity"}
-
-
- {mode === 'AA' && (
- <>
-
If two angles of one triangle are equal to two angles of another triangle, then the triangles are similar.
-
-
-
First Angle
-
∠A = ∠D = {Math.round(angleA)}°
-
-
-
Second Angle
-
∠B = ∠E = {Math.round(angleB)}°
-
-
- >
- )}
- {mode === 'SAS' && (
- <>
-
If two sides are proportional and the included angles are equal, the triangles are similar.
-
-
-
Side Ratio (c)
-
DE / AB = {(c1*scale).toFixed(0)} / {c1.toFixed(0)} = {scale.toFixed(1)}
-
-
-
Side Ratio (b)
-
DF / AC = {(b1*scale).toFixed(0)} / {b1.toFixed(0)} = {scale.toFixed(1)}
-
-
-
Included Angle: ∠A = ∠D = {Math.round(angleA)}°
- >
- )}
- {mode === 'SSS' && (
- <>
-
If the corresponding sides of two triangles are proportional, then the triangles are similar.
-
Scale Factor k = {scale.toFixed(1)}
-
-
- DE/AB = {scale.toFixed(1)}
-
-
- EF/BC = {scale.toFixed(1)}
-
-
- DF/AC = {scale.toFixed(1)}
-
-
- >
- )}
-
-
- Drag vertex B on the first triangle to explore different shapes!
-
+
+
+ {mode === "AA" && "Angle-Angle (AA) Similarity"}
+ {mode === "SAS" && "Side-Angle-Side (SAS) Similarity"}
+ {mode === "SSS" && "Side-Side-Side (SSS) Similarity"}
+
+
+ {mode === "AA" && (
+ <>
+
+ If two angles of one triangle are equal to two angles of another
+ triangle, then the triangles are similar.
+
+
+
+
+ First Angle
+
+
+ ∠A = ∠D = {Math.round(angleA)}°
+
+
+
+
+ Second Angle
+
+
+ ∠B = ∠E = {Math.round(angleB)}°
+
+
+
+ >
+ )}
+ {mode === "SAS" && (
+ <>
+
+ If two sides are proportional and the included angles are equal,
+ the triangles are similar.
+
+
+
+
+ Side Ratio (c)
+
+
+ DE / AB = {(c1 * scale).toFixed(0)} / {c1.toFixed(0)} ={" "}
+ {scale.toFixed(1)}
+
+
+
+
+ Side Ratio (b)
+
+
+ DF / AC = {(b1 * scale).toFixed(0)} / {b1.toFixed(0)} ={" "}
+ {scale.toFixed(1)}
+
+
+
+
+ Included Angle: ∠A = ∠D = {Math.round(angleA)}°
+
+ >
+ )}
+ {mode === "SSS" && (
+ <>
+
+ If the corresponding sides of two triangles are proportional,
+ then the triangles are similar.
+
+
+ Scale Factor k = {scale.toFixed(1)}
+
+
+
+ DE/AB = {scale.toFixed(1)}
+
+
+ EF/BC = {scale.toFixed(1)}
+
+
+ DF/AC = {scale.toFixed(1)}
+
+
+ >
+ )}
+
+
+ Drag vertex B on the first triangle to explore
+ different shapes!
+
);
};
-export default SimilarityTestsWidget;
\ No newline at end of file
+export default SimilarityTestsWidget;
diff --git a/src/components/lessons/SimilarityWidget.tsx b/src/components/lessons/SimilarityWidget.tsx
index 1d92921..833590d 100644
--- a/src/components/lessons/SimilarityWidget.tsx
+++ b/src/components/lessons/SimilarityWidget.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useRef } from "react";
const SimilarityWidget: React.FC = () => {
const [ratio, setRatio] = useState(0.5); // Position of D along AB (0 to 1)
@@ -13,22 +13,22 @@ const SimilarityWidget: React.FC = () => {
// Calculate D and E based on ratio
const D = {
x: A.x + (B.x - A.x) * ratio,
- y: A.y + (B.y - A.y) * ratio
+ y: A.y + (B.y - A.y) * ratio,
};
-
+
const E = {
x: A.x + (C.x - A.x) * ratio,
- y: A.y + (C.y - A.y) * ratio
+ y: A.y + (C.y - A.y) * ratio,
};
const handleInteraction = (clientY: number) => {
if (!svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
const y = clientY - rect.top;
-
+
// Clamp y between A.y and B.y
const clampedY = Math.max(A.y, Math.min(B.y, y));
-
+
// Calculate new ratio
const newRatio = (clampedY - A.y) / (B.y - A.y);
setRatio(Math.max(0.1, Math.min(0.9, newRatio))); // clamp to avoid degenerate
@@ -47,70 +47,152 @@ const SimilarityWidget: React.FC = () => {
return (
-
isDragging.current = false}
- onMouseLeave={() => isDragging.current = false}
+ onMouseUp={() => (isDragging.current = false)}
+ onMouseLeave={() => (isDragging.current = false)}
>
{/* Main Triangle */}
-
-
+
+
{/* Filled Top Triangle (Similar) */}
-
+
{/* Parallel Line DE */}
-
-
+
+
{/* Labels */}
- A
- B
- C
- D
- E
+
+ A
+
+
+ B
+
+
+ C
+
+
+ D
+
+
+ E
+
{/* Drag Handle */}
-
-
-
+
+
-
Triangle Proportionality
-
Drag the red line. Because DE || BC, the small triangle is similar to the large triangle.
-
-
-
-
Scale Factor
-
{ratio.toFixed(2)}
-
+
+ Triangle Proportionality
+
+
+ Drag the red line. Because DE || BC, the small triangle is similar to
+ the large triangle.
+
-
-
Corresponding Sides Ratio:
-
-
AD / AB
-
=
-
AE / AC
-
=
-
{ratio.toFixed(2)}
-
+
+
+
+ Scale Factor
+
+
+ {ratio.toFixed(2)}
+
+
+
+
+
+ Corresponding Sides Ratio:
+
+
+
AD / AB
+
=
+
AE / AC
+
=
+
{ratio.toFixed(2)}
-
-
-
Area Ratio (k²):
-
-
Area(ADE)
-
/
-
Area(ABC)
-
=
-
{(ratio * ratio).toFixed(2)}
-
+
+
+
+
+ Area Ratio (k²):
+
+
+
Area(ADE)
+
/
+
Area(ABC)
+
=
+
{(ratio * ratio).toFixed(2)}
-
+
+
);
diff --git a/src/components/lessons/UserDashboard.tsx b/src/components/lessons/UserDashboard.tsx
deleted file mode 100644
index 27b4a0c..0000000
--- a/src/components/lessons/UserDashboard.tsx
+++ /dev/null
@@ -1,378 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import {
- ArrowLeft, User, Shield, Clock, BookOpen, Calculator, Award,
- TrendingUp, CheckCircle2, Circle, Lock, Eye, EyeOff, AlertCircle,
- Check, Sparkles,
-} from 'lucide-react';
-import { useAuth, UserRecord } from './auth/AuthContext';
-import { useProgress } from './progress/ProgressContext';
-import { useGoldCoins } from './practice/GoldCoinContext';
-import { LESSONS, EBRW_LESSONS } from '../constants';
-import Mascot from './Mascot';
-
-// Animated count-up
-function useCountUp(target: number, duration = 900) {
- const [count, setCount] = useState(0);
- const started = useRef(false);
- useEffect(() => {
- if (started.current) return;
- started.current = true;
- const startTime = performance.now();
- const animate = (now: number) => {
- const progress = Math.min((now - startTime) / duration, 1);
- const eased = 1 - Math.pow(1 - progress, 2.5);
- setCount(Math.round(eased * target));
- if (progress < 1) requestAnimationFrame(animate);
- };
- requestAnimationFrame(animate);
- }, [target, duration]);
- return count;
-}
-
-interface UserDashboardProps {
- onExit: () => void;
-}
-
-export default function UserDashboard({ onExit }: UserDashboardProps) {
- const { username, role, getUserRecord, changePassword, updateDisplayName } = useAuth();
- const { getSubjectStats, getLessonStatus } = useProgress();
- const { totalCoins, state: coinState } = useGoldCoins();
-
- const user = getUserRecord(username || '');
- const mathStats = getSubjectStats('math');
- const ebrwStats = getSubjectStats('ebrw');
-
- // Account settings
- const [currentPassword, setCurrentPassword] = useState('');
- const [newPassword, setNewPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [showCurrentPw, setShowCurrentPw] = useState(false);
- const [showNewPw, setShowNewPw] = useState(false);
- const [pwMsg, setPwMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
- const [pwLoading, setPwLoading] = useState(false);
-
- const [editName, setEditName] = useState(false);
- const [nameInput, setNameInput] = useState(user?.displayName || '');
- const [nameSaved, setNameSaved] = useState(false);
-
- const animCoins = useCountUp(totalCoins, 1200);
-
- // Count completed topics across all practice
- const topicsAttempted = Object.keys(coinState.topicProgress).length;
-
- // Calculate total accuracy
- let totalAttempted = 0;
- let totalCorrect = 0;
- Object.values(coinState.topicProgress).forEach((tp: any) => {
- (['easy', 'medium', 'hard'] as const).forEach(d => {
- totalAttempted += tp[d]?.attempted || 0;
- totalCorrect += tp[d]?.correct || 0;
- });
- });
- const accuracy = totalAttempted > 0 ? Math.round((totalCorrect / totalAttempted) * 100) : 0;
-
- const handleChangePassword = async (e: React.FormEvent) => {
- e.preventDefault();
- setPwMsg(null);
- if (newPassword !== confirmPassword) {
- setPwMsg({ type: 'error', text: 'New passwords do not match.' });
- return;
- }
- setPwLoading(true);
- const result = await changePassword(username || '', currentPassword, newPassword);
- setPwLoading(false);
- if (result.success) {
- setPwMsg({ type: 'success', text: 'Password changed successfully!' });
- setCurrentPassword('');
- setNewPassword('');
- setConfirmPassword('');
- } else {
- setPwMsg({ type: 'error', text: result.error || 'Failed to change password.' });
- }
- };
-
- const handleSaveName = () => {
- if (username && nameInput.trim()) {
- updateDisplayName(username, nameInput.trim());
- setEditName(false);
- setNameSaved(true);
- setTimeout(() => setNameSaved(false), 2000);
- }
- };
-
- // Progress ring
- function ProgressRing({ percent, size = 72, stroke = 6, color }: { percent: number; size?: number; stroke?: number; color: string }) {
- const r = (size - stroke) / 2;
- const circ = 2 * Math.PI * r;
- const offset = circ - (percent / 100) * circ;
- return (
-
-
-
-
- {percent}%
-
-
- );
- }
-
- function StatusIcon({ status }: { status: string }) {
- if (status === 'completed') return
;
- if (status === 'in_progress') return
;
- return
;
- }
-
- return (
-
-
- {/* Header */}
-
-
-
- Back to Home
-
-
My Dashboard
-
-
-
-
-
-
- {/* ── Welcome Hero ── */}
-
-
-
-
-
-
-
-
-
-
-
- {editName ? (
-
- setNameInput(e.target.value)}
- className="text-xl font-bold text-slate-900 bg-white border border-slate-200 rounded-lg px-2 py-0.5 focus:outline-none focus:ring-2 focus:ring-cyan-400 w-48"
- autoFocus onKeyDown={e => e.key === 'Enter' && handleSaveName()} />
- Save
- setEditName(false)} className="text-xs text-slate-400 hover:text-slate-600">Cancel
-
- ) : (
- <>
-
{user?.displayName || username}
-
{ setNameInput(user?.displayName || ''); setEditName(true); }}
- className="text-xs text-cyan-500 hover:text-cyan-700 font-medium">edit
- {nameSaved &&
Saved}
- >
- )}
-
-
-
- {role === 'admin' && }
- {role}
-
- @{username}
-
-
-
- {user?.lastLoginAt && (
-
-
- Last login: {new Date(user.lastLoginAt).toLocaleString()}
- {user.lastLoginIp && user.lastLoginIp !== 'unknown' && from {user.lastLoginIp} }
-
- )}
-
-
-
- {/* ── Stats Overview ── */}
-
-
-
{mathStats.completed + ebrwStats.completed}
-
Lessons Done
-
-
-
- {animCoins}
-
-
Gold Coins
-
-
-
{accuracy}%
-
Accuracy
-
-
-
{topicsAttempted}
-
Topics Practiced
-
-
-
- {/* ── Lesson Progress ── */}
-
-
- {/* Math */}
-
-
-
-
-
-
-
-
Mathematics
-
{mathStats.completed}/{mathStats.total} lessons completed
-
-
-
-
-
-
- {LESSONS.map(l => (
-
-
- {l.title}
-
- ))}
-
-
-
- {/* EBRW */}
-
-
-
-
-
-
-
-
Reading & Writing
-
{ebrwStats.completed}/{ebrwStats.total} lessons completed
-
-
-
-
-
-
- {EBRW_LESSONS.map(l => (
-
-
- {l.title}
-
- ))}
-
-
-
-
- {/* ── Practice Performance ── */}
-
-
-
-
-
-
-
Practice Performance
-
{totalAttempted} questions attempted across {topicsAttempted} topics
-
-
-
- {topicsAttempted === 0 ? (
-
-
- No practice sessions yet. Start practicing to see your performance!
-
- ) : (
-
- {Object.entries(coinState.topicProgress).map(([topicId, tp]: [string, any]) => {
- const easy = tp.easy || { attempted: 0, correct: 0 };
- const medium = tp.medium || { attempted: 0, correct: 0 };
- const hard = tp.hard || { attempted: 0, correct: 0 };
- const total = easy.attempted + medium.attempted + hard.attempted;
- const correct = easy.correct + medium.correct + hard.correct;
- const acc = total > 0 ? Math.round((correct / total) * 100) : 0;
- return (
-
-
{topicId}
-
- {correct}/{total} correct
- = 70 ? 'text-emerald-500' : acc >= 40 ? 'text-amber-500' : 'text-rose-500'}`}>{acc}%
-
-
-
= 70 ? 'bg-emerald-400' : acc >= 40 ? 'bg-amber-400' : 'bg-rose-400'}`} style={{ width: `${acc}%` }} />
-
-
- E: {easy.correct}/{easy.attempted}
- M: {medium.correct}/{medium.attempted}
- H: {hard.correct}/{hard.attempted}
-
-
- );
- })}
-
- )}
-
-
- {/* ── Account Settings ── */}
-
-
-
-
-
-
-
Change Password
-
Update your account password
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index fd3a406..9bc0e36 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -1,8 +1,7 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "../../lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
@@ -22,8 +21,8 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
- }
-)
+ },
+);
function Badge({
className,
@@ -32,7 +31,7 @@ function Badge({
...props
}: React.ComponentProps<"span"> &
VariantProps
& { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span"
+ const Comp = asChild ? Slot : "span";
return (
- )
+ );
}
-export { Badge, badgeVariants }
+export { Badge, badgeVariants };
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 46b3a48..b4d6e96 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -2,7 +2,7 @@ import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils";
+import { cn } from "../../lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
index 681ad98..1be309a 100644
--- a/src/components/ui/card.tsx
+++ b/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("leading-none font-semibold", className)}
{...props}
/>
- )
+ );
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
- )
+ );
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn("px-6", className)}
{...props}
/>
- )
+ );
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
- )
+ );
}
export {
@@ -89,4 +89,4 @@ export {
CardAction,
CardDescription,
CardContent,
-}
+};
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
index 71cff4c..b7ee2ae 100644
--- a/src/components/ui/carousel.tsx
+++ b/src/components/ui/carousel.tsx
@@ -1,43 +1,43 @@
-import * as React from "react"
+import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
-} from "embla-carousel-react"
-import { ArrowLeft, ArrowRight } from "lucide-react"
+} from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
+import { cn } from "../../lib/utils";
+import { Button } from "./button";
-type CarouselApi = UseEmblaCarouselType[1]
-type UseCarouselParameters = Parameters
-type CarouselOptions = UseCarouselParameters[0]
-type CarouselPlugin = UseCarouselParameters[1]
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
- opts?: CarouselOptions
- plugins?: CarouselPlugin
- orientation?: "horizontal" | "vertical"
- setApi?: (api: CarouselApi) => void
-}
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
type CarouselContextProps = {
- carouselRef: ReturnType[0]
- api: ReturnType[1]
- scrollPrev: () => void
- scrollNext: () => void
- canScrollPrev: boolean
- canScrollNext: boolean
-} & CarouselProps
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
-const CarouselContext = React.createContext(null)
+const CarouselContext = React.createContext(null);
function useCarousel() {
- const context = React.useContext(CarouselContext)
+ const context = React.useContext(CarouselContext);
if (!context) {
- throw new Error("useCarousel must be used within a ")
+ throw new Error("useCarousel must be used within a ");
}
- return context
+ return context;
}
function Carousel({
@@ -54,53 +54,53 @@ function Carousel({
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
- plugins
- )
- const [canScrollPrev, setCanScrollPrev] = React.useState(false)
- const [canScrollNext, setCanScrollNext] = React.useState(false)
+ plugins,
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
- if (!api) return
- setCanScrollPrev(api.canScrollPrev())
- setCanScrollNext(api.canScrollNext())
- }, [])
+ if (!api) return;
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
const scrollPrev = React.useCallback(() => {
- api?.scrollPrev()
- }, [api])
+ api?.scrollPrev();
+ }, [api]);
const scrollNext = React.useCallback(() => {
- api?.scrollNext()
- }, [api])
+ api?.scrollNext();
+ }, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "ArrowLeft") {
- event.preventDefault()
- scrollPrev()
+ event.preventDefault();
+ scrollPrev();
} else if (event.key === "ArrowRight") {
- event.preventDefault()
- scrollNext()
+ event.preventDefault();
+ scrollNext();
}
},
- [scrollPrev, scrollNext]
- )
+ [scrollPrev, scrollNext],
+ );
React.useEffect(() => {
- if (!api || !setApi) return
- setApi(api)
- }, [api, setApi])
+ if (!api || !setApi) return;
+ setApi(api);
+ }, [api, setApi]);
React.useEffect(() => {
- if (!api) return
- onSelect(api)
- api.on("reInit", onSelect)
- api.on("select", onSelect)
+ if (!api) return;
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
return () => {
- api?.off("select", onSelect)
- }
- }, [api, onSelect])
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
return (
- )
+ );
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
- const { carouselRef, orientation } = useCarousel()
+ const { carouselRef, orientation } = useCarousel();
return (
) {
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
- const { orientation } = useCarousel()
+ const { orientation } = useCarousel();
return (
) {
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function CarouselPrevious({
@@ -175,7 +175,7 @@ function CarouselPrevious({
size = "icon",
...props
}: React.ComponentProps
) {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
Previous slide
- )
+ );
}
function CarouselNext({
@@ -205,7 +205,7 @@ function CarouselNext({
size = "icon",
...props
}: React.ComponentProps) {
- const { orientation, scrollNext, canScrollNext } = useCarousel()
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
Next slide
- )
+ );
}
export {
@@ -236,4 +236,4 @@ export {
CarouselItem,
CarouselPrevious,
CarouselNext,
-}
+};
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index daf6bf4..0f8f709 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -1,32 +1,32 @@
-import * as React from "react"
-import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { XIcon } from "lucide-react"
+import * as React from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
+import { cn } from "../../lib/utils";
+import { Button } from "./button";
function Dialog({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogPortal({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogClose({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogOverlay({
@@ -38,11 +38,11 @@ function DialogOverlay({
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function DialogContent({
@@ -51,7 +51,7 @@ function DialogContent({
showCloseButton = true,
...props
}: React.ComponentProps & {
- showCloseButton?: boolean
+ showCloseButton?: boolean;
}) {
return (
@@ -60,7 +60,7 @@ function DialogContent({
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
- className
+ className,
)}
{...props}
>
@@ -76,7 +76,7 @@ function DialogContent({
)}
- )
+ );
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -86,7 +86,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
- )
+ );
}
function DialogFooter({
@@ -95,14 +95,14 @@ function DialogFooter({
children,
...props
}: React.ComponentProps<"div"> & {
- showCloseButton?: boolean
+ showCloseButton?: boolean;
}) {
return (
@@ -113,7 +113,7 @@ function DialogFooter({
)}
- )
+ );
}
function DialogTitle({
@@ -126,7 +126,7 @@ function DialogTitle({
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
- )
+ );
}
function DialogDescription({
@@ -139,7 +139,7 @@ function DialogDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
- )
+ );
}
export {
@@ -153,4 +153,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
-}
+};
diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx
index 869955f..6e99bdd 100644
--- a/src/components/ui/drawer.tsx
+++ b/src/components/ui/drawer.tsx
@@ -1,30 +1,30 @@
-import * as React from "react"
-import { Drawer as DrawerPrimitive } from "vaul"
+import * as React from "react";
+import { Drawer as DrawerPrimitive } from "vaul";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function Drawer({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DrawerTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DrawerPortal({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DrawerClose({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DrawerOverlay({
@@ -36,11 +36,11 @@ function DrawerOverlay({
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function DrawerContent({
@@ -59,7 +59,7 @@ function DrawerContent({
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
- className
+ className,
)}
{...props}
>
@@ -67,7 +67,7 @@ function DrawerContent({
{children}
- )
+ );
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -76,11 +76,11 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -90,7 +90,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
- )
+ );
}
function DrawerTitle({
@@ -103,7 +103,7 @@ function DrawerTitle({
className={cn("text-foreground font-semibold", className)}
{...props}
/>
- )
+ );
}
function DrawerDescription({
@@ -116,7 +116,7 @@ function DrawerDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
- )
+ );
}
export {
@@ -130,4 +130,4 @@ export {
DrawerFooter,
DrawerTitle,
DrawerDescription,
-}
+};
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
index eaed9ba..eef120f 100644
--- a/src/components/ui/dropdown-menu.tsx
+++ b/src/components/ui/dropdown-menu.tsx
@@ -1,13 +1,13 @@
-import * as React from "react"
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
-import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DropdownMenuPortal({
@@ -15,7 +15,7 @@ function DropdownMenuPortal({
}: React.ComponentProps) {
return (
- )
+ );
}
function DropdownMenuTrigger({
@@ -26,7 +26,7 @@ function DropdownMenuTrigger({
data-slot="dropdown-menu-trigger"
{...props}
/>
- )
+ );
}
function DropdownMenuContent({
@@ -41,12 +41,12 @@ function DropdownMenuContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function DropdownMenuGroup({
@@ -54,7 +54,7 @@ function DropdownMenuGroup({
}: React.ComponentProps) {
return (
- )
+ );
}
function DropdownMenuItem({
@@ -63,8 +63,8 @@ function DropdownMenuItem({
variant = "default",
...props
}: React.ComponentProps & {
- inset?: boolean
- variant?: "default" | "destructive"
+ inset?: boolean;
+ variant?: "default" | "destructive";
}) {
return (
- )
+ );
}
function DropdownMenuCheckboxItem({
@@ -91,7 +91,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- className
+ className,
)}
checked={checked}
{...props}
@@ -103,7 +103,7 @@ function DropdownMenuCheckboxItem({
{children}
- )
+ );
}
function DropdownMenuRadioGroup({
@@ -114,7 +114,7 @@ function DropdownMenuRadioGroup({
data-slot="dropdown-menu-radio-group"
{...props}
/>
- )
+ );
}
function DropdownMenuRadioItem({
@@ -127,7 +127,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- className
+ className,
)}
{...props}
>
@@ -138,7 +138,7 @@ function DropdownMenuRadioItem({
{children}
- )
+ );
}
function DropdownMenuLabel({
@@ -146,7 +146,7 @@ function DropdownMenuLabel({
inset,
...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
return (
- )
+ );
}
function DropdownMenuSeparator({
@@ -171,7 +171,7 @@ function DropdownMenuSeparator({
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
- )
+ );
}
function DropdownMenuShortcut({
@@ -183,17 +183,17 @@ function DropdownMenuShortcut({
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function DropdownMenuSub({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DropdownMenuSubTrigger({
@@ -202,7 +202,7 @@ function DropdownMenuSubTrigger({
children,
...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
return (
{children}
- )
+ );
}
function DropdownMenuSubContent({
@@ -229,11 +229,11 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
export {
@@ -252,4 +252,4 @@ export {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
-}
+};
diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx
index db0dc12..ebd1a7e 100644
--- a/src/components/ui/field.tsx
+++ b/src/components/ui/field.tsx
@@ -1,9 +1,9 @@
-import { useMemo } from "react"
-import { cva, type VariantProps } from "class-variance-authority"
+import { useMemo } from "react";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
-import { Separator } from "@/components/ui/separator"
+import { cn } from "../../lib/utils";
+import { Label } from "./label";
+import { Separator } from "./separator";
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
@@ -12,11 +12,11 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function FieldLegend({
@@ -32,11 +32,11 @@ function FieldLegend({
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -44,12 +44,12 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
[data-slot=field-group]]:gap-4",
- className
+ "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
+ className,
)}
{...props}
/>
- )
+ );
}
const fieldVariants = cva(
@@ -73,8 +73,8 @@ const fieldVariants = cva(
defaultVariants: {
orientation: "vertical",
},
- }
-)
+ },
+);
function Field({
className,
@@ -89,7 +89,7 @@ function Field({
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
- )
+ );
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -98,11 +98,11 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function FieldLabel({
@@ -114,13 +114,13 @@ function FieldLabel({
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
- "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -129,11 +129,11 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
@@ -141,14 +141,14 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function FieldSeparator({
@@ -156,7 +156,7 @@ function FieldSeparator({
className,
...props
}: React.ComponentProps<"div"> & {
- children?: React.ReactNode
+ children?: React.ReactNode;
}) {
return (
@@ -178,7 +178,7 @@ function FieldSeparator({
)}
- )
+ );
}
function FieldError({
@@ -187,37 +187,37 @@ function FieldError({
errors,
...props
}: React.ComponentProps<"div"> & {
- errors?: Array<{ message?: string } | undefined>
+ errors?: Array<{ message?: string } | undefined>;
}) {
const content = useMemo(() => {
if (children) {
- return children
+ return children;
}
if (!errors?.length) {
- return null
+ return null;
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
- ]
+ ];
if (uniqueErrors?.length == 1) {
- return uniqueErrors[0]?.message
+ return uniqueErrors[0]?.message;
}
return (
{uniqueErrors.map(
(error, index) =>
- error?.message && {error.message}
+ error?.message && {error.message} ,
)}
- )
- }, [children, errors])
+ );
+ }, [children, errors]);
if (!content) {
- return null
+ return null;
}
return (
@@ -229,7 +229,7 @@ function FieldError({
>
{content}
- )
+ );
}
export {
@@ -243,4 +243,4 @@ export {
FieldSet,
FieldContent,
FieldTitle,
-}
+};
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 8916905..a50fd41 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -1,6 +1,5 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
+import * as React from "react";
+import { cn } from "../../lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
@@ -11,11 +10,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Input }
+export { Input };
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
index f752f82..a9610ff 100644
--- a/src/components/ui/label.tsx
+++ b/src/components/ui/label.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import { Label as LabelPrimitive } from "radix-ui"
+import * as React from "react";
+import { Label as LabelPrimitive } from "radix-ui";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function Label({
className,
@@ -12,11 +12,11 @@ function Label({
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Label }
+export { Label };
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 4c24b2a..7695eec 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -1,9 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import { Separator as SeparatorPrimitive } from "radix-ui"
+import * as React from "react";
+import { Separator as SeparatorPrimitive } from "radix-ui";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function Separator({
className,
@@ -18,11 +18,11 @@ function Separator({
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Separator }
+export { Separator };
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
index 5963090..f089706 100644
--- a/src/components/ui/sheet.tsx
+++ b/src/components/ui/sheet.tsx
@@ -1,31 +1,31 @@
-"use client"
+"use client";
-import * as React from "react"
-import { XIcon } from "lucide-react"
-import { Dialog as SheetPrimitive } from "radix-ui"
+import * as React from "react";
+import { XIcon } from "lucide-react";
+import { Dialog as SheetPrimitive } from "radix-ui";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function Sheet({ ...props }: React.ComponentProps) {
- return
+ return ;
}
function SheetTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetClose({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetPortal({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetOverlay({
@@ -37,11 +37,11 @@ function SheetOverlay({
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SheetContent({
@@ -51,8 +51,8 @@ function SheetContent({
showCloseButton = true,
...props
}: React.ComponentProps & {
- side?: "top" | "right" | "bottom" | "left"
- showCloseButton?: boolean
+ side?: "top" | "right" | "bottom" | "left";
+ showCloseButton?: boolean;
}) {
return (
@@ -69,7 +69,7 @@ function SheetContent({
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
- className
+ className,
)}
{...props}
>
@@ -82,7 +82,7 @@ function SheetContent({
)}
- )
+ );
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -92,7 +92,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
- )
+ );
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -102,7 +102,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
- )
+ );
}
function SheetTitle({
@@ -115,7 +115,7 @@ function SheetTitle({
className={cn("text-foreground font-semibold", className)}
{...props}
/>
- )
+ );
}
function SheetDescription({
@@ -128,7 +128,7 @@ function SheetDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
- )
+ );
}
export {
@@ -140,4 +140,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
-}
+};
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 68ed47a..db249b0 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -1,56 +1,56 @@
-"use client"
+"use client";
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { PanelLeftIcon } from "lucide-react"
-import { Slot } from "radix-ui"
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeftIcon } from "lucide-react";
+import { Slot } from "radix-ui";
-import { useIsMobile } from "@/hooks/use-mobile"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Separator } from "@/components/ui/separator"
+import { useIsMobile } from "../../hooks/use-mobile";
+import { cn } from "../../lib/utils";
+import { Button } from "./button";
+import { Input } from "./input";
+import { Separator } from "./separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
-} from "@/components/ui/sheet"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "./sheet";
+import { Skeleton } from "./skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
-} from "@/components/ui/tooltip"
+} from "./tooltip";
-const SIDEBAR_COOKIE_NAME = "sidebar_state"
-const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
-const SIDEBAR_WIDTH = "16rem"
-const SIDEBAR_WIDTH_MOBILE = "18rem"
-const SIDEBAR_WIDTH_ICON = "3rem"
-const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+const SIDEBAR_COOKIE_NAME = "sidebar_state";
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH_MOBILE = "18rem";
+const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
- state: "expanded" | "collapsed"
- open: boolean
- setOpen: (open: boolean) => void
- openMobile: boolean
- setOpenMobile: (open: boolean) => void
- isMobile: boolean
- toggleSidebar: () => void
-}
+ state: "expanded" | "collapsed";
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
-const SidebarContext = React.createContext(null)
+const SidebarContext = React.createContext(null);
function useSidebar() {
- const context = React.useContext(SidebarContext)
+ const context = React.useContext(SidebarContext);
if (!context) {
- throw new Error("useSidebar must be used within a SidebarProvider.")
+ throw new Error("useSidebar must be used within a SidebarProvider.");
}
- return context
+ return context;
}
function SidebarProvider({
@@ -62,36 +62,36 @@ function SidebarProvider({
children,
...props
}: React.ComponentProps<"div"> & {
- defaultOpen?: boolean
- open?: boolean
- onOpenChange?: (open: boolean) => void
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
}) {
- const isMobile = useIsMobile()
- const [openMobile, setOpenMobile] = React.useState(false)
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
- const [_open, _setOpen] = React.useState(defaultOpen)
- const open = openProp ?? _open
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
- const openState = typeof value === "function" ? value(open) : value
+ const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
- setOpenProp(openState)
+ setOpenProp(openState);
} else {
- _setOpen(openState)
+ _setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
- document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
- [setOpenProp, open]
- )
+ [setOpenProp, open],
+ );
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
- return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
- }, [isMobile, setOpen, setOpenMobile])
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
@@ -100,18 +100,18 @@ function SidebarProvider({
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
- event.preventDefault()
- toggleSidebar()
+ event.preventDefault();
+ toggleSidebar();
}
- }
+ };
- window.addEventListener("keydown", handleKeyDown)
- return () => window.removeEventListener("keydown", handleKeyDown)
- }, [toggleSidebar])
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
- const state = open ? "expanded" : "collapsed"
+ const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo(
() => ({
@@ -123,8 +123,8 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
- [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
- )
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ );
return (
@@ -140,7 +140,7 @@ function SidebarProvider({
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
- className
+ className,
)}
{...props}
>
@@ -148,7 +148,7 @@ function SidebarProvider({
- )
+ );
}
function Sidebar({
@@ -159,11 +159,11 @@ function Sidebar({
children,
...props
}: React.ComponentProps<"div"> & {
- side?: "left" | "right"
- variant?: "sidebar" | "floating" | "inset"
- collapsible?: "offcanvas" | "icon" | "none"
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
}) {
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
@@ -171,13 +171,13 @@ function Sidebar({
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
- className
+ className,
)}
{...props}
>
{children}
- )
+ );
}
if (isMobile) {
@@ -202,7 +202,7 @@ function Sidebar({
{children}
- )
+ );
}
return (
@@ -223,7 +223,7 @@ function Sidebar({
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
- : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
+ : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
@@ -250,14 +250,14 @@ function Sidebar({
// so keep this container visually transparent.
variant === "floating"
? "bg-transparent"
- : "bg-sidebar group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
+ : "bg-sidebar group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm",
)}
>
{children}
- )
+ );
}
function SidebarTrigger({
@@ -265,7 +265,7 @@ function SidebarTrigger({
onClick,
...props
}: React.ComponentProps) {
- const { toggleSidebar } = useSidebar()
+ const { toggleSidebar } = useSidebar();
return (
{
- onClick?.(event)
- toggleSidebar()
+ onClick?.(event);
+ toggleSidebar();
}}
{...props}
>
Toggle Sidebar
- )
+ );
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
- const { toggleSidebar } = useSidebar()
+ const { toggleSidebar } = useSidebar();
return (
) {
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
- "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
+ "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-0.5 sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
@@ -318,11 +318,11 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarInput({
@@ -336,7 +336,7 @@ function SidebarInput({
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
- )
+ );
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -347,7 +347,7 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
- )
+ );
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -358,7 +358,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
- )
+ );
}
function SidebarSeparator({
@@ -372,7 +372,7 @@ function SidebarSeparator({
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
- )
+ );
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -382,11 +382,11 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -397,7 +397,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
- )
+ );
}
function SidebarGroupLabel({
@@ -405,7 +405,7 @@ function SidebarGroupLabel({
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
- const Comp = asChild ? Slot.Root : "div"
+ const Comp = asChild ? Slot.Root : "div";
return (
svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarGroupAction({
@@ -426,7 +426,7 @@ function SidebarGroupAction({
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
- const Comp = asChild ? Slot.Root : "button"
+ const Comp = asChild ? Slot.Root : "button";
return (
- )
+ );
}
function SidebarGroupContent({
@@ -455,7 +455,7 @@ function SidebarGroupContent({
className={cn("w-full text-sm", className)}
{...props}
/>
- )
+ );
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
@@ -466,7 +466,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
- )
+ );
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
@@ -477,7 +477,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
className={cn("group/menu-item relative", className)}
{...props}
/>
- )
+ );
}
const sidebarMenuButtonVariants = cva(
@@ -499,8 +499,8 @@ const sidebarMenuButtonVariants = cva(
variant: "default",
size: "default",
},
- }
-)
+ },
+);
function SidebarMenuButton({
asChild = false,
@@ -511,12 +511,12 @@ function SidebarMenuButton({
className,
...props
}: React.ComponentProps<"button"> & {
- asChild?: boolean
- isActive?: boolean
- tooltip?: string | React.ComponentProps
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
} & VariantProps) {
- const Comp = asChild ? Slot.Root : "button"
- const { isMobile, state } = useSidebar()
+ const Comp = asChild ? Slot.Root : "button";
+ const { isMobile, state } = useSidebar();
const button = (
- )
+ );
if (!tooltip) {
- return button
+ return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
- }
+ };
}
return (
@@ -549,7 +549,7 @@ function SidebarMenuButton({
{...tooltip}
/>
- )
+ );
}
function SidebarMenuAction({
@@ -558,10 +558,10 @@ function SidebarMenuAction({
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
- asChild?: boolean
- showOnHover?: boolean
+ asChild?: boolean;
+ showOnHover?: boolean;
}) {
- const Comp = asChild ? Slot.Root : "button"
+ const Comp = asChild ? Slot.Root : "button";
return (
- )
+ );
}
function SidebarMenuBadge({
@@ -599,11 +599,11 @@ function SidebarMenuBadge({
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarMenuSkeleton({
@@ -611,12 +611,12 @@ function SidebarMenuSkeleton({
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
- showIcon?: boolean
+ showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
- return `${Math.floor(Math.random() * 40) + 50}%`
- }, [])
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
return (
- )
+ );
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
@@ -652,11 +652,11 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarMenuSubItem({
@@ -670,7 +670,7 @@ function SidebarMenuSubItem({
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
- )
+ );
}
function SidebarMenuSubButton({
@@ -680,11 +680,11 @@ function SidebarMenuSubButton({
className,
...props
}: React.ComponentProps<"a"> & {
- asChild?: boolean
- size?: "sm" | "md"
- isActive?: boolean
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
}) {
- const Comp = asChild ? Slot.Root : "a"
+ const Comp = asChild ? Slot.Root : "a";
return (
- )
+ );
}
export {
@@ -730,4 +730,4 @@ export {
SidebarSeparator,
SidebarTrigger,
useSidebar,
-}
+};
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
index 32ea0ef..f0d550c 100644
--- a/src/components/ui/skeleton.tsx
+++ b/src/components/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
- )
+ );
}
-export { Skeleton }
+export { Skeleton };
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
index 5513a5c..89357fb 100644
--- a/src/components/ui/table.tsx
+++ b/src/components/ui/table.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
@@ -14,7 +14,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
{...props}
/>
- )
+ );
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
@@ -24,7 +24,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
className={cn("[&_tr]:border-b", className)}
{...props}
/>
- )
+ );
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
@@ -34,7 +34,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
- )
+ );
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
@@ -43,11 +43,11 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
@@ -56,11 +56,11 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
@@ -69,11 +69,11 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
@@ -82,11 +82,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableCaption({
@@ -99,7 +99,7 @@ function TableCaption({
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
- )
+ );
}
export {
@@ -111,4 +111,4 @@ export {
TableRow,
TableCell,
TableCaption,
-}
+};
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
index a3b416a..a459874 100644
--- a/src/components/ui/tooltip.tsx
+++ b/src/components/ui/tooltip.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import { Tooltip as TooltipPrimitive } from "radix-ui"
+import * as React from "react";
+import { Tooltip as TooltipPrimitive } from "radix-ui";
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils";
function TooltipProvider({
delayDuration = 0,
@@ -13,19 +13,19 @@ function TooltipProvider({
delayDuration={delayDuration}
{...props}
/>
- )
+ );
}
function Tooltip({
...props
}: React.ComponentProps
) {
- return
+ return ;
}
function TooltipTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function TooltipContent({
@@ -41,7 +41,7 @@ function TooltipContent({
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
- className
+ className,
)}
{...props}
>
@@ -49,7 +49,7 @@ function TooltipContent({
- )
+ );
}
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/src/data/questData.ts b/src/data/questData.ts
deleted file mode 100644
index 5f1d233..0000000
--- a/src/data/questData.ts
+++ /dev/null
@@ -1,344 +0,0 @@
-import type { QuestArc } from "../types/quest";
-
-// ─── QUEST DATA ───────────────────────────────────────────────────────────────
-// Replace each node's `progress` and `status` with live API values.
-// Everything else (titles, flavour, rewards) is content — edit freely.
-
-export const QUEST_ARCS: QuestArc[] = [
- // ── ARC 1: The Calm Seas ──────────────────────────────────────────────────
- {
- id: "east_blue",
- name: "The Calm Seas",
- subtitle: "Every great voyage begins at shore",
- emoji: "🌊",
- accentColor: "#0ea5e9",
- accentDark: "#0369a1",
- bgFrom: "#0c4a6e",
- bgTo: "#075985",
- nodes: [
- {
- id: "eb_1",
- title: "First Steps",
- flavourText:
- '"I\'ll become the greatest sailor who ever lived!" — Every legend begins with a single step.',
- islandName: "Hawthorn Cove",
- emoji: "🏝️",
- requirement: {
- type: "questions",
- target: 10,
- label: "questions answered",
- },
- progress: 10,
- status: "completed",
- reward: { xp: 50, title: "Cabin Hand" },
- },
- {
- id: "eb_2",
- title: "Cast Off",
- flavourText:
- '"The sea doesn\'t care who you were — only who you become." Chart your course.',
- islandName: "Redmast Port",
- emoji: "⚓",
- requirement: {
- type: "sessions",
- target: 3,
- label: "practice sessions",
- },
- progress: 3,
- status: "completed",
- reward: { xp: 75 },
- },
- {
- id: "eb_3",
- title: "The Tangerine Coast",
- flavourText:
- '"Even alone, I protect my crew." Keep your streak burning bright.',
- islandName: "Citrus Bay",
- emoji: "🍊",
- requirement: { type: "streak", target: 3, label: "day streak" },
- progress: 3,
- status: "completed",
- reward: {
- xp: 100,
- item: "streak_shield",
- itemLabel: "Streak Shield ×1",
- },
- },
- {
- id: "eb_4",
- title: "The Fog Village",
- flavourText:
- '"I\'ve fooled everyone — except myself." Prove yourself across new territory.',
- islandName: "Mistholm Village",
- emoji: "🌿",
- requirement: { type: "topics", target: 5, label: "topics practiced" },
- progress: 3,
- status: "claimable",
- reward: { xp: 125, title: "Deckhand" },
- },
- {
- id: "eb_5",
- title: "The Floating Galley",
- flavourText:
- '"Nothing happened." Cut through the noise with razor accuracy.',
- islandName: "The Iron Kitchen",
- emoji: "🍖",
- requirement: {
- type: "accuracy",
- target: 75,
- label: "% accuracy (any session)",
- },
- progress: 58,
- status: "active",
- reward: {
- xp: 150,
- item: "xp_boost",
- itemLabel: "2× XP Boost (1 session)",
- },
- },
- {
- id: "eb_6",
- title: "The Sharkfin Strait",
- flavourText:
- '"This is my dream!" Conquer the Calm Seas before the Grand Voyage beckons.',
- islandName: "Sharkfin Strait",
- emoji: "🦈",
- requirement: {
- type: "questions",
- target: 100,
- label: "questions answered",
- },
- progress: 0,
- status: "locked",
- reward: { xp: 300, title: "First Mate" },
- },
- ],
- },
-
- // ── ARC 2: The Amber Wastes ───────────────────────────────────────────────
- {
- id: "alabasta",
- name: "The Amber Wastes",
- subtitle: "Through the desert sands, to glory",
- emoji: "🏜️",
- accentColor: "#f59e0b",
- accentDark: "#b45309",
- bgFrom: "#78350f",
- bgTo: "#92400e",
- nodes: [
- {
- id: "al_1",
- title: "Crossing the Mirrorlake",
- flavourText:
- '"A true sailor never makes excuses after losing." Enter the warzone.',
- islandName: "Mirrorlake Basin",
- emoji: "💧",
- requirement: {
- type: "sessions",
- target: 5,
- label: "practice sessions",
- },
- progress: 5,
- status: "completed",
- reward: { xp: 150 },
- },
- {
- id: "al_2",
- title: "The Sand March",
- flavourText:
- '"They underestimated us." Grind through the scorching heat.',
- islandName: "The Amber Dunes",
- emoji: "🌵",
- requirement: {
- type: "questions",
- target: 50,
- label: "questions answered",
- },
- progress: 50,
- status: "completed",
- reward: {
- xp: 175,
- item: "xp_boost",
- itemLabel: "1.5× XP Boost (1 session)",
- },
- },
- {
- id: "al_3",
- title: "The Sunstone Palace",
- flavourText: '"I refuse to let my crew fall!" Climb the leaderboard.',
- islandName: "Sunstone City",
- emoji: "🏰",
- requirement: {
- type: "leaderboard",
- target: 10,
- label: "leaderboard rank",
- },
- progress: 22,
- status: "active",
- reward: { xp: 250, title: "Corsair" },
- },
- {
- id: "al_4",
- title: "Blades in the Bazaar",
- flavourText:
- '"I\'ll cut through iron." Maintain brutal accuracy under pressure.',
- islandName: "Bazaar Streets",
- emoji: "⚔️",
- requirement: {
- type: "accuracy",
- target: 85,
- label: "% accuracy (any session)",
- },
- progress: 0,
- status: "locked",
- reward: {
- xp: 300,
- item: "streak_shield",
- itemLabel: "Streak Shield ×2",
- },
- },
- {
- id: "al_5",
- title: "The Warlord Falls",
- flavourText:
- "\"I'm not dying here, partner.\" Prove you're worthy of the Wastes.",
- islandName: "The Throne Dune",
- emoji: "👑",
- requirement: { type: "streak", target: 7, label: "day streak" },
- progress: 0,
- status: "locked",
- reward: { xp: 400, title: "Corsair" },
- },
- {
- id: "al_6",
- title: "The Princess's Farewell",
- flavourText:
- '"Even if our paths split, you\'ll always sail with my crew." The arc is complete.',
- islandName: "Mirrorlake Harbour",
- emoji: "🌅",
- requirement: { type: "xp", target: 1000, label: "total XP earned" },
- progress: 0,
- status: "locked",
- reward: { xp: 500, title: "Sea Emperor" },
- },
- ],
- },
-
- // ── ARC 3: The Sky Reaches ────────────────────────────────────────────────
- {
- id: "skypiea",
- name: "The Sky Reaches",
- subtitle: "Ascend to the island above the clouds",
- emoji: "☁️",
- accentColor: "#a855f7",
- accentDark: "#7c3aed",
- bgFrom: "#3b0764",
- bgTo: "#4c1d95",
- nodes: [
- {
- id: "sk_1",
- title: "The Skyward Torrent",
- flavourText:
- '"The sky island is real!" Believe it — launch yourself upward.',
- islandName: "Upper Cloudreach",
- emoji: "🌤️",
- requirement: {
- type: "topics",
- target: 3,
- label: "topics at 70%+ accuracy",
- },
- progress: 0,
- status: "locked",
- reward: { xp: 200 },
- },
- {
- id: "sk_2",
- title: "The Trial of Storms",
- flavourText:
- '"Follow the wind, follow the stars." Navigate every corner of the cloudscape.',
- islandName: "The Tempest Ordeal",
- emoji: "🎯",
- requirement: {
- type: "topics",
- target: 8,
- label: "distinct topics practiced",
- },
- progress: 0,
- status: "locked",
- reward: {
- xp: 250,
- item: "xp_boost",
- itemLabel: "2× XP Boost (2 sessions)",
- },
- },
- {
- id: "sk_3",
- title: "The Sky God's Wrath",
- flavourText: '"I am the heavens." Are you good enough to defy a deity?',
- islandName: "The Celestial Ark",
- emoji: "⚡",
- requirement: {
- type: "accuracy",
- target: 90,
- label: "% accuracy (any session)",
- },
- progress: 0,
- status: "locked",
- reward: { xp: 400, title: "Sea Emperor" },
- },
- {
- id: "sk_4",
- title: "The Ancient Bell",
- flavourText:
- '"I hear the torrent calling." Ring the bell — make history echo.',
- islandName: "The Cloudvine Spire",
- emoji: "🔔",
- requirement: {
- type: "questions",
- target: 250,
- label: "questions answered",
- },
- progress: 0,
- status: "locked",
- reward: {
- xp: 500,
- item: "streak_shield",
- itemLabel: "Streak Shield ×3",
- },
- },
- {
- id: "sk_5",
- title: "The Gilded Ruins",
- flavourText:
- '"THE GREAT CAPTAIN WAS HERE." Touch the treasure that all legends sought.',
- islandName: "Aureveil",
- emoji: "💰",
- requirement: { type: "xp", target: 3000, label: "total XP earned" },
- progress: 0,
- status: "locked",
- reward: { xp: 750, title: "Grand Captain" },
- },
- {
- id: "sk_6",
- title: "The Grand Captain",
- flavourText:
- '"This is my treasure!" You\'ve reached the summit — your target score awaits.',
- islandName: "The Last Isle",
- emoji: "🏴☠️",
- requirement: {
- type: "sessions",
- target: 30,
- label: "total sessions completed",
- },
- progress: 0,
- status: "locked",
- reward: {
- xp: 1000,
- title: "Grand Captain",
- item: "xp_boost",
- itemLabel: "Permanent 1.2× XP",
- },
- },
- ],
- },
-];
diff --git a/src/hooks/useCrewRank.ts b/src/hooks/useCrewRank.ts
deleted file mode 100644
index a48d9ee..0000000
--- a/src/hooks/useCrewRank.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { QUEST_ARCS } from "../data/questData";
-
-// Returns the player's current crew rank, or a default if none earned yet
-export function getCrewRank(arcs = QUEST_ARCS): string {
- const earned = arcs
- .flatMap((a) => a.nodes)
- .filter((n) => n.status === "completed" && n.reward.title)
- .map((n) => n.reward.title!);
-
- // Return the last one — questData is ordered by difficulty,
- // so the last earned title is always the highest rank
- return earned.at(-1) ?? "Cabin Hand";
-}
diff --git a/src/hooks/useSatTimer.ts b/src/hooks/useSatTimer.ts
index 22bd4d5..37bf6fe 100644
--- a/src/hooks/useSatTimer.ts
+++ b/src/hooks/useSatTimer.ts
@@ -4,7 +4,7 @@ import { useSatExam } from "../stores/useSatExam";
export const useSatTimer = () => {
const phase = useSatExam((s) => s.phase);
const getRemainingTime = useSatExam((s) => s.getRemainingTime);
- const startBreak = useSatExam((s) => s.startBreak);
+
const skipBreak = useSatExam((s) => s.skipBreak);
const finishExam = useSatExam((s) => s.finishExam);
diff --git a/src/pages/ErrorPage.tsx b/src/pages/ErrorPage.tsx
deleted file mode 100644
index 935634c..0000000
--- a/src/pages/ErrorPage.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-// src/pages/ErrorPage.tsx
-import { useRouteError, isRouteErrorResponse } from "react-router-dom";
-
-export default function ErrorPage() {
- const error = useRouteError();
-
- console.error(error);
-
- let title = "Something went wrong";
- let message = "An unexpected error occurred.";
-
- if (isRouteErrorResponse(error)) {
- title = `${error.status} ${error.statusText}`;
- message = error.data?.message || message;
- }
-
- return (
-
-
-
{title}
-
{message}
-
-
(window.location.href = "/")}
- className="px-4 py-2 bg-black text-white rounded-lg"
- >
- Go Home
-
-
-
- );
-}
diff --git a/src/pages/auth/Register.tsx b/src/pages/auth/Register.tsx
index 86602a5..57d61f0 100644
--- a/src/pages/auth/Register.tsx
+++ b/src/pages/auth/Register.tsx
@@ -6,7 +6,6 @@ import {
Loader2,
Mail,
Lock,
- User,
ImageIcon,
BookOpen,
Star,
diff --git a/src/pages/student/Analytics.tsx b/src/pages/student/Analytics.tsx
deleted file mode 100644
index 7293e27..0000000
--- a/src/pages/student/Analytics.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { List, SquarePen, DecimalsArrowRight, MapPin } from "lucide-react";
-import { Progress } from "../../components/ui/progress";
-import { Button } from "../../components/ui/button";
-import {
- Card,
- CardHeader,
- CardTitle,
- CardContent,
- CardFooter,
-} from "../../components/ui/card";
-import { Field, FieldLabel } from "../../components/ui/field";
-import { CircularProgress } from "../../components/CircularProgress";
-
-export const Analytics = () => {
- return (
-
-
- Analytics
-
-
-
-
-
-
-
- 145 th
-
-
-
-
-
-
-
-
-
-
-
- Details
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Score
-
- 854
- /1600
-
-
-
-
-
-
-
-
- );
-};
diff --git a/src/pages/student/Home.tsx b/src/pages/student/Home.tsx
index 3dcbf60..54e9c59 100644
--- a/src/pages/student/Home.tsx
+++ b/src/pages/student/Home.tsx
@@ -7,7 +7,6 @@ import { formatStatus } from "../../lib/utils";
import { useNavigate } from "react-router-dom";
import { SearchOverlay } from "../../components/SearchOverlay";
import { InfoHeader } from "../../components/InfoHeader";
-import { InventoryButton } from "../../components/InventoryButton";
// ─── Shared blob/dot background (same as break/results screens) ────────────────
const DOTS = [
diff --git a/src/pages/student/Lessons.tsx b/src/pages/student/Lessons.tsx
index 485ca61..1f37430 100644
--- a/src/pages/student/Lessons.tsx
+++ b/src/pages/student/Lessons.tsx
@@ -491,7 +491,9 @@ export const Lessons = () => {
setLessonLoading(true);
const authStorage = localStorage.getItem("auth-storage");
if (!authStorage) return;
+
const {
+ // @ts-ignore
state: { token },
} = JSON.parse(authStorage) as { state?: { token?: string } };
if (!token) return;
@@ -631,7 +633,7 @@ export const Lessons = () => {
lesson={lesson}
index={i}
searchQuery={searchQuery}
- onClick={() => handleLessonClick(lesson.id)}
+ onClick={() => handleLessonClick(lesson.id, lesson.title)}
/>
))}
diff --git a/src/pages/student/Practice.tsx b/src/pages/student/Practice.tsx
index 873cac1..4af3279 100644
--- a/src/pages/student/Practice.tsx
+++ b/src/pages/student/Practice.tsx
@@ -8,8 +8,6 @@ import {
Zap,
} from "lucide-react";
import { useNavigate } from "react-router-dom";
-import { useExamConfigStore } from "../../stores/useExamConfigStore";
-import { LevelBar } from "../../components/LevelBar";
import { InfoHeader } from "../../components/InfoHeader";
const DOTS = [
diff --git a/src/pages/student/QuestMap.tsx b/src/pages/student/QuestMap.tsx
index e58c235..0b65ddf 100644
--- a/src/pages/student/QuestMap.tsx
+++ b/src/pages/student/QuestMap.tsx
@@ -756,9 +756,9 @@ const RouteSegment = ({
isNext,
accent,
}: RouteSegmentProps) => {
- const lineRef = useRef
(null!);
- const glowRef = useRef(null!);
- const shipRef = useRef(null!);
+ const lineRef = useRef(null);
+ const glowRef = useRef(null);
+ const shipRef = useRef(null);
const shipT = useRef(0);
// CatmullRom curve bowing sideways — alternate direction per segment
@@ -799,8 +799,10 @@ const RouteSegment = ({
useFrame((_, dt) => {
// Scroll dashes forward along the route
if (lineRef.current) {
- const mat = lineRef.current.material as THREE.LineDashedMaterial;
- if (dashSpeed > 0) mat.dashOffset -= dt * dashSpeed;
+ // material typings may not include dashOffset; use any and guard the value
+ const lineMat = lineRef.current.material as any;
+ if (dashSpeed > 0)
+ lineMat.dashOffset = (lineMat.dashOffset ?? 0) - dt * dashSpeed;
}
// Pulse glow on active segments
if (glowRef.current && (isActive || isNext)) {
@@ -837,9 +839,14 @@ const RouteSegment = ({
{/* Dashed route line */}
{
+ // r may be an SVGLineElement in JSX DOM typings; treat as any to satisfy TS and assign to Line ref
+ lineRef.current = r as THREE.Line | null;
+ }}
+ // @ts-ignore - geometry is a three.js prop, not an SVG attribute
geometry={lineGeo}
- onUpdate={(self) => self.computeLineDistances()}
+ // onUpdate receives a three.js Line; use any to avoid DOM typings
+ onUpdate={(self: any) => self.computeLineDistances()}
>
void;
- scrollRef: React.RefObject;
+ scrollRef: React.RefObject;
user: any;
onClaim: (n: QuestNode) => void;
}) => {
@@ -1557,7 +1564,7 @@ export const QuestMap = () => {
const [claimResult, setClaimResult] = useState(
null,
);
- const [claimLoading, setClaimLoading] = useState(false);
+
const [claimError, setClaimError] = useState(null);
const [selectedNode, setSelectedNode] = useState(null);
@@ -1597,14 +1604,12 @@ export const QuestMap = () => {
setClaimingNode(node);
setClaimResult(null);
setClaimError(null);
- setClaimLoading(true);
+
try {
const result = await api.claimReward(token, node.node_id);
setClaimResult(result);
} catch (err) {
setClaimError(err instanceof Error ? err.message : "Claim failed");
- } finally {
- setClaimLoading(false);
}
},
[token],
diff --git a/src/pages/student/Rewards.tsx b/src/pages/student/Rewards.tsx
index c29952d..e479fb8 100644
--- a/src/pages/student/Rewards.tsx
+++ b/src/pages/student/Rewards.tsx
@@ -434,9 +434,10 @@ export const Rewards = () => {
if (!user) return;
const authStorage = localStorage.getItem("auth-storage");
if (!authStorage) return;
- const {
- state: { token },
- } = JSON.parse(authStorage) as { state?: { token?: string } };
+ const parsed = JSON.parse(authStorage) as {
+ state?: { token?: string };
+ } | null;
+ const token = parsed?.state?.token;
if (!token) return;
try {
setLoading(true);
@@ -481,7 +482,7 @@ export const Rewards = () => {
// ✅ FIX 2: Safely cast user_rank — null becomes undefined so all optional chaining works
const ur = (leaderboard?.user_rank ?? undefined) as
- | Record
+ | Record
| undefined;
const islandStats = getIslandStats(ur, activeTab);
diff --git a/src/pages/student/drills/page.tsx b/src/pages/student/drills/page.tsx
index 54878b0..c60d84f 100644
--- a/src/pages/student/drills/page.tsx
+++ b/src/pages/student/drills/page.tsx
@@ -324,9 +324,10 @@ export const Drills = () => {
setLoading(true);
const authStorage = localStorage.getItem("auth-storage");
if (!authStorage) return;
- const {
- state: { token },
- } = JSON.parse(authStorage) as { state?: { token?: string } };
+ const parsed = JSON.parse(authStorage) as {
+ state?: { token?: string };
+ } | null;
+ const token = parsed?.state?.token;
if (!token) return;
const response = await api.fetchAllTopics(token);
setTopics(response);
diff --git a/src/pages/student/lessons/AreaVolumeLesson.tsx b/src/pages/student/lessons/AreaVolumeLesson.tsx
index 89af881..3e4b48e 100644
--- a/src/pages/student/lessons/AreaVolumeLesson.tsx
+++ b/src/pages/student/lessons/AreaVolumeLesson.tsx
@@ -47,9 +47,9 @@ function FormulaCard({
/>
-
+
{diagram}
{example}
diff --git a/src/pages/student/lessons/CirclePropertiesLesson.tsx b/src/pages/student/lessons/CirclePropertiesLesson.tsx
index 032097c..f0ada24 100644
--- a/src/pages/student/lessons/CirclePropertiesLesson.tsx
+++ b/src/pages/student/lessons/CirclePropertiesLesson.tsx
@@ -469,7 +469,7 @@ const CirclePropertiesLesson: React.FC
= ({ onFinish }) => {
Practice Time
- {CIRCLE_PROP_QUIZ_DATA.map((quiz, idx) => (
+ {CIRCLE_PROP_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/CirclesLesson.tsx b/src/pages/student/lessons/CirclesLesson.tsx
index 62e8a6e..6c69458 100644
--- a/src/pages/student/lessons/CirclesLesson.tsx
+++ b/src/pages/student/lessons/CirclesLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
Circle,
Target,
diff --git a/src/pages/student/lessons/CongruenceSimilarityLesson.tsx b/src/pages/student/lessons/CongruenceSimilarityLesson.tsx
index 0ce4f7a..25d07e0 100644
--- a/src/pages/student/lessons/CongruenceSimilarityLesson.tsx
+++ b/src/pages/student/lessons/CongruenceSimilarityLesson.tsx
@@ -490,7 +490,7 @@ const CongruenceSimilarityLesson: React.FC = ({ onFinish }) => {
Practice Time
- {SIMILARITY_QUIZ_DATA.map((quiz, idx) => (
+ {SIMILARITY_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/DataAnalysisLesson.tsx b/src/pages/student/lessons/DataAnalysisLesson.tsx
index aabe218..917b6a0 100644
--- a/src/pages/student/lessons/DataAnalysisLesson.tsx
+++ b/src/pages/student/lessons/DataAnalysisLesson.tsx
@@ -266,7 +266,7 @@ const DataAnalysisLesson: React.FC = ({ onFinish }) => {
Practice Time
- {DATA_ANALYSIS_QUIZ_DATA.map((quiz, idx) => (
+ {DATA_ANALYSIS_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/EBRWBoundariesLesson.tsx b/src/pages/student/lessons/EBRWBoundariesLesson.tsx
index 8375d10..b312b54 100644
--- a/src/pages/student/lessons/EBRWBoundariesLesson.tsx
+++ b/src/pages/student/lessons/EBRWBoundariesLesson.tsx
@@ -296,7 +296,7 @@ const EBRWBoundariesLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWCentralIdeasLesson.tsx b/src/pages/student/lessons/EBRWCentralIdeasLesson.tsx
index df84755..25225f6 100644
--- a/src/pages/student/lessons/EBRWCentralIdeasLesson.tsx
+++ b/src/pages/student/lessons/EBRWCentralIdeasLesson.tsx
@@ -142,7 +142,7 @@ const EBRWCentralIdeasLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWCommandEvidenceLesson.tsx b/src/pages/student/lessons/EBRWCommandEvidenceLesson.tsx
index ab638a8..b88aef8 100644
--- a/src/pages/student/lessons/EBRWCommandEvidenceLesson.tsx
+++ b/src/pages/student/lessons/EBRWCommandEvidenceLesson.tsx
@@ -158,7 +158,7 @@ const EBRWCommandEvidenceLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWCommasLesson.tsx b/src/pages/student/lessons/EBRWCommasLesson.tsx
index 19ed13d..8f828b2 100644
--- a/src/pages/student/lessons/EBRWCommasLesson.tsx
+++ b/src/pages/student/lessons/EBRWCommasLesson.tsx
@@ -209,7 +209,7 @@ const EBRWCommasLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWCraftStructureLesson.tsx b/src/pages/student/lessons/EBRWCraftStructureLesson.tsx
index 9ec9ecb..48d0cc2 100644
--- a/src/pages/student/lessons/EBRWCraftStructureLesson.tsx
+++ b/src/pages/student/lessons/EBRWCraftStructureLesson.tsx
@@ -88,7 +88,7 @@ const EBRWCraftStructureLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWCrossTextLesson.tsx b/src/pages/student/lessons/EBRWCrossTextLesson.tsx
index 05a8584..82464d1 100644
--- a/src/pages/student/lessons/EBRWCrossTextLesson.tsx
+++ b/src/pages/student/lessons/EBRWCrossTextLesson.tsx
@@ -125,7 +125,7 @@ const EBRWCrossTextLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWDashesApostrophesLesson.tsx b/src/pages/student/lessons/EBRWDashesApostrophesLesson.tsx
index 459f919..e256b88 100644
--- a/src/pages/student/lessons/EBRWDashesApostrophesLesson.tsx
+++ b/src/pages/student/lessons/EBRWDashesApostrophesLesson.tsx
@@ -195,7 +195,7 @@ const EBRWDashesApostrophesLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWExplicitMeaningLesson.tsx b/src/pages/student/lessons/EBRWExplicitMeaningLesson.tsx
index 8765528..9d4dae1 100644
--- a/src/pages/student/lessons/EBRWExplicitMeaningLesson.tsx
+++ b/src/pages/student/lessons/EBRWExplicitMeaningLesson.tsx
@@ -105,6 +105,7 @@ const EBRWExplicitMeaningLesson: React.FC = ({ onFinish }) => {
{isPast ? (
) : (
+ // @ts-ignore
)}
diff --git a/src/pages/student/lessons/EBRWExpressionIdeasLesson.tsx b/src/pages/student/lessons/EBRWExpressionIdeasLesson.tsx
index e90d150..1027197 100644
--- a/src/pages/student/lessons/EBRWExpressionIdeasLesson.tsx
+++ b/src/pages/student/lessons/EBRWExpressionIdeasLesson.tsx
@@ -105,6 +105,7 @@ const EBRWExpressionIdeasLesson: React.FC
= ({ onFinish }) => {
{isPast ? (
) : (
+ // @ts-ignore
)}
diff --git a/src/pages/student/lessons/EBRWFormStructureSenseLesson.tsx b/src/pages/student/lessons/EBRWFormStructureSenseLesson.tsx
index 1a4d696..18dcc0b 100644
--- a/src/pages/student/lessons/EBRWFormStructureSenseLesson.tsx
+++ b/src/pages/student/lessons/EBRWFormStructureSenseLesson.tsx
@@ -220,6 +220,7 @@ const EBRWFormStructureSenseLesson: React.FC
= ({ onFinish }) => {
{isPast ? (
) : (
+ // @ts-ignore
)}
diff --git a/src/pages/student/lessons/EBRWGraphicDisplaysLesson.tsx b/src/pages/student/lessons/EBRWGraphicDisplaysLesson.tsx
index 1e69218..3e0edd2 100644
--- a/src/pages/student/lessons/EBRWGraphicDisplaysLesson.tsx
+++ b/src/pages/student/lessons/EBRWGraphicDisplaysLesson.tsx
@@ -273,6 +273,7 @@ const EBRWGraphicDisplaysLesson: React.FC
= ({ onFinish }) => {
{isPast ? (
) : (
+ // @ts-ignore
)}
diff --git a/src/pages/student/lessons/EBRWInferencesLesson.tsx b/src/pages/student/lessons/EBRWInferencesLesson.tsx
index 9ac3f05..d1a1266 100644
--- a/src/pages/student/lessons/EBRWInferencesLesson.tsx
+++ b/src/pages/student/lessons/EBRWInferencesLesson.tsx
@@ -155,6 +155,7 @@ const EBRWInferencesLesson: React.FC
= ({ onFinish }) => {
{isPast ? (
) : (
+ // @ts-ignore
)}
diff --git a/src/pages/student/lessons/EBRWMainIdeaLesson.tsx b/src/pages/student/lessons/EBRWMainIdeaLesson.tsx
index 596fde4..9f91917 100644
--- a/src/pages/student/lessons/EBRWMainIdeaLesson.tsx
+++ b/src/pages/student/lessons/EBRWMainIdeaLesson.tsx
@@ -102,6 +102,7 @@ const EBRWMainIdeaLesson: React.FC
= ({ onFinish }) => {
{isPast ? (
) : (
+ // @ts-ignore
)}
diff --git a/src/pages/student/lessons/EBRWPronounsLesson.tsx b/src/pages/student/lessons/EBRWPronounsLesson.tsx
index d6669e5..9517215 100644
--- a/src/pages/student/lessons/EBRWPronounsLesson.tsx
+++ b/src/pages/student/lessons/EBRWPronounsLesson.tsx
@@ -208,7 +208,7 @@ const EBRWPronounsLesson: React.FC
= ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWRhetoricalSynthesisLesson.tsx b/src/pages/student/lessons/EBRWRhetoricalSynthesisLesson.tsx
index 1d18d88..a880370 100644
--- a/src/pages/student/lessons/EBRWRhetoricalSynthesisLesson.tsx
+++ b/src/pages/student/lessons/EBRWRhetoricalSynthesisLesson.tsx
@@ -190,6 +190,7 @@ const EBRWRhetoricalSynthesisLesson: React.FC = ({ onFinish }) => {
{isPast ? (
) : (
+ // @ts-ignore
)}
diff --git a/src/pages/student/lessons/EBRWSemicolonsColonsLesson.tsx b/src/pages/student/lessons/EBRWSemicolonsColonsLesson.tsx
index 19af518..961699f 100644
--- a/src/pages/student/lessons/EBRWSemicolonsColonsLesson.tsx
+++ b/src/pages/student/lessons/EBRWSemicolonsColonsLesson.tsx
@@ -189,7 +189,7 @@ const EBRWSemicolonsColonsLesson: React.FC
= ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWSentenceStructureLesson.tsx b/src/pages/student/lessons/EBRWSentenceStructureLesson.tsx
index 356573e..fe843d8 100644
--- a/src/pages/student/lessons/EBRWSentenceStructureLesson.tsx
+++ b/src/pages/student/lessons/EBRWSentenceStructureLesson.tsx
@@ -214,7 +214,7 @@ const EBRWSentenceStructureLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWSubjectVerbLesson.tsx b/src/pages/student/lessons/EBRWSubjectVerbLesson.tsx
index 6046875..147eedc 100644
--- a/src/pages/student/lessons/EBRWSubjectVerbLesson.tsx
+++ b/src/pages/student/lessons/EBRWSubjectVerbLesson.tsx
@@ -203,7 +203,7 @@ const EBRWSubjectVerbLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWTextStructurePurposeLesson.tsx b/src/pages/student/lessons/EBRWTextStructurePurposeLesson.tsx
index 8ecdb9f..dad22e7 100644
--- a/src/pages/student/lessons/EBRWTextStructurePurposeLesson.tsx
+++ b/src/pages/student/lessons/EBRWTextStructurePurposeLesson.tsx
@@ -270,7 +270,7 @@ const EBRWTextStructurePurposeLesson: React.FC = ({
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWTransitionsLesson.tsx b/src/pages/student/lessons/EBRWTransitionsLesson.tsx
index 2776f40..806cc65 100644
--- a/src/pages/student/lessons/EBRWTransitionsLesson.tsx
+++ b/src/pages/student/lessons/EBRWTransitionsLesson.tsx
@@ -171,7 +171,7 @@ const EBRWTransitionsLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWVerbsLesson.tsx b/src/pages/student/lessons/EBRWVerbsLesson.tsx
index d9506ad..3360213 100644
--- a/src/pages/student/lessons/EBRWVerbsLesson.tsx
+++ b/src/pages/student/lessons/EBRWVerbsLesson.tsx
@@ -211,7 +211,7 @@ const EBRWVerbsLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWVocabMeaningLesson.tsx b/src/pages/student/lessons/EBRWVocabMeaningLesson.tsx
index 5e0ecd9..fb016aa 100644
--- a/src/pages/student/lessons/EBRWVocabMeaningLesson.tsx
+++ b/src/pages/student/lessons/EBRWVocabMeaningLesson.tsx
@@ -45,7 +45,7 @@ const EBRWVocabMeaningLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWVocabPreciseLesson.tsx b/src/pages/student/lessons/EBRWVocabPreciseLesson.tsx
index 13e146d..220646a 100644
--- a/src/pages/student/lessons/EBRWVocabPreciseLesson.tsx
+++ b/src/pages/student/lessons/EBRWVocabPreciseLesson.tsx
@@ -45,7 +45,7 @@ const EBRWVocabPreciseLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
diff --git a/src/pages/student/lessons/EBRWWordsInContextLesson.tsx b/src/pages/student/lessons/EBRWWordsInContextLesson.tsx
index 4a06926..e022f73 100644
--- a/src/pages/student/lessons/EBRWWordsInContextLesson.tsx
+++ b/src/pages/student/lessons/EBRWWordsInContextLesson.tsx
@@ -275,7 +275,7 @@ const EBRWWordsInContextLesson: React.FC = ({ onFinish }) => {
}: {
index: number;
title: string;
- icon: React.ElementType;
+ icon: React.ComponentType>;
}) => {
const isActive = activeSection === index;
const isPast = activeSection > index;
@@ -288,11 +288,7 @@ const EBRWWordsInContextLesson: React.FC = ({ onFinish }) => {
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0
${isActive ? "bg-fuchsia-600 text-white" : isPast ? "bg-fuchsia-400 text-white" : "bg-slate-200 text-slate-500"}`}
>
- {isPast ? (
-
- ) : (
-
- )}
+ {isPast ? : }
{
-
+
diff --git a/src/pages/student/lessons/LinearEq2VarLesson.tsx b/src/pages/student/lessons/LinearEq2VarLesson.tsx
index 437b77d..559ca85 100644
--- a/src/pages/student/lessons/LinearEq2VarLesson.tsx
+++ b/src/pages/student/lessons/LinearEq2VarLesson.tsx
@@ -1,12 +1,4 @@
-import React from "react";
-import {
- Grid,
- TrendingUp,
- Layers,
- ArrowRight,
- Hash,
- BookOpen,
-} from "lucide-react";
+import { Grid, TrendingUp, Layers, Hash, BookOpen } from "lucide-react";
import LessonShell, {
ConceptCard,
FormulaBox,
diff --git a/src/pages/student/lessons/LinearEquationsLesson.tsx b/src/pages/student/lessons/LinearEquationsLesson.tsx
index 874d315..67bb003 100644
--- a/src/pages/student/lessons/LinearEquationsLesson.tsx
+++ b/src/pages/student/lessons/LinearEquationsLesson.tsx
@@ -57,9 +57,9 @@ const BalanceScaleWidget = () => {
-
+
= ({ onFinish }) => {
Practice Time
- {LINEAR_EQ_QUIZ_DATA.map((quiz, idx) => (
+ {LINEAR_EQ_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/LinearFunctionsLesson.tsx b/src/pages/student/lessons/LinearFunctionsLesson.tsx
index 8a7f5d3..e6b2d37 100644
--- a/src/pages/student/lessons/LinearFunctionsLesson.tsx
+++ b/src/pages/student/lessons/LinearFunctionsLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
TrendingUp,
Hash,
@@ -186,7 +185,7 @@ export default function LinearFunctionsLesson({ onFinish }: LessonProps) {
key={c}
className="flex gap-3 items-center bg-white/60 rounded-lg p-3 border border-blue-100"
>
-
+
{c}
{d}
diff --git a/src/pages/student/lessons/LinearInequalitiesLesson.tsx b/src/pages/student/lessons/LinearInequalitiesLesson.tsx
index aae3b7d..a23413d 100644
--- a/src/pages/student/lessons/LinearInequalitiesLesson.tsx
+++ b/src/pages/student/lessons/LinearInequalitiesLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
Scale,
ArrowRight,
@@ -9,7 +8,6 @@ import {
} from "lucide-react";
import LessonShell, {
ConceptCard,
- FormulaBox,
ExampleCard,
TipCard,
PracticeFromDataset,
diff --git a/src/pages/student/lessons/LinearParallelPerpendicularLesson.tsx b/src/pages/student/lessons/LinearParallelPerpendicularLesson.tsx
index 5b34e43..7317422 100644
--- a/src/pages/student/lessons/LinearParallelPerpendicularLesson.tsx
+++ b/src/pages/student/lessons/LinearParallelPerpendicularLesson.tsx
@@ -327,7 +327,7 @@ const LinearParallelPerpendicularLesson: React.FC = ({
Practice Time
- {LINEAR_PARALLEL_PERP_QUIZ_DATA.map((quiz, idx) => (
+ {LINEAR_PARALLEL_PERP_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/LinearTransformationsLesson.tsx b/src/pages/student/lessons/LinearTransformationsLesson.tsx
index 1b5e5ab..6824a45 100644
--- a/src/pages/student/lessons/LinearTransformationsLesson.tsx
+++ b/src/pages/student/lessons/LinearTransformationsLesson.tsx
@@ -303,7 +303,7 @@ const LinearTransformationsLesson: React.FC = ({ onFinish }) => {
Practice Time
- {LINEAR_TRANSFORMATIONS_QUIZ_DATA.map((quiz, idx) => (
+ {LINEAR_TRANSFORMATIONS_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/LinesAnglesTrianglesLesson.tsx b/src/pages/student/lessons/LinesAnglesTrianglesLesson.tsx
index 1d4c805..e1ed94e 100644
--- a/src/pages/student/lessons/LinesAnglesTrianglesLesson.tsx
+++ b/src/pages/student/lessons/LinesAnglesTrianglesLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
ArrowRight,
Triangle,
diff --git a/src/pages/student/lessons/NonlinearEq1VarLesson.tsx b/src/pages/student/lessons/NonlinearEq1VarLesson.tsx
index f4ac3f1..514e028 100644
--- a/src/pages/student/lessons/NonlinearEq1VarLesson.tsx
+++ b/src/pages/student/lessons/NonlinearEq1VarLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { Layers, Hash, Target, Zap, RotateCcw, BookOpen } from "lucide-react";
import LessonShell, {
ConceptCard,
diff --git a/src/pages/student/lessons/OneVariableDataLesson.tsx b/src/pages/student/lessons/OneVariableDataLesson.tsx
index 0827648..5d08d32 100644
--- a/src/pages/student/lessons/OneVariableDataLesson.tsx
+++ b/src/pages/student/lessons/OneVariableDataLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
BarChart,
Box,
diff --git a/src/pages/student/lessons/PolynomialFunctionsLesson.tsx b/src/pages/student/lessons/PolynomialFunctionsLesson.tsx
index 4630cb1..07dd74b 100644
--- a/src/pages/student/lessons/PolynomialFunctionsLesson.tsx
+++ b/src/pages/student/lessons/PolynomialFunctionsLesson.tsx
@@ -457,7 +457,7 @@ const PolynomialFunctionsLesson: React.FC = ({ onFinish }) => {
Practice Time
- {ADV_POLYNOMIAL_QUIZ.map((quiz, idx) => (
+ {ADV_POLYNOMIAL_QUIZ.map((quiz) => (
diff --git a/src/pages/student/lessons/ProbabilityLesson.tsx b/src/pages/student/lessons/ProbabilityLesson.tsx
index 49d9d38..176d239 100644
--- a/src/pages/student/lessons/ProbabilityLesson.tsx
+++ b/src/pages/student/lessons/ProbabilityLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { Target, Hash, GitBranch, Layers, Table, BookOpen } from "lucide-react";
import LessonShell, {
ConceptCard,
diff --git a/src/pages/student/lessons/QuadraticEquationsLesson.tsx b/src/pages/student/lessons/QuadraticEquationsLesson.tsx
index 63757cf..461ad70 100644
--- a/src/pages/student/lessons/QuadraticEquationsLesson.tsx
+++ b/src/pages/student/lessons/QuadraticEquationsLesson.tsx
@@ -618,7 +618,7 @@ const QuadraticEquationsLesson: React.FC = ({ onFinish }) => {
Practice Time
- {QUADRATIC_EQ_QUIZ_DATA.map((quiz, idx) => (
+ {QUADRATIC_EQ_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/RationalRadicalLesson.tsx b/src/pages/student/lessons/RationalRadicalLesson.tsx
index 004f41d..9a639f1 100644
--- a/src/pages/student/lessons/RationalRadicalLesson.tsx
+++ b/src/pages/student/lessons/RationalRadicalLesson.tsx
@@ -450,7 +450,7 @@ const RationalRadicalLesson: React.FC = ({ onFinish }) => {
Practice Time
- {ADV_RATIONAL_QUIZ.map((quiz, idx) => (
+ {ADV_RATIONAL_QUIZ.map((quiz) => (
diff --git a/src/pages/student/lessons/RatiosLesson.tsx b/src/pages/student/lessons/RatiosLesson.tsx
index d1eefa3..f0c39a7 100644
--- a/src/pages/student/lessons/RatiosLesson.tsx
+++ b/src/pages/student/lessons/RatiosLesson.tsx
@@ -32,7 +32,7 @@ const RatiosLesson: React.FC = ({ onFinish }) => {
Practice Time
- {RATIOS_QUIZ_DATA.map((quiz, idx) => (
+ {RATIOS_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/RatiosRatesLesson.tsx b/src/pages/student/lessons/RatiosRatesLesson.tsx
index fc64b59..5eb0038 100644
--- a/src/pages/student/lessons/RatiosRatesLesson.tsx
+++ b/src/pages/student/lessons/RatiosRatesLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
Scale,
ArrowRight,
diff --git a/src/pages/student/lessons/RightTrianglesTrigLesson.tsx b/src/pages/student/lessons/RightTrianglesTrigLesson.tsx
index 4365d08..7df6d0f 100644
--- a/src/pages/student/lessons/RightTrianglesTrigLesson.tsx
+++ b/src/pages/student/lessons/RightTrianglesTrigLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
Triangle,
Ruler,
diff --git a/src/pages/student/lessons/SampleStatsLesson.tsx b/src/pages/student/lessons/SampleStatsLesson.tsx
index 3e1de79..af898fb 100644
--- a/src/pages/student/lessons/SampleStatsLesson.tsx
+++ b/src/pages/student/lessons/SampleStatsLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { Scale, Target, BarChart, Layers, Hash, BookOpen } from "lucide-react";
import LessonShell, {
ConceptCard,
diff --git a/src/pages/student/lessons/SystemsEq2VarLesson.tsx b/src/pages/student/lessons/SystemsEq2VarLesson.tsx
index 9210dcc..03ebba1 100644
--- a/src/pages/student/lessons/SystemsEq2VarLesson.tsx
+++ b/src/pages/student/lessons/SystemsEq2VarLesson.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import {
Target,
ArrowRight,
diff --git a/src/pages/student/lessons/SystemsEquationsLesson.tsx b/src/pages/student/lessons/SystemsEquationsLesson.tsx
index cc1ffc6..e117dbd 100644
--- a/src/pages/student/lessons/SystemsEquationsLesson.tsx
+++ b/src/pages/student/lessons/SystemsEquationsLesson.tsx
@@ -384,7 +384,7 @@ const SystemsEquationsLesson: React.FC = ({ onFinish }) => {
Practice Time
- {SYSTEMS_QUIZ_DATA.map((quiz, idx) => (
+ {SYSTEMS_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/lessons/SystemsLinearEqLesson.tsx b/src/pages/student/lessons/SystemsLinearEqLesson.tsx
index d8c4799..562b855 100644
--- a/src/pages/student/lessons/SystemsLinearEqLesson.tsx
+++ b/src/pages/student/lessons/SystemsLinearEqLesson.tsx
@@ -1,8 +1,6 @@
-import React from "react";
import { Layers, ArrowRight, Hash, Lightbulb, BookOpen } from "lucide-react";
import LessonShell, {
ConceptCard,
- FormulaBox,
ExampleCard,
TipCard,
PracticeFromDataset,
diff --git a/src/pages/student/lessons/TrigLesson.tsx b/src/pages/student/lessons/TrigLesson.tsx
index 6e235f7..c6d0e4b 100644
--- a/src/pages/student/lessons/TrigLesson.tsx
+++ b/src/pages/student/lessons/TrigLesson.tsx
@@ -688,7 +688,7 @@ const TrigLesson: React.FC = ({ onFinish }) => {
Practice Time
- {TRIG_QUIZ_DATA.map((quiz, idx) => (
+ {TRIG_QUIZ_DATA.map((quiz) => (
diff --git a/src/pages/student/practice/Results.tsx b/src/pages/student/practice/Results.tsx
index d1089d2..4fa6289 100644
--- a/src/pages/student/practice/Results.tsx
+++ b/src/pages/student/practice/Results.tsx
@@ -2,8 +2,9 @@ import { useNavigate } from "react-router-dom";
import { useResults } from "../../../stores/useResults";
import { LucideArrowLeft } from "lucide-react";
import { CircularLevelProgress } from "../../../components/CircularLevelProgress";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useExamConfigStore } from "../../../stores/useExamConfigStore";
+import { useAuthStore } from "../../../stores/authStore";
// ─── Shared styles injected once ─────────────────────────────────────────────
const STYLES = `
@@ -404,13 +405,16 @@ export const Results = () => {
const clearResults = useResults((s) => s.clearResults);
const { payload } = useExamConfigStore();
const isTargeted = payload?.mode === "TARGETED";
+ const fetchUser = useAuthStore((s) => s.fetchUser);
- function handleFinishExam() {
+ const handleFinishExam = useCallback(async () => {
useExamConfigStore.getState().clearPayload();
-
clearResults();
+
+ await fetchUser(); // ← refreshes user in authStore after exam completes
+
navigate("/student/home");
- }
+ }, [clearResults, fetchUser, navigate]);
if (isTargeted) return ;
diff --git a/src/pages/student/practice/Test.tsx b/src/pages/student/practice/Test.tsx
index 86caa46..e7520c1 100644
--- a/src/pages/student/practice/Test.tsx
+++ b/src/pages/student/practice/Test.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState, useRef } from "react";
import { Navigate, useNavigate } from "react-router-dom";
+// @ts-ignore
import { BlockMath, InlineMath } from "react-katex";
import {
Binary,
@@ -931,6 +932,7 @@ export const Test = () => {
if (!user) return;
const payload = useExamConfigStore.getState().payload;
try {
+ // @ts-ignore
const response = await api.startSession(token as string, payload);
setSessionId(response.id);
await loadSessionQuestions(response.id);
@@ -1067,6 +1069,7 @@ export const Test = () => {
(e) => !correctedRef.current.has(e.questionId),
);
if (!remaining.length) {
+ // @ts-ignore
const next = await api.fetchNextModule(token!, sessionId);
if (next.status === "COMPLETED") finishExam();
return;
@@ -2152,7 +2155,7 @@ export const Test = () => {
Take a breather — next module coming up
-
+
Next module in
@@ -2198,7 +2201,7 @@ export const Test = () => {
["🏆", "Nice!", "Results"],
["🔥", "100%", "Effort"],
].map(([e, v, l]) => (
-
+
{e}
{v}
diff --git a/src/pages/student/targeted-practice/page.tsx b/src/pages/student/targeted-practice/page.tsx
index 8645e97..ad005e5 100644
--- a/src/pages/student/targeted-practice/page.tsx
+++ b/src/pages/student/targeted-practice/page.tsx
@@ -513,9 +513,10 @@ export const TargetedPractice = () => {
setLoading(true);
const authStorage = localStorage.getItem("auth-storage");
if (!authStorage) return;
- const {
- state: { token },
- } = JSON.parse(authStorage) as { state?: { token?: string } };
+ const parsed = JSON.parse(authStorage) as {
+ state?: { token?: string };
+ } | null;
+ const token = parsed?.state?.token;
if (!token) return;
const response = await api.fetchAllTopics(token);
setTopics(response);
@@ -707,9 +708,13 @@ export const TargetedPractice = () => {
color: meta.color,
}}
>
- {t.section === "Reading & Writing"
- ? "R&W"
- : t.section}
+ {(() => {
+ const s = String(t.section);
+ return s === "EBRW" ||
+ s === "Reading & Writing"
+ ? "R&W"
+ : s;
+ })()}
)}
diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts
index c486c83..2929101 100644
--- a/src/stores/authStore.ts
+++ b/src/stores/authStore.ts
@@ -16,6 +16,7 @@ interface AuthState {
registrationMessage: string | null;
login: (credentials: LoginRequest) => Promise
;
register: (credentials: RegistrationRequest) => Promise;
+ fetchUser: () => Promise;
logout: () => void;
clearError: () => void;
}
@@ -85,6 +86,18 @@ export const useAuthStore = create()(
}
},
+ fetchUser: async () => {
+ const token = useAuthStore.getState().token;
+ if (!token) return;
+
+ try {
+ const user = await api.fetchUser(token);
+ set({ user });
+ } catch (error) {
+ console.error("Failed to refresh user:", error);
+ }
+ },
+
logout: () => {
set({
user: null,
diff --git a/src/stores/useInventoryStore.ts b/src/stores/useInventoryStore.ts
index f96068c..2bb8e76 100644
--- a/src/stores/useInventoryStore.ts
+++ b/src/stores/useInventoryStore.ts
@@ -54,7 +54,7 @@ export const useInventoryStore = create()(
lastActivatedId: itemId,
}),
- activateItemError: (itemId, error) => set({ activatingId: null, error }),
+ activateItemError: (error) => set({ activatingId: null, error }),
clearLastActivated: () => set({ lastActivatedId: null }),
diff --git a/src/stores/useQuestStore.ts b/src/stores/useQuestStore.ts
index 8722543..47c38bb 100644
--- a/src/stores/useQuestStore.ts
+++ b/src/stores/useQuestStore.ts
@@ -2,7 +2,6 @@ import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import type { QuestArc, QuestNode } from "../types/quest";
import { CREW_RANKS } from "../types/quest";
-import { QUEST_ARCS } from "../data/questData";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -58,8 +57,8 @@ interface QuestStore {
export const useQuestStore = create()(
persist(
(set) => ({
- arcs: QUEST_ARCS,
- activeArcId: QUEST_ARCS[0].id,
+ arcs: [],
+ activeArcId: "",
earnedXP: 0,
earnedTitles: [],
diff --git a/src/stores/useSatExam.ts b/src/stores/useSatExam.ts
index 0e19f48..1b7cf05 100644
--- a/src/stores/useSatExam.ts
+++ b/src/stores/useSatExam.ts
@@ -81,7 +81,7 @@ export const useSatExam = create()(
startBreak: () => {
const endTime = Date.now() + BREAK_DURATION * 1000;
- set((state) => ({
+ set(() => ({
phase: "BREAK",
endTime,
questionIndex: 0, // optional: reset question index for next module UX
diff --git a/src/types/search.ts b/src/types/search.ts
index a51429c..cd73e07 100644
--- a/src/types/search.ts
+++ b/src/types/search.ts
@@ -6,6 +6,7 @@ export type SearchItem =
description?: string;
status?: string;
group: string;
+ route: string;
}
| {
type: "route";
@@ -13,4 +14,5 @@ export type SearchItem =
description?: string;
route: string;
group: string;
+ status?: string;
};
diff --git a/src/types/session.ts b/src/types/session.ts
index 69a8236..88a5bd3 100644
--- a/src/types/session.ts
+++ b/src/types/session.ts
@@ -24,21 +24,9 @@ export interface SessionRequest {
section?: string;
}
-// export interface TargetedSessionResponse {
-// id: string;
-// practice_sheet_id: null;
-// status: string;
-// current_module_index: number;
-// current_model_id: null;
-// current_module_title: null;
-// answers: Answer[];
-// started_at: string;
-// score: number;
-// }
-
export interface SessionResponse {
id: string;
- practice_sheet_id: string;
+ practice_sheet_id?: string;
status: string;
current_module_index: number;
current_model_id: string;
diff --git a/src/utils/api.ts b/src/utils/api.ts
index 958acb0..f17bda8 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -95,6 +95,10 @@ class ApiClient {
}
}
+ async fetchUser(token: string): Promise {
+ return this.authenticatedRequest("/auth/me/", token);
+ }
+
// Auth endpoints
async login(credentials: LoginRequest): Promise {
return this.request("/auth/login/", {
diff --git a/src/utils/math.ts b/src/utils/math.ts
new file mode 100644
index 0000000..ccfd101
--- /dev/null
+++ b/src/utils/math.ts
@@ -0,0 +1,17 @@
+export const round = (num: number, decimals: number = 2): number => {
+ return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
+};
+
+export const calculateDistanceSquared = (x1: number, y1: number, x2: number, y2: number): number => {
+ return Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
+};
+
+// Map a coordinate value to an SVG pixel value
+export const scaleToSvg = (val: number, domainMin: number, domainMax: number, rangeMin: number, rangeMax: number) => {
+ return ((val - domainMin) / (domainMax - domainMin)) * (rangeMax - rangeMin) + rangeMin;
+};
+
+// Map an SVG pixel value back to a coordinate value
+export const scaleFromSvg = (val: number, domainMin: number, domainMax: number, rangeMin: number, rangeMax: number) => {
+ return ((val - rangeMin) / (rangeMax - rangeMin)) * (domainMax - domainMin) + domainMin;
+};