diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9ba7f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+dist
+.DS_Store
+server/public
+vite.config.ts.*
+*.tar.gz
\ No newline at end of file
diff --git a/.replit b/.replit
index e69de29..b5f2185 100644
--- a/.replit
+++ b/.replit
@@ -0,0 +1,32 @@
+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"]
+
+[workflows]
+runButton = "Project"
+
+[[workflows.workflow]]
+name = "Project"
+mode = "parallel"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "workflow.run"
+args = "Start application"
+
+[[workflows.workflow]]
+name = "Start application"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "shell.exec"
+args = "npm run dev"
+waitForPort = 5000
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..4b4d09e
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..425281c
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,41 @@
+import { Switch, Route } from "wouter";
+import { queryClient } from "./lib/queryClient";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { AuthProvider } from "@/context/AuthContext";
+import { ThemeProvider } from "@/context/ThemeContext";
+import LandingPage from "@/pages/LandingPage";
+import DashboardPage from "@/pages/DashboardPage";
+import LoginPage from "@/pages/LoginPage";
+import OnboardingPage from "@/pages/OnboardingPage";
+import NotFound from "@/pages/not-found";
+
+function Router() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/ai/DailyJoke.tsx b/client/src/components/ai/DailyJoke.tsx
new file mode 100644
index 0000000..a39396a
--- /dev/null
+++ b/client/src/components/ai/DailyJoke.tsx
@@ -0,0 +1,139 @@
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { useVoice } from "@/hooks/useVoice";
+import { aiService } from "@/services/aiService";
+import { Brain, Volume2, RefreshCw } from "lucide-react";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function DailyJoke() {
+ const { speak } = useVoice();
+
+ const {
+ data: jokeData,
+ isLoading,
+ error,
+ refetch
+ } = useQuery({
+ queryKey: ["/api/ai/daily-joke"],
+ staleTime: 30 * 60 * 1000, // Cache for 30 minutes
+ retry: 2,
+ });
+
+ const handlePlayJoke = () => {
+ if (jokeData?.joke) {
+ speak(jokeData.joke);
+ }
+ };
+
+ const handleRefreshJoke = () => {
+ refetch();
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
+
+
+
AI Joke Unavailable
+
+ Failed to load today's AI joke. Please try again.
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
Daily AI Joke
+
+
+
+
+
+
+
+ {jokeData?.joke || "Loading today's AI-generated joke..."}
+
+
+
+
+
+ {jokeData?.cached ? "Cached" : "Fresh"}
+
+ Powered by Local AI
+
+
+ {jokeData?.timestamp && (
+
+ {new Date(jokeData.timestamp).toLocaleDateString()}
+
+ )}
+
+
+ {jokeData?.processingTime && (
+
+ Generated in {jokeData.processingTime}ms
+
+ )}
+
+
+
+
+ );
+}
diff --git a/client/src/components/financial/FinancialOverview.tsx b/client/src/components/financial/FinancialOverview.tsx
new file mode 100644
index 0000000..3e7a67c
--- /dev/null
+++ b/client/src/components/financial/FinancialOverview.tsx
@@ -0,0 +1,319 @@
+import { useQuery } from "@tanstack/react-query";
+import { financialService } from "@/services/financialService";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+ DollarSign,
+ TrendingUp,
+ TrendingDown,
+ Wallet,
+ ArrowUpRight,
+ ArrowDownRight,
+ Mic,
+ Calendar
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { formatDistanceToNow } from "date-fns";
+
+interface FinancialOverviewProps {
+ period?: "week" | "month" | "year";
+ limit?: number;
+ showHeader?: boolean;
+ className?: string;
+}
+
+export default function FinancialOverview({
+ period = "month",
+ limit = 5,
+ showHeader = true,
+ className
+}: FinancialOverviewProps) {
+ const {
+ data: summaryData,
+ isLoading: summaryLoading
+ } = useQuery({
+ queryKey: ["/api/financial/summary", { period }],
+ staleTime: 60 * 1000,
+ });
+
+ const {
+ data: recordsData,
+ isLoading: recordsLoading
+ } = useQuery({
+ queryKey: ["/api/financial/records"],
+ staleTime: 30 * 1000,
+ });
+
+ const isLoading = summaryLoading || recordsLoading;
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 2,
+ }).format(amount);
+ };
+
+ const getChangeColor = (value: number) => {
+ if (value > 0) return "text-green-600 dark:text-green-400";
+ if (value < 0) return "text-red-600 dark:text-red-400";
+ return "text-muted-foreground";
+ };
+
+ const getTransactionIcon = (type: string, amount: number) => {
+ if (type === "income") {
+ return ;
+ }
+ return ;
+ };
+
+ const getCategoryColor = (category: string, type: string) => {
+ if (type === "income") {
+ return "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400";
+ }
+
+ const categoryColors: Record = {
+ "food": "bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400",
+ "transport": "bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400",
+ "entertainment": "bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-400",
+ "utilities": "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400",
+ "healthcare": "bg-pink-100 text-pink-800 dark:bg-pink-900/20 dark:text-pink-400",
+ "shopping": "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/20 dark:text-indigo-400",
+ };
+
+ return categoryColors[category.toLowerCase()] || "bg-slate-100 text-slate-800 dark:bg-slate-900/20 dark:text-slate-400";
+ };
+
+ if (isLoading) {
+ return (
+
+ {/* Summary Cards Skeleton */}
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Recent Transactions Skeleton */}
+
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ );
+ }
+
+ const summary = summaryData?.summary || { income: 0, expenses: 0, net: 0 };
+ const records = recordsData?.records || [];
+ const recentRecords = records.slice(0, limit);
+
+ return (
+
+ {/* Financial Summary Cards */}
+
+ {/* Income Card */}
+
+
+
+
+
Income
+
+ {formatCurrency(summary.income)}
+
+
+
+
+
+
+
+
+ This {period}
+
+
+
+
+
+ {/* Expenses Card */}
+
+
+
+
+
Expenses
+
+ {formatCurrency(summary.expenses)}
+
+
+
+
+
+
+
+
+ This {period}
+
+
+
+
+
+ {/* Net Balance Card */}
+
+
+
+
+
Net Balance
+
+ {formatCurrency(summary.net)}
+
+
+
= 0
+ ? "bg-primary-100 dark:bg-primary-900/50"
+ : "bg-red-100 dark:bg-red-900/50"
+ )}>
+ = 0
+ ? "text-primary-600 dark:text-primary-400"
+ : "text-red-600 dark:text-red-400"
+ )} />
+
+
+
+ {summary.net >= 0 ? (
+
+ Positive
+
+ ) : (
+
+ Deficit
+
+ )}
+
+
+
+
+
+ {/* Recent Transactions */}
+
+ {showHeader && (
+
+
+
+
+ Recent Transactions
+
+
+
+
+ )}
+
+
+ {recentRecords.length === 0 ? (
+
+
+
No financial records found
+
Add transactions using voice commands or manually
+
+ ) : (
+
+ {recentRecords.map((record) => (
+
+
+
+ {getTransactionIcon(record.type, parseFloat(record.amount))}
+
+
+
+
+
+ {record.description || record.category}
+
+ {record.createdViaVoice && (
+
+
+ Voice
+
+ )}
+
+
+
+
+ {record.category}
+
+
+
+
+
+ {formatDistanceToNow(new Date(record.date), { addSuffix: true })}
+
+
+
+
+
+
+
+
+ {record.type === "income" ? "+" : "-"}{formatCurrency(parseFloat(record.amount))}
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx
new file mode 100644
index 0000000..27c8a1d
--- /dev/null
+++ b/client/src/components/layout/Header.tsx
@@ -0,0 +1,152 @@
+import { Link, useLocation } from "wouter";
+import { useAuth } from "@/hooks/useAuth";
+import { useTheme } from "@/context/ThemeContext";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Moon,
+ Sun,
+ Mic,
+ User,
+ Settings,
+ LogOut,
+ Activity
+} from "lucide-react";
+
+export default function Header() {
+ const { user, logout } = useAuth();
+ const { theme, toggleTheme } = useTheme();
+ const [, setLocation] = useLocation();
+
+ const handleLogout = async () => {
+ await logout();
+ setLocation("/");
+ };
+
+ const getUserInitials = (user: any) => {
+ if (user.firstName && user.lastName) {
+ return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase();
+ }
+ return user.username?.[0]?.toUpperCase() || "U";
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..6e52de1
--- /dev/null
+++ b/client/src/components/layout/Sidebar.tsx
@@ -0,0 +1,190 @@
+import { Link, useLocation } from "wouter";
+import { useAuth } from "@/hooks/useAuth";
+import { cn } from "@/lib/utils";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Separator } from "@/components/ui/separator";
+import {
+ Calendar,
+ DollarSign,
+ LayoutDashboard,
+ Mic,
+ Brain,
+ Settings,
+ Users,
+ BarChart3,
+ Shield,
+ Zap
+} from "lucide-react";
+
+interface SidebarProps {
+ className?: string;
+}
+
+const navigation = [
+ {
+ name: "Dashboard",
+ href: "/dashboard",
+ icon: LayoutDashboard,
+ },
+ {
+ name: "Tasks",
+ href: "/tasks",
+ icon: Calendar,
+ badge: "3", // This would come from task count
+ },
+ {
+ name: "Finances",
+ href: "/finances",
+ icon: DollarSign,
+ },
+ {
+ name: "Voice Commands",
+ href: "/voice",
+ icon: Mic,
+ },
+ {
+ name: "AI Assistant",
+ href: "/ai",
+ icon: Brain,
+ },
+];
+
+const managementNavigation = [
+ {
+ name: "Analytics",
+ href: "/analytics",
+ icon: BarChart3,
+ roles: ["pro", "admin"],
+ },
+ {
+ name: "Settings",
+ href: "/settings",
+ icon: Settings,
+ },
+];
+
+const adminNavigation = [
+ {
+ name: "User Management",
+ href: "/admin/users",
+ icon: Users,
+ roles: ["admin"],
+ },
+ {
+ name: "System Status",
+ href: "/admin/system",
+ icon: Shield,
+ roles: ["admin"],
+ },
+ {
+ name: "Model Management",
+ href: "/admin/models",
+ icon: Zap,
+ roles: ["admin"],
+ },
+];
+
+export default function Sidebar({ className }: SidebarProps) {
+ const [location] = useLocation();
+ const { user } = useAuth();
+
+ const isActive = (href: string) => {
+ if (href === "/dashboard") {
+ return location === "/" || location === "/dashboard";
+ }
+ return location.startsWith(href);
+ };
+
+ const canAccess = (roles?: string[]) => {
+ if (!roles) return true;
+ return user && roles.includes(user.role);
+ };
+
+ return (
+
+
+
+
+
+ Navigation
+
+
+ {navigation.map((item) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Management
+
+
+ {managementNavigation
+ .filter((item) => canAccess(item.roles))
+ .map((item) => (
+
+
+
+ ))}
+
+
+
+ {user?.role === "admin" && (
+ <>
+
+
+
+ Administration
+
+
+ {adminNavigation
+ .filter((item) => canAccess(item.roles))
+ .map((item) => (
+
+
+
+ ))}
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/client/src/components/tasks/TaskList.tsx b/client/src/components/tasks/TaskList.tsx
new file mode 100644
index 0000000..ab1cec4
--- /dev/null
+++ b/client/src/components/tasks/TaskList.tsx
@@ -0,0 +1,305 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { taskService } from "@/services/taskService";
+import { useToast } from "@/hooks/use-toast";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+ Calendar,
+ Clock,
+ CheckCircle2,
+ Circle,
+ Mic,
+ Plus,
+ MoreHorizontal
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { formatDistanceToNow, isAfter, isBefore, startOfDay } from "date-fns";
+
+interface TaskListProps {
+ status?: string;
+ limit?: number;
+ showHeader?: boolean;
+ className?: string;
+}
+
+export default function TaskList({
+ status,
+ limit,
+ showHeader = true,
+ className
+}: TaskListProps) {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+
+ const {
+ data: tasksData,
+ isLoading,
+ error
+ } = useQuery({
+ queryKey: ["/api/tasks", { status }],
+ staleTime: 30 * 1000,
+ });
+
+ const completeTaskMutation = useMutation({
+ mutationFn: (taskId: number) => taskService.completeTask(taskId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
+ toast({
+ title: "Task completed",
+ description: "Task marked as completed successfully",
+ });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: "Failed to complete task",
+ variant: "destructive",
+ });
+ },
+ });
+
+ const deleteTaskMutation = useMutation({
+ mutationFn: (taskId: number) => taskService.deleteTask(taskId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
+ toast({
+ title: "Task deleted",
+ description: "Task deleted successfully",
+ });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: "Failed to delete task",
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleCompleteTask = (taskId: number) => {
+ completeTaskMutation.mutate(taskId);
+ };
+
+ const getPriorityColor = (priority: string) => {
+ switch (priority) {
+ case "high":
+ return "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400";
+ case "medium":
+ return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400";
+ case "low":
+ return "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400";
+ default:
+ return "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400";
+ }
+ };
+
+ const getTaskStatus = (task: any) => {
+ if (task.status === "completed") return "completed";
+ if (!task.dueDate) return "normal";
+
+ const now = new Date();
+ const dueDate = new Date(task.dueDate);
+ const today = startOfDay(now);
+ const taskDueDay = startOfDay(dueDate);
+
+ if (isBefore(taskDueDay, today)) return "overdue";
+ if (taskDueDay.getTime() === today.getTime()) return "due-today";
+ return "normal";
+ };
+
+ const getStatusBadge = (task: any) => {
+ const status = getTaskStatus(task);
+
+ switch (status) {
+ case "completed":
+ return Completed;
+ case "overdue":
+ return Overdue;
+ case "due-today":
+ return Due Today;
+ default:
+ return null;
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ {showHeader && (
+
+
+
+ Tasks
+
+
+ )}
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {showHeader && (
+
+
+
+ Tasks
+
+
+ )}
+
+
+
+
Failed to load tasks
+
+
+
+ );
+ }
+
+ const tasks = tasksData?.tasks || [];
+ const displayTasks = limit ? tasks.slice(0, limit) : tasks;
+
+ if (displayTasks.length === 0) {
+ return (
+
+ {showHeader && (
+
+
+
+ Tasks
+
+
+ )}
+
+
+
+
No tasks found
+
Create a task using voice commands or the dashboard
+
+
+
+ );
+ }
+
+ return (
+
+ {showHeader && (
+
+
+
+
+ Tasks
+ {tasks.length}
+
+
+
+
+ )}
+
+
+
+ {displayTasks.map((task) => (
+
+ {/* Completion Checkbox */}
+
+ handleCompleteTask(task.id)}
+ disabled={completeTaskMutation.isPending}
+ className="w-5 h-5"
+ />
+
+
+ {/* Task Content */}
+
+
+
+
+ {task.title}
+
+
+ {task.description && (
+
+ {task.description}
+
+ )}
+
+
+ {/* Priority Badge */}
+
+ {task.priority}
+
+
+ {/* Voice Created Indicator */}
+ {task.createdViaVoice && (
+
+
+ Voice
+
+ )}
+
+ {/* Due Date */}
+ {task.dueDate && (
+
+
+
+ {formatDistanceToNow(new Date(task.dueDate), { addSuffix: true })}
+
+
+ )}
+
+
+
+ {/* Status Badge */}
+
+ {getStatusBadge(task)}
+
+
+
+
+ ))}
+
+
+ {limit && tasks.length > limit && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..e6a723d
--- /dev/null
+++ b/client/src/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..8722561
--- /dev/null
+++ b/client/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/client/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/client/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/client/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..2174f71
--- /dev/null
+++ b/client/src/components/ui/calendar.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/client/src/components/ui/carousel.tsx b/client/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..9c2b9bf
--- /dev/null
+++ b/client/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/client/src/components/ui/chart.tsx b/client/src/components/ui/chart.tsx
new file mode 100644
index 0000000..39fba6d
--- /dev/null
+++ b/client/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+