From bd35f6a8529c3e00df17e2a738fde57fdac3049d Mon Sep 17 00:00:00 2001 From: shafin-r Date: Thu, 12 Mar 2026 02:39:34 +0600 Subject: [PATCH] chore(build): refactor codebase for production --- src/App.tsx | 5 - src/components/AppSidebar.tsx | 1 - src/components/Calculator.tsx | 2 +- src/components/ChestOpenModal.tsx | 2 +- src/components/InfoHeader.tsx | 1 + src/components/InventoryButton.tsx | 1 - src/components/LessonModal.tsx | 11 +- src/components/PredictedScoreCard.tsx | 1 + src/components/QuestNodeModal.tsx | 1 - src/components/QuestProgressCard.tsx | 507 ------------- src/components/RenderQuestionText.tsx | 1 + src/components/SearchOverlay.tsx | 8 +- .../lessons/BoxPlotComparisonWidget.tsx | 208 ++++-- .../lessons/CircleTheoremsWidget.tsx | 160 ++-- .../lessons/ClauseBreakdownWidget.tsx | 148 +++- .../lessons/ContextEliminationWidget.tsx | 108 ++- src/components/lessons/CoordinatePlane.tsx | 208 ++++-- src/components/lessons/DataClaimWidget.tsx | 442 ++++++++--- src/components/lessons/DecisionTreeWidget.tsx | 221 +++--- .../lessons/ExponentialExplorer.tsx | 198 +++-- .../lessons/HistogramBuilderWidget.tsx | 153 ++-- src/components/lessons/LessonShell.tsx | 1 - .../lessons/LinearTransformationWidget.tsx | 220 ++++-- .../lessons/MultiStepPercentWidget.tsx | 209 ++++-- src/components/lessons/PolygonWidget.tsx | 119 ++- .../lessons/PolynomialBehaviorWidget.tsx | 189 +++-- .../lessons/ProbabilityTreeWidget.tsx | 571 +++++++++----- .../lessons/RadicalSolutionWidget.tsx | 287 ++++--- .../lessons/SimilarityTestsWidget.tsx | 703 +++++++++++++----- src/components/lessons/SimilarityWidget.tsx | 186 +++-- src/components/lessons/UserDashboard.tsx | 378 ---------- src/components/ui/badge.tsx | 19 +- src/components/ui/button.tsx | 2 +- src/components/ui/card.tsx | 26 +- src/components/ui/carousel.tsx | 134 ++-- src/components/ui/dialog.tsx | 42 +- src/components/ui/drawer.tsx | 34 +- src/components/ui/dropdown-menu.tsx | 64 +- src/components/ui/field.tsx | 78 +- src/components/ui/input.tsx | 11 +- src/components/ui/label.tsx | 12 +- src/components/ui/separator.tsx | 14 +- src/components/ui/sheet.tsx | 40 +- src/components/ui/sidebar.tsx | 252 +++---- src/components/ui/skeleton.tsx | 6 +- src/components/ui/table.tsx | 30 +- src/components/ui/tooltip.tsx | 18 +- src/data/questData.ts | 344 --------- src/hooks/useCrewRank.ts | 13 - src/hooks/useSatTimer.ts | 2 +- src/pages/ErrorPage.tsx | 32 - src/pages/auth/Register.tsx | 1 - src/pages/student/Analytics.tsx | 71 -- src/pages/student/Home.tsx | 1 - src/pages/student/Lessons.tsx | 4 +- src/pages/student/Practice.tsx | 2 - src/pages/student/QuestMap.tsx | 29 +- src/pages/student/Rewards.tsx | 9 +- src/pages/student/drills/page.tsx | 7 +- .../student/lessons/AreaVolumeLesson.tsx | 4 +- .../lessons/CirclePropertiesLesson.tsx | 2 +- src/pages/student/lessons/CirclesLesson.tsx | 1 - .../lessons/CongruenceSimilarityLesson.tsx | 2 +- .../student/lessons/DataAnalysisLesson.tsx | 2 +- .../student/lessons/EBRWBoundariesLesson.tsx | 2 +- .../lessons/EBRWCentralIdeasLesson.tsx | 2 +- .../lessons/EBRWCommandEvidenceLesson.tsx | 2 +- .../student/lessons/EBRWCommasLesson.tsx | 2 +- .../lessons/EBRWCraftStructureLesson.tsx | 2 +- .../student/lessons/EBRWCrossTextLesson.tsx | 2 +- .../lessons/EBRWDashesApostrophesLesson.tsx | 2 +- .../lessons/EBRWExplicitMeaningLesson.tsx | 1 + .../lessons/EBRWExpressionIdeasLesson.tsx | 1 + .../lessons/EBRWFormStructureSenseLesson.tsx | 1 + .../lessons/EBRWGraphicDisplaysLesson.tsx | 1 + .../student/lessons/EBRWInferencesLesson.tsx | 1 + .../student/lessons/EBRWMainIdeaLesson.tsx | 1 + .../student/lessons/EBRWPronounsLesson.tsx | 2 +- .../lessons/EBRWRhetoricalSynthesisLesson.tsx | 1 + .../lessons/EBRWSemicolonsColonsLesson.tsx | 2 +- .../lessons/EBRWSentenceStructureLesson.tsx | 2 +- .../student/lessons/EBRWSubjectVerbLesson.tsx | 2 +- .../EBRWTextStructurePurposeLesson.tsx | 2 +- .../student/lessons/EBRWTransitionsLesson.tsx | 2 +- src/pages/student/lessons/EBRWVerbsLesson.tsx | 2 +- .../lessons/EBRWVocabMeaningLesson.tsx | 2 +- .../lessons/EBRWVocabPreciseLesson.tsx | 2 +- .../lessons/EBRWWordsInContextLesson.tsx | 8 +- .../lessons/EquivalentExpressionsLesson.tsx | 1 - .../lessons/EvalStatisticalClaimsLesson.tsx | 1 - .../student/lessons/LinearEq1VarLesson.tsx | 6 +- .../student/lessons/LinearEq2VarLesson.tsx | 10 +- .../student/lessons/LinearEquationsLesson.tsx | 6 +- .../student/lessons/LinearFunctionsLesson.tsx | 3 +- .../lessons/LinearInequalitiesLesson.tsx | 2 - .../LinearParallelPerpendicularLesson.tsx | 2 +- .../lessons/LinearTransformationsLesson.tsx | 2 +- .../lessons/LinesAnglesTrianglesLesson.tsx | 1 - .../student/lessons/NonlinearEq1VarLesson.tsx | 1 - .../student/lessons/OneVariableDataLesson.tsx | 1 - .../lessons/PolynomialFunctionsLesson.tsx | 2 +- .../student/lessons/ProbabilityLesson.tsx | 1 - .../lessons/QuadraticEquationsLesson.tsx | 2 +- .../student/lessons/RationalRadicalLesson.tsx | 2 +- src/pages/student/lessons/RatiosLesson.tsx | 2 +- .../student/lessons/RatiosRatesLesson.tsx | 1 - .../lessons/RightTrianglesTrigLesson.tsx | 1 - .../student/lessons/SampleStatsLesson.tsx | 1 - .../student/lessons/SystemsEq2VarLesson.tsx | 1 - .../lessons/SystemsEquationsLesson.tsx | 2 +- .../student/lessons/SystemsLinearEqLesson.tsx | 2 - src/pages/student/lessons/TrigLesson.tsx | 2 +- src/pages/student/practice/Results.tsx | 12 +- src/pages/student/practice/Test.tsx | 7 +- src/pages/student/targeted-practice/page.tsx | 17 +- src/stores/authStore.ts | 13 + src/stores/useInventoryStore.ts | 2 +- src/stores/useQuestStore.ts | 5 +- src/stores/useSatExam.ts | 2 +- src/types/search.ts | 2 + src/types/session.ts | 14 +- src/utils/api.ts | 4 + src/utils/math.ts | 17 + 123 files changed, 3501 insertions(+), 3254 deletions(-) delete mode 100644 src/components/QuestProgressCard.tsx delete mode 100644 src/components/lessons/UserDashboard.tsx delete mode 100644 src/data/questData.ts delete mode 100644 src/hooks/useCrewRank.ts delete mode 100644 src/pages/ErrorPage.tsx delete mode 100644 src/pages/student/Analytics.tsx create mode 100644 src/utils/math.ts diff --git a/src/App.tsx b/src/App.tsx index 25f6ca5..8f07a0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,6 @@ import { StudentLayout } from "./pages/student/StudentLayout"; import { TargetedPractice } from "./pages/student/targeted-practice/page"; import { Drills } from "./pages/student/drills/page"; import { HardTestModules } from "./pages/student/hard-test-modules/page"; -import { Analytics } from "./pages/student/Analytics"; import { QuestMap } from "./pages/student/QuestMap"; import { Register } from "./pages/auth/Register"; @@ -61,10 +60,6 @@ function App() { path: "profile", element: , }, - { - path: "analytics", - element: , - }, { path: "quests", element: , diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 29ec224..0c2c28e 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -14,7 +14,6 @@ import { ChevronDown, BookOpen, Home, - Video, Target, Zap, Trophy, diff --git a/src/components/Calculator.tsx b/src/components/Calculator.tsx index f7b702b..b343f49 100644 --- a/src/components/Calculator.tsx +++ b/src/components/Calculator.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useCallback } from "react"; +import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { X, Calculator, Maximize2, Minimize2 } from "lucide-react"; diff --git a/src/components/ChestOpenModal.tsx b/src/components/ChestOpenModal.tsx index 4456836..317ff6e 100644 --- a/src/components/ChestOpenModal.tsx +++ b/src/components/ChestOpenModal.tsx @@ -417,7 +417,7 @@ interface Props { onClose: () => void; } -export const ChestOpenModal = ({ node, claimResult, onClose }: Props) => { +export const ChestOpenModal = ({ claimResult, onClose }: Props) => { const [phase, setPhase] = useState("idle"); const [showXP, setShowXP] = useState(false); const timerRef = useRef | null>(null); diff --git a/src/components/InfoHeader.tsx b/src/components/InfoHeader.tsx index 1a2a4f2..dfb2338 100644 --- a/src/components/InfoHeader.tsx +++ b/src/components/InfoHeader.tsx @@ -773,6 +773,7 @@ export const InfoHeader = ({ onViewAll, mode = "DEFAULT" }: Props) => {

{roleLabel}

+ {/* @ts-ignore */} diff --git a/src/components/InventoryButton.tsx b/src/components/InventoryButton.tsx index 46796ca..4c2f584 100644 --- a/src/components/InventoryButton.tsx +++ b/src/components/InventoryButton.tsx @@ -3,7 +3,6 @@ import { useInventoryStore, getLiveEffects, formatTimeLeft, - hasActiveEffect, } from "../stores/useInventoryStore"; import { InventoryModal } from "./InventoryModal"; diff --git a/src/components/LessonModal.tsx b/src/components/LessonModal.tsx index 8bfbd78..f720daa 100644 --- a/src/components/LessonModal.tsx +++ b/src/components/LessonModal.tsx @@ -23,14 +23,6 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const isVideoLesson = (id: string) => UUID_REGEX.test(id); -function getLocalLessonTitle(lessonId: string): string { - const comp = LESSON_COMPONENT_MAP[lessonId as LessonId] as any; - if (comp?.displayName) return comp.displayName; - return lessonId - .replace(/[-_]/g, " ") - .replace(/\b\w/g, (c) => c.toUpperCase()); -} - const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap'); @@ -207,11 +199,12 @@ export const LessonModal = ({ const authStorage = localStorage.getItem("auth-storage"); if (!authStorage) throw new Error("No auth storage"); const { + // @ts-ignore state: { token }, } = JSON.parse(authStorage) as { state?: { token?: string } }; if (!token) throw new Error("No token"); - // fetchLessonById returns LessonDetails directly + // @ts-ignore const response: LessonDetails = await api.fetchLessonById( token, lessonId, diff --git a/src/components/PredictedScoreCard.tsx b/src/components/PredictedScoreCard.tsx index 842c001..1f131c8 100644 --- a/src/components/PredictedScoreCard.tsx +++ b/src/components/PredictedScoreCard.tsx @@ -281,6 +281,7 @@ const SectionDetail = ({
+ {/* @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 */} -
-
-
-
- {nextRankLabel} -
- - {/* 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 ? ( - - ) : ( - - )} -
- ); - }) - )} -
- - {/* 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 */} -
- 0102030405060 -
+
+ + + + {/* Axis */} +
+ 0 + 10 + 20 + 30 + 40 + 50 + 60 +
+
+ +
+
+
+ + setShift(parseInt(e.target.value))} + className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600" + /> +
+
+ + setSpread(parseFloat(e.target.value))} + className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600" + /> +
-
-
-
- - setShift(parseInt(e.target.value))} className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600"/> -
-
- - 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 && (
- {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) => ( -
-
+
+
{s.name}
))} @@ -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) => ( -
-
+
+
{s.name}
))} @@ -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 ( - - ); - })} + {(["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 ( + + ); + }, + )}
{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

-
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' } @@ -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 &&
} -
- - + ); + })() + : /* Decision question */ + (() => { + const node = lastStep.node; + return ( +
+

+ {node.question} +

+ {node.hint && ( +

{node.hint}

+ )} + {!node.hint &&
} +
+ + +
-
- ); - })() - )} + ); + })()}
{/* Footer */} @@ -221,14 +262,16 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' } Try again - {atLeaf && scenarios.length > 1 && activeScenario < scenarios.length - 1 && ( - - )} + {atLeaf && + scenarios.length > 1 && + activeScenario < scenarios.length - 1 && ( + + )}
); 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} +
+
-
-
- - setA(parseFloat(e.target.value))} className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"/> -
-
- - 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)"}

