diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9ba7f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+dist
+.DS_Store
+server/public
+vite.config.ts.*
+*.tar.gz
\ No newline at end of file
diff --git a/.replit b/.replit
index e69de29..adcfadc 100644
--- a/.replit
+++ b/.replit
@@ -0,0 +1,39 @@
+modules = ["nodejs-20", "web", "postgresql-16"]
+run = "npm run dev"
+hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
+
+[nix]
+channel = "stable-24_05"
+
+[deployment]
+deploymentTarget = "autoscale"
+build = ["npm", "run", "build"]
+run = ["npm", "run", "start"]
+
+[[ports]]
+localPort = 5000
+externalPort = 80
+
+[env]
+PORT = "5000"
+
+[workflows]
+runButton = "Project"
+
+[[workflows.workflow]]
+name = "Project"
+mode = "parallel"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "workflow.run"
+args = "Start application"
+
+[[workflows.workflow]]
+name = "Start application"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "shell.exec"
+args = "npm run dev"
+waitForPort = 5000
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..0d05af3
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..69ad194
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,51 @@
+import { useEffect } from "react";
+import { Switch, Route } from "wouter";
+import { queryClient } from "./lib/queryClient";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { ThemeProvider } from "@/lib/theme";
+import { useStore } from "@/lib/store";
+import { getOrCreateDeviceId } from "@/lib/device";
+
+import Home from "@/pages/Home";
+import History from "@/pages/History";
+import Settings from "@/pages/Settings";
+import NotFound from "@/pages/not-found";
+
+function Router() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+function App() {
+ const setDeviceId = useStore((state) => state.setDeviceId);
+ const deviceId = useStore((state) => state.deviceId);
+
+ // Initialize device ID on app start
+ useEffect(() => {
+ if (!deviceId) {
+ const id = getOrCreateDeviceId();
+ setDeviceId(id);
+ }
+ }, [deviceId, setDeviceId]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/atoms/Button.tsx b/client/src/components/atoms/Button.tsx
new file mode 100644
index 0000000..850b15b
--- /dev/null
+++ b/client/src/components/atoms/Button.tsx
@@ -0,0 +1,27 @@
+import { Button as ShadcnButton } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { forwardRef } from "react";
+
+export interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
+ size?: "default" | "sm" | "lg" | "icon";
+ asChild?: boolean;
+}
+
+const Button = forwardRef(
+ ({ className, variant = "default", size = "default", ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+
+Button.displayName = "Button";
+
+export { Button };
diff --git a/client/src/components/atoms/ProgressRing.tsx b/client/src/components/atoms/ProgressRing.tsx
new file mode 100644
index 0000000..370caa6
--- /dev/null
+++ b/client/src/components/atoms/ProgressRing.tsx
@@ -0,0 +1,53 @@
+interface ProgressRingProps {
+ progress: number; // 0-1
+ size?: number;
+ strokeWidth?: number;
+ className?: string;
+}
+
+export function ProgressRing({
+ progress,
+ size = 256,
+ strokeWidth = 8,
+ className = ""
+}: ProgressRingProps) {
+ const radius = (size - strokeWidth) / 2;
+ const circumference = radius * 2 * Math.PI;
+ const strokeDasharray = circumference;
+ const strokeDashoffset = circumference * (1 - progress);
+
+ return (
+
+ );
+}
diff --git a/client/src/components/atoms/SoundToggle.tsx b/client/src/components/atoms/SoundToggle.tsx
new file mode 100644
index 0000000..a59a4f1
--- /dev/null
+++ b/client/src/components/atoms/SoundToggle.tsx
@@ -0,0 +1,27 @@
+import { Volume2, VolumeX } from "lucide-react";
+import { Button } from "./Button";
+
+interface SoundToggleProps {
+ enabled: boolean;
+ onToggle: () => void;
+ className?: string;
+}
+
+export function SoundToggle({ enabled, onToggle, className = "" }: SoundToggleProps) {
+ return (
+
+ );
+}
diff --git a/client/src/components/molecules/DurationSelector.tsx b/client/src/components/molecules/DurationSelector.tsx
new file mode 100644
index 0000000..db14183
--- /dev/null
+++ b/client/src/components/molecules/DurationSelector.tsx
@@ -0,0 +1,126 @@
+import { useState } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Button } from "@/components/atoms/Button";
+
+interface Preset {
+ name: string;
+ description: string;
+ focus: number;
+ break: number;
+}
+
+interface DurationSelectorProps {
+ selectedFocus: number;
+ selectedBreak: number;
+ onDurationChange: (focus: number, breakDuration: number) => void;
+ className?: string;
+}
+
+const presets: Preset[] = [
+ { name: "Classic", description: "25 min focus / 5 min break", focus: 25, break: 5 },
+ { name: "Extended", description: "50 min focus / 10 min break", focus: 50, break: 10 },
+];
+
+export function DurationSelector({
+ selectedFocus,
+ selectedBreak,
+ onDurationChange,
+ className = ""
+}: DurationSelectorProps) {
+ const [customFocus, setCustomFocus] = useState(selectedFocus);
+ const [customBreak, setCustomBreak] = useState(selectedBreak);
+ const [selectedPreset, setSelectedPreset] = useState(
+ presets.find(p => p.focus === selectedFocus && p.break === selectedBreak)?.name || null
+ );
+
+ const handlePresetSelect = (preset: Preset) => {
+ setSelectedPreset(preset.name);
+ setCustomFocus(preset.focus);
+ setCustomBreak(preset.break);
+ onDurationChange(preset.focus, preset.break);
+ };
+
+ const handleCustomChange = (focus: number, breakDuration: number) => {
+ setSelectedPreset(null);
+ setCustomFocus(focus);
+ setCustomBreak(breakDuration);
+ onDurationChange(focus, breakDuration);
+ };
+
+ return (
+
+
+ Duration
+
+
+ {presets.map((preset) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/client/src/components/molecules/SuggestionChips.tsx b/client/src/components/molecules/SuggestionChips.tsx
new file mode 100644
index 0000000..eb7c35c
--- /dev/null
+++ b/client/src/components/molecules/SuggestionChips.tsx
@@ -0,0 +1,89 @@
+import { useQuery } from "@tanstack/react-query";
+import { Lightbulb, ArrowRight } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/atoms/Button";
+import { useStore } from "@/lib/store";
+
+interface Suggestion {
+ minutes: number;
+ reason: string;
+}
+
+interface SuggestionChipsProps {
+ onSuggestionSelect: (minutes: number) => void;
+ className?: string;
+}
+
+export function SuggestionChips({ onSuggestionSelect, className = "" }: SuggestionChipsProps) {
+ const deviceId = useStore((state) => state.deviceId);
+
+ const { data: suggestions, isLoading } = useQuery<{ suggestions: Suggestion[] }>({
+ queryKey: ['/api/suggestions', { deviceId }],
+ enabled: !!deviceId,
+ });
+
+ if (isLoading) {
+ return (
+
+
+
+
+ Smart Suggestions
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ const suggestionList = suggestions?.suggestions || [
+ { minutes: 25, reason: "Classic Pomodoro technique - great for starting out." },
+ { minutes: 15, reason: "Short sprints help build focus habits." },
+ { minutes: 40, reason: "Extended sessions for deep work." }
+ ];
+
+ return (
+
+
+
+
+ Smart Suggestions
+
+
+
+
+ {suggestionList.map((suggestion, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/client/src/components/molecules/TimerControls.tsx b/client/src/components/molecules/TimerControls.tsx
new file mode 100644
index 0000000..c837ea2
--- /dev/null
+++ b/client/src/components/molecules/TimerControls.tsx
@@ -0,0 +1,61 @@
+import { Play, Pause, RotateCcw, SkipForward } from "lucide-react";
+import { Button } from "@/components/atoms/Button";
+
+interface TimerControlsProps {
+ isRunning: boolean;
+ onPlayPause: () => void;
+ onReset: () => void;
+ onSkip: () => void;
+ className?: string;
+}
+
+export function TimerControls({
+ isRunning,
+ onPlayPause,
+ onReset,
+ onSkip,
+ className = ""
+}: TimerControlsProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/organisms/HistoryChart.tsx b/client/src/components/organisms/HistoryChart.tsx
new file mode 100644
index 0000000..0cd311f
--- /dev/null
+++ b/client/src/components/organisms/HistoryChart.tsx
@@ -0,0 +1,179 @@
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useStore } from "@/lib/store";
+
+interface StatsData {
+ last7d: {
+ sessions: number;
+ completionRate: number;
+ avgFocusMin: number;
+ };
+ last30d: {
+ sessions: number;
+ completionRate: number;
+ avgFocusMin: number;
+ };
+}
+
+interface HistoryChartProps {
+ className?: string;
+}
+
+export function HistoryChart({ className = "" }: HistoryChartProps) {
+ const deviceId = useStore((state) => state.deviceId);
+ const localSessions = useStore((state) => state.localSessions);
+
+ const { data: statsData } = useQuery({
+ queryKey: ['/api/stats/summary', { deviceId }],
+ enabled: !!deviceId,
+ });
+
+ // Calculate local stats as fallback
+ const getLocalStats = () => {
+ const now = new Date();
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+
+ const focusSessions = localSessions.filter(s => s.type === 'focus');
+
+ const last7dSessions = focusSessions.filter(s =>
+ new Date(s.startedAt) >= sevenDaysAgo
+ );
+
+ const last30dSessions = focusSessions.filter(s =>
+ new Date(s.startedAt) >= thirtyDaysAgo
+ );
+
+ const calculatePeriodStats = (sessions: any[]) => {
+ const completed = sessions.filter(s => s.completed);
+ return {
+ sessions: sessions.length,
+ completionRate: sessions.length > 0 ? completed.length / sessions.length : 0,
+ avgFocusMin: completed.length > 0
+ ? completed.reduce((sum, s) => sum + s.intendedMinutes, 0) / completed.length
+ : 0,
+ };
+ };
+
+ return {
+ last7d: calculatePeriodStats(last7dSessions),
+ last30d: calculatePeriodStats(last30dSessions),
+ };
+ };
+
+ const stats = statsData || getLocalStats();
+
+ // Generate mock chart data for visualization
+ const generateChartData = () => {
+ const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+ const heights = [60, 80, 45, 90, 75, 30, 25]; // Mock data
+
+ return days.map((day, index) => ({
+ day,
+ height: heights[index],
+ sessions: Math.floor(heights[index] / 20),
+ }));
+ };
+
+ const chartData = generateChartData();
+
+ return (
+
+ {/* Daily Sessions Chart */}
+
+
+ Daily Sessions
+
+
+
+ {chartData.map((data, index) => (
+
+ ))}
+
+
+
+
+ {/* Completion Rate Trend */}
+
+
+ Completion Rate Trend
+
+
+
+
+
+
+
+
+ {/* Stats Summary */}
+
+
+
+
+
+ {stats.last7d.sessions}
+
+ Sessions (7d)
+
+
+
+
+
+
+ {Math.round(stats.last7d.completionRate * 100)}%
+
+ Completion (7d)
+
+
+
+
+
+
+ {Math.round(stats.last7d.avgFocusMin)}m
+
+ Avg Focus (7d)
+
+
+
+
+
+
+ {stats.last30d.sessions}
+
+ Sessions (30d)
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/organisms/PomodoroTimer.tsx b/client/src/components/organisms/PomodoroTimer.tsx
new file mode 100644
index 0000000..59da3b1
--- /dev/null
+++ b/client/src/components/organisms/PomodoroTimer.tsx
@@ -0,0 +1,233 @@
+import { useEffect, useRef } from "react";
+import { Target, Coffee } from "lucide-react";
+import { Card, CardContent } from "@/components/ui/card";
+import { ProgressRing } from "@/components/atoms/ProgressRing";
+import { TimerControls } from "@/components/molecules/TimerControls";
+import { useStore } from "@/lib/store";
+import { audioManager } from "@/lib/audio";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+import type { InsertSession } from "@shared/schema";
+
+interface PomodoroTimerProps {
+ className?: string;
+}
+
+export function PomodoroTimer({ className = "" }: PomodoroTimerProps) {
+ const timer = useStore((state) => state.timer);
+ const settings = useStore((state) => state.settings);
+ const deviceId = useStore((state) => state.deviceId);
+ const setTimer = useStore((state) => state.setTimer);
+ const addLocalSession = useStore((state) => state.addLocalSession);
+
+ const timerRef = useRef();
+ const queryClient = useQueryClient();
+
+ const sessionMutation = useMutation({
+ mutationFn: async (sessionData: InsertSession) => {
+ const response = await apiRequest('POST', '/api/sessions', sessionData);
+ return response.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/suggestions'] });
+ queryClient.invalidateQueries({ queryKey: ['/api/stats/summary'] });
+ },
+ });
+
+ // Format time display
+ const formatTime = (seconds: number) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ };
+
+ // Calculate progress (0-1)
+ const progress = timer.totalTime > 0 ? (timer.totalTime - timer.currentTime) / timer.totalTime : 0;
+
+ // Timer tick effect
+ useEffect(() => {
+ if (timer.isRunning && timer.currentTime > 0) {
+ timerRef.current = setTimeout(() => {
+ setTimer({ currentTime: timer.currentTime - 1 });
+ }, 1000);
+ } else if (timer.currentTime === 0 && timer.isRunning) {
+ // Timer finished
+ handleTimerComplete();
+ }
+
+ return () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ };
+ }, [timer.isRunning, timer.currentTime]);
+
+ const handleTimerComplete = async () => {
+ setTimer({ isRunning: false });
+
+ // Play completion sound
+ if (settings.soundEnabled) {
+ await audioManager.playSound(settings.soundType, settings.soundVolume);
+ }
+
+ // Record session
+ if (timer.startedAt) {
+ const sessionData: InsertSession = {
+ deviceId,
+ type: timer.phase,
+ intendedMinutes: timer.totalTime / 60,
+ actualSeconds: timer.totalTime,
+ startedAt: timer.startedAt,
+ endedAt: new Date(),
+ completed: true,
+ interruptions: timer.interruptions,
+ };
+
+ // Add to local storage immediately
+ const localSession = { ...sessionData, id: `local-${Date.now()}` };
+ addLocalSession(localSession);
+
+ // Sync to server
+ try {
+ await sessionMutation.mutateAsync(sessionData);
+ } catch (error) {
+ console.warn('Failed to sync session to server:', error);
+ }
+ }
+
+ // Auto-start next phase if enabled
+ if (settings.autoStartBreaks) {
+ startNextPhase();
+ } else {
+ // Switch to next phase but don't start
+ switchToNextPhase();
+ }
+ };
+
+ const startNextPhase = () => {
+ const nextPhase = timer.phase === 'focus' ? 'break' : 'focus';
+ const nextDuration = nextPhase === 'focus'
+ ? settings.focusDuration
+ : (timer.sessionCount % settings.longBreakInterval === 0 && timer.sessionCount > 0)
+ ? settings.longBreakDuration
+ : settings.shortBreakDuration;
+
+ const durationSeconds = nextDuration * 60;
+
+ setTimer({
+ phase: nextPhase,
+ currentTime: durationSeconds,
+ totalTime: durationSeconds,
+ isRunning: true,
+ startedAt: new Date(),
+ sessionCount: nextPhase === 'focus' ? timer.sessionCount + 1 : timer.sessionCount,
+ interruptions: 0,
+ });
+ };
+
+ const switchToNextPhase = () => {
+ const nextPhase = timer.phase === 'focus' ? 'break' : 'focus';
+ const nextDuration = nextPhase === 'focus'
+ ? settings.focusDuration
+ : (timer.sessionCount % settings.longBreakInterval === 0 && timer.sessionCount > 0)
+ ? settings.longBreakDuration
+ : settings.shortBreakDuration;
+
+ const durationSeconds = nextDuration * 60;
+
+ setTimer({
+ phase: nextPhase,
+ currentTime: durationSeconds,
+ totalTime: durationSeconds,
+ isRunning: false,
+ sessionCount: nextPhase === 'focus' ? timer.sessionCount + 1 : timer.sessionCount,
+ interruptions: 0,
+ });
+ };
+
+ const handlePlayPause = async () => {
+ if (!timer.isRunning) {
+ // Request audio permission on first interaction
+ await audioManager.requestAudioPermission();
+
+ if (!timer.startedAt) {
+ setTimer({ startedAt: new Date() });
+ }
+ } else {
+ // Pausing counts as interruption
+ setTimer({ interruptions: timer.interruptions + 1 });
+ }
+
+ setTimer({ isRunning: !timer.isRunning });
+ };
+
+ const handleReset = () => {
+ setTimer({
+ isRunning: false,
+ currentTime: timer.totalTime,
+ interruptions: 0,
+ startedAt: undefined,
+ });
+ };
+
+ const handleSkip = () => {
+ if (timer.phase === 'break') {
+ startNextPhase();
+ }
+ };
+
+ const getNextPhaseInfo = () => {
+ if (timer.phase === 'focus') {
+ const isLongBreak = timer.sessionCount % settings.longBreakInterval === 0 && timer.sessionCount > 0;
+ return isLongBreak ? 'Long Break' : 'Short Break';
+ }
+ return 'Focus Time';
+ };
+
+ return (
+
+
+ {/* Timer Display */}
+
+
+
+ {/* Time Display Overlay */}
+
+
+ {formatTime(timer.currentTime)}
+
+
+ {timer.phase === 'focus' ? 'Focus Time' : 'Break Time'}
+
+
+
+
+ {/* Timer Controls */}
+
+
+ {/* Session Info */}
+
+
+
+
+ Session {timer.sessionCount + 1} of{' '}
+ {settings.longBreakInterval}
+
+
+
+
+
+ Next: {getNextPhaseInfo()}
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/organisms/Sidebar.tsx b/client/src/components/organisms/Sidebar.tsx
new file mode 100644
index 0000000..654fa55
--- /dev/null
+++ b/client/src/components/organisms/Sidebar.tsx
@@ -0,0 +1,132 @@
+import { Clock, BarChart3, Settings, Sun, Moon } from "lucide-react";
+import { Button } from "@/components/atoms/Button";
+import { useTheme } from "@/lib/theme";
+import { useStore } from "@/lib/store";
+import { Link, useLocation } from "wouter";
+
+interface SidebarProps {
+ className?: string;
+ isMobile?: boolean;
+ onNavigate?: () => void;
+}
+
+export function Sidebar({ className = "", isMobile = false, onNavigate }: SidebarProps) {
+ const { theme, toggleTheme } = useTheme();
+ const [location] = useLocation();
+ const localSessions = useStore((state) => state.localSessions);
+
+ // Calculate today's stats
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ const todaySessions = localSessions.filter(session => {
+ const sessionDate = new Date(session.startedAt);
+ sessionDate.setHours(0, 0, 0, 0);
+ return sessionDate.getTime() === today.getTime();
+ });
+
+ const todayFocusSessions = todaySessions.filter(s => s.type === 'focus');
+ const completedToday = todayFocusSessions.filter(s => s.completed);
+ const completionRate = todayFocusSessions.length > 0
+ ? Math.round((completedToday.length / todayFocusSessions.length) * 100)
+ : 0;
+
+ const navItems = [
+ { path: "/", icon: Clock, label: "Timer", testId: "nav-timer" },
+ { path: "/history", icon: BarChart3, label: "History", testId: "nav-history" },
+ { path: "/settings", icon: Settings, label: "Settings", testId: "nav-settings" },
+ ];
+
+ const isActive = (path: string) => {
+ if (path === "/" && location === "/") return true;
+ if (path !== "/" && location.startsWith(path)) return true;
+ return false;
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/templates/MainLayout.tsx b/client/src/components/templates/MainLayout.tsx
new file mode 100644
index 0000000..42ad7a4
--- /dev/null
+++ b/client/src/components/templates/MainLayout.tsx
@@ -0,0 +1,158 @@
+import { useState, useEffect } from "react";
+import { Menu, Keyboard } from "lucide-react";
+import { Button } from "@/components/atoms/Button";
+import { SoundToggle } from "@/components/atoms/SoundToggle";
+import { Sidebar } from "@/components/organisms/Sidebar";
+import { useStore } from "@/lib/store";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+
+interface MainLayoutProps {
+ children: React.ReactNode;
+ title: string;
+ subtitle: string;
+}
+
+export function MainLayout({ children, title, subtitle }: MainLayoutProps) {
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+ const [shortcutsOpen, setShortcutsOpen] = useState(false);
+
+ const settings = useStore((state) => state.settings);
+ const updateSettings = useStore((state) => state.updateSettings);
+
+ // Handle keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Only handle shortcuts when not in input fields
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
+ return;
+ }
+
+ switch (e.code) {
+ case 'Space':
+ e.preventDefault();
+ // Timer controls will handle this
+ break;
+ case 'KeyR':
+ e.preventDefault();
+ // Timer controls will handle this
+ break;
+ case 'KeyS':
+ e.preventDefault();
+ // Timer controls will handle this
+ break;
+ case 'KeyD':
+ if (e.ctrlKey || e.metaKey) {
+ e.preventDefault();
+ // Theme toggle will be handled by the theme provider
+ }
+ break;
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, []);
+
+ const handleSoundToggle = () => {
+ updateSettings({ soundEnabled: !settings.soundEnabled });
+ };
+
+ const closeSidebar = () => setSidebarOpen(false);
+
+ return (
+
+ {/* Sidebar Navigation */}
+
+
+ {/* Mobile Sidebar Overlay */}
+ {sidebarOpen && (
+
+ )}
+
+ {/* Main Content Area */}
+
+ {/* Header */}
+
+
+ {/* Page Content */}
+
+ {children}
+
+
+
+ {/* Keyboard Shortcuts Modal */}
+
+
+ );
+}
diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..e6a723d
--- /dev/null
+++ b/client/src/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..8722561
--- /dev/null
+++ b/client/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/client/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/client/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/client/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..2174f71
--- /dev/null
+++ b/client/src/components/ui/calendar.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/client/src/components/ui/carousel.tsx b/client/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..9c2b9bf
--- /dev/null
+++ b/client/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/client/src/components/ui/chart.tsx b/client/src/components/ui/chart.tsx
new file mode 100644
index 0000000..39fba6d
--- /dev/null
+++ b/client/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+