'use client'; import { useState, useEffect, useMemo } from 'react'; import Link from 'next/link'; import { useTimerStore } from '@/lib/store'; import { computeStreak } from '@/lib/stats'; import { StatsView } from '@/components/StatsView'; import { StreakCard } from '@/components/StreakCard'; import { TimerCard } from '@/components/TimerCard'; import { downloadExportAll, readFileAsText, parseImportData, importTimers, importRoutines } from '@/lib/export'; import { useRoutineStore } from '@/lib/routine-store'; import { importCalendar } from '@/lib/calendar-import'; import { ArrowLeft, Download, Upload, Calendar, Search, Filter, RotateCcw, FileSpreadsheet, Eye, Check, X } from 'lucide-react'; import { getCategoryById, getAllCategories } from '@/lib/categories'; import type { CalendarImportResult } from '@/lib/calendar-import'; import type { Timer } from '@/lib/timer-engine'; export default function HistoryPage() { const timers = useTimerStore((s) => s.timers); const now = useTimerStore((s) => s.now); const routines = useRoutineStore((s) => s.routines); const [mounted, setMounted] = useState(false); const [search, setSearch] = useState(''); const [filterCategory, setFilterCategory] = useState(''); const [filterUrgency, setFilterUrgency] = useState(''); const [importStatus, setImportStatus] = useState(null); const [tab, setTab] = useState<'stats' | 'history' | 'import'>('stats'); const [icsPreview, setIcsPreview] = useState(null); useEffect(() => { setMounted(true); }, []); const streak = useMemo(() => computeStreak(timers, now), [timers, now]); const completedTimers = useMemo(() => { return timers .filter((t) => ['dismissed', 'completed'].includes(t.state)) .filter((t) => { if (search) { const q = search.toLowerCase(); if (!t.label.toLowerCase().includes(q) && !t.description?.toLowerCase().includes(q)) { return false; } } if (filterCategory && t.category !== filterCategory) return false; if (filterUrgency && t.urgency !== filterUrgency) return false; return true; }) .sort((a, b) => (b.completedAt ?? b.dismissedAt ?? b.createdAt) - (a.completedAt ?? a.dismissedAt ?? a.createdAt)); }, [timers, search, filterCategory, filterUrgency]); const categories = useMemo(() => getAllCategories(), []); if (!mounted) return null; const handleExport = () => downloadExportAll(timers, routines); const handleJsonImport = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { const text = await readFileAsText(file); const data = parseImportData(text); if (!data) { setImportStatus('Invalid file format. Expected a ChronoMind export JSON.'); return; } const timerResult = importTimers(data, timers); // Add the imported timers to the store const existingIds = new Set(timers.map((t) => t.id)); const newTimers = data.timers.filter((t) => !existingIds.has(t.id)); if (newTimers.length > 0) { useTimerStore.setState((s) => ({ timers: [...s.timers, ...newTimers] })); } // Import routines if present const routineResult = importRoutines(data, routines); if (data.routines && data.routines.length > 0) { const existingRoutineIds = new Set(routines.map((r) => r.id)); const newRoutines = data.routines.filter((r) => !existingRoutineIds.has(r.id)); if (newRoutines.length > 0) { useRoutineStore.setState((s) => ({ routines: [...s.routines, ...newRoutines] })); } } const parts = [`Imported ${timerResult.imported} timers`]; if (routineResult.imported > 0) parts.push(`${routineResult.imported} routines`); const totalSkipped = timerResult.skipped + routineResult.skipped; if (totalSkipped > 0) parts.push(`${totalSkipped} skipped`); const allErrors = [...timerResult.errors, ...routineResult.errors]; setImportStatus(`${parts.join(', ')}.${allErrors.length > 0 ? ` Errors: ${allErrors.join(', ')}` : ''}`); } catch { setImportStatus('Failed to read file.'); } e.target.value = ''; }; const handleIcsPreview = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { const text = await readFileAsText(file); const result = importCalendar(text, timers); if (result.events.length === 0) { setImportStatus(`No events found.${result.errors.length > 0 ? ` Errors: ${result.errors.join(', ')}` : ''}`); return; } setIcsPreview(result); setImportStatus(null); } catch { setImportStatus('Failed to parse .ics file.'); } e.target.value = ''; }; const confirmIcsImport = () => { if (!icsPreview) return; const newTimers = icsPreview.events.map((e) => e.timer); useTimerStore.setState((s) => ({ timers: [...s.timers, ...newTimers] })); const conflictCount = icsPreview.events.filter((e) => e.conflicts.length > 0).length; setImportStatus(`Imported ${icsPreview.events.length} calendar events.${conflictCount > 0 ? ` ${conflictCount} have conflicts.` : ''}`); setIcsPreview(null); }; const handleRepeatTimer = (timer: Timer) => { if (timer.type === 'countdown' && timer.duration) { useTimerStore.getState().addCountdown({ label: timer.label, durationMs: timer.duration, urgency: timer.urgency, category: timer.category }); } else if (timer.type === 'alarm') { const target = new Date(); const orig = new Date(timer.targetTime); target.setHours(orig.getHours(), orig.getMinutes(), 0, 0); if (target.getTime() <= Date.now()) target.setDate(target.getDate() + 1); useTimerStore.getState().addAlarm({ label: timer.label, targetTime: target.getTime(), urgency: timer.urgency, category: timer.category }); } else if (timer.type === 'event') { return; // events are one-off } }; const handleCsvExport = () => { const headers = ['Label', 'Type', 'State', 'Urgency', 'Category', 'Created', 'Completed', 'Duration (min)']; const rows = timers.map((t) => [ `"${t.label.replace(/"/g, '""')}"`, t.type, t.state, t.urgency, t.category ?? '', new Date(t.createdAt).toISOString(), t.completedAt ? new Date(t.completedAt).toISOString() : t.dismissedAt ? new Date(t.dismissedAt).toISOString() : '', t.duration ? Math.round(t.duration / 60_000).toString() : '', ]); const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chronomind-export-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); }; return (
Back to Dashboard

History & Stats

{/* Tabs */}
{[ { key: 'stats' as const, label: 'Statistics' }, { key: 'history' as const, label: 'History' }, { key: 'import' as const, label: 'Import / Export' }, ].map((t) => ( ))}
{/* Stats tab */} {tab === 'stats' && (
)} {/* History tab */} {tab === 'history' && (
{/* Search + filters */}
setSearch(e.target.value)} placeholder="Search timers..." className="w-full pl-9 pr-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2" style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)', color: 'var(--cm-text-primary)', }} />
{/* Timer list */} {completedTimers.length > 0 ? (

{completedTimers.length} timer{completedTimers.length !== 1 ? 's' : ''} in history

{completedTimers.map((timer) => (
{timer.type !== 'event' && ( )}
))}
) : (

{search || filterCategory || filterUrgency ? 'No timers match your filters.' : 'No completed timers yet.'}

)}
)} {/* Import/Export tab */} {tab === 'import' && (
{/* Warning */}
Your timers are stored locally in your browser. Export regularly to back up your data. Cloud sync is coming in a future version.
{/* Export */}

Export Data

Download all {timers.length} timers and {routines.length} routines as a JSON file.

{/* Import JSON */}

Import Data (JSON)

Restore timers and routines from a previously exported ChronoMind JSON file.

{/* Import .ics */}

Import Calendar (.ics)

Import events from a .ics file (Google Calendar, Outlook, Apple Calendar exports). Events become alarms with auto-generated pre-warning cascades.

{/* Calendar import preview */} {icsPreview && (

Preview: {icsPreview.events.length} events

{icsPreview.events.map((evt, i) => (
{evt.timer.label} {new Date(evt.timer.targetTime).toLocaleString()}
{evt.conflicts.length > 0 && ( Conflict )}
))}
{icsPreview.errors.length > 0 && (

{icsPreview.errors.length} error(s): {icsPreview.errors.slice(0, 3).join(', ')}

)}
)} {/* Import status */} {importStatus && (
{importStatus}
)}
)}
); }