From abe4041cd1d40d4e49f14e0cbedb5c068209560f Mon Sep 17 00:00:00 2001
From: marco370 <48531002-marco370@users.noreply.replit.com>
Date: Sat, 11 Oct 2025 09:36:55 +0000
Subject: [PATCH] Add basic UI components and structure for the application
Initial commit adds core UI components, including layout elements, form controls, and navigation elements, along with the main application structure and routing.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/nGJAldO
---
.gitignore | 6 +
.replit | 50 +
client/index.html | 26 +
client/src/App.tsx | 84 +
client/src/components/app-sidebar.tsx | 134 +
client/src/components/kpi-card.tsx | 37 +
client/src/components/status-badge.tsx | 31 +
client/src/components/theme-provider.tsx | 73 +
client/src/components/theme-toggle.tsx | 20 +
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/hooks/useAuth.ts | 15 +
client/src/index.css | 355 +
client/src/lib/authUtils.ts | 3 +
client/src/lib/queryClient.ts | 57 +
client/src/lib/utils.ts | 6 +
client/src/main.tsx | 5 +
client/src/pages/dashboard.tsx | 192 +
client/src/pages/guards.tsx | 293 +
client/src/pages/landing.tsx | 117 +
client/src/pages/not-found.tsx | 21 +
client/src/pages/notifications.tsx | 128 +
client/src/pages/reports.tsx | 227 +
client/src/pages/shifts.tsx | 274 +
client/src/pages/sites.tsx | 286 +
components.json | 20 +
design_guidelines.md | 152 +
drizzle.config.ts | 14 +
package-lock.json | 8457 ++++++++++++++++++
package.json | 109 +
postcss.config.js | 6 +
replit.md | 140 +
server/db.ts | 16 +
server/index.ts | 71 +
server/replitAuth.ts | 158 +
server/routes.ts | 253 +
server/storage.ts | 235 +
server/vite.ts | 85 +
shared/schema.ts | 315 +
tailwind.config.ts | 107 +
tsconfig.json | 23 +
vite.config.ts | 40 +
89 files changed, 17604 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/app-sidebar.tsx
create mode 100644 client/src/components/kpi-card.tsx
create mode 100644 client/src/components/status-badge.tsx
create mode 100644 client/src/components/theme-provider.tsx
create mode 100644 client/src/components/theme-toggle.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/hooks/useAuth.ts
create mode 100644 client/src/index.css
create mode 100644 client/src/lib/authUtils.ts
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/dashboard.tsx
create mode 100644 client/src/pages/guards.tsx
create mode 100644 client/src/pages/landing.tsx
create mode 100644 client/src/pages/not-found.tsx
create mode 100644 client/src/pages/notifications.tsx
create mode 100644 client/src/pages/reports.tsx
create mode 100644 client/src/pages/shifts.tsx
create mode 100644 client/src/pages/sites.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 replit.md
create mode 100644 server/db.ts
create mode 100644 server/index.ts
create mode 100644 server/replitAuth.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..03709f0
--- /dev/null
+++ b/.replit
@@ -0,0 +1,50 @@
+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 = 33035
+externalPort = 3001
+
+[[ports]]
+localPort = 41343
+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_database:1.0.0", "javascript_log_in_with_replit:1.0.0"]
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..aae19fb
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ VigilanzaTurni - Sistema Gestione Turni Professionale per Istituti di Vigilanza
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..2c81685
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,84 @@
+import { Switch, Route } from "wouter";
+import { queryClient } from "./lib/queryClient";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { ThemeProvider } from "@/components/theme-provider";
+import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
+import { AppSidebar } from "@/components/app-sidebar";
+import { useAuth } from "@/hooks/useAuth";
+import NotFound from "@/pages/not-found";
+import Landing from "@/pages/landing";
+import Dashboard from "@/pages/dashboard";
+import Guards from "@/pages/guards";
+import Sites from "@/pages/sites";
+import Shifts from "@/pages/shifts";
+import Reports from "@/pages/reports";
+import Notifications from "@/pages/notifications";
+
+function Router() {
+ const { isAuthenticated, isLoading } = useAuth();
+
+ return (
+
+ {isLoading || !isAuthenticated ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+}
+
+function AppContent() {
+ const { isAuthenticated, isLoading } = useAuth();
+
+ // Sidebar style configuration for operational dashboard
+ const sidebarStyle = {
+ "--sidebar-width": "16rem",
+ "--sidebar-width-icon": "3rem",
+ } as React.CSSProperties;
+
+ return (
+
+
+ {!isLoading && isAuthenticated ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx
new file mode 100644
index 0000000..bc51cc8
--- /dev/null
+++ b/client/src/components/app-sidebar.tsx
@@ -0,0 +1,134 @@
+import {
+ Calendar,
+ Shield,
+ MapPin,
+ Users,
+ BarChart3,
+ Bell,
+ Settings,
+} from "lucide-react";
+import { Link, useLocation } from "wouter";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarHeader,
+ SidebarFooter,
+} from "@/components/ui/sidebar";
+import { useAuth } from "@/hooks/useAuth";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { ThemeToggle } from "@/components/theme-toggle";
+
+const menuItems = [
+ {
+ title: "Dashboard",
+ url: "/",
+ icon: Shield,
+ roles: ["admin", "coordinator", "guard", "client"],
+ },
+ {
+ title: "Turni",
+ url: "/shifts",
+ icon: Calendar,
+ roles: ["admin", "coordinator", "guard"],
+ },
+ {
+ title: "Guardie",
+ url: "/guards",
+ icon: Users,
+ roles: ["admin", "coordinator"],
+ },
+ {
+ title: "Siti",
+ url: "/sites",
+ icon: MapPin,
+ roles: ["admin", "coordinator", "client"],
+ },
+ {
+ title: "Report",
+ url: "/reports",
+ icon: BarChart3,
+ roles: ["admin", "coordinator", "client"],
+ },
+ {
+ title: "Notifiche",
+ url: "/notifications",
+ icon: Bell,
+ roles: ["admin", "coordinator", "guard"],
+ },
+];
+
+export function AppSidebar() {
+ const { user } = useAuth();
+ const [location] = useLocation();
+
+ const filteredItems = menuItems.filter(
+ (item) => user && item.roles.includes(user.role)
+ );
+
+ return (
+
+
+
+
+
+
VigilanzaTurni
+
Sistema Gestione
+
+
+
+
+
+
+ Menu Principale
+
+
+ {filteredItems.map((item) => (
+
+
+
+
+ {item.title}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ {user?.firstName?.[0]}{user?.lastName?.[0]}
+
+
+
+
+ {user?.firstName} {user?.lastName}
+
+
+ {user?.role}
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/kpi-card.tsx b/client/src/components/kpi-card.tsx
new file mode 100644
index 0000000..2cbccc3
--- /dev/null
+++ b/client/src/components/kpi-card.tsx
@@ -0,0 +1,37 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { LucideIcon } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface KPICardProps extends React.HTMLAttributes {
+ title: string;
+ value: string | number;
+ icon: LucideIcon;
+ trend?: {
+ value: string;
+ isPositive: boolean;
+ };
+}
+
+export function KPICard({ title, value, icon: Icon, trend, className, ...props }: KPICardProps) {
+ return (
+
+
+
+ {title}
+
+
+
+
+ {value}
+ {trend && (
+
+ {trend.value}
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/status-badge.tsx b/client/src/components/status-badge.tsx
new file mode 100644
index 0000000..9bba669
--- /dev/null
+++ b/client/src/components/status-badge.tsx
@@ -0,0 +1,31 @@
+import { Badge } from "@/components/ui/badge";
+import { cn } from "@/lib/utils";
+
+interface StatusBadgeProps {
+ status: "active" | "inactive" | "late" | "emergency" | "pending" | "completed";
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function StatusBadge({ status, children, className }: StatusBadgeProps) {
+ const statusColors = {
+ active: "bg-[hsl(140,60%,45%)] text-white border-[hsl(140,60%,35%)]",
+ inactive: "bg-muted text-muted-foreground border-muted-border",
+ late: "bg-[hsl(25,90%,55%)] text-white border-[hsl(25,90%,45%)]",
+ emergency: "bg-destructive text-destructive-foreground border-destructive-border",
+ pending: "bg-[hsl(45,90%,55%)] text-white border-[hsl(45,90%,45%)]",
+ completed: "bg-[hsl(140,60%,45%)] text-white border-[hsl(140,60%,35%)]",
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/client/src/components/theme-provider.tsx b/client/src/components/theme-provider.tsx
new file mode 100644
index 0000000..74c81c4
--- /dev/null
+++ b/client/src/components/theme-provider.tsx
@@ -0,0 +1,73 @@
+import { createContext, useContext, useEffect, useState } from "react";
+
+type Theme = "dark" | "light" | "system";
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+type ThemeProviderState = {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+};
+
+const initialState: ThemeProviderState = {
+ theme: "system",
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "dark",
+ storageKey = "vite-ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
+ );
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined)
+ throw new Error("useTheme must be used within a ThemeProvider");
+
+ return context;
+};
diff --git a/client/src/components/theme-toggle.tsx b/client/src/components/theme-toggle.tsx
new file mode 100644
index 0000000..31a37ee
--- /dev/null
+++ b/client/src/components/theme-toggle.tsx
@@ -0,0 +1,20 @@
+import { Moon, Sun } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useTheme } from "@/components/theme-provider";
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+ );
+}
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 (
+