"use client"; import React, { useMemo, useState, useEffect, useRef, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import Link from "next/link"; import { useParams } from "next/navigation"; import TopStatsBar from "@/components/shared/TopStatsBar"; import useCourseStore from "@/stores/useCourseStore"; import type { RemedyNode } from "@/stores/useCourseStore"; import useUserStore from "@/stores/useUserStore"; /* ═══════════════════ Fallback mock nodes (for demo courseIds with no mock data) ═══════════════════ */ interface MapNode { id: string; title: string; status: "completed" | "available" | "locked"; x: number; y: number; } /* ── Per-library-course metadata: each course has its own independent title + nodes ── */ const LIBRARY_COURSE_META: Record = { ja: { title: "Japanese", nodes: [ { id: "ja-n1", title: "Hiragana Basics", status: "completed" }, { id: "ja-n2", title: "Katakana Basics", status: "available" }, { id: "ja-n3", title: "Greetings & Self-Introduction", status: "locked" }, { id: "ja-n4", title: "Numbers & Counting", status: "locked" }, { id: "ja-n5", title: "Basic Particles (は・が・を)", status: "locked" }, { id: "ja-n6", title: "Daily Conversation", status: "locked" }, { id: "ja-n7", title: "Verb Conjugation", status: "locked" }, { id: "ja-n8", title: "Adjectives & Descriptions", status: "locked" }, { id: "ja-n9", title: "Kanji Level 1", status: "locked" }, { id: "ja-n10", title: "Reading Comprehension", status: "locked" }, { id: "ja-n11", title: "Listening Practice", status: "locked" }, { id: "ja-n12", title: "Travel Japanese", status: "locked" }, { id: "ja-n13", title: "Polite vs Casual Speech", status: "locked" }, { id: "ja-n14", title: "Culture & Etiquette", status: "locked" }, { id: "ja-n15", title: "Final Challenge", status: "locked" }, ], }, es: { title: "Spanish", nodes: [ { id: "es-n1", title: "Alphabet & Pronunciation", status: "completed" }, { id: "es-n2", title: "Greetings & Introductions", status: "available" }, { id: "es-n3", title: "Articles & Gender", status: "locked" }, { id: "es-n4", title: "Basic Verbs (Ser/Estar)", status: "locked" }, { id: "es-n5", title: "Numbers & Time", status: "locked" }, { id: "es-n6", title: "Food & Ordering", status: "locked" }, { id: "es-n7", title: "Present Tense Conjugation", status: "locked" }, { id: "es-n8", title: "Prepositions & Directions", status: "locked" }, { id: "es-n9", title: "Past Tense (Pretérito)", status: "locked" }, { id: "es-n10", title: "Shopping & Money", status: "locked" }, { id: "es-n11", title: "Subjunctive Mood", status: "locked" }, { id: "es-n12", title: "Travel Conversation", status: "locked" }, { id: "es-n13", title: "Reading Practice", status: "locked" }, { id: "es-n14", title: "Culture & Idioms", status: "locked" }, { id: "es-n15", title: "Final Challenge", status: "locked" }, ], }, py: { title: "Python", nodes: [ { id: "py-n1", title: "Variables & Data Types", status: "completed" }, { id: "py-n2", title: "Control Flow (if/else)", status: "available" }, { id: "py-n3", title: "Loops (for/while)", status: "locked" }, { id: "py-n4", title: "Functions & Scope", status: "locked" }, { id: "py-n5", title: "Lists & Tuples", status: "locked" }, { id: "py-n6", title: "Dictionaries & Sets", status: "locked" }, { id: "py-n7", title: "String Manipulation", status: "locked" }, { id: "py-n8", title: "File I/O", status: "locked" }, { id: "py-n9", title: "Error Handling", status: "locked" }, { id: "py-n10", title: "Modules & Packages", status: "locked" }, { id: "py-n11", title: "List Comprehensions", status: "locked" }, { id: "py-n12", title: "OOP Basics", status: "locked" }, { id: "py-n13", title: "Decorators & Generators", status: "locked" }, { id: "py-n14", title: "Testing with pytest", status: "locked" }, { id: "py-n15", title: "Final Project", status: "locked" }, ], }, py2: { title: "Python Advanced", nodes: [ { id: "py2-n1", title: "Async & Concurrency", status: "completed" }, { id: "py2-n2", title: "Metaprogramming", status: "available" }, { id: "py2-n3", title: "Design Patterns", status: "locked" }, { id: "py2-n4", title: "Type Hints & Mypy", status: "locked" }, { id: "py2-n5", title: "Performance Optimization", status: "locked" }, { id: "py2-n6", title: "Context Managers", status: "locked" }, { id: "py2-n7", title: "Networking & APIs", status: "locked" }, { id: "py2-n8", title: "Data Processing (Pandas)", status: "locked" }, { id: "py2-n9", title: "Database Integration", status: "locked" }, { id: "py2-n10", title: "Web Scraping", status: "locked" }, { id: "py2-n11", title: "CLI Tools (Click/Typer)", status: "locked" }, { id: "py2-n12", title: "Package Publishing", status: "locked" }, { id: "py2-n13", title: "CI/CD Pipelines", status: "locked" }, { id: "py2-n14", title: "Security Best Practices", status: "locked" }, { id: "py2-n15", title: "Capstone Project", status: "locked" }, ], }, gen: { title: "Study Skills", nodes: [ { id: "gen-n1", title: "Active Recall", status: "completed" }, { id: "gen-n2", title: "Spaced Repetition", status: "available" }, { id: "gen-n3", title: "Note-Taking Methods", status: "locked" }, { id: "gen-n4", title: "Mind Mapping", status: "locked" }, { id: "gen-n5", title: "Pomodoro Technique", status: "locked" }, { id: "gen-n6", title: "Goal Setting (SMART)", status: "locked" }, { id: "gen-n7", title: "Critical Thinking", status: "locked" }, { id: "gen-n8", title: "Speed Reading", status: "locked" }, { id: "gen-n9", title: "Memory Palace", status: "locked" }, { id: "gen-n10", title: "Group Study Strategies", status: "locked" }, { id: "gen-n11", title: "Test Preparation", status: "locked" }, { id: "gen-n12", title: "Time Management", status: "locked" }, { id: "gen-n13", title: "Self-Assessment", status: "locked" }, { id: "gen-n14", title: "Growth Mindset", status: "locked" }, { id: "gen-n15", title: "Final Review", status: "locked" }, ], }, ml: { title: "ML Basics", nodes: [ { id: "ml-n1", title: "What is Machine Learning?", status: "completed" }, { id: "ml-n2", title: "Supervised vs Unsupervised", status: "available" }, { id: "ml-n3", title: "Linear Regression", status: "locked" }, { id: "ml-n4", title: "Logistic Regression", status: "locked" }, { id: "ml-n5", title: "Decision Trees", status: "locked" }, { id: "ml-n6", title: "K-Nearest Neighbors", status: "locked" }, { id: "ml-n7", title: "Support Vector Machines", status: "locked" }, { id: "ml-n8", title: "Neural Network Intro", status: "locked" }, { id: "ml-n9", title: "Training & Validation", status: "locked" }, { id: "ml-n10", title: "Overfitting & Regularization", status: "locked" }, { id: "ml-n11", title: "Feature Engineering", status: "locked" }, { id: "ml-n12", title: "Model Evaluation Metrics", status: "locked" }, { id: "ml-n13", title: "Ensemble Methods", status: "locked" }, { id: "ml-n14", title: "Deploying Models", status: "locked" }, { id: "ml-n15", title: "Final Challenge", status: "locked" }, ], }, ds: { title: "Data Science", nodes: [ { id: "ds-n1", title: "Data Science Overview", status: "completed" }, { id: "ds-n2", title: "Data Collection & Cleaning", status: "available" }, { id: "ds-n3", title: "Exploratory Data Analysis", status: "locked" }, { id: "ds-n4", title: "Statistical Foundations", status: "locked" }, { id: "ds-n5", title: "Data Visualization", status: "locked" }, { id: "ds-n6", title: "Hypothesis Testing", status: "locked" }, { id: "ds-n7", title: "Regression Analysis", status: "locked" }, { id: "ds-n8", title: "Clustering Techniques", status: "locked" }, { id: "ds-n9", title: "Time Series Analysis", status: "locked" }, { id: "ds-n10", title: "Natural Language Processing", status: "locked" }, { id: "ds-n11", title: "Big Data Tools", status: "locked" }, { id: "ds-n12", title: "Dashboard & Reporting", status: "locked" }, { id: "ds-n13", title: "A/B Testing", status: "locked" }, { id: "ds-n14", title: "Ethics in Data Science", status: "locked" }, { id: "ds-n15", title: "Capstone Project", status: "locked" }, ], }, }; const FALLBACK_NODES: { id: string; title: string; status: "completed" | "available" | "locked" }[] = [ { id: "demo-n1", title: "Introduction", status: "completed" }, { id: "demo-n2", title: "Core Concepts", status: "available" }, { id: "demo-n3", title: "Deep Dive", status: "locked" }, { id: "demo-n4", title: "Practice Lab", status: "locked" }, { id: "demo-n5", title: "Advanced Topics", status: "locked" }, { id: "demo-n6", title: "Connections", status: "locked" }, { id: "demo-n7", title: "Analysis", status: "locked" }, { id: "demo-n8", title: "Synthesis", status: "locked" }, { id: "demo-n9", title: "Application", status: "locked" }, { id: "demo-n10", title: "Case Study", status: "locked" }, { id: "demo-n11", title: "Research", status: "locked" }, { id: "demo-n12", title: "Review", status: "locked" }, { id: "demo-n13", title: "Mastery", status: "locked" }, { id: "demo-n14", title: "Integration", status: "locked" }, { id: "demo-n15", title: "Final Challenge", status: "locked" }, ]; /* ═══════════════════ Page ═══════════════════ */ export default function MapClient() { const params = useParams(); const courseId = params.courseId as string; const loadMockCourses = useCourseStore((s) => s.loadMockCourses); const getCourse = useCourseStore((s) => s.getCourse); const getNodesForCourse = useCourseStore((s) => s.getNodesForCourse); const getRemedyNodesForCourse = useCourseStore((s) => s.getRemedyNodesForCourse); const setLastActiveCourse = useUserStore((s) => s.setLastActiveCourse); const [loaded, setLoaded] = useState(false); useEffect(() => { loadMockCourses(); setLoaded(true); }, [loadMockCourses]); /* Lock body scroll while map page is mounted (zoom 1.05 causes overflow) */ useEffect(() => { const prev = document.documentElement.style.overflow; document.documentElement.style.overflow = "hidden"; document.body.style.overflow = "hidden"; return () => { document.documentElement.style.overflow = prev; document.body.style.overflow = ""; }; }, []); useEffect(() => { if (courseId) setLastActiveCourse(courseId); }, [courseId, setLastActiveCourse]); const course = getCourse(courseId); const storeNodes = getNodesForCourse(courseId); // Build MapNode array – use store data if available, otherwise fallback // Dynamic positions: zigzag pattern, each node spaced vertically const NODES: MapNode[] = useMemo(() => { const X_PATTERN = [50, 28, 68, 32, 58, 40, 65, 30, 55, 42, 62, 35, 58, 45, 50]; const buildPositions = (nodes: { id: string; title: string; status: "completed" | "available" | "locked" }[]) => { const count = nodes.length; const spacing = 120; // px per node return nodes.map((n, i) => ({ id: n.id, title: n.title, status: n.status, x: X_PATTERN[i % X_PATTERN.length], y: (count - 1 - i) * spacing + 60, // bottom-to-top, first node at bottom })); }; if (storeNodes.length > 0) { return buildPositions(storeNodes); } if (!loaded) return []; // Use per-course fallback nodes if available const libMeta = LIBRARY_COURSE_META[courseId]; if (libMeta) return buildPositions(libMeta.nodes); return buildPositions(FALLBACK_NODES); }, [storeNodes, loaded, courseId]); // Total height for scrollable map const mapHeight = Math.max(560, NODES.length * 120 + 120); /* ── Scroll to bottom on load ── */ const mapContainerRef = useRef(null); useEffect(() => { if (NODES.length > 0 && mapContainerRef.current) { mapContainerRef.current.scrollTop = mapContainerRef.current.scrollHeight; } }, [NODES.length]); /* ── Mouse drag to pan ── */ const isDragging = useRef(false); const dragStart = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 }); const handleMouseDown = useCallback((e: React.MouseEvent) => { const container = mapContainerRef.current; if (!container) return; isDragging.current = true; dragStart.current = { x: e.clientX, y: e.clientY, scrollLeft: container.scrollLeft, scrollTop: container.scrollTop, }; container.style.cursor = "grabbing"; container.style.userSelect = "none"; }, []); const handleMouseMove = useCallback((e: React.MouseEvent) => { if (!isDragging.current) return; const container = mapContainerRef.current; if (!container) return; const dx = e.clientX - dragStart.current.x; const dy = e.clientY - dragStart.current.y; container.scrollTop = dragStart.current.scrollTop - dy; container.scrollLeft = dragStart.current.scrollLeft - dx; }, []); const handleMouseUp = useCallback(() => { isDragging.current = false; const container = mapContainerRef.current; if (container) { container.style.cursor = "grab"; container.style.userSelect = ""; } }, []); const pageTitle = course?.unitTitle ?? LIBRARY_COURSE_META[courseId]?.title ?? "Course Map"; const remedyNodes = getRemedyNodesForCourse(courseId); return (
{/* Background effects */}
{/* ── Center: Map canvas (drag to pan, hidden scrollbar) ── */}
{/* SVG path connecting nodes */} {NODES.slice(0, -1).map((node, i) => { const next = NODES[i + 1]; const nx = node.x; const ny = node.y; const nnx = next.x; const nny = next.y; const mx = (nx + nnx) / 2; const my = (ny + nny) / 2; const isActive = node.status !== "locked" || next.status !== "locked"; return ( ); })} {/* Nodes */} {NODES.map((node, i) => { const remedy = remedyNodes.find((r) => r.sourceNodeId === node.id); return ( ); })}
{/* ── Right: AI Chat Assistant ── */}
); } /* ═══════════════════ AI Chat Assistant ═══════════════════ */ interface ChatMessage { id: number; role: "user" | "assistant"; text: string; } function AIChatAssistant() { const [input, setInput] = useState(""); const [messages, setMessages] = useState([]); const chatEndRef = React.useRef(null); useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleSend = () => { const trimmed = input.trim(); if (!trimmed) return; const userMsg: ChatMessage = { id: Date.now(), role: "user", text: trimmed }; const botMsg: ChatMessage = { id: Date.now() + 1, role: "assistant", text: trimmed }; setMessages((prev) => [...prev, userMsg, botMsg]); setInput(""); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return ( {/* Header */}
{/* Owl avatar */}

AI Chat Assistant

Ask me anything about this course!

{/* Chat messages area */}
{messages.length === 0 && (

Type a message to start chatting with your AI study companion!

)} {messages.map((msg) => (
{msg.text}
))}
{/* Divider */}
{/* Input area */}