diff --git a/app/components/UserTracker.tsx b/app/components/UserTracker.tsx new file mode 100644 index 0000000..dd20012 --- /dev/null +++ b/app/components/UserTracker.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { usePathname } from "next/navigation"; +import Link from "next/link"; +import { CheckCircle2, Circle, LayoutDashboard, ChevronRight, ChevronLeft, Award } from "lucide-react"; +import { + recordVisit, + recordStudyTime, + isChapterCompleted, + toggleChapterCompletion, + SUBJECTS_METADATA, + loadProgressData, + getLevelProgress +} from "@/lib/progressTracker"; + +export default function UserTracker() { + const pathname = usePathname(); + const [isStudyPage, setIsStudyPage] = useState(false); + const [subjectCode, setSubjectCode] = useState(""); + const [chapterId, setChapterId] = useState(""); + const [isCompleted, setIsCompleted] = useState(false); + const [subjectProgress, setSubjectProgress] = useState({ completed: 0, total: 0, percentage: 0 }); + const [levelInfo, setLevelInfo] = useState({ level: 1, rank: "CSE Novice" }); + const [isExpanded, setIsExpanded] = useState(false); + const [showNotification, setShowNotification] = useState(false); + const [notificationMsg, setNotificationMsg] = useState(""); + + const activePageRef = useRef(null); + const activeSubjectRef = useRef(null); + + // 1. Pathname tracking and visit logging + useEffect(() => { + activePageRef.current = pathname; + const parts = pathname.split("/").filter(Boolean); + + // Learning page format: /semX/subjectCode/chapterId + if (parts.length >= 3 && parts[0].startsWith("sem")) { + const sem = parts[0]; + const sub = parts[1]; + const ch = parts[2]; + + const meta = SUBJECTS_METADATA[sub]; + if (meta) { + setIsStudyPage(true); + setSubjectCode(sub); + setChapterId(ch); + activeSubjectRef.current = sub; + + // Record the visit + recordVisit(pathname); + + // Load initial states + setIsCompleted(isChapterCompleted(sub, ch)); + calculateProgress(sub); + updateLevel(); + return; + } + } + + setIsStudyPage(false); + setSubjectCode(""); + setChapterId(""); + activeSubjectRef.current = null; + }, [pathname]); + + // Helper to calculate progress for floating widget + const calculateProgress = (sub: string) => { + const meta = SUBJECTS_METADATA[sub]; + if (!meta) return; + + const data = loadProgressData(); + const completedMap = data.completedChapters[sub] || {}; + const completedCount = meta.chapters.filter((ch) => completedMap[ch.id]).length; + const totalCount = meta.chapters.length; + const percentage = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; + + setSubjectProgress({ + completed: completedCount, + total: totalCount, + percentage + }); + }; + + const updateLevel = () => { + setLevelInfo(getLevelProgress()); + }; + + // 2. Heartbeat timer for active time tracking + useEffect(() => { + const interval = setInterval(() => { + // Only log time if tab is focused, and user is on a study page + if ( + typeof document !== "undefined" && + document.visibilityState === "visible" && + activeSubjectRef.current && + isStudyPage + ) { + // Log 5 seconds of active study + recordStudyTime(5, activeSubjectRef.current); + + // Periodically refresh level info to show level up notifications + const prevLevel = levelInfo.level; + const freshLevel = getLevelProgress(); + if (freshLevel.level > prevLevel) { + triggerNotification(`🎉 Level Up! You are now Level ${freshLevel.level} (${freshLevel.rank})`); + } + setLevelInfo(freshLevel); + } + }, 5000); + + return () => clearInterval(interval); + }, [isStudyPage, levelInfo.level]); + + // 3. Notification system + const triggerNotification = (msg: string) => { + setNotificationMsg(msg); + setShowNotification(true); + setTimeout(() => { + setShowNotification(false); + }, 4500); + }; + + const handleToggleComplete = () => { + if (!subjectCode || !chapterId) return; + + const nextState = toggleChapterCompletion(subjectCode, chapterId); + setIsCompleted(nextState); + calculateProgress(subjectCode); + updateLevel(); + + if (nextState) { + triggerNotification("🌟 Chapter completed! (+50 XP)"); + } else { + triggerNotification("Chapter marked as in-progress."); + } + }; + + if (!isStudyPage) return null; + + const subjectMeta = SUBJECTS_METADATA[subjectCode]; + if (!subjectMeta) return null; + + const chapterTitle = subjectMeta.chapters.find((ch) => ch.id === chapterId)?.title || chapterId; + + return ( + <> + {/* Floating Notifications */} + {showNotification && ( +
+ + {notificationMsg} +
+ )} + + {/* Floating Study Assistant Widget */} +
+ {/* Toggle Button */} + + + {/* Content (only visible when expanded) */} + {isExpanded && ( +
+ {/* Title Details */} +
+
+ + {subjectMeta.name} + + + Lvl {levelInfo.level} + +
+

+ {chapterTitle} +

+
+ + {/* Subject Progress Bar */} +
+
+ Course Progress + {subjectProgress.percentage}% ({subjectProgress.completed}/{subjectProgress.total}) +
+
+
+
+
+ + {/* Actions: Complete/In Progress & Dashboard shortcut */} +
+ + + + + Dashboard + +
+
+ )} +
+ + ); +} diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx index cc69e67..97b30b1 100644 --- a/app/components/navbar.tsx +++ b/app/components/navbar.tsx @@ -54,6 +54,9 @@ export default function Navbar() {
  • QUIZ
  • +
  • + DASHBOARD +
  • {/* Mobile Hamburger */}
    diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..b9f85ad --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,526 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import Navbar from "@/app/components/navbar"; +import { + loadProgressData, + saveProgressData, + getLevelProgress, + getWeeklyStudyDuration, + getMonthlyStudyDurationMinutes, + toggleChapterCompletion, + SUBJECTS_METADATA, + BADGES, + ProgressState +} from "@/lib/progressTracker"; +import { + BookOpen, + Calendar, + Clock, + Award, + ChevronDown, + ChevronUp, + Play, + CheckCircle, + ExternalLink, + RotateCcw +} from "lucide-react"; +import { Righteous } from "next/font/google"; + +const righteous = Righteous({ + subsets: ["latin"], + weight: "400", + variable: "--font-righteous", +}); + +export default function Dashboard() { + const [mounted, setMounted] = useState(false); + const [progressState, setProgressState] = useState(null); + const [levelInfo, setLevelInfo] = useState({ xp: 0, nextLevelXp: 150, level: 1, rank: "CSE Novice" }); + const [weeklyStats, setWeeklyStats] = useState<{ [dayName: string]: number }>({}); + const [monthlyMinutes, setMonthlyMinutes] = useState(0); + const [expandedSubject, setExpandedSubject] = useState(null); + + // Load progress data after client-side hydration completes + useEffect(() => { + setMounted(true); + refreshData(); + }, []); + + const refreshData = () => { + const data = loadProgressData(); + setProgressState(data); + setLevelInfo(getLevelProgress()); + setWeeklyStats(getWeeklyStudyDuration()); + setMonthlyMinutes(getMonthlyStudyDurationMinutes()); + }; + + const handleToggleChapter = (subCode: string, chId: string) => { + toggleChapterCompletion(subCode, chId); + refreshData(); + }; + + const resetAllProgress = () => { + if (confirm("⚠️ Are you sure you want to reset all your study progress, learning time, and badges? This cannot be undone.")) { + const freshState = { + completedChapters: {}, + timeLogs: {}, + subjectTimeLogs: {}, + recentlyOpened: [], + lastActive: null, + completedQuizzes: {}, + unlockedBadges: [], + }; + localStorage.setItem("opencse_progress_v1", JSON.stringify(freshState)); + refreshData(); + } + }; + + if (!mounted || !progressState) { + return ( +
    +
    +
    + Loading your progress... +
    +
    + ); + } + + // Calculate totals + const totalStudySeconds = Object.values(progressState.timeLogs).reduce((acc, curr) => acc + curr, 0); + const totalHours = (totalStudySeconds / 3600).toFixed(1); + + let totalChaptersCompleted = 0; + Object.keys(progressState.completedChapters).forEach((sub) => { + const chs = progressState.completedChapters[sub] || {}; + totalChaptersCompleted += Object.values(chs).filter(Boolean).length; + }); + + const subjects = Object.values(SUBJECTS_METADATA); + + // Calculate total available subjects completed + const completedSubjectsCount = subjects.filter((subj) => { + const completedMap = progressState.completedChapters[subj.code] || {}; + return subj.chapters.every((ch) => completedMap[ch.id]); + }).length; + + // Relative time formatter helper + const getRelativeTime = (isoString: string) => { + const date = new Date(isoString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return "Just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + return `${diffDays}d ago`; + }; + + // Find max value in weekly study times to scale SVG chart heights + const weeklyVals = Object.values(weeklyStats); + const maxWeeklyMin = Math.max(...weeklyVals, 10); // fallback to 10 min scale + + return ( +
    + + +
    + + {/* Header Title Section */} +
    +
    +

    + STUDENT DASHBOARD +

    +

    + Track your college learning stats, manage subjects, and unlock badges. +

    +
    + + +
    + + {/* 1. LEVEL & EXPERIENCE CARD (Hero Banner) */} +
    + + {/* Avatar and Level Circle */} +
    +
    + + {levelInfo.level} + +
    + Level +
    +
    + + {levelInfo.rank} + +
    + + {/* XP Bar and Level Details */} +
    +
    +
    +

    Academic Status

    +

    Level up by completing chapters, studying, and completing quizzes.

    +
    + + {levelInfo.xp} / {levelInfo.nextLevelXp} XP + +
    + + {/* ProgressBar */} +
    +
    +
    + + {/* Quick Stats Grid */} +
    +
    + {totalHours}h + Study Time +
    +
    + {totalChaptersCompleted} + Chapters Done +
    +
    + {completedSubjectsCount} / 12 + Subjects Mastered +
    +
    +
    +
    + + {/* TWO COLUMN GRID */} +
    + + {/* LEFT COLUMN: Study Time Chart & Subject Progress (Span 2) */} +
    + + {/* 2. WEEKLY STUDY TIME CHART */} +
    +
    +

    + + Weekly Activity log +

    +
    + This Month + {monthlyMinutes} mins +
    +
    + + {/* Custom SVG Bar Chart */} +
    + + {/* Grid Lines */} + + + + + {/* Render 7 bars for Mon-Sun */} + {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((day, idx) => { + const min = weeklyStats[day] || 0; + const pct = min / maxWeeklyMin; + const barHeight = Math.max(pct * 110, 4); // minimum 4px height if studied + const x = 30 + idx * 65; + const y = 130 - barHeight; + + return ( + + {/* Bar Gradient Definition inside svg or inline styles */} + + {/* Tooltip on hover */} + + {min}m + + + ); + })} + + {/* Gradients definitions */} + + + + + + + +
    + + {/* Chart Labels */} +
    + {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((day) => ( + + {day} + + ))} +
    +
    + + {/* 3. SUBJECT-WISE & TOPIC-WISE PROGRESS */} +
    +

    + + Subject Progress +

    + +
    + {subjects.map((subj) => { + const completedMap = progressState.completedChapters[subj.code] || {}; + const compCount = subj.chapters.filter((ch) => completedMap[ch.id]).length; + const totCount = subj.chapters.length; + const progressPct = totCount > 0 ? Math.round((compCount / totCount) * 100) : 0; + + const isExpanded = expandedSubject === subj.code; + + return ( +
    + {/* Subject Header */} + + + {/* Expandable Chapters List */} + {isExpanded && ( +
    +

    Topic Checklist

    +
    + {subj.chapters.map((ch) => { + const done = !!completedMap[ch.id]; + const semCode = subj.semester.toLowerCase().replace("semester-", "sem"); + const topicLink = `/${semCode}/${subj.code}/${ch.id}`; + + return ( +
    + {/* Completion Checkbox */} + + + {/* Direct Reading Link */} + + + +
    + ); + })} +
    +
    + )} +
    + ); + })} +
    +
    + +
    + + {/* RIGHT COLUMN: Quick Resume, Recent timeline, Badges (Span 1) */} +
    + + {/* 4. QUICK RESUME */} + {progressState.lastActive && ( +
    +
    + + Quick Resume + + + {/* Parse page components details */} + {(() => { + const parts = progressState.lastActive.split("/").filter(Boolean); + const subCode = parts[1]; + const chId = parts[2]; + const subMeta = SUBJECTS_METADATA[subCode]; + const chTitle = subMeta?.chapters.find(c => c.id === chId)?.title || chId; + + return ( +
    + {subMeta?.name || "Learning"} +

    + {chTitle} +

    + + + + Resume Studying + +
    + ); + })()} +
    + )} + + {/* 5. PREVIOUSLY OPENED FILES / RESOURCES */} +
    +

    + + Recent History +

    + + {progressState.recentlyOpened.length === 0 ? ( +

    No open files logged. Start reading to see your history!

    + ) : ( +
    + {progressState.recentlyOpened.map((item, idx) => ( +
    + {/* Timeline Dot */} +
    + {idx + 1} +
    + + {/* Log details */} +
    + + {item.chapterTitle} + +
    + {item.subjectTitle} + {getRelativeTime(item.timestamp)} +
    +
    +
    + ))} +
    + )} +
    + + {/* 6. EARNED REWARDS AND BADGES */} +
    +

    + + Achievements +

    + + {/* Badges Grid */} +
    + {BADGES.map((badge) => { + const unlocked = progressState.unlockedBadges.includes(badge.id); + + return ( +
    + {/* Badge Icon */} + {badge.icon} + + {/* Tooltip detailing description */} +
    +

    {badge.title}

    +

    {badge.description}

    +

    + {unlocked ? "✓ Unlocked" : "🔒 Locked"} +

    +
    +
    + ); + })} +
    +
    + +
    + +
    + +
    +
    + ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 9104a0e..f09a959 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import "./globals.css"; import Footer from "./components/footer"; import ProgressBar from "./components/ProgressBar"; +import UserTracker from "./components/UserTracker"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -39,6 +40,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen bg-[#1B0D00] text-[#FAE8D7]`} > +
    {children} diff --git a/app/quiz/[slug]/QuizClient.tsx b/app/quiz/[slug]/QuizClient.tsx index f467a04..4304d54 100644 --- a/app/quiz/[slug]/QuizClient.tsx +++ b/app/quiz/[slug]/QuizClient.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import Navbar from "@/app/components/navbar"; import { Righteous, Road_Rage } from "next/font/google"; import type { Quiz, Question } from "@/lib/quizData"; +import { recordQuizCompletion } from "@/lib/progressTracker"; function shuffleArray(items: T[]) { const array = [...items]; @@ -88,6 +89,7 @@ export default function QuizClient({ quiz, inline, onClose, autoStart }: Props) function handleNext() { if (current + 1 >= total) { setState("finished"); + recordQuizCompletion(quiz.slug, score, total); return; } diff --git a/lib/progressTracker.ts b/lib/progressTracker.ts new file mode 100644 index 0000000..c5999ef --- /dev/null +++ b/lib/progressTracker.ts @@ -0,0 +1,640 @@ +// lib/progressTracker.ts + +export interface RecentResource { + pathname: string; + semester: string; + subjectCode: string; + chapterId: string; + subjectTitle: string; + chapterTitle: string; + timestamp: string; +} + +export interface ProgressState { + completedChapters: { + [subjectCode: string]: { + [chapterId: string]: boolean; + }; + }; + timeLogs: { + [dateStr: string]: number; // 'YYYY-MM-DD' -> seconds + }; + subjectTimeLogs: { + [subjectCode: string]: number; // subjectCode -> seconds + }; + recentlyOpened: RecentResource[]; + lastActive: string | null; + completedQuizzes: { + [quizSlug: string]: { + score: number; + total: number; + completedAt: string; + }; + }; + unlockedBadges: string[]; +} + +export interface SubjectMetadata { + code: string; + name: string; + semester: string; + chapters: { id: string; title: string }[]; +} + +// Full metadata for the 12 available subjects on openCSE +export const SUBJECTS_METADATA: Record = { + c: { + code: "c", + name: "C Programming", + semester: "Semester-1", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Computing" }, + { id: "ch2", title: "Overview of C" }, + { id: "ch3", title: "Data Types, I/O, Decision Making and Loops" }, + { id: "ch4", title: "Arrays, Strings, and Functions" }, + { id: "ch5", title: "Pointers, Structures, and Unions" }, + { id: "ch6", title: "File Management, Dynamic Memory, and Preprocessors" }, + ], + }, + em1: { + code: "em1", + name: "Engineering Mathematics-1", + semester: "Semester-1", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Differential Calculus" }, + { id: "ch2", title: "Linear Algebra" }, + { id: "ch3", title: "Ordinary Differential Equations" }, + { id: "ch4", title: "Laplace Transforms" }, + ], + }, + ep: { + code: "ep", + name: "Engineering Physics", + semester: "Semester-1", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Vector Algebra & Fields" }, + { id: "ch2", title: "Electrostatics & Magnetostatics" }, + { id: "ch3", title: "Electrodynamics & Maxwell's Equations" }, + { id: "ch4", title: "Semiconductors & Superconductivity" }, + { id: "ch5", title: "LASERs & Optical Fiber" }, + ], + }, + dsc: { + code: "dsc", + name: "Data Structures using C", + semester: "Semester-2", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Arrays" }, + { id: "ch2", title: "Linked Lists" }, + { id: "ch3", title: "Stacks" }, + ], + }, + em2: { + code: "em2", + name: "Engineering Mathematics-2", + semester: "Semester-2", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Sequences and Series" }, + { id: "ch2", title: "Numerical Analysis" }, + { id: "ch3", title: "Complex Variables" }, + { id: "ch4", title: "Integral Calculus" }, + ], + }, + oops: { + code: "oops", + name: "OOPs with Java", + semester: "Semester-2", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Java" }, + { id: "ch2", title: "Classes and Objects" }, + { id: "ch3", title: "Inheritance & Polymorphism" }, + { id: "ch4", title: "Packages & Interfaces" }, + { id: "ch5", title: "Exception Handling" }, + { id: "ch6", title: "Threads" }, + { id: "ch7", title: "Generics" }, + { id: "ch8", title: "Java Library & Swing GUI" }, + ], + }, + coa: { + code: "coa", + name: "Computer Organization & Architecture", + semester: "Semester-3", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Computer Architecture" }, + { id: "ch2", title: "Performance Analysis" }, + { id: "ch3", title: "MIPS - Language of the Computer" }, + { id: "ch4", title: "Computer Arithmetic" }, + { id: "ch5", title: "Building a Datapath" }, + { id: "ch6", title: "Pipelining" }, + { id: "ch7", title: "Memory Hierarchy" }, + { id: "ch8", title: "Storage and I/O Systems" }, + ], + }, + dbms: { + code: "dbms", + name: "Database Management Systems", + semester: "Semester-4", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Databases" }, + { id: "ch2", title: "Entity-Relationship Model" }, + { id: "ch3", title: "Relational Model and SQL" }, + ], + }, + dops: { + code: "dops", + name: "DevOps & Linux Administration", + semester: "Semester-4", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Linux" }, + { id: "ch2", title: "Linux Terminal & File System" }, + { id: "ch3", title: "Basic Linux Commands" }, + { id: "ch4", title: "Users, Permissions & Packages" }, + { id: "ch5", title: "Shell Scripting Basics" }, + { id: "ch6", title: "Git & GitHub Basics" }, + { id: "ch7", title: "Introduction to DevOps" }, + { id: "ch8", title: "CI/CD, Docker & Cloud Basics" }, + ], + }, + os: { + code: "os", + name: "Operating Systems", + semester: "Semester-4", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Operating Systems" }, + { id: "ch2", title: "Process Management" }, + { id: "ch3", title: "CPU Scheduling" }, + { id: "ch4", title: "Process Synchronization" }, + { id: "ch5", title: "Deadlocks" }, + { id: "ch6", title: "Memory Management" }, + { id: "ch7", title: "Paging and Segmentation" }, + { id: "ch8", title: "File Systems and I/O Management" }, + ], + }, + cd: { + code: "cd", + name: "Compiler Design", + semester: "Semester-5", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Compiler Design" }, + { id: "ch2", title: "Structure & Phases of a Compiler" }, + { id: "ch3", title: "Compiler Writing Tools" }, + { id: "ch4", title: "Lexical Analysis & Tokens" }, + { id: "ch5", title: "Bootstrapping & Cross Compilers" }, + { id: "ch6", title: "Finite Automata & DFA Construction" }, + { id: "ch7", title: "Introduction to Syntax Analysis" }, + { id: "ch8", title: "Top-Down Parsing & LL(1)" }, + { id: "ch9", title: "Bottom-Up Parsing & Shift-Reduce" }, + { id: "ch10", title: "LR(0) & SLR Parsing" }, + { id: "ch11", title: "CLR(1) & LALR Parsing" }, + { id: "ch12", title: "Advanced Parsing & Ambiguity" }, + ], + }, + cle: { + code: "cle", + name: "Cyber Laws and Ethics", + semester: "Semester-5", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Cyber Laws & Ethics" }, + { id: "ch2", title: "Cyber Crimes" }, + { id: "ch3", title: "IT Act 2000 & Amendments" }, + { id: "ch4", title: "Data Privacy & Protection" }, + { id: "ch5", title: "Digital Signatures & IPR" }, + { id: "ch6", title: "Ethical Hacking & Security Ethics" }, + { id: "ch7", title: "Social Media & Internet Ethics" }, + ], + }, + ml: { + code: "ml", + name: "Machine Learning", + semester: "Semester-6", + chapters: [ + { id: "ch0", title: "Course Outline" }, + { id: "ch1", title: "Introduction to Machine Learning" }, + { id: "ch2", title: "Supervised Learning: Regression" }, + { id: "ch3", title: "Supervised Learning: Classification" }, + { id: "ch4", title: "Unsupervised Learning" }, + { id: "ch5", title: "Unit 5: Trends and Applications" }, + { id: "ch6", title: "Unit 6: Advanced Topics & MLOps (Bonus)" }, + ], + }, +}; + +export interface BadgeDefinition { + id: string; + title: string; + description: string; + icon: string; +} + +export const BADGES: BadgeDefinition[] = [ + { + id: "first_chapter", + title: "First Steps", + description: "Visit or complete your first learning chapter", + icon: "🧭", + }, + { + id: "study_novice", + title: "Focused Scholar", + description: "Spend 5 minutes in active study time", + icon: "⏱️", + }, + { + id: "dedicated_learner", + title: "Dedicated Learner", + description: "Spend 30 minutes in active study time", + icon: "📚", + }, + { + id: "quiz_whiz", + title: "Quiz Whiz", + description: "Complete your first inline chapter quiz", + icon: "⚡", + }, + { + id: "consistency_champ", + title: "Consistency Champ", + description: "Study on at least 3 different days", + icon: "🔥", + }, + { + id: "c_programming_master", + title: "C Architect", + description: "Complete all chapters of the C Programming course", + icon: "💻", + }, + { + id: "os_expert", + title: "Kernel Knight", + description: "Complete all chapters of Operating Systems", + icon: "🔧", + }, + { + id: "sem1_conqueror", + title: "Semester 1 Pioneer", + description: "Complete all available subjects in Semester 1", + icon: "🎓", + }, + { + id: "ultimate_scholar", + title: "Grand Scholar", + description: "Complete 100% of at least 3 subjects", + icon: "🏆", + }, +]; + +const LOCAL_STORAGE_KEY = "opencse_progress_v1"; + +const DEFAULT_STATE: ProgressState = { + completedChapters: {}, + timeLogs: {}, + subjectTimeLogs: {}, + recentlyOpened: [], + lastActive: null, + completedQuizzes: {}, + unlockedBadges: [], +}; + +// Safe wrapper for server-side rendering (SSR) environments +function getStorage(): Storage | null { + if (typeof window !== "undefined") { + return window.localStorage; + } + return null; +} + +export function loadProgressData(): ProgressState { + const storage = getStorage(); + if (!storage) return DEFAULT_STATE; + + try { + const raw = storage.getItem(LOCAL_STORAGE_KEY); + if (!raw) { + // Create fresh state + storage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(DEFAULT_STATE)); + return DEFAULT_STATE; + } + const state = JSON.parse(raw) as ProgressState; + // Backfill any missing arrays/objects to prevent runtime crashes + return { + completedChapters: state.completedChapters || {}, + timeLogs: state.timeLogs || {}, + subjectTimeLogs: state.subjectTimeLogs || {}, + recentlyOpened: state.recentlyOpened || [], + lastActive: state.lastActive || null, + completedQuizzes: state.completedQuizzes || {}, + unlockedBadges: state.unlockedBadges || [], + }; + } catch (e) { + console.error("Error loading progress data", e); + return DEFAULT_STATE; + } +} + +export function saveProgressData(state: ProgressState) { + const storage = getStorage(); + if (!storage) return; + try { + storage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state)); + } catch (e) { + console.error("Error saving progress data", e); + } +} + +export function recordVisit(pathname: string) { + // Parse semester, subject code, and chapter from pathname + // Expected formats: /sem1/c/ch1 or /sem6/ml/ch2-data-preprocessing, etc. + const parts = pathname.split("/").filter(Boolean); + if (parts.length < 3 || !parts[0].startsWith("sem")) return; + + const [semester, subjectCode, rawChapterId] = parts; + const subjectMeta = SUBJECTS_METADATA[subjectCode]; + if (!subjectMeta) return; + + // Normalize chapter code (for subpages or subtopics, map to parent chapter) + let chapterId = rawChapterId; + let chapterTitle = rawChapterId; + + // Let's check if rawChapterId matches a predefined chapter, or if it is a subtopic + const directChapter = subjectMeta.chapters.find((ch) => ch.id === rawChapterId); + if (directChapter) { + chapterTitle = directChapter.title; + } else { + // If it's a subtopic, map to its chapter code if it starts with chX + const match = rawChapterId.match(/^(ch\d+)/); + if (match) { + const parentId = match[1]; + const parentCh = subjectMeta.chapters.find((ch) => ch.id === parentId); + if (parentCh) { + chapterId = parentId; + chapterTitle = `${parentCh.title} (${rawChapterId.replace(parentId + "-", "").replace(/-/g, " ")})`; + } + } + } + + const state = loadProgressData(); + state.lastActive = pathname; + + // Build the resource record + const newRecord: RecentResource = { + pathname, + semester, + subjectCode, + chapterId, + subjectTitle: subjectMeta.name, + chapterTitle, + timestamp: new Date().toISOString(), + }; + + // Add to recently opened, filtering duplicates and keeping max 8 + const filtered = state.recentlyOpened.filter((item) => item.pathname !== pathname); + state.recentlyOpened = [newRecord, ...filtered].slice(0, 8); + + // Trigger Badge unlock checks + if (!state.unlockedBadges.includes("first_chapter")) { + state.unlockedBadges.push("first_chapter"); + } + + saveProgressData(state); +} + +export function recordStudyTime(seconds: number, subjectCode?: string) { + if (seconds <= 0) return; + const state = loadProgressData(); + + // 1. Log overall time by date (YYYY-MM-DD) + const today = new Date().toLocaleDateString("en-CA"); // YYYY-MM-DD format + state.timeLogs[today] = (state.timeLogs[today] || 0) + seconds; + + // 2. Log time by subject + if (subjectCode && SUBJECTS_METADATA[subjectCode]) { + state.subjectTimeLogs[subjectCode] = (state.subjectTimeLogs[subjectCode] || 0) + seconds; + } + + // 3. Check for time-based badges + const totalSeconds = Object.values(state.timeLogs).reduce((acc, curr) => acc + curr, 0); + + if (totalSeconds >= 300 && !state.unlockedBadges.includes("study_novice")) { + state.unlockedBadges.push("study_novice"); + } + if (totalSeconds >= 1800 && !state.unlockedBadges.includes("dedicated_learner")) { + state.unlockedBadges.push("dedicated_learner"); + } + + // 4. Check for consistency badge (study on 3 distinct days) + const activeDays = Object.keys(state.timeLogs).filter((date) => state.timeLogs[date] > 30); + if (activeDays.length >= 3 && !state.unlockedBadges.includes("consistency_champ")) { + state.unlockedBadges.push("consistency_champ"); + } + + saveProgressData(state); +} + +export function toggleChapterCompletion(subjectCode: string, chapterId: string): boolean { + const state = loadProgressData(); + + if (!state.completedChapters[subjectCode]) { + state.completedChapters[subjectCode] = {}; + } + + const isCompleted = !state.completedChapters[subjectCode][chapterId]; + state.completedChapters[subjectCode][chapterId] = isCompleted; + + // Perform badge check + checkSubjectCompletionBadges(state, subjectCode); + + saveProgressData(state); + return isCompleted; +} + +export function isChapterCompleted(subjectCode: string, chapterId: string): boolean { + const state = loadProgressData(); + return !!state.completedChapters[subjectCode]?.[chapterId]; +} + +export function recordQuizCompletion(quizSlug: string, score: number, total: number) { + const state = loadProgressData(); + + // Record quiz score (only keep highest score) + const existing = state.completedQuizzes[quizSlug]; + if (!existing || existing.score < score) { + state.completedQuizzes[quizSlug] = { + score, + total, + completedAt: new Date().toISOString(), + }; + } + + // Award quiz whiz badge + if (!state.unlockedBadges.includes("quiz_whiz")) { + state.unlockedBadges.push("quiz_whiz"); + } + + // Automatically mark the chapter as completed if they score >= 70% + const quizToChapterMap: Record = { + "c-intro": { subject: "c", chapter: "ch1" }, + "c-overview": { subject: "c", chapter: "ch2" }, + "c-data-types": { subject: "c", chapter: "ch3" }, + "c-arrays-functions": { subject: "c", chapter: "ch4" }, + "c-pointers-structures": { subject: "c", chapter: "ch5" }, + "c-file-memory-preprocessors": { subject: "c", chapter: "ch6" }, + }; + + const mapping = quizToChapterMap[quizSlug]; + if (mapping && score / total >= 0.7) { + if (!state.completedChapters[mapping.subject]) { + state.completedChapters[mapping.subject] = {}; + } + state.completedChapters[mapping.subject][mapping.chapter] = true; + checkSubjectCompletionBadges(state, mapping.subject); + } + + saveProgressData(state); +} + +function checkSubjectCompletionBadges(state: ProgressState, subjectCode: string) { + const meta = SUBJECTS_METADATA[subjectCode]; + if (!meta) return; + + const completed = state.completedChapters[subjectCode] || {}; + const allChaptersCompleted = meta.chapters.every((ch) => completed[ch.id]); + + if (allChaptersCompleted) { + // 1. Award subject-specific badge + if (subjectCode === "c" && !state.unlockedBadges.includes("c_programming_master")) { + state.unlockedBadges.push("c_programming_master"); + } + if (subjectCode === "os" && !state.unlockedBadges.includes("os_expert")) { + state.unlockedBadges.push("os_expert"); + } + + // 2. Check Semester 1 complete + const sem1Subjects = ["c", "em1", "ep"]; + const sem1Complete = sem1Subjects.every((code) => { + const subMeta = SUBJECTS_METADATA[code]; + const subCompleted = state.completedChapters[code] || {}; + return subMeta.chapters.every((ch) => subCompleted[ch.id]); + }); + if (sem1Complete && !state.unlockedBadges.includes("sem1_conqueror")) { + state.unlockedBadges.push("sem1_conqueror"); + } + + // 3. Count fully completed subjects + let fullyCompletedCount = 0; + for (const code of Object.keys(SUBJECTS_METADATA)) { + const subMeta = SUBJECTS_METADATA[code]; + const subCompleted = state.completedChapters[code] || {}; + const complete = subMeta.chapters.every((ch) => subCompleted[ch.id]); + if (complete) fullyCompletedCount++; + } + + if (fullyCompletedCount >= 3 && !state.unlockedBadges.includes("ultimate_scholar")) { + state.unlockedBadges.push("ultimate_scholar"); + } + } +} + +// Experience points and Leveling +export function getLevelProgress(): { xp: number; nextLevelXp: number; level: number; rank: string } { + const state = loadProgressData(); + + // Compute total XP + let xp = 0; + + // Visit = 5 XP (computed by recently opened resource length + a base if they studied) + const uniqueVisited = state.recentlyOpened.length; + xp += uniqueVisited * 5; + + // Completed chapter = 50 XP + Object.keys(state.completedChapters).forEach((subj) => { + const chapters = state.completedChapters[subj] || {}; + const count = Object.values(chapters).filter(Boolean).length; + xp += count * 50; + }); + + // Completed quiz = 100 XP + Object.keys(state.completedQuizzes).forEach(() => { + xp += 100; + }); + + // Add study hours bonus: 1 XP per 10 seconds studied + const totalSeconds = Object.values(state.timeLogs).reduce((acc, curr) => acc + curr, 0); + xp += Math.floor(totalSeconds / 10); + + // Level calculation: Every 150 XP is a level + const xpPerLevel = 150; + const level = Math.floor(xp / xpPerLevel) + 1; + const xpInCurrentLevel = xp % xpPerLevel; + + // Student Ranks + let rank = "CSE Novice"; + if (level >= 8) rank = "Grandmaster Coder"; + else if (level >= 6) rank = "Algorithmic Sage"; + else if (level >= 4) rank = "System Architect"; + else if (level >= 2) rank = "Code Warrior"; + + return { + xp: xpInCurrentLevel, + nextLevelXp: xpPerLevel, + level, + rank, + }; +} + +export function getWeeklyStudyDuration(): { [dayName: string]: number } { + const state = loadProgressData(); + const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const result: { [dayName: string]: number } = { + Mon: 0, + Tue: 0, + Wed: 0, + Thu: 0, + Fri: 0, + Sat: 0, + Sun: 0, + }; + + // Get last 7 days including today + const today = new Date(); + for (let i = 0; i < 7; i++) { + const date = new Date(today); + date.setDate(today.getDate() - i); + const dateStr = date.toLocaleDateString("en-CA"); // YYYY-MM-DD + const dayName = daysOfWeek[date.getDay()]; + + // Add seconds spent + const seconds = state.timeLogs[dateStr] || 0; + result[dayName] = Math.round(seconds / 60); // convert to minutes + } + + return result; +} + +export function getMonthlyStudyDurationMinutes(): number { + const state = loadProgressData(); + const today = new Date(); + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth(); // 0-indexed + + let totalSeconds = 0; + Object.keys(state.timeLogs).forEach((dateStr) => { + const date = new Date(dateStr); + if (date.getFullYear() === currentYear && date.getMonth() === currentMonth) { + totalSeconds += state.timeLogs[dateStr] || 0; + } + }); + + return Math.round(totalSeconds / 60); +}