-
-
- - setK(parseFloat(e.target.value))} className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600"/> -
-
-
+
+
+ + setA(parseFloat(e.target.value))} + className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600" + /> +
+
+ + 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)"} +

+
+
+ + 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

-
- - -
-
+
+

Test Scores Distribution

+
+ + +
+
-
- {/* 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)} +

+
-
-
- - 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) -
-
+
+
+ + 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) +
+
-
- - setK(parseInt(e.target.value))} - className="w-full h-2 bg-emerald-100 rounded-lg appearance-none cursor-pointer accent-emerald-600 mt-1" - /> -
+
+ + setK(parseInt(e.target.value))} + className="w-full h-2 bg-emerald-100 rounded-lg appearance-none cursor-pointer accent-emerald-600 mt-1" + /> +
-
- -
-
-
+
+ +
+
+
-
-
- - - - - - - - - {/* 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 (
-
-
-
- -
- setChange1(parseInt(e.target.value))} - className="flex-1 accent-indigo-600" - /> - {change1 > 0 ? '+' : ''}{change1}% -
-
-
- -
- setChange2(parseInt(e.target.value))} - className="flex-1 accent-rose-600" - /> - {change2 > 0 ? '+' : ''}{change2}% -
-
-
+
+
+
+ +
+ setChange1(parseInt(e.target.value))} + className="flex-1 accent-indigo-600" + /> + + {change1 > 0 ? "+" : ""} + {change1}% + +
+
+
+ +
+ 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 (
- - 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}°
-
+ + 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)

-
- - -
-
-
-

Leading Coefficient

-
- - -
-
+
+
+

+ Degree (Highest Power) +

+
+ + +
+
+

+ Leading Coefficient +

+
+ + +
+
+
-
- - - - - - - - - - - - - - - - -
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 */} -
-
-
- -
- - {initR} - -
-
-
- -
- - {initB} - -
-
-
+ {/* Controls */} +
+
+
+ +
+ + {initR} + +
+
+
+ +
+ + {initB} + +
+
+
-
- - -
-
+
+ + +
+
-
- - {/* 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} -
-
- -
- - 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}
+
+ +
+ + 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 = () => {
- {(['AA', 'SAS', 'SSS'] as Mode[]).map(m => ( + {(["AA", "SAS", "SSS"] as Mode[]).map((m) => (
- Scale (k) - setScale(parseFloat(e.target.value))} - className="w-24 h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-rose-600" - /> - {scale.toFixed(1)}x + + Scale (k) + + setScale(parseFloat(e.target.value))} + className="w-24 h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-rose-600" + /> + + {scale.toFixed(1)}x +
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 */} -
-
- -

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()} /> - - -
- ) : ( - <> -

{user?.displayName || username}

- - {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

-
-
- -
- {pwMsg && ( -
- {pwMsg.type === 'success' ? : } - {pwMsg.text} -
- )} - -
- - setCurrentPassword(e.target.value)} - className="w-full px-3 py-2 pr-9 text-sm border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-cyan-400" required /> - -
- -
- - setNewPassword(e.target.value)} - className="w-full px-3 py-2 pr-9 text-sm border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-cyan-400" required minLength={4} /> - -
- -
- - setConfirmPassword(e.target.value)} - className="w-full px-3 py-2 text-sm border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-cyan-400" required minLength={4} /> -
- - -
-
- -
-
- ); -} 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 ( - ) + ); } function CarouselNext({ @@ -205,7 +205,7 @@ function CarouselNext({ size = "icon", ...props }: React.ComponentProps) { - const { orientation, scrollNext, canScrollNext } = useCarousel() + const { orientation, scrollNext, canScrollNext } = useCarousel(); return ( - ) + ); } 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 ( - ) + ); } function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return (
- ) + ); } 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}

- - -
-
- ); -} 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; +};