learning_ai_clock/web/src/components/Dashboard.tsx

228 lines
8.5 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { useTimerStore } from '@/lib/store';
import { useTickLoop } from '@/lib/use-tick';
import { useKeyboardShortcuts, SHORTCUT_MAP } from '@/lib/use-keyboard-shortcuts';
import { TimerCard } from './TimerCard';
import { PomodoroView } from './PomodoroView';
import { QuickTimerBar } from './QuickTimerBar';
import { CreateTimerModal } from './CreateTimerModal';
import { AlarmOverlay } from './AlarmOverlay';
import { requestNotificationPermission } from '@/lib/notifications';
import { formatTime, formatDate } from '@/lib/format';
import { Plus, Clock, Bell, Keyboard } from 'lucide-react';
export function Dashboard() {
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [showShortcuts, setShowShortcuts] = useState(false);
const [mounted, setMounted] = useState(false);
const timers = useTimerStore((s) => s.timers);
const now = useTimerStore((s) => s.now);
const { pause, resume } = useTimerStore();
// Start the tick loop
useTickLoop();
// Keyboard shortcuts
const getFirstActiveTimer = useCallback(() => {
return timers.find((t) => ['active', 'warning', 'paused'].includes(t.state));
}, [timers]);
useKeyboardShortcuts({
onNewTimer: () => setIsCreateOpen(true),
onQuickTimer: () => setIsCreateOpen(true),
onTogglePause: () => {
const t = getFirstActiveTimer();
if (!t) return;
if (t.state === 'paused') resume(t.id);
else if (t.type !== 'alarm') pause(t.id);
},
onDismiss: () => {
if (isCreateOpen) setIsCreateOpen(false);
else if (showShortcuts) setShowShortcuts(false);
},
onShowHelp: () => setShowShortcuts((p) => !p),
});
// Hydration guard
useEffect(() => {
setMounted(true);
requestNotificationPermission();
}, []);
if (!mounted) {
return (
<div
className="min-h-screen flex items-center justify-center"
style={{ backgroundColor: 'var(--cm-bg-canvas)' }}
>
<Clock size={32} className="animate-spin" style={{ color: 'var(--cm-accent)' }} />
</div>
);
}
const activeTimers = timers.filter((t) =>
['active', 'warning', 'snoozed', 'firing', 'paused'].includes(t.state)
);
const completedTimers = timers
.filter((t) => ['dismissed', 'completed'].includes(t.state))
.slice(-10)
.reverse();
// Tab title update
useEffect(() => {
const next = activeTimers
.filter((t) => ['active', 'warning'].includes(t.state))
.sort((a, b) => a.targetTime - b.targetTime)[0];
if (next) {
const remaining = Math.max(0, next.targetTime - now);
const mins = Math.floor(remaining / 60000);
const secs = Math.floor((remaining % 60000) / 1000);
document.title = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}${next.label} | ChronoMind`;
} else {
document.title = 'ChronoMind — Smart Pre-Warning Timer';
}
}, [now, activeTimers]);
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
{/* Alarm overlay for firing timers */}
<AlarmOverlay />
{/* Header */}
<header
className="sticky top-0 z-40 border-b backdrop-blur-md"
style={{
backgroundColor: 'rgba(6, 7, 10, 0.85)',
borderColor: 'var(--cm-border)',
}}
>
<div className="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<Clock size={24} style={{ color: 'var(--cm-accent)' }} />
<h1 className="text-lg font-bold" style={{ color: 'var(--cm-text-primary)' }}>
ChronoMind
</h1>
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-mono" style={{ color: 'var(--cm-text-tertiary)' }}>
{formatTime(now)} · {formatDate(now)}
</span>
<button
onClick={() => setShowShortcuts((p) => !p)}
className="p-2 rounded-lg transition-colors cursor-pointer"
style={{ color: 'var(--cm-text-tertiary)' }}
title="Keyboard shortcuts (?)"
>
<Keyboard size={18} />
</button>
<button
onClick={() => setIsCreateOpen(true)}
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all cursor-pointer"
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
>
<Plus size={16} /> New Timer
</button>
</div>
</div>
</header>
{/* Keyboard shortcuts overlay */}
{showShortcuts && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/60" onClick={() => setShowShortcuts(false)} />
<div
className="relative rounded-2xl border p-6 max-w-sm w-full mx-4"
style={{ backgroundColor: 'var(--cm-bg-elevated)', borderColor: 'var(--cm-border)' }}
>
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--cm-text-primary)' }}>
Keyboard Shortcuts
</h3>
<div className="space-y-2">
{SHORTCUT_MAP.map((s) => (
<div key={s.key} className="flex items-center justify-between">
<span className="text-sm" style={{ color: 'var(--cm-text-secondary)' }}>{s.description}</span>
<kbd
className="px-2 py-1 rounded text-xs font-mono"
style={{ backgroundColor: 'var(--cm-surface-muted)', color: 'var(--cm-text-primary)' }}
>
{s.key}
</kbd>
</div>
))}
</div>
</div>
</div>
)}
{/* Main content */}
<main className="max-w-3xl mx-auto px-4 py-6">
{/* Quick timer bar */}
<div className="mb-6">
<QuickTimerBar />
</div>
{/* Active timers */}
{activeTimers.length > 0 ? (
<div className="space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-wider flex items-center gap-2" style={{ color: 'var(--cm-text-tertiary)' }}>
<Bell size={14} /> Active ({activeTimers.length})
</h2>
{activeTimers
.sort((a, b) => a.targetTime - b.targetTime)
.map((timer) =>
timer.type === 'pomodoro' ? (
<PomodoroView key={timer.id} timer={timer} />
) : (
<TimerCard key={timer.id} timer={timer} />
)
)}
</div>
) : (
/* Empty state */
<div className="text-center py-20">
<Clock size={64} className="mx-auto mb-4 opacity-20" style={{ color: 'var(--cm-text-tertiary)' }} />
<h2 className="text-xl font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
No active timers
</h2>
<p className="mb-6" style={{ color: 'var(--cm-text-tertiary)' }}>
Create your first timer and never be caught off-guard again.
</p>
<button
onClick={() => setIsCreateOpen(true)}
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold transition-all cursor-pointer"
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
>
<Plus size={18} /> Create Timer
</button>
</div>
)}
{/* Completed timers */}
{completedTimers.length > 0 && (
<div className="mt-8 space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-wider" style={{ color: 'var(--cm-text-tertiary)' }}>
Recent ({completedTimers.length})
</h2>
{completedTimers.map((timer) => (
<TimerCard key={timer.id} timer={timer} />
))}
</div>
)}
</main>
{/* Create Timer Modal */}
<CreateTimerModal isOpen={isCreateOpen} onClose={() => setIsCreateOpen(false)} />
{/* Footer */}
<footer className="max-w-3xl mx-auto px-4 py-8 text-center">
<p className="text-xs" style={{ color: 'var(--cm-text-tertiary)' }}>
ChronoMind Press <kbd className="px-1 py-0.5 rounded text-xs font-mono" style={{ backgroundColor: 'var(--cm-surface-muted)' }}>?</kbd> for shortcuts
</p>
</footer>
</div>
);
}