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
new file mode 100644
index 0000000..43487c6
--- /dev/null
+++ b/.replit
@@ -0,0 +1,46 @@
+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
+
+[[ports]]
+localPort = 38607
+externalPort = 3000
+
+[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
+
+[agent]
+integrations = ["javascript_mem_db:1.0.0"]
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..31e4d45
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+ TaskFlow - Personal Task Management
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..63deb8c
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,257 @@
+import { useState } from 'react';
+import { queryClient } from "./lib/queryClient";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+
+// Components
+import BottomNavigation from './components/BottomNavigation';
+import TaskCreationModal from './components/TaskCreationModal';
+import TaskList from './components/TaskList';
+import CalendarView from './components/CalendarView';
+import KanbanBoard from './components/KanbanBoard';
+import ProjectTemplate from './components/ProjectTemplate';
+import ThemeToggle from './components/ThemeToggle';
+import { Task } from './components/TaskCard';
+import { addDays, subDays } from 'date-fns';
+
+// Mock data for prototype
+const initialTasks: Task[] = [
+ {
+ id: '1',
+ title: 'Review quarterly reports',
+ description: 'Analyze Q3 performance metrics and prepare summary',
+ status: 'todo',
+ priority: 'high',
+ dueDate: addDays(new Date(), 2),
+ timeTracked: 0,
+ isTracking: false,
+ projectId: 'project-1'
+ },
+ {
+ id: '2',
+ title: 'Update mobile app interface',
+ description: 'Implement new design system and improve user experience',
+ status: 'inProgress',
+ priority: 'high',
+ dueDate: addDays(new Date(), 1),
+ timeTracked: 120,
+ isTracking: true,
+ projectId: 'project-2'
+ },
+ {
+ id: '3',
+ title: 'Team standup meeting',
+ status: 'todo',
+ priority: 'low',
+ dueDate: new Date(),
+ timeTracked: 0,
+ isTracking: false
+ },
+ {
+ id: '4',
+ title: 'Fix critical login bug',
+ description: 'Users unable to login with special characters in password',
+ status: 'todo',
+ priority: 'high',
+ dueDate: subDays(new Date(), 1),
+ timeTracked: 30,
+ isTracking: false,
+ projectId: 'project-2'
+ },
+ {
+ id: '5',
+ title: 'Deploy new features',
+ status: 'done',
+ priority: 'medium',
+ dueDate: subDays(new Date(), 2),
+ timeTracked: 120,
+ isTracking: false,
+ projectId: 'project-1'
+ }
+];
+
+function App() {
+ const [currentTab, setCurrentTab] = useState('tasks');
+ const [tasks, setTasks] = useState(initialTasks);
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+
+ const handleCreateTask = (newTask: Partial) => {
+ const task: Task = {
+ id: Date.now().toString(),
+ title: newTask.title || '',
+ description: newTask.description,
+ status: 'todo',
+ priority: newTask.priority || 'medium',
+ dueDate: newTask.dueDate,
+ timeTracked: 0,
+ isTracking: false,
+ projectId: newTask.projectId
+ };
+ setTasks(prev => [...prev, task]);
+ console.log('Task created:', task);
+ };
+
+ const handleTaskUpdate = (taskId: string, updates: Partial) => {
+ setTasks(prev => prev.map(task =>
+ task.id === taskId ? { ...task, ...updates } : task
+ ));
+ console.log('Task updated:', taskId, updates);
+ };
+
+ const handleTaskStatusChange = (taskId: string, newStatus: Task['status']) => {
+ handleTaskUpdate(taskId, { status: newStatus });
+ };
+
+ const handleTaskDrop = (taskId: string, newDate: Date) => {
+ handleTaskUpdate(taskId, { dueDate: newDate });
+ };
+
+ const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
+ console.log('Creating project from template:', { templateId, startDate, projectName });
+ // In a real app, this would create multiple tasks based on the template
+ };
+
+ const renderContent = () => {
+ switch (currentTab) {
+ case 'tasks':
+ return (
+ console.log('Edit task:', task.title)}
+ />
+ );
+
+ case 'calendar':
+ return (
+ console.log('Date selected:', date.toLocaleDateString())}
+ />
+ );
+
+ case 'kanban':
+ return (
+
+ );
+
+ case 'templates':
+ return (
+
+ );
+
+ case 'settings':
+ return (
+
+
+
Settings
+
+
+
+
+
Dark Mode
+
Toggle between light and dark themes
+
+
+
+
+
+
+
+
About TaskFlow
+
+ A personal task management app with calendar scheduling, kanban boards, and project templates.
+
+
+ Version 1.0.0 • Built with React and Tailwind CSS
+
+
+
+
+
+ );
+
+ default:
+ return (
+
+ );
+ }
+ };
+
+ return (
+
+
+
+ {/* Header */}
+
+
+ {/* Main Content */}
+
+ {renderContent()}
+
+
+ {/* Bottom Navigation */}
+
{
+ if (tab === 'templates') {
+ setCurrentTab('templates');
+ } else {
+ setCurrentTab(tab);
+ }
+ }}
+ onCreateTask={() => setIsCreateModalOpen(true)}
+ />
+
+ {/* Task Creation Modal */}
+ setIsCreateModalOpen(false)}
+ onSave={handleCreateTask}
+ />
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/BottomNavigation.tsx b/client/src/components/BottomNavigation.tsx
new file mode 100644
index 0000000..f260fc7
--- /dev/null
+++ b/client/src/components/BottomNavigation.tsx
@@ -0,0 +1,80 @@
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Home, Calendar, LayoutGrid, Plus, Settings } from 'lucide-react';
+import { useState } from 'react';
+
+interface BottomNavigationProps {
+ onTabChange?: (tab: string) => void;
+ onCreateTask?: () => void;
+ activeTab?: string;
+}
+
+export default function BottomNavigation({ onTabChange, onCreateTask, activeTab = 'tasks' }: BottomNavigationProps) {
+ const [currentTab, setCurrentTab] = useState(activeTab);
+
+ const tabs = [
+ { id: 'tasks', label: 'Tasks', icon: Home, badge: 3 },
+ { id: 'calendar', label: 'Calendar', icon: Calendar },
+ { id: 'create', label: 'Create', icon: Plus, isCreate: true },
+ { id: 'kanban', label: 'Board', icon: LayoutGrid },
+ { id: 'templates', label: 'Templates', icon: Settings }
+ ];
+
+ const handleTabClick = (tabId: string, isCreate?: boolean) => {
+ if (isCreate) {
+ onCreateTask?.();
+ console.log('Create task triggered');
+ return;
+ }
+
+ setCurrentTab(tabId);
+ onTabChange?.(tabId);
+ console.log('Tab changed to:', tabId);
+ };
+
+ return (
+
+
+ {tabs.map((tab) => {
+ const isActive = currentTab === tab.id && !tab.isCreate;
+ const Icon = tab.icon;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/CalendarView.tsx b/client/src/components/CalendarView.tsx
new file mode 100644
index 0000000..3cdb6b5
--- /dev/null
+++ b/client/src/components/CalendarView.tsx
@@ -0,0 +1,169 @@
+import { useState } from 'react';
+import { Card } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
+import { Task } from './TaskCard';
+import { addDays, format, isSameDay, startOfWeek } from 'date-fns';
+
+interface CalendarViewProps {
+ tasks: Task[];
+ onTaskDrop?: (taskId: string, newDate: Date) => void;
+ onDateSelect?: (date: Date) => void;
+}
+
+export default function CalendarView({ tasks, onTaskDrop, onDateSelect }: CalendarViewProps) {
+ const [currentDate, setCurrentDate] = useState(new Date());
+ const [draggedTask, setDraggedTask] = useState(null);
+
+ // Get two weeks starting from current week
+ const startDate = startOfWeek(currentDate);
+ const dates = Array.from({ length: 14 }, (_, i) => addDays(startDate, i));
+
+ const getTasksForDate = (date: Date) => {
+ return tasks.filter(task =>
+ task.dueDate && isSameDay(task.dueDate, date)
+ );
+ };
+
+ const handleDragStart = (taskId: string) => {
+ setDraggedTask(taskId);
+ console.log('Drag started for task:', taskId);
+ };
+
+ const handleDragEnd = () => {
+ setDraggedTask(null);
+ };
+
+ const handleDrop = (date: Date) => {
+ if (draggedTask) {
+ onTaskDrop?.(draggedTask, date);
+ console.log(`Task ${draggedTask} dropped on`, date.toLocaleDateString());
+ setDraggedTask(null);
+ }
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ };
+
+ const navigateWeek = (direction: 'prev' | 'next') => {
+ const newDate = addDays(currentDate, direction === 'next' ? 7 : -7);
+ setCurrentDate(newDate);
+ console.log(`Navigating to week of ${newDate.toLocaleDateString()}`);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Next Two Weeks
+
+
+
+
+
+
+
+
+
+
+ {/* Calendar Grid */}
+
+ {dates.map((date, index) => {
+ const dayTasks = getTasksForDate(date);
+ const isToday = isSameDay(date, new Date());
+ const isWeekend = date.getDay() === 0 || date.getDay() === 6;
+
+ return (
+
handleDrop(date)}
+ onClick={() => {
+ onDateSelect?.(date);
+ console.log('Date selected:', date.toLocaleDateString());
+ }}
+ data-testid={`calendar-date-${format(date, 'yyyy-MM-dd')}`}
+ >
+
+
+ {format(date, 'EEE')}
+
+
+ {format(date, 'd')}
+
+
+
+
+ {dayTasks.slice(0, 3).map((task, taskIndex) => (
+
handleDragStart(task.id)}
+ onDragEnd={handleDragEnd}
+ className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
+ data-testid={`calendar-task-${task.id}`}
+ >
+
+
+ {task.title}
+
+
+
+ ))}
+
+ {dayTasks.length > 3 && (
+
+ +{dayTasks.length - 3} more
+
+ )}
+
+
+ );
+ })}
+
+
+ {/* Legend */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/KanbanBoard.tsx b/client/src/components/KanbanBoard.tsx
new file mode 100644
index 0000000..2fb250e
--- /dev/null
+++ b/client/src/components/KanbanBoard.tsx
@@ -0,0 +1,208 @@
+import { useState } from 'react';
+import { Card } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { LayoutGrid, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
+import { Task } from './TaskCard';
+import TaskCard from './TaskCard';
+import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval } from 'date-fns';
+
+interface KanbanBoardProps {
+ tasks: Task[];
+ onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void;
+ onTaskUpdate?: (taskId: string, updates: Partial) => void;
+}
+
+type ViewMode = 'traditional' | 'weekly' | 'monthly';
+
+export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }: KanbanBoardProps) {
+ const [viewMode, setViewMode] = useState('traditional');
+ const [currentDate, setCurrentDate] = useState(new Date());
+ const [draggedTask, setDraggedTask] = useState(null);
+
+ const columns = [
+ { id: 'todo', title: 'To Do', status: 'todo' as const },
+ { id: 'inProgress', title: 'In Progress', status: 'inProgress' as const },
+ { id: 'done', title: 'Done', status: 'done' as const }
+ ];
+
+ const getTasksForColumn = (status: Task['status']) => {
+ let filteredTasks = tasks.filter(task => task.status === status);
+
+ if (viewMode === 'weekly') {
+ const weekStart = startOfWeek(currentDate);
+ const weekEnd = endOfWeek(currentDate);
+ filteredTasks = filteredTasks.filter(task =>
+ task.dueDate && isWithinInterval(task.dueDate, { start: weekStart, end: weekEnd })
+ );
+ } else if (viewMode === 'monthly') {
+ const monthStart = startOfMonth(currentDate);
+ const monthEnd = endOfMonth(currentDate);
+ filteredTasks = filteredTasks.filter(task =>
+ task.dueDate && isWithinInterval(task.dueDate, { start: monthStart, end: monthEnd })
+ );
+ }
+
+ return filteredTasks;
+ };
+
+ const handleDragStart = (taskId: string) => {
+ setDraggedTask(taskId);
+ };
+
+ const handleDragEnd = () => {
+ setDraggedTask(null);
+ };
+
+ const handleDrop = (status: Task['status']) => {
+ if (draggedTask) {
+ onTaskStatusChange?.(draggedTask, status);
+ console.log(`Task ${draggedTask} moved to ${status}`);
+ setDraggedTask(null);
+ }
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ };
+
+ const navigatePeriod = (direction: 'prev' | 'next') => {
+ if (viewMode === 'weekly') {
+ setCurrentDate(addWeeks(currentDate, direction === 'next' ? 1 : -1));
+ } else if (viewMode === 'monthly') {
+ setCurrentDate(addMonths(currentDate, direction === 'next' ? 1 : -1));
+ }
+ };
+
+ const getPeriodTitle = () => {
+ if (viewMode === 'weekly') {
+ return `Week of ${format(startOfWeek(currentDate), 'MMM d')}`;
+ } else if (viewMode === 'monthly') {
+ return format(currentDate, 'MMMM yyyy');
+ }
+ return 'All Tasks';
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Task Board
+
+
+
+ {viewMode !== 'traditional' && (
+
+
+
+
+ {getPeriodTitle()}
+
+
+
+
+ )}
+
+
+ {/* View Mode Tabs */}
+
setViewMode(value as ViewMode)}>
+
+
+
+ Traditional
+
+
+
+ Weekly
+
+
+
+ Monthly
+
+
+
+
+ {/* Kanban Columns */}
+
+ {columns.map((column) => {
+ const columnTasks = getTasksForColumn(column.status);
+
+ return (
+
handleDrop(column.status)}
+ data-testid={`column-${column.id}`}
+ >
+
+
+ {column.title}
+
+
+ {columnTasks.length}
+
+
+
+
+ {columnTasks.map((task) => (
+
handleDragStart(task.id)}
+ onDragEnd={handleDragEnd}
+ className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
+ >
+ {
+ onTaskUpdate?.(task.id, { isTracking: true });
+ console.log(`Timer started for ${task.title}`);
+ }}
+ onPause={() => {
+ onTaskUpdate?.(task.id, { isTracking: false });
+ console.log(`Timer paused for ${task.title}`);
+ }}
+ onEdit={() => {
+ console.log(`Edit task ${task.title}`);
+ }}
+ isDragging={draggedTask === task.id}
+ />
+
+ ))}
+
+ {columnTasks.length === 0 && (
+
+ No tasks in {column.title.toLowerCase()}
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/ProjectTemplate.tsx b/client/src/components/ProjectTemplate.tsx
new file mode 100644
index 0000000..8a01f76
--- /dev/null
+++ b/client/src/components/ProjectTemplate.tsx
@@ -0,0 +1,310 @@
+import { useState } from 'react';
+import { Card } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Calendar } from '@/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Plus, Calendar as CalendarIcon, Clock, Copy, Play } from 'lucide-react';
+import { Task } from './TaskCard';
+
+interface TemplateTask {
+ id: string;
+ title: string;
+ description?: string;
+ priority: 'low' | 'medium' | 'high';
+ dayOffset: number; // Days relative to project start date (negative for before, positive for after)
+ estimatedHours?: number;
+}
+
+interface ProjectTemplate {
+ id: string;
+ name: string;
+ description?: string;
+ tasks: TemplateTask[];
+ category: string;
+}
+
+interface ProjectTemplateProps {
+ templates?: ProjectTemplate[];
+ onCreateFromTemplate?: (templateId: string, startDate: Date, projectName: string) => void;
+ onCreateTemplate?: (template: Omit) => void;
+}
+
+const defaultTemplates: ProjectTemplate[] = [
+ {
+ id: 'web-project',
+ name: 'Website Launch',
+ description: 'Complete website development and launch process',
+ category: 'Development',
+ tasks: [
+ { id: '1', title: 'Project planning', priority: 'high', dayOffset: -14, estimatedHours: 4 },
+ { id: '2', title: 'Design mockups', priority: 'high', dayOffset: -10, estimatedHours: 16 },
+ { id: '3', title: 'Frontend development', priority: 'high', dayOffset: -7, estimatedHours: 40 },
+ { id: '4', title: 'Backend API', priority: 'medium', dayOffset: -5, estimatedHours: 24 },
+ { id: '5', title: 'Testing and QA', priority: 'high', dayOffset: -2, estimatedHours: 8 },
+ { id: '6', title: 'Deploy to production', priority: 'high', dayOffset: 0, estimatedHours: 2 }
+ ]
+ },
+ {
+ id: 'product-launch',
+ name: 'Product Launch',
+ description: 'Marketing and launch campaign for new product',
+ category: 'Marketing',
+ tasks: [
+ { id: '1', title: 'Market research', priority: 'high', dayOffset: -21, estimatedHours: 12 },
+ { id: '2', title: 'Create marketing materials', priority: 'medium', dayOffset: -14, estimatedHours: 20 },
+ { id: '3', title: 'Press release', priority: 'medium', dayOffset: -7, estimatedHours: 4 },
+ { id: '4', title: 'Social media campaign', priority: 'high', dayOffset: -3, estimatedHours: 8 },
+ { id: '5', title: 'Launch event', priority: 'high', dayOffset: 0, estimatedHours: 6 }
+ ]
+ }
+];
+
+export default function ProjectTemplate({
+ templates = defaultTemplates,
+ onCreateFromTemplate,
+ onCreateTemplate
+}: ProjectTemplateProps) {
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [projectName, setProjectName] = useState('');
+ const [startDate, setStartDate] = useState();
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
+ const [isLaunchOpen, setIsLaunchOpen] = useState(false);
+ const [isCalendarOpen, setIsCalendarOpen] = useState(false);
+
+ const handleLaunchProject = () => {
+ if (selectedTemplate && startDate && projectName.trim()) {
+ onCreateFromTemplate?.(selectedTemplate.id, startDate, projectName.trim());
+ console.log('Launching project:', {
+ template: selectedTemplate.name,
+ name: projectName,
+ startDate: startDate.toLocaleDateString()
+ });
+
+ setProjectName('');
+ setStartDate(undefined);
+ setSelectedTemplate(null);
+ setIsLaunchOpen(false);
+ }
+ };
+
+ const formatDayOffset = (offset: number) => {
+ if (offset === 0) return 'Launch day';
+ if (offset < 0) return `${Math.abs(offset)} days before`;
+ return `${offset} days after`;
+ };
+
+ const getTotalEstimatedHours = (template: ProjectTemplate) => {
+ return template.tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
+ };
+
+ const getTemplatesByCategory = () => {
+ const categories = Array.from(new Set(templates.map(t => t.category)));
+ return categories.map(category => ({
+ category,
+ templates: templates.filter(t => t.category === category)
+ }));
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Project Templates
+
+
+
+
+
+ {/* Templates by Category */}
+ {getTemplatesByCategory().map(({ category, templates: categoryTemplates }) => (
+
+
+ {category}
+
+
+
+ {categoryTemplates.map((template) => (
+
+
+
+
+
+ {template.name}
+
+ {template.description && (
+
+ {template.description}
+
+ )}
+
+
+
+ {template.tasks.length} tasks
+
+
+
+
+
+
+ {getTotalEstimatedHours(template)}h estimated
+
+
+
+ {Math.abs(Math.min(...template.tasks.map(t => t.dayOffset)))} day timeline
+
+
+
+
+
Sample tasks:
+
+ {template.tasks.slice(0, 3).map((task) => (
+
+ {task.title}
+
+ {formatDayOffset(task.dayOffset)}
+
+
+ ))}
+ {template.tasks.length > 3 && (
+
+ +{template.tasks.length - 3} more tasks
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ ))}
+
+ {/* Launch Project Dialog */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/TaskCard.tsx b/client/src/components/TaskCard.tsx
new file mode 100644
index 0000000..9178727
--- /dev/null
+++ b/client/src/components/TaskCard.tsx
@@ -0,0 +1,148 @@
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Clock, Calendar, Play, Pause, MoreHorizontal } from "lucide-react";
+import { useState } from "react";
+
+export interface Task {
+ id: string;
+ title: string;
+ description?: string;
+ status: 'todo' | 'inProgress' | 'done';
+ priority: 'low' | 'medium' | 'high';
+ dueDate?: Date;
+ timeTracked: number; // in minutes
+ isTracking: boolean;
+ projectId?: string;
+ notes?: string;
+}
+
+interface TaskCardProps {
+ task: Task;
+ onPlay?: () => void;
+ onPause?: () => void;
+ onEdit?: () => void;
+ onStatusChange?: (status: Task['status']) => void;
+ isDragging?: boolean;
+}
+
+export default function TaskCard({ task, onPlay, onPause, onEdit, onStatusChange, isDragging }: TaskCardProps) {
+ const [isTimerRunning, setIsTimerRunning] = useState(task.isTracking);
+
+ const handleToggleTimer = () => {
+ if (isTimerRunning) {
+ onPause?.();
+ console.log(`Timer paused for task: ${task.title}`);
+ } else {
+ onPlay?.();
+ console.log(`Timer started for task: ${task.title}`);
+ }
+ setIsTimerRunning(!isTimerRunning);
+ };
+
+ const getPriorityColor = (priority: string) => {
+ switch (priority) {
+ case 'high': return 'bg-destructive text-destructive-foreground';
+ case 'medium': return 'bg-yellow-500 text-white';
+ default: return 'bg-muted text-muted-foreground';
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'done': return 'bg-green-500 text-white';
+ case 'inProgress': return 'bg-primary text-primary-foreground';
+ default: return 'bg-muted text-muted-foreground';
+ }
+ };
+
+ const formatTime = (minutes: number) => {
+ const hours = Math.floor(minutes / 60);
+ const mins = minutes % 60;
+ return `${hours}h ${mins}m`;
+ };
+
+ return (
+
+
+
+
+ {task.title}
+
+ {task.description && (
+
+ {task.description}
+
+ )}
+
+
+
+ {task.priority}
+
+
+
+ {task.status.replace(/([A-Z])/g, ' $1').toLowerCase()}
+
+
+ {task.dueDate && (
+
+
+
+ {task.dueDate.toLocaleDateString()}
+
+
+ )}
+
+
+ {task.timeTracked > 0 && (
+
+
+
+ {formatTime(task.timeTracked)}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/TaskCreationModal.tsx b/client/src/components/TaskCreationModal.tsx
new file mode 100644
index 0000000..c45bb2a
--- /dev/null
+++ b/client/src/components/TaskCreationModal.tsx
@@ -0,0 +1,152 @@
+import { useState } from 'react';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Calendar } from '@/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { CalendarIcon, Plus } from 'lucide-react';
+import { Task } from './TaskCard';
+
+interface TaskCreationModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSave: (task: Partial) => void;
+}
+
+export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreationModalProps) {
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
+ const [dueDate, setDueDate] = useState();
+ const [isCalendarOpen, setIsCalendarOpen] = useState(false);
+
+ const handleSave = () => {
+ if (!title.trim()) return;
+
+ const newTask: Partial = {
+ title: title.trim(),
+ description: description.trim() || undefined,
+ priority,
+ dueDate,
+ status: 'todo',
+ timeTracked: 0,
+ isTracking: false
+ };
+
+ onSave(newTask);
+ console.log('New task created:', newTask);
+
+ // Reset form
+ setTitle('');
+ setDescription('');
+ setPriority('medium');
+ setDueDate(undefined);
+ onClose();
+ };
+
+ const handleClose = () => {
+ setTitle('');
+ setDescription('');
+ setPriority('medium');
+ setDueDate(undefined);
+ onClose();
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/TaskList.tsx b/client/src/components/TaskList.tsx
new file mode 100644
index 0000000..91e5d4d
--- /dev/null
+++ b/client/src/components/TaskList.tsx
@@ -0,0 +1,188 @@
+import { useState } from 'react';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Search, Filter, SortAsc } from 'lucide-react';
+import { Task } from './TaskCard';
+import TaskCard from './TaskCard';
+
+interface TaskListProps {
+ tasks: Task[];
+ onTaskUpdate?: (taskId: string, updates: Partial) => void;
+ onTaskEdit?: (task: Task) => void;
+}
+
+type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
+type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
+
+export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListProps) {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [sortBy, setSortBy] = useState('dueDate');
+ const [filterBy, setFilterBy] = useState('all');
+
+ const isOverdue = (task: Task) => {
+ if (!task.dueDate) return false;
+ return task.dueDate < new Date() && task.status !== 'done';
+ };
+
+ const filteredAndSortedTasks = tasks
+ .filter(task => {
+ // Search filter
+ const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ task.description?.toLowerCase().includes(searchQuery.toLowerCase());
+
+ if (!matchesSearch) return false;
+
+ // Status filter
+ switch (filterBy) {
+ case 'overdue':
+ return isOverdue(task);
+ case 'all':
+ return true;
+ default:
+ return task.status === filterBy;
+ }
+ })
+ .sort((a, b) => {
+ switch (sortBy) {
+ case 'dueDate':
+ if (!a.dueDate && !b.dueDate) return 0;
+ if (!a.dueDate) return 1;
+ if (!b.dueDate) return -1;
+ return a.dueDate.getTime() - b.dueDate.getTime();
+
+ case 'priority':
+ const priorityOrder = { high: 3, medium: 2, low: 1 };
+ return priorityOrder[b.priority] - priorityOrder[a.priority];
+
+ case 'title':
+ return a.title.localeCompare(b.title);
+
+ case 'status':
+ const statusOrder = { todo: 1, inProgress: 2, done: 3 };
+ return statusOrder[a.status] - statusOrder[b.status];
+
+ default:
+ return 0;
+ }
+ });
+
+ const getFilterCount = (filter: FilterOption) => {
+ switch (filter) {
+ case 'overdue':
+ return tasks.filter(isOverdue).length;
+ case 'all':
+ return tasks.length;
+ default:
+ return tasks.filter(task => task.status === filter).length;
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ My Tasks
+
+
+ {filteredAndSortedTasks.length} tasks
+
+
+
+ {/* Search and Filters */}
+
+
+
+ {
+ setSearchQuery(e.target.value);
+ console.log('Search query:', e.target.value);
+ }}
+ className="pl-10"
+ data-testid="input-search-tasks"
+ />
+
+
+
+
+
+
+
+
+
+ {/* Task List */}
+
+ {filteredAndSortedTasks.length === 0 ? (
+
+
+ {searchQuery || filterBy !== 'all'
+ ? 'No tasks match your filters'
+ : 'No tasks yet. Create your first task!'
+ }
+
+
+ ) : (
+ filteredAndSortedTasks.map((task) => (
+
{
+ onTaskUpdate?.(task.id, { isTracking: true, timeTracked: task.timeTracked });
+ console.log(`Timer started for ${task.title}`);
+ }}
+ onPause={() => {
+ onTaskUpdate?.(task.id, { isTracking: false });
+ console.log(`Timer paused for ${task.title}`);
+ }}
+ onEdit={() => {
+ onTaskEdit?.(task);
+ console.log(`Edit task ${task.title}`);
+ }}
+ onStatusChange={(newStatus) => {
+ onTaskUpdate?.(task.id, { status: newStatus });
+ console.log(`Task ${task.title} status changed to ${newStatus}`);
+ }}
+ />
+ ))
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/ThemeToggle.tsx b/client/src/components/ThemeToggle.tsx
new file mode 100644
index 0000000..4aa2e60
--- /dev/null
+++ b/client/src/components/ThemeToggle.tsx
@@ -0,0 +1,54 @@
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Moon, Sun } from 'lucide-react';
+
+export default function ThemeToggle() {
+ const [isDark, setIsDark] = useState(false);
+
+ useEffect(() => {
+ // Check for saved theme preference or default to light mode
+ const savedTheme = localStorage.getItem('theme');
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+
+ const shouldBeDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
+ setIsDark(shouldBeDark);
+
+ // Apply theme to document
+ if (shouldBeDark) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ }, []);
+
+ const toggleTheme = () => {
+ const newTheme = !isDark;
+ setIsDark(newTheme);
+
+ if (newTheme) {
+ document.documentElement.classList.add('dark');
+ localStorage.setItem('theme', 'dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ localStorage.setItem('theme', 'light');
+ }
+
+ console.log('Theme changed to:', newTheme ? 'dark' : 'light');
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/BottomNavigation.tsx b/client/src/components/examples/BottomNavigation.tsx
new file mode 100644
index 0000000..bef822e
--- /dev/null
+++ b/client/src/components/examples/BottomNavigation.tsx
@@ -0,0 +1,17 @@
+import BottomNavigation from '../BottomNavigation';
+
+export default function BottomNavigationExample() {
+ return (
+
+
+
App Content Area
+
+ This is where your main app content would be displayed.
+ The bottom navigation is fixed at the bottom of the screen.
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/CalendarView.tsx b/client/src/components/examples/CalendarView.tsx
new file mode 100644
index 0000000..861ada2
--- /dev/null
+++ b/client/src/components/examples/CalendarView.tsx
@@ -0,0 +1,50 @@
+import CalendarView from '../CalendarView';
+import { Task } from '../TaskCard';
+import { addDays } from 'date-fns';
+
+const mockTasks: Task[] = [
+ {
+ id: '1',
+ title: 'Team meeting',
+ status: 'todo',
+ priority: 'high',
+ dueDate: new Date(),
+ timeTracked: 0,
+ isTracking: false
+ },
+ {
+ id: '2',
+ title: 'Review design mockups',
+ status: 'inProgress',
+ priority: 'medium',
+ dueDate: addDays(new Date(), 2),
+ timeTracked: 30,
+ isTracking: false
+ },
+ {
+ id: '3',
+ title: 'Client presentation',
+ status: 'todo',
+ priority: 'high',
+ dueDate: addDays(new Date(), 5),
+ timeTracked: 0,
+ isTracking: false
+ },
+ {
+ id: '4',
+ title: 'Code review',
+ status: 'todo',
+ priority: 'low',
+ dueDate: addDays(new Date(), 1),
+ timeTracked: 0,
+ isTracking: false
+ }
+];
+
+export default function CalendarViewExample() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/KanbanBoard.tsx b/client/src/components/examples/KanbanBoard.tsx
new file mode 100644
index 0000000..4b593cb
--- /dev/null
+++ b/client/src/components/examples/KanbanBoard.tsx
@@ -0,0 +1,75 @@
+import KanbanBoard from '../KanbanBoard';
+import { Task } from '../TaskCard';
+import { addDays } from 'date-fns';
+
+const mockTasks: Task[] = [
+ {
+ id: '1',
+ title: 'Design new landing page',
+ description: 'Create wireframes and mockups for the new product landing page',
+ status: 'todo',
+ priority: 'high',
+ dueDate: addDays(new Date(), 3),
+ timeTracked: 0,
+ isTracking: false,
+ projectId: 'project-1'
+ },
+ {
+ id: '2',
+ title: 'Implement user authentication',
+ description: 'Set up login, registration, and password reset functionality',
+ status: 'inProgress',
+ priority: 'high',
+ dueDate: addDays(new Date(), 1),
+ timeTracked: 120,
+ isTracking: true,
+ projectId: 'project-2'
+ },
+ {
+ id: '3',
+ title: 'Write API documentation',
+ status: 'inProgress',
+ priority: 'medium',
+ dueDate: addDays(new Date(), 5),
+ timeTracked: 45,
+ isTracking: false,
+ projectId: 'project-1'
+ },
+ {
+ id: '4',
+ title: 'Set up CI/CD pipeline',
+ description: 'Configure automated testing and deployment',
+ status: 'done',
+ priority: 'medium',
+ dueDate: addDays(new Date(), -2),
+ timeTracked: 180,
+ isTracking: false,
+ projectId: 'project-2'
+ },
+ {
+ id: '5',
+ title: 'Review code changes',
+ status: 'todo',
+ priority: 'low',
+ dueDate: new Date(),
+ timeTracked: 0,
+ isTracking: false
+ },
+ {
+ id: '6',
+ title: 'Update dependencies',
+ status: 'done',
+ priority: 'low',
+ dueDate: addDays(new Date(), -1),
+ timeTracked: 30,
+ isTracking: false
+ }
+];
+
+export default function KanbanBoardExample() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/ProjectTemplate.tsx b/client/src/components/examples/ProjectTemplate.tsx
new file mode 100644
index 0000000..aefded3
--- /dev/null
+++ b/client/src/components/examples/ProjectTemplate.tsx
@@ -0,0 +1,9 @@
+import ProjectTemplate from '../ProjectTemplate';
+
+export default function ProjectTemplateExample() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/TaskCard.tsx b/client/src/components/examples/TaskCard.tsx
new file mode 100644
index 0000000..1a8b4b9
--- /dev/null
+++ b/client/src/components/examples/TaskCard.tsx
@@ -0,0 +1,33 @@
+import TaskCard, { Task } from '../TaskCard';
+
+const mockTask: Task = {
+ id: '1',
+ title: 'Design mobile task interface',
+ description: 'Create wireframes and mockups for the mobile-first task management interface',
+ status: 'inProgress',
+ priority: 'high',
+ dueDate: new Date('2024-09-20'),
+ timeTracked: 45,
+ isTracking: false,
+ projectId: 'project-1',
+ notes: 'Focus on mobile-first design principles'
+};
+
+export default function TaskCardExample() {
+ return (
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/TaskCreationModal.tsx b/client/src/components/examples/TaskCreationModal.tsx
new file mode 100644
index 0000000..36311a7
--- /dev/null
+++ b/client/src/components/examples/TaskCreationModal.tsx
@@ -0,0 +1,29 @@
+import { useState } from 'react';
+import TaskCreationModal from '../TaskCreationModal';
+import { Button } from '@/components/ui/button';
+import { Plus } from 'lucide-react';
+
+export default function TaskCreationModalExample() {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
+
+
setIsOpen(false)}
+ onSave={(task) => {
+ console.log('Task saved:', task);
+ setIsOpen(false);
+ }}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/TaskList.tsx b/client/src/components/examples/TaskList.tsx
new file mode 100644
index 0000000..919ca36
--- /dev/null
+++ b/client/src/components/examples/TaskList.tsx
@@ -0,0 +1,77 @@
+import TaskList from '../TaskList';
+import { Task } from '../TaskCard';
+import { addDays, subDays } from 'date-fns';
+
+const mockTasks: Task[] = [
+ {
+ id: '1',
+ title: 'Review quarterly reports',
+ description: 'Analyze Q3 performance metrics and prepare summary',
+ status: 'todo',
+ priority: 'high',
+ dueDate: addDays(new Date(), 2),
+ timeTracked: 0,
+ isTracking: false,
+ projectId: 'project-1'
+ },
+ {
+ id: '2',
+ title: 'Update website copy',
+ description: 'Revise landing page content based on user feedback',
+ status: 'inProgress',
+ priority: 'medium',
+ dueDate: addDays(new Date(), 5),
+ timeTracked: 90,
+ isTracking: true,
+ projectId: 'project-2'
+ },
+ {
+ id: '3',
+ title: 'Team standup meeting',
+ status: 'todo',
+ priority: 'low',
+ dueDate: new Date(),
+ timeTracked: 0,
+ isTracking: false
+ },
+ {
+ id: '4',
+ title: 'Fix login bug',
+ description: 'Users unable to login with special characters in password',
+ status: 'todo',
+ priority: 'high',
+ dueDate: subDays(new Date(), 1), // Overdue
+ timeTracked: 30,
+ isTracking: false,
+ projectId: 'project-2'
+ },
+ {
+ id: '5',
+ title: 'Deploy new features',
+ status: 'done',
+ priority: 'medium',
+ dueDate: subDays(new Date(), 2),
+ timeTracked: 120,
+ isTracking: false,
+ projectId: 'project-1'
+ },
+ {
+ id: '6',
+ title: 'Write documentation',
+ description: 'Document new API endpoints for external developers',
+ status: 'inProgress',
+ priority: 'medium',
+ dueDate: addDays(new Date(), 7),
+ timeTracked: 60,
+ isTracking: false,
+ projectId: 'project-1'
+ }
+];
+
+export default function TaskListExample() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/examples/ThemeToggle.tsx b/client/src/components/examples/ThemeToggle.tsx
new file mode 100644
index 0000000..44e6d29
--- /dev/null
+++ b/client/src/components/examples/ThemeToggle.tsx
@@ -0,0 +1,19 @@
+import ThemeToggle from '../ThemeToggle';
+
+export default function ThemeToggleExample() {
+ return (
+
+
+
+
Dark Mode
+
Toggle between light and dark themes
+
+
+
+
+
+
This card will change appearance when you toggle the theme.
+
+
+ );
+}
\ No newline at end of file
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..fc7f964
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,51 @@
+"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..b59d7ad
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,38 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ // Whitespace-nowrap: Badges should never wrap.
+ "whitespace-nowrap inline-flex items-center rounded-md 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" +
+ " hover-elevate " ,
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow-xs",
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow-xs",
+
+ outline: " border [border-color:var(--badge-outline)] shadow-xs",
+ },
+ },
+ 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..409cfbf
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,62 @@
+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 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0" +
+ " hover-elevate active-elevate-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground border border-primary-border",
+ destructive:
+ "bg-destructive text-destructive-foreground border border-destructive-border",
+ outline:
+ // Shows the background color of whatever card / sidebar / accent background it is inside of.
+ // Inherits the current text color.
+ " border [border-color:var(--button-outline)] shadow-xs active:shadow-none ",
+ secondary: "border bg-secondary text-secondary-foreground border border-secondary-border ",
+ // Add a transparent border so that when someone toggles a border on later, it doesn't shift layout/size.
+ ghost: "border border-transparent",
+ },
+ // Heights are set as "min" heights, because sometimes Ai will place large amount of content
+ // inside buttons. With a min-height they will look appropriate with small amounts of content,
+ // but will expand to fit large amounts of content.
+ size: {
+ default: "min-h-9 px-4 py-2",
+ sm: "min-h-8 rounded-md px-3 text-xs",
+ lg: "min-h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ 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..c65c4c6
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,85 @@
+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 (
+