"use client"; import React, { useEffect, useMemo, useState } from "react"; import { motion } from "framer-motion"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Progress } from "@/components/ui/progress"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Switch } from "@/components/ui/switch"; import { CalendarDays, CheckCircle2, Flame, RefreshCcw, Target, TrendingUp } from "lucide-react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RTooltip, ResponsiveContainer, Legend, } from "recharts"; interface DayPlan { date: string; planned: number; actual: number; } interface PlanState { totalPages: number; startDate: string; endDate: string; includeWeekends: boolean; days: DayPlan[]; } function toYMD(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function fromYMD(s: string): Date { const [y, m, d] = s.split("-").map(Number); return new Date(y, (m || 1) - 1, d || 1); } function isWeekend(d: Date): boolean { const day = d.getDay(); return day === 0 || day === 6; } function eachDay(start: Date, end: Date, includeWeekends = true): string[] { const out: string[] = []; const cur = new Date(start); cur.setHours(0, 0, 0, 0); const last = new Date(end); last.setHours(0, 0, 0, 0); while (cur <= last) { if (includeWeekends || !isWeekend(cur)) { out.push(toYMD(cur)); } cur.setDate(cur.getDate() + 1); } return out; } function distribute(total: number, buckets: number): number[] { if (buckets <= 0) return []; const base = Math.floor(total / buckets); const remainder = total - base * buckets; const arr = Array.from({ length: buckets }, (_, i) => base + (i < remainder ? 1 : 0)); return arr; } function formatPretty(s: string): string { try { const d = fromYMD(s); return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }); } catch { return s; } } const LS_KEY = "study-tracker-plan-v1"; function savePlan(p: PlanState) { if (typeof window === "undefined") return; localStorage.setItem(LS_KEY, JSON.stringify(p)); } function loadPlan(): PlanState | null { if (typeof window === "undefined") return null; const raw = localStorage.getItem(LS_KEY); if (!raw) return null; try { const parsed = JSON.parse(raw) as PlanState; return parsed; } catch { return null; } } export default function StudyTrackerApp() { const todayYMD = toYMD(new Date()); const [totalPages, setTotalPages] = useState(200); const [endDate, setEndDate] = useState(toYMD(new Date(Date.now() + 7 * 86400000))); const [includeWeekends, setIncludeWeekends] = useState(true); const [days, setDays] = useState([]); useEffect(() => { const stored = loadPlan(); if (stored) { setTotalPages(stored.totalPages); setEndDate(stored.endDate); setIncludeWeekends(stored.includeWeekends); setDays(stored.days); } else { buildPlan(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function buildPlan() { const start = new Date(); start.setHours(0, 0, 0, 0); const end = fromYMD(endDate); end.setHours(0, 0, 0, 0); if (isNaN(totalPages) || totalPages <= 0 || end < start) { setDays([]); return; } const slots = eachDay(start, end, includeWeekends); const allocation = distribute(totalPages, slots.length); const newDays: DayPlan[] = slots.map((ymd, i) => { const existing = days.find((d) => d.date === ymd); return { date: ymd, planned: allocation[i] || 0, actual: existing?.actual || 0, }; }); setDays(newDays); const state: PlanState = { totalPages, startDate: todayYMD, endDate, includeWeekends, days: newDays, }; savePlan(state); } useEffect(() => { const state: PlanState = { totalPages, startDate: todayYMD, endDate, includeWeekends, days, }; savePlan(state); }, [days, totalPages, endDate, includeWeekends, todayYMD]); const stats = useMemo(() => { const studied = days.reduce((sum, d) => sum + (d.actual || 0), 0); const planned = days.reduce((sum, d) => sum + (d.planned || 0), 0); const pct = planned > 0 ? Math.min(100, Math.round((studied / planned) * 100)) : 0; const remaining = Math.max(0, planned - studied); const avgPerDay = days.length ? Math.round(planned / days.length) : 0; return { studied, planned, pct, remaining, avgPerDay }; }, [days]); const chartData = useMemo(() => { let cumPlanned = 0; let cumActual = 0; return days.map((d) => { cumPlanned += d.planned; cumActual += d.actual || 0; return { date: formatPretty(d.date), Planned: cumPlanned, Actual: cumActual, }; }); }, [days]); function updateActual(ymd: string, value: number) { setDays((prev) => prev.map((d) => (d.date === ymd ? { ...d, actual: Math.max(0, Math.floor(value || 0)) } : d))); } function bumpActual(ymd: string, inc: number) { setDays((prev) => prev.map((d) => (d.date === ymd ? { ...d, actual: Math.max(0, (d.actual || 0) + inc) } : d))); } function resetProgress() { setDays((prev) => prev.map((d) => ({ ...d, actual: 0 }))); } function resetAll() { setTotalPages(200); setEndDate(toYMD(new Date(Date.now() + 7 * 86400000))); setIncludeWeekends(true); setDays([]); if (typeof window !== "undefined") localStorage.removeItem(LS_KEY); buildPlan(); } const deadlinePassed = new Date(endDate).setHours(0, 0, 0, 0) < new Date().setHours(0, 0, 0, 0); return (

Study Tracker

Split your pages, track your progress, and finish on time.

Plan your study
setTotalPages(Math.max(0, Math.floor(Number(e.target.value) || 0)))} />
setEndDate(e.target.value)} />
Include weekends
Count Saturdays & Sundays in the plan
{deadlinePassed && (
The deadline date is in the past. Please choose a future date.
)}
Overall progress
{stats.studied} / {stats.planned} pages {stats.pct}%
{stats.remaining} pages remaining
Pace
Average planned per day: {stats.avgPerDay}
Days in plan: {days.length}
Finish by: {formatPretty(endDate)}
Streaks
Daily Progress chart Planned vs. Actual (cumulative) {chartData.length > 0 ? ( ) : (
Create a plan to see the chart.
)}
{days.map((d) => { const met = d.actual >= d.planned && d.planned > 0; return (
{formatPretty(d.date)}
Planned

Pages planned for this day

Target: {d.planned} pages
updateActual(d.date, Number(e.target.value))} />
{met && (
Great job! You hit today’s target 🎉
)}
); })}
Data is stored locally in your browser. Coming next: user accounts and cloud sync.
); } function StreakBadge({ days }: { days: DayPlan[] }) { const today = toYMD(new Date()); const upToToday = days.filter((d) => d.date <= today); let streak = 0; for (let i = upToToday.length - 1; i >= 0; i--) { const d = upToToday[i]; if (d.actual >= d.planned && d.planned > 0) streak++; else break; } const best = (() => { let bestStreak = 0; let cur = 0; for (const d of upToToday) { if (d.actual >= d.planned && d.planned > 0) cur++; else { bestStreak = Math.max(bestStreak, cur); cur = 0; } } return Math.max(bestStreak, cur); })(); return (
Current streak
{streak}🔥
Best streak
{best}
); }