From 28ad3f1535d2ece6a19ad43fb8e0c41699943614 Mon Sep 17 00:00:00 2001
From: paul-nothaft <40865108-paul-nothaft@users.noreply.replit.com>
Date: Thu, 11 Sep 2025 08:59:20 +0000
Subject: [PATCH] Add core components for task management and navigation
This commit introduces the foundational UI components and logic for the task management application, including task creation, calendar views, Kanban boards, and navigation elements.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/yy9YLEW
---
.gitignore | 6 +
.replit | 46 +
client/index.html | 18 +
client/src/App.tsx | 257 +
client/src/components/BottomNavigation.tsx | 80 +
client/src/components/CalendarView.tsx | 169 +
client/src/components/KanbanBoard.tsx | 208 +
client/src/components/ProjectTemplate.tsx | 310 +
client/src/components/TaskCard.tsx | 148 +
client/src/components/TaskCreationModal.tsx | 152 +
client/src/components/TaskList.tsx | 188 +
client/src/components/ThemeToggle.tsx | 54 +
.../components/examples/BottomNavigation.tsx | 17 +
.../src/components/examples/CalendarView.tsx | 50 +
.../src/components/examples/KanbanBoard.tsx | 75 +
.../components/examples/ProjectTemplate.tsx | 9 +
client/src/components/examples/TaskCard.tsx | 33 +
.../components/examples/TaskCreationModal.tsx | 29 +
client/src/components/examples/TaskList.tsx | 77 +
.../src/components/examples/ThemeToggle.tsx | 19 +
client/src/components/ui/accordion.tsx | 56 +
client/src/components/ui/alert-dialog.tsx | 139 +
client/src/components/ui/alert.tsx | 59 +
client/src/components/ui/aspect-ratio.tsx | 5 +
client/src/components/ui/avatar.tsx | 51 +
client/src/components/ui/badge.tsx | 38 +
client/src/components/ui/breadcrumb.tsx | 115 +
client/src/components/ui/button.tsx | 62 +
client/src/components/ui/calendar.tsx | 68 +
client/src/components/ui/card.tsx | 85 +
client/src/components/ui/carousel.tsx | 260 +
client/src/components/ui/chart.tsx | 365 +
client/src/components/ui/checkbox.tsx | 28 +
client/src/components/ui/collapsible.tsx | 11 +
client/src/components/ui/command.tsx | 151 +
client/src/components/ui/context-menu.tsx | 198 +
client/src/components/ui/dialog.tsx | 122 +
client/src/components/ui/drawer.tsx | 118 +
client/src/components/ui/dropdown-menu.tsx | 198 +
client/src/components/ui/form.tsx | 178 +
client/src/components/ui/hover-card.tsx | 29 +
client/src/components/ui/input-otp.tsx | 69 +
client/src/components/ui/input.tsx | 23 +
client/src/components/ui/label.tsx | 24 +
client/src/components/ui/menubar.tsx | 256 +
client/src/components/ui/navigation-menu.tsx | 128 +
client/src/components/ui/pagination.tsx | 117 +
client/src/components/ui/popover.tsx | 29 +
client/src/components/ui/progress.tsx | 28 +
client/src/components/ui/radio-group.tsx | 42 +
client/src/components/ui/resizable.tsx | 45 +
client/src/components/ui/scroll-area.tsx | 46 +
client/src/components/ui/select.tsx | 160 +
client/src/components/ui/separator.tsx | 29 +
client/src/components/ui/sheet.tsx | 140 +
client/src/components/ui/sidebar.tsx | 727 ++
client/src/components/ui/skeleton.tsx | 15 +
client/src/components/ui/slider.tsx | 26 +
client/src/components/ui/switch.tsx | 27 +
client/src/components/ui/table.tsx | 117 +
client/src/components/ui/tabs.tsx | 53 +
client/src/components/ui/textarea.tsx | 22 +
client/src/components/ui/toast.tsx | 127 +
client/src/components/ui/toaster.tsx | 33 +
client/src/components/ui/toggle-group.tsx | 61 +
client/src/components/ui/toggle.tsx | 43 +
client/src/components/ui/tooltip.tsx | 30 +
client/src/hooks/use-mobile.tsx | 19 +
client/src/hooks/use-toast.ts | 191 +
client/src/index.css | 328 +
client/src/lib/queryClient.ts | 57 +
client/src/lib/utils.ts | 6 +
client/src/main.tsx | 5 +
client/src/pages/not-found.tsx | 21 +
components.json | 20 +
design_guidelines.md | 79 +
drizzle.config.ts | 14 +
package-lock.json | 9116 +++++++++++++++++
package.json | 105 +
postcss.config.js | 6 +
server/index.ts | 71 +
server/routes.ts | 15 +
server/storage.ts | 38 +
server/vite.ts | 85 +
shared/schema.ts | 18 +
tailwind.config.ts | 107 +
tsconfig.json | 23 +
vite.config.ts | 37 +
88 files changed, 17059 insertions(+)
create mode 100644 .gitignore
create mode 100644 .replit
create mode 100644 client/index.html
create mode 100644 client/src/App.tsx
create mode 100644 client/src/components/BottomNavigation.tsx
create mode 100644 client/src/components/CalendarView.tsx
create mode 100644 client/src/components/KanbanBoard.tsx
create mode 100644 client/src/components/ProjectTemplate.tsx
create mode 100644 client/src/components/TaskCard.tsx
create mode 100644 client/src/components/TaskCreationModal.tsx
create mode 100644 client/src/components/TaskList.tsx
create mode 100644 client/src/components/ThemeToggle.tsx
create mode 100644 client/src/components/examples/BottomNavigation.tsx
create mode 100644 client/src/components/examples/CalendarView.tsx
create mode 100644 client/src/components/examples/KanbanBoard.tsx
create mode 100644 client/src/components/examples/ProjectTemplate.tsx
create mode 100644 client/src/components/examples/TaskCard.tsx
create mode 100644 client/src/components/examples/TaskCreationModal.tsx
create mode 100644 client/src/components/examples/TaskList.tsx
create mode 100644 client/src/components/examples/ThemeToggle.tsx
create mode 100644 client/src/components/ui/accordion.tsx
create mode 100644 client/src/components/ui/alert-dialog.tsx
create mode 100644 client/src/components/ui/alert.tsx
create mode 100644 client/src/components/ui/aspect-ratio.tsx
create mode 100644 client/src/components/ui/avatar.tsx
create mode 100644 client/src/components/ui/badge.tsx
create mode 100644 client/src/components/ui/breadcrumb.tsx
create mode 100644 client/src/components/ui/button.tsx
create mode 100644 client/src/components/ui/calendar.tsx
create mode 100644 client/src/components/ui/card.tsx
create mode 100644 client/src/components/ui/carousel.tsx
create mode 100644 client/src/components/ui/chart.tsx
create mode 100644 client/src/components/ui/checkbox.tsx
create mode 100644 client/src/components/ui/collapsible.tsx
create mode 100644 client/src/components/ui/command.tsx
create mode 100644 client/src/components/ui/context-menu.tsx
create mode 100644 client/src/components/ui/dialog.tsx
create mode 100644 client/src/components/ui/drawer.tsx
create mode 100644 client/src/components/ui/dropdown-menu.tsx
create mode 100644 client/src/components/ui/form.tsx
create mode 100644 client/src/components/ui/hover-card.tsx
create mode 100644 client/src/components/ui/input-otp.tsx
create mode 100644 client/src/components/ui/input.tsx
create mode 100644 client/src/components/ui/label.tsx
create mode 100644 client/src/components/ui/menubar.tsx
create mode 100644 client/src/components/ui/navigation-menu.tsx
create mode 100644 client/src/components/ui/pagination.tsx
create mode 100644 client/src/components/ui/popover.tsx
create mode 100644 client/src/components/ui/progress.tsx
create mode 100644 client/src/components/ui/radio-group.tsx
create mode 100644 client/src/components/ui/resizable.tsx
create mode 100644 client/src/components/ui/scroll-area.tsx
create mode 100644 client/src/components/ui/select.tsx
create mode 100644 client/src/components/ui/separator.tsx
create mode 100644 client/src/components/ui/sheet.tsx
create mode 100644 client/src/components/ui/sidebar.tsx
create mode 100644 client/src/components/ui/skeleton.tsx
create mode 100644 client/src/components/ui/slider.tsx
create mode 100644 client/src/components/ui/switch.tsx
create mode 100644 client/src/components/ui/table.tsx
create mode 100644 client/src/components/ui/tabs.tsx
create mode 100644 client/src/components/ui/textarea.tsx
create mode 100644 client/src/components/ui/toast.tsx
create mode 100644 client/src/components/ui/toaster.tsx
create mode 100644 client/src/components/ui/toggle-group.tsx
create mode 100644 client/src/components/ui/toggle.tsx
create mode 100644 client/src/components/ui/tooltip.tsx
create mode 100644 client/src/hooks/use-mobile.tsx
create mode 100644 client/src/hooks/use-toast.ts
create mode 100644 client/src/index.css
create mode 100644 client/src/lib/queryClient.ts
create mode 100644 client/src/lib/utils.ts
create mode 100644 client/src/main.tsx
create mode 100644 client/src/pages/not-found.tsx
create mode 100644 components.json
create mode 100644 design_guidelines.md
create mode 100644 drizzle.config.ts
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 server/index.ts
create mode 100644 server/routes.ts
create mode 100644 server/storage.ts
create mode 100644 server/vite.ts
create mode 100644 shared/schema.ts
create mode 100644 tailwind.config.ts
create mode 100644 tsconfig.json
create mode 100644 vite.config.ts
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 (
+