refactor: refactor the color schema

This commit is contained in:
2026-03-17 18:26:53 +05:30
parent 8ddd2df981
commit caf527f584
59 changed files with 3617 additions and 2642 deletions

View File

@@ -0,0 +1,106 @@
"use client";
import * as React from "react";
import {
LayoutDashboard,
Plus,
FolderPlus,
Package,
MapPin,
} from "lucide-react";
import { NavUser } from "@/components/nav-user";
import { ROUTES } from "@/utils/routes";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarGroup,
SidebarGroupLabel,
} from "@/components/ui/sidebar";
import Link from "next/link";
import { usePathname } from "next/navigation";
// This is the navigation data
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Dashboard",
url: ROUTES.DASHBOARD,
icon: LayoutDashboard,
},
{
title: "New Analysis",
url: ROUTES.NEW_ANALYSIS,
icon: Plus,
},
{
title: "Project",
url: ROUTES.PROJECT,
icon: FolderPlus,
},
{
title: "Package",
url: ROUTES.PACKAGE,
icon: Package,
},
{
title: "Location",
url: ROUTES.LOCATION,
icon: MapPin,
},
],
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname();
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<div className="flex items-center gap-2 py-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Package className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-semibold">Vision Road</span>
<span className="truncate text-xs">Analytics Platform</span>
</div>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarMenu>
{data.navMain.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={pathname === item.url}
>
<Link href={item.url}>
{item.icon && <item.icon />}
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}

View File

@@ -7,7 +7,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { FolderOpen, MapPin, Building2 } from "lucide-react"
import { type Project } from "@/lib/api"
interface CompactProjectSelectorProps {
@@ -26,7 +25,7 @@ export function CompactProjectSelector({
isLoading = false
}: CompactProjectSelectorProps) {
return (
<div className="flex flex-wrap items-center gap-3 p-3 rounded-xl bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50">
<div className="flex flex-wrap items-center gap-3 p-3 rounded-md border">
{/* Project Dropdown */}
<div className="flex flex-col min-w-[120px]">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Project</span>
@@ -35,7 +34,7 @@ export function CompactProjectSelector({
onValueChange={onProjectChange}
disabled={isLoading || projects.length === 0}
>
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold text-gray-900 dark:text-white bg-transparent text-left">
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold bg-transparent text-left">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
@@ -51,10 +50,10 @@ export function CompactProjectSelector({
{/* Corridor Badge */}
{selectedProject?.corridor_name && (
<>
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-2" />
<div className="h-8 w-px bg-border mx-2" />
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Corridor</span>
<span className="text-xs font-bold text-gray-600 dark:text-gray-400">
<span className="text-xs font-bold text-muted-foreground">
{selectedProject.corridor_name}
</span>
</div>
@@ -64,10 +63,10 @@ export function CompactProjectSelector({
{/* State Badge */}
{selectedProject?.state && (
<>
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-2" />
<div className="h-8 w-px bg-border mx-2" />
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">State</span>
<span className="text-xs font-bold text-gray-600 dark:text-gray-400">
<span className="text-xs font-bold text-muted-foreground">
{selectedProject.state}
</span>
</div>

View File

@@ -3,8 +3,8 @@
import { useEffect, useState } from "react"
import dynamic from "next/dynamic"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, MapPin, AlertTriangle, RectangleHorizontal } from "lucide-react"
import { fetchAllDetections, type Detection } from "@/lib/api"
import { Loader2, MapPin } from "lucide-react"
import { type Detection } from "@/lib/api"
// Dynamically import the map to avoid SSR issues with Leaflet
const DashboardMapContent = dynamic(
@@ -89,18 +89,18 @@ export function DashboardMap({
}
return (
<Card className={`overflow-hidden pb-0 bg-white/60 rounded-b-none shadow-none border-none ${className}`}>
<Card className={`overflow-hidden ${className}`}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
<MapPin className="h-5 w-5 text-white" />
<div className="p-2 rounded-md bg-secondary text-secondary-foreground flex items-center justify-center">
<MapPin className="h-5 w-5" />
</div>
<div>
<CardTitle className="text-base font-bold text-[#2563eb]">
<CardTitle className="text-base font-bold">
Detection Map
</CardTitle>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-muted-foreground">
{isLoading ? "Loading..." : `${validDetections.length} detections with GPS coordinates`}
</p>
</div>
@@ -109,43 +109,43 @@ export function DashboardMap({
{/* Compact Color Legend */}
<div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1 text-[10px] max-w-[60%]">
<div className="flex items-center gap-1">
<div className="w-2.5 h-2.5 rounded-full bg-[#ef4444] shadow-sm shadow-[#ef4444]/30" />
<span className="text-gray-600 dark:text-gray-400 font-medium">Potholes ({counts.pothole})</span>
<div className="w-2.5 h-2.5 rounded-full bg-red-500" />
<span className="text-muted-foreground font-medium">Potholes ({counts.pothole})</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2.5 h-2.5 rounded-full bg-[#3b82f6] shadow-sm shadow-[#3b82f6]/30" />
<span className="text-gray-600 dark:text-gray-400 font-medium">Defected Signs ({counts.defected_sign_board})</span>
<div className="w-2.5 h-2.5 rounded-full bg-blue-500" />
<span className="text-muted-foreground font-medium">Defected Signs ({counts.defected_sign_board})</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2.5 h-2.5 rounded-full bg-[#f59e0b] shadow-sm shadow-[#f59e0b]/30" />
<span className="text-gray-600 dark:text-gray-400 font-medium">Cracks ({counts.road_crack})</span>
<div className="w-2.5 h-2.5 rounded-full bg-amber-500" />
<span className="text-muted-foreground font-medium">Cracks ({counts.road_crack})</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2.5 h-2.5 rounded-full bg-[#6366f1] shadow-sm shadow-[#6366f1]/30" />
<span className="text-gray-600 dark:text-gray-400 font-medium">Markings ({counts.damaged_road_marking})</span>
<div className="w-2.5 h-2.5 rounded-full bg-indigo-500" />
<span className="text-muted-foreground font-medium">Markings ({counts.damaged_road_marking})</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2.5 h-2.5 rounded-full bg-[#10b981] shadow-sm shadow-[#10b981]/30" />
<span className="text-gray-600 dark:text-gray-400 font-medium">Good Signs ({counts.good_sign_board})</span>
<div className="w-2.5 h-2.5 rounded-full bg-emerald-500" />
<span className="text-muted-foreground font-medium">Good Signs ({counts.good_sign_board})</span>
</div>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="h-[500px] p-2 w-full relative">
<div className="h-[500px] p-2 w-full relative">
{isLoading ? (
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
<Loader2 className="h-8 w-8 text-indigo-500" />
<div className="h-full w-full flex items-center justify-center bg-muted/50">
<Loader2 className="h-8 w-8 text-primary animate-spin" />
</div>
) : error ? (
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
<p className="text-gray-500">{error}</p>
<div className="h-full w-full flex items-center justify-center bg-muted/50">
<p className="text-muted-foreground">{error}</p>
</div>
) : validDetections.length === 0 ? (
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
<div className="h-full w-full flex items-center justify-center bg-muted/50">
<div className="text-center">
<p className="text-gray-500">No detections with GPS coordinates found</p>
<p className="text-sm text-gray-400 mt-1">Process some videos to see detections on the map</p>
<p className="text-muted-foreground">No detections with GPS coordinates found</p>
<p className="text-sm text-muted-foreground mt-1 opacity-70">Process some videos to see detections on the map</p>
</div>
</div>
) : (

View File

@@ -9,7 +9,7 @@ export function DashboardSkeleton() {
{/* Stats Cards Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="bg-white/60 backdrop-blur-lg border-none shadow-none overflow-hidden py-8 px-6">
<Card key={i} className="py-8 px-6">
<CardContent className="p-0 flex items-center justify-between gap-6">
<div className="flex flex-col gap-2 flex-1">
<Skeleton className="h-4 w-32" />
@@ -25,8 +25,8 @@ export function DashboardSkeleton() {
{/* Charts Row Skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{[...Array(2)].map((_, i) => (
<Card key={i} className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
<CardHeader className="pb-2 border-none">
<Card key={i}>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<Skeleton className="w-10 h-10 rounded-md" />
<Skeleton className="h-6 w-48" />
@@ -40,7 +40,7 @@ export function DashboardSkeleton() {
</div>
{/* Map Skeleton */}
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">

View File

@@ -112,7 +112,7 @@ export function DetectionDonutChart({
if (active && payload && payload.length) {
const data = payload[0]
return (
<div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg">
<div className="bg-popover border border-border rounded-lg px-3 py-2 shadow-lg">
<p className="font-medium text-sm">{data.name}</p>
<p className="text-sm text-muted-foreground">
Count: <span className="font-semibold text-foreground">{data.value}</span>

View File

@@ -36,26 +36,26 @@ export function FilterSelector({
isLoading = false
}: FilterSelectorProps) {
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex flex-col gap-5 p-5">
{/* Project Dropdown */}
<div className="space-y-2">
<div className="flex items-center gap-2 mb-1">
<div className="p-1 rounded-md bg-blue-100 dark:bg-blue-900/50">
<FolderOpen className="h-3.5 w-3.5 text-blue-600" />
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
<FolderOpen className="h-4 w-4" />
</div>
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Project</span>
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Project</span>
</div>
<Select
value={selectedProjectId || ""}
onValueChange={onProjectChange}
disabled={isLoading || projects.length === 0}
>
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent className="rounded-xl border-slate-200">
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id} className="rounded-lg">
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
@@ -65,25 +65,25 @@ export function FilterSelector({
{/* Package Dropdown */}
{selectedProjectId && (
<div className="space-y-2">
<div className="flex items-center gap-2 mb-1">
<div className="p-1 rounded-md bg-emerald-100 dark:bg-emerald-900/50">
<Package className="h-3.5 w-3.5 text-emerald-600" />
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
<Package className="h-4 w-4" />
</div>
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Package</span>
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Package</span>
</div>
<Select
value={selectedPackageId || "all"}
onValueChange={onPackageChange}
disabled={isLoading}
>
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
<SelectValue placeholder="All packages" />
</SelectTrigger>
<SelectContent className="rounded-xl border-slate-200">
<SelectItem value="all" className="rounded-lg">All Packages</SelectItem>
<SelectContent>
<SelectItem value="all">All Packages</SelectItem>
{packages.map((pkg) => (
<SelectItem key={pkg.id} value={pkg.id} className="rounded-lg">
<SelectItem key={pkg.id} value={pkg.id}>
{pkg.name}
</SelectItem>
))}
@@ -94,25 +94,25 @@ export function FilterSelector({
{/* Location Dropdown */}
{selectedPackageId && selectedPackageId !== "all" && (
<div className="space-y-2">
<div className="flex items-center gap-2 mb-1">
<div className="p-1 rounded-md bg-purple-100 dark:bg-purple-900/50">
<MapPin className="h-3.5 w-3.5 text-purple-600" />
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
<MapPin className="h-4 w-4" />
</div>
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Location</span>
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Location</span>
</div>
<Select
value={selectedLocationId || "all"}
onValueChange={onLocationChange}
disabled={isLoading}
>
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
<SelectValue placeholder="All locations" />
</SelectTrigger>
<SelectContent className="rounded-xl border-slate-200">
<SelectItem value="all" className="rounded-lg">All Locations</SelectItem>
<SelectContent>
<SelectItem value="all">All Locations</SelectItem>
{locations.map((loc) => (
<SelectItem key={loc.id} value={loc.id} className="rounded-lg">
<SelectItem key={loc.id} value={loc.id}>
{loc.name}
</SelectItem>
))}

View File

@@ -2,88 +2,41 @@
import { Card, CardContent } from "@/components/ui/card"
import { LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
interface GradientStatsCardProps {
title: string
subtitle?: string
value: number | string
icon: LucideIcon
gradient: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
gradient?: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
isLoading?: boolean
}
const gradientStyles = {
green: {
background: "bg-card",
border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
},
coral: {
background: "bg-card",
border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
},
blue: {
background: "bg-card",
border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
},
purple: {
background: "bg-card",
border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
},
orange: {
background: "bg-card",
border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
},
indigo: {
background: "bg-card",
border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
},
emerald: {
background: "bg-card",
border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
}
}
export function GradientStatsCard({
title,
subtitle,
value,
icon: Icon,
gradient,
isLoading = false
}: GradientStatsCardProps) {
const styles = gradientStyles[gradient]
return (
<Card className="bg-white/60 backdrop-blur-lg border-none shadow-none hover:shadow-none overflow-hidden py-8 px-6">
<CardContent className="p-0 flex items-center justify-between gap-6">
<Card className="overflow-hidden border shadow-sm">
<CardContent className="p-6 flex items-center justify-between gap-4">
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-sm font-bold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-widest">
<h3 className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
{title}
</h3>
<div className="flex flex-col">
{isLoading ? (
<div className="h-14 w-24 bg-gray-100 dark:bg-gray-800 rounded-xl mt-2" />
<div className="h-10 w-24 bg-muted animate-pulse rounded-md mt-1" />
) : (
<>
<p className="text-4xl font-bold heading-blue-gradient">
<p className="text-3xl font-bold tracking-tight">
{value}
</p>
{subtitle && (
<p className="text-xs font-medium text-gray-500 mt-1">
<p className="text-[10px] font-semibold text-muted-foreground mt-0.5">
{subtitle}
</p>
)}
@@ -92,14 +45,8 @@ export function GradientStatsCard({
</div>
</div>
<div
style={{
background: "linear-gradient(180deg, #225999 0%, #56A5FF 100%)",
}}
className={`
w-16 h-16 self-start rounded-lg flex items-center justify-center
`}>
<Icon className="h-10 w-10 text-white" />
<div className="p-3 rounded-lg bg-secondary shrink-0">
<Icon className="h-6 w-6 text-primary" />
</div>
</CardContent>
</Card>

View File

@@ -100,7 +100,7 @@ export function LocationBarChart({ data, isLoading = false }: LocationBarChartPr
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg">
<div className="bg-popover border border-border rounded-lg px-3 py-2 shadow-lg">
<p className="font-medium text-xs mb-1">{label}</p>
{payload.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-xs">

View File

@@ -0,0 +1,52 @@
"use client"
import { Card, CardContent } from "@/components/ui/card"
import { LucideIcon } from "lucide-react"
interface StatsCardProps {
title: string
subtitle?: string
value: number | string
icon: LucideIcon
isLoading?: boolean
}
export function StatsCard({
title,
subtitle,
value,
icon: Icon,
isLoading = false
}: StatsCardProps) {
return (
<Card className="py-6 px-6">
<CardContent className="p-0 flex items-center justify-between gap-6">
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
{title}
</h3>
<div className="flex flex-col">
{isLoading ? (
<div className="h-10 w-24 bg-muted rounded-md mt-2 animate-pulse" />
) : (
<>
<p className="text-3xl font-bold">
{value}
</p>
{subtitle && (
<p className="text-xs text-muted-foreground mt-1">
{subtitle}
</p>
)}
</>
)}
</div>
</div>
<div className="p-3 rounded-md bg-secondary text-secondary-foreground">
<Icon className="h-6 w-6" />
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,230 +0,0 @@
"use client"
import { Plus, Edit2, Trash2, ChevronLeft, ChevronRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Skeleton } from "@/components/ui/skeleton"
interface Column<T> {
key: string
header: string
render?: (item: T) => React.ReactNode
}
interface DataTableProps<T> {
title: string
data: T[]
columns: Column<T>[]
onAddNew: () => void
addButtonText: string
isLoading?: boolean
onEdit?: (item: T) => void
onDelete?: (item: T) => void
pagination?: {
skip: number
limit: number
totalItems?: number
onPageChange: (newSkip: number) => void
onLimitChange: (newLimit: number) => void
}
}
export function DataTable<T extends Record<string, any>>({
title,
data,
columns,
onAddNew,
addButtonText,
isLoading = false,
onEdit,
onDelete,
pagination
}: DataTableProps<T>) {
const showActions = !!onEdit || !!onDelete;
const totalCols = columns.length + (showActions ? 1 : 0);
return (
<div>
{/* Header with Title and Add Button */}
<div className="flex justify-between items-center p-3 ">
<div className="flex items-center gap-3">
<h2 className="text-2xl font-bold tracking-tight heading-blue-gradient">{title}</h2>
<span className="badge-blue min-w-[28px] h-[28px] px-2">
{data.length}
</span>
</div>
<Button
onClick={onAddNew}
className="btn-blue-gradient rounded-full text-sm px-4 py-2.5 h-auto flex items-center gap-2"
>
<Plus className="h-4 w-4" />
{addButtonText}
</Button>
</div>
{/* Table */}
<div className="rounded-lg overflow-hidden">
<div className="overflow-auto max-h-[450px]">
<table className="min-w-full rounded-lg!">
<thead className="bg-white/40 backdrop-blur-md rounded-lg! sticky top-0 z-10">
<tr>
{columns.map((col) => (
<th
key={col.key}
className="px-4 py-3 text-slate-500 text-left font-bold"
>
{col.header}
</th>
))}
{showActions && (
<th className="px-4 py-3 text-slate-500 text-left font-bold">
Actions
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-gray-300 backdrop-blur-lg bg-white/70">
{isLoading ? (
Array.from({ length: 5 }).map((_, idx) => (
<tr key={idx} className="border-b border-gray-100 dark:border-gray-800">
{columns.map((col) => (
<td key={col.key} className="px-4 py-4">
<Skeleton className="h-4 w-full max-w-[120px]" />
</td>
))}
{showActions && (
<td className="px-4 py-4 text-right">
<div className="flex justify-end gap-2">
<Skeleton className="h-8 w-8 rounded-sm" />
<Skeleton className="h-8 w-8 rounded-sm" />
</div>
</td>
)}
</tr>
))
) : data.length === 0 ? (
<tr>
<td colSpan={totalCols} className="px-4 py-12 text-center">
<div className="flex flex-col items-center justify-center gap-2">
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-2">
<Plus className="w-6 h-6 text-gray-400" />
</div>
<p className="text-gray-500 dark:text-gray-400 font-medium">No data available</p>
<p className="text-xs text-gray-400 dark:text-gray-500">Click "{addButtonText}" to get started</p>
</div>
</td>
</tr>
) : (
data.map((item, idx) => (
<tr
key={idx}
className="hover:bg-blue-50/30 dark:hover:bg-blue-900/10 transition-colors duration-200"
>
{columns.map((col) => (
<td key={col.key} className="px-4 py-3 text-[13px] text-gray-700 dark:text-gray-300 font-medium">
{col.render ? col.render(item) : (item[col.key as keyof T] !== null && item[col.key as keyof T] !== undefined ? String(item[col.key as keyof T]) : "—")}
</td>
))}
{showActions && (
<td className="px-4 py-3 text-right">
<div className="flex justify-end gap-2">
<TooltipProvider>
{onEdit && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(item)}
className="h-8 w-8 rounded-sm bg-white text-blue-500 border-slate-200! border hover:bg-blue-500 hover:text-white"
>
<Edit2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p>Edit</p>
</TooltipContent>
</Tooltip>
)}
{onDelete && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(item)}
className="h-8 w-8 rounded-sm border border-slate-200! bg-white text-red-400 hover:bg-red-400 hover:text-white"
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p>Delete</p>
</TooltipContent>
</Tooltip>
)}
</TooltipProvider>
</div>
</td>
)}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Pagination Controls */}
{pagination && (
<div className="flex items-center justify-between px-4 py-3 bg-white/40 backdrop-blur-md rounded-b-lg border-t border-gray-200 mt-1">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-gray-500">Rows per page:</span>
<select
value={pagination.limit}
onChange={(e) => pagination.onLimitChange(Number(e.target.value))}
className="bg-transparent text-xs font-bold text-gray-700 outline-none cursor-pointer focus:ring-0"
>
{[10, 25, 50, 100].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</select>
</div>
<span className="text-xs text-gray-500">
Showing {pagination.skip + 1} - {pagination.skip + data.length}
{pagination.totalItems !== undefined && ` of ${pagination.totalItems}`}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
disabled={pagination.skip === 0 || isLoading}
onClick={() => pagination.onPageChange(Math.max(0, pagination.skip - pagination.limit))}
className="h-8 w-8 p-0 rounded-md hover:bg-blue-50 text-gray-600 disabled:opacity-30"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
disabled={data.length < pagination.limit || isLoading}
onClick={() => pagination.onPageChange(pagination.skip + pagination.limit)}
className="h-8 w-8 p-0 rounded-md hover:bg-blue-50 text-gray-600 disabled:opacity-30"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,64 @@
"use client";
import { Table } from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
export function TableFooter<TData>({ table }: { table: Table<TData> }) {
return (
<div className="flex items-center justify-end px-4 py-4 border-t gap-6 lg:gap-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount() || 1}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
"use client";
import { SearchIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
const SearchBar = () => {
return (
<div className="relative w-full">
<SearchIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search..."
className="pl-8 h-9 text-sm"
/>
</div>
);
};
export default SearchBar;

View File

@@ -0,0 +1,45 @@
"use client";
import { Button } from "@/components/ui/button";
import SearchBar from "./SearchBar";
import { PlusIcon } from "lucide-react";
interface TopHeaderProps {
title?: string;
itemCount?: number;
onAddNew?: () => void;
addButtonText?: string;
}
const TopHeader = ({ title, itemCount, onAddNew, addButtonText = "Add New" }: TopHeaderProps) => {
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold tracking-tight">{title}</h2>
{itemCount !== undefined && (
<span className="flex items-center justify-center bg-secondary text-secondary-foreground min-w-[24px] h-[24px] px-2 text-xs font-bold rounded-full">
{itemCount}
</span>
)}
</div>
{onAddNew && (
<Button
onClick={onAddNew}
size="sm"
className="flex items-center gap-2"
>
<PlusIcon className="w-4 h-4" />
{addButtonText}
</Button>
)}
</div>
<div className="flex w-full max-w-sm ml-auto">
<SearchBar />
</div>
</div>
);
};
export default TopHeader;

View File

@@ -0,0 +1,33 @@
"use client";
import { flexRender } from "@tanstack/react-table";
import {
TableHead,
TableHeader as ShadTableHeader,
TableRow,
} from "@/components/ui/table";
import { Table } from "@tanstack/react-table";
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
return (
<ShadTableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</ShadTableHeader>
);
};
export default TableHeader;

View File

@@ -0,0 +1,186 @@
"use client";
import React from "react";
import {
ColumnDef,
SortingState,
flexRender,
getCoreRowModel,
useReactTable,
getSortedRowModel,
getPaginationRowModel,
} from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Edit2, Trash2 } from "lucide-react";
import TopHeader from "./Header";
import TableHeader from "./TableHeader";
import { TableFooter } from "./Footer";
import { cn } from "@/lib/utils";
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
title?: string;
onAddNew?: () => void;
addButtonText?: string;
isLoading?: boolean;
onEdit?: (item: TData) => void;
onDelete?: (item: TData) => void;
pagination?: {
skip: number;
limit: number;
totalItems?: number;
onPageChange: (newSkip: number) => void;
onLimitChange: (newLimit: number) => void;
};
}
export function DataTable<TData, TValue>({
columns: initialColumns,
data,
title,
onAddNew,
addButtonText,
isLoading = false,
onEdit,
onDelete,
pagination,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [sorting, setSorting] = React.useState<SortingState>([]);
const columns = React.useMemo(() => {
const cols = [...initialColumns];
if (onEdit || onDelete) {
cols.push({
id: "actions",
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const item = row.original;
return (
<div className="flex justify-end gap-2">
{onEdit && (
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(item)}
className="h-8 w-8"
>
<Edit2 className="h-4 w-4" />
</Button>
)}
{onDelete && (
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(item)}
className="h-8 w-8 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
);
},
});
}
return cols;
}, [initialColumns, onEdit, onDelete]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: !!pagination,
pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1,
state: {
rowSelection,
sorting,
pagination: pagination ? {
pageIndex: Math.floor(pagination.skip / pagination.limit),
pageSize: pagination.limit,
} : undefined,
},
onPaginationChange: (updater) => {
if (typeof updater === 'function' && pagination) {
const newState = updater({
pageIndex: Math.floor(pagination.skip / pagination.limit),
pageSize: pagination.limit,
});
pagination.onLimitChange(newState.pageSize);
pagination.onPageChange(newState.pageIndex * newState.pageSize);
}
},
initialState: {
pagination: {
pageSize: 10,
pageIndex: 0,
},
},
});
return (
<div className="rounded-md border">
<TopHeader
title={title}
itemCount={data.length}
onAddNew={onAddNew}
addButtonText={addButtonText}
/>
<div className="relative">
<Table>
<TableHeader table={table} />
<TableBody className="bg-transparent">
{isLoading ? (
Array.from({ length: 5 }).map((_, idx) => (
<TableRow key={idx}>
{columns.map((_, colIdx) => (
<TableCell key={colIdx} className="px-8 py-4">
<Skeleton className="h-4 w-full max-w-[120px]" />
</TableCell>
))}
</TableRow>
))
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-32 text-center text-sm"
>
No results found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<TableFooter table={table} />
</div>
);
}

View File

@@ -30,7 +30,6 @@ function FitBounds({ bounds }: { bounds: LatLngBounds }) {
useEffect(() => {
if (!map || !bounds.isValid()) return
// Small delay to ensure the container dimensions are fully calculated (prevents _leaflet_pos error)
const timer = setTimeout(() => {
map.invalidateSize()
map.fitBounds(bounds, {
@@ -47,7 +46,6 @@ function FitBounds({ bounds }: { bounds: LatLngBounds }) {
}
export default function MapModal({ open, onClose, detections, detectionType }: MapModalProps) {
// Filter detections with valid GPS coordinates
const validDetections = detections.filter(d => d.latitude && d.longitude)
if (validDetections.length === 0) {
@@ -57,46 +55,35 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
<DialogHeader>
<DialogTitle>Detection Map</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-center h-full text-muted-foreground">
No GPS coordinates available for detections
<div className="flex items-center justify-center h-full text-muted-foreground text-sm font-medium">
No GPS data available for detections.
</div>
</DialogContent>
</Dialog>
)
}
// Extract route coordinates (start to end)
const routeCoordinates: [number, number][] = validDetections.map(d => [d.latitude, d.longitude])
// Calculate bounds to fit all markers
const bounds = new LatLngBounds(
new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]),
new LatLng(routeCoordinates[0][0], routeCoordinates[0][1])
)
const bounds = new LatLngBounds(new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]), new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]))
routeCoordinates.forEach(coord => bounds.extend(new LatLng(coord[0], coord[1])))
// Center point (middle of route)
const center: [number, number] = [
(bounds.getNorth() + bounds.getSouth()) / 2,
(bounds.getEast() + bounds.getWest()) / 2
]
const center: [number, number] = [(bounds.getNorth() + bounds.getSouth()) / 2, (bounds.getEast() + bounds.getWest()) / 2]
const isCombined = detectionType === "pot-sign-detection"
const isPothole = detectionType === "pothole-detection"
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-6xl h-[80vh] p-0 flex flex-col overflow-hidden border-none shadow-2xl">
<DialogHeader className="px-6 pt-6 pb-4 bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800 shrink-0">
<DialogContent className="max-w-6xl h-[85vh] p-0 flex flex-col overflow-hidden border-none shadow-2xl">
<DialogHeader className="px-6 py-4 border-b shrink-0">
<DialogTitle className="text-xl font-bold">
{isCombined ? "Pothole & Signboard" : isPothole ? "Pothole" : "Signboard"} Detection Map
{isCombined ? "Combined" : isPothole ? "Pothole" : "Signboard"} Detection Map
</DialogTitle>
<p className="text-sm text-muted-foreground">
{validDetections.length} detection{validDetections.length !== 1 ? "s" : ""} with GPS coordinates
<p className="text-xs text-muted-foreground font-medium">
{validDetections.length} points of interest identified with GPS data
</p>
</DialogHeader>
<div className="flex-1 w-full bg-gray-50 dark:bg-gray-950 relative">
<div className="flex-1 w-full relative bg-muted/20">
<MapContainer
center={center}
zoom={13}
@@ -108,56 +95,47 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* Route line */}
<Polyline
positions={routeCoordinates}
color="#3b82f6"
weight={3}
opacity={0.7}
weight={4}
opacity={0.6}
/>
{/* Detection markers */}
{validDetections.map((detection, idx) => {
const type = (detection.type || "").toLowerCase()
let markerColor = "#64748b" // Default Slate
let strokeColor = "#475569"
if (type === "pothole") {
markerColor = "#ef4444"
strokeColor = "#b91c1c"
} else if (type === "defected_sign_board") {
markerColor = "#3b82f6"
strokeColor = "#1d4ed8"
} else if (type === "road_crack") {
markerColor = "#f59e0b"
strokeColor = "#b45309"
} else if (type === "damaged_road_marking") {
markerColor = "#6366f1"
strokeColor = "#4338ca"
} else if (type === "good_sign_board") {
markerColor = "#10b981"
strokeColor = "#047857"
const colors: Record<string, { fill: string, stroke: string }> = {
'pothole': { fill: '#ef4444', stroke: '#b91c1c' },
'defected_sign_board': { fill: '#3b82f6', stroke: '#1d4ed8' },
'road_crack': { fill: '#f59e0b', stroke: '#b45309' },
'damaged_road_marking': { fill: '#6366f1', stroke: '#4338ca' },
'good_sign_board': { fill: '#10b981', stroke: '#047857' }
}
const color = colors[type] || { fill: '#64748b', stroke: '#475569' }
return (
<CircleMarker
key={`${detection.id}-${idx}`}
center={[detection.latitude, detection.longitude]}
radius={8}
fillColor={markerColor}
color={strokeColor}
radius={7}
fillColor={color.fill}
color={color.stroke}
weight={2}
opacity={1}
fillOpacity={0.8}
fillOpacity={0.9}
>
<Popup>
<div className="text-xs space-y-1">
<div className="font-semibold capitalize">
<div className="text-[11px] space-y-2 p-1">
<div className="font-bold border-b pb-1 capitalize">
{(detection.type || "").replace(/_/g, " ")} #{detection.id}
</div>
<div>Frame: {detection.frame_number}</div>
<div>Confidence: {(detection.confidence * 100).toFixed(1)}%</div>
<div className="font-mono text-[10px]">
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
<span className="text-muted-foreground">Frame:</span>
<span className="font-semibold text-right">{detection.frame_number}</span>
<span className="text-muted-foreground">Confidence:</span>
<span className="font-semibold text-right">{(detection.confidence * 100).toFixed(0)}%</span>
</div>
<div className="font-mono bg-muted p-1.5 rounded border text-center text-[9px]">
{detection.latitude.toFixed(6)}, {detection.longitude.toFixed(6)}
</div>
</div>
@@ -165,8 +143,6 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
</CircleMarker>
)
})}
{/* Auto-fit bounds */}
<FitBounds bounds={bounds} />
</MapContainer>
</div>

View File

@@ -0,0 +1,40 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,73 @@
"use client"
import { ChevronRight, type LucideIcon } from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon?: LucideIcon
isActive?: boolean
items?: {
title: string
url: string
}[]
}[]
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}

View File

@@ -0,0 +1,89 @@
"use client"
import {
Folder,
Forward,
MoreHorizontal,
Trash2,
type LucideIcon,
} from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function NavProjects({
projects,
}: {
projects: {
name: string
url: string
icon: LucideIcon
}[]
}) {
const { isMobile } = useSidebar()
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}

114
src/components/nav-user.tsx Normal file
View File

@@ -0,0 +1,114 @@
"use client"
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -11,17 +11,16 @@ interface PageHeaderProps {
export function PageHeader({ title, description, icon: Icon, children }: PageHeaderProps) {
return (
<div className="flex items-center gap-5">
<div className="p-3 rounded-lg bg-blue-gradient flex items-center justify-center relative overflow-hidden">
{Icon && <Icon className="h-8 w-8 text-white relative z-10" />}
<div className="flex items-center gap-6">
<div className="p-3.5 rounded-xl bg-primary text-primary-foreground flex items-center justify-center shadow-lg shadow-primary/5 ring-1 ring-white/10">
{Icon && <Icon className="h-7 w-7" />}
{children}
</div>
<div className="flex flex-col">
<h1 className="text-3xl font-bold heading-blue-gradient">
<h1 className="text-3xl font-extrabold tracking-tight ">
{title}
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium">
<p className="text-muted-foreground mt-1 text-sm font-semibold tracking-wide opacity-80">
{description}
</p>
</div>

View File

@@ -2,8 +2,8 @@ import Image from "next/image"
export function PoweredBy() {
return (
<div className=" flex items-center justify-center gap-2 mt-3 transition-opacity duration-300">
<span className="font-medium powered-blue-gradient">
<div className="flex items-center justify-center gap-2 mt-3 transition-opacity duration-300">
<span className="font-medium text-muted-foreground">
Powered by
</span>
<Image
@@ -11,7 +11,6 @@ export function PoweredBy() {
alt="Sentient Geeks"
width={130}
height={20}
className="fill-blue-500"
/>
</div>
)

View File

@@ -5,7 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Loader2 } from "lucide-react"
import { Loader2, Check } from "lucide-react"
import {
fetchProjects,
fetchPackagesByProject,
@@ -15,6 +15,7 @@ import {
type Location,
type SessionContext
} from "@/lib/api"
import { cn } from "@/lib/utils"
type ProjectSelectionSectionProps = {
onSelectionComplete: (session: SessionContext) => void
@@ -148,18 +149,18 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
}
return (
<Card className="bg-white/40 backdrop-blur-sm overflow-hidden">
<Card className="overflow-hidden">
<CardHeader className="pb-6">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-6">
<div>
<CardTitle className="text-2xl font-semibold">Select Project Location</CardTitle>
<CardTitle className="text-2xl font-bold">Select Project Location</CardTitle>
<CardDescription className="mt-2 text-base">
Select Project, Package & Location to begin intelligent road analysis with advanced computer vision.
Select Project, Package & Location to begin intelligent road analysis.
</CardDescription>
</div>
{/* Step Progress Indicator */}
<div className="flex items-center justify-center gap-2 pt-2">
<div className="flex items-center justify-center pt-2">
{[1, 2, 3].map((step, index) => {
const status = getStepStatus(step)
const labels = ['Project', 'Package', 'Location']
@@ -167,21 +168,27 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
<div key={step} className="flex items-center">
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold transition-colors ${status === 'completed' ? 'bg-linear-to-b from-[#225999] to-[#56A5FF] text-primary-foreground' :
status === 'active' ? 'bg-linear-to-b from-[#225999] to-[#56A5FF] text-primary-foreground ring-4 ring-primary/20' :
'bg-muted text-muted-foreground'
}`}
className={cn(
"w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold transition-all",
status === 'completed' ? "bg-primary text-primary-foreground" :
status === 'active' ? "bg-primary text-primary-foreground ring-4 ring-primary/10" :
"bg-muted text-muted-foreground"
)}
>
{step}
{status === 'completed' ? <Check className="h-5 w-5" /> : step}
</div>
<span className={`text-xs mt-1.5 font-medium ${status === 'pending' ? 'text-muted-foreground/60' : 'text-foreground'
}`}>
<span className={cn(
"text-xs mt-2 font-medium",
status === 'pending' ? "text-muted-foreground/60" : "text-foreground"
)}>
{labels[index]}
</span>
</div>
{index < 2 && (
<div className={`w-12 h-0.5 mx-2 mt-[-16px] rounded-full ${getStepStatus(step + 1) !== 'pending' ? 'bg-linear-to-b from-[#225999] to-[#56A5FF]' : 'bg-border'
}`} />
<div className={cn(
"w-16 h-0.5 mx-2 mb-6 rounded-full",
getStepStatus(step + 1) !== 'pending' ? "bg-primary" : "bg-border"
)} />
)}
</div>
)
@@ -190,126 +197,117 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
</div>
</CardHeader>
<CardContent className="pt-6 space-y-6">
<CardContent className="space-y-8">
{/* Error Display */}
{error && (
<div className="flex items-start gap-3 p-4 rounded-xl bg-destructive/10 border border-destructive/20 text-destructive">
<div className="w-5 h-5 rounded-full bg-destructive/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs font-bold">!</span>
</div>
<p className="text-sm leading-relaxed">{error}</p>
<div className="p-4 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Project Dropdown */}
<div className="space-y-3">
<Label htmlFor="project" className="text-sm font-semibold text-foreground">
<div className="space-y-2">
<Label htmlFor="project" className="text-sm font-semibold">
Project
</Label>
<div className="input-glow rounded-lg">
<Select
value={selectedProject?.id || ""}
onValueChange={handleProjectChange}
disabled={loadingProjects}
>
<SelectTrigger id="project" className="h-12 bg-background/50 border-border/50 hover:border-primary/50">
{loadingProjects ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-muted-foreground">Loading...</span>
</div>
) : (
<SelectValue placeholder="Select a project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
<span className="font-medium">{project.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select
value={selectedProject?.id || ""}
onValueChange={handleProjectChange}
disabled={loadingProjects}
>
<SelectTrigger id="project" className="h-11">
{loadingProjects ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span>
</div>
) : (
<SelectValue placeholder="Select project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Package Dropdown */}
<div className="space-y-3">
<Label htmlFor="package" className="text-sm font-semibold text-foreground">
<div className="space-y-2">
<Label htmlFor="package" className="text-sm font-semibold">
Package
</Label>
<div className="input-glow rounded-lg">
<Select
value={selectedPackage?.id || ""}
onValueChange={handlePackageChange}
disabled={!selectedProject || loadingPackages}
>
<SelectTrigger id="package" className="h-12 bg-background/50 border-border/50 hover:border-primary/50">
{loadingPackages ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-muted-foreground">Loading...</span>
</div>
) : (
<SelectValue placeholder={selectedProject ? "Select a package" : "Select project first"} />
)}
</SelectTrigger>
<SelectContent>
{packages.map((pkg) => (
<SelectItem key={pkg.id} value={pkg.id}>
<span className="font-medium">{pkg.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select
value={selectedPackage?.id || ""}
onValueChange={handlePackageChange}
disabled={!selectedProject || loadingPackages}
>
<SelectTrigger id="package" className="h-11">
{loadingPackages ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span>
</div>
) : (
<SelectValue placeholder={selectedProject ? "Select package" : "Select project first"} />
)}
</SelectTrigger>
<SelectContent>
{packages.map((pkg) => (
<SelectItem key={pkg.id} value={pkg.id}>
{pkg.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Location Dropdown */}
<div className="space-y-3">
<Label htmlFor="location" className="text-sm font-semibold text-foreground">
<div className="space-y-2">
<Label htmlFor="location" className="text-sm font-semibold">
Location
</Label>
<div className="input-glow rounded-lg">
<Select
value={selectedLocation?.id || ""}
onValueChange={handleLocationChange}
disabled={!selectedPackage || loadingLocations}
>
<SelectTrigger id="location" className="h-12 bg-background/50 border-border/50 hover:border-primary/50">
{loadingLocations ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-muted-foreground">Loading...</span>
</div>
) : (
<SelectValue placeholder={selectedPackage ? "Select a location" : "Select package first"} />
)}
</SelectTrigger>
<SelectContent>
{locations.map((location) => (
<SelectItem key={location.id} value={location.id}>
<span className="font-medium">{location.segment_name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select
value={selectedLocation?.id || ""}
onValueChange={handleLocationChange}
disabled={!selectedPackage || loadingLocations}
>
<SelectTrigger id="location" className="h-11">
{loadingLocations ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span>
</div>
) : (
<SelectValue placeholder={selectedPackage ? "Select location" : "Select package first"} />
)}
</SelectTrigger>
<SelectContent>
{locations.map((location) => (
<SelectItem key={location.id} value={location.id}>
{location.segment_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Selected Summary */}
{isComplete && (
<div className="px-4 py-3 rounded-lg bg-primary/5 border border-primary/15">
<p className="text-xs text-muted-foreground flex items-center gap-2 flex-wrap">
<span className="font-medium">Path:</span>
<div className="px-4 py-3 rounded-md bg-secondary/50 border text-sm">
<div className="flex items-center gap-2 flex-wrap text-muted-foreground">
<span className="font-semibold text-foreground">Selection:</span>
<span className="text-foreground">{selectedProject?.name}</span>
<span>/</span>
<span className="text-foreground">{selectedPackage?.name}</span>
<span>/</span>
<span className="text-foreground">{selectedLocation?.segment_name}</span>
</p>
</div>
</div>
)}
@@ -317,15 +315,10 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
<Button
onClick={handleProceed}
disabled={!isComplete}
className={`w-full h-14 text-base font-semibold ${isComplete ? 'btn-blue-gradient text-white' : ''
}`}
className="w-full h-12 text-base font-bold uppercase tracking-wide"
size="lg"
>
{isComplete ? (
"Proceed to Upload"
) : (
"Complete all selections to proceed"
)}
{isComplete ? "Proceed to Upload" : "Select all fields to proceed"}
</Button>
</CardContent>
</Card>

View File

@@ -1,175 +0,0 @@
"use client"
import { useRouter, usePathname } from "next/navigation"
import { LayoutDashboard, Plus, User, FolderPlus, Package, MapPin } from "lucide-react"
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip"
import { ROUTES } from "@/utils/routes"
interface NavItem {
title: string
href: string
icon: React.ElementType
gradient: string
disabled?: boolean
}
const navItems: NavItem[] = [
{
title: "Dashboard",
href: ROUTES.DASHBOARD,
icon: LayoutDashboard,
gradient: "from-blue-400 to-indigo-500"
},
{
title: "Project",
href: ROUTES.PROJECT,
icon: FolderPlus,
gradient: "from-blue-400 to-blue-600"
},
{
title: "Package",
href: ROUTES.PACKAGE,
icon: Package,
gradient: "from-violet-400 to-purple-500"
},
{
title: "Location",
href: ROUTES.LOCATION,
icon: MapPin,
gradient: "from-amber-400 to-orange-500"
},
{
title: "Account",
href: ROUTES.ACCOUNT,
icon: User,
gradient: "from-purple-400 to-pink-500",
disabled: true
}
]
export function SidebarNavigation() {
const router = useRouter()
const pathname = usePathname()
const handleNavigation = (item: NavItem) => {
if (item.disabled) return
router.push(item.href)
}
const isNewAnalysisActive = pathname === ROUTES.NEW_ANALYSIS
return (
<aside className="fixed left-0 top-0 h-screen w-20 bg-white/40 backdrop-blur-xl border-r border-slate-200/50 z-50 flex flex-col items-center py-8">
{/* Navigation Items */}
<nav className="flex-1 flex flex-col items-center gap-4">
{/* Dashboard - first item */}
{navItems.slice(0, 1).map((item) => {
const isActive = pathname === item.href
const Icon = item.icon
return (
<div key={item.href} className="relative group">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => handleNavigation(item)}
className={`
relative w-12 h-12 rounded-md flex items-center justify-center
${isActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
transition-all ease-in-out duration-200
`}
>
<Icon
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
/>
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={12}>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
</div>
)
})}
{/* New Analysis - Special eye-catchy button */}
<div className="relative group">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => router.push(ROUTES.NEW_ANALYSIS)}
className={`
relative w-12 h-12 rounded-md flex items-center justify-center
${isNewAnalysisActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
ease-in-out duration-200 transition-all
`}
>
<Plus
className={`h-6 w-6 ${isNewAnalysisActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
/>
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={12}>
<p>New Analysis</p>
</TooltipContent>
</Tooltip>
</div>
{/* Divider */}
<div className="w-8 h-px bg-slate-200 my-1" />
{/* Create items */}
{navItems.slice(1, 4).map((item) => {
const isActive = pathname === item.href
const Icon = item.icon
return (
<div key={item.href} className="relative group">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => handleNavigation(item)}
className={`
relative w-12 h-12 rounded-md flex items-center justify-center
${isActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
transition-all duration-200
`}
>
<Icon
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
/>
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={12}>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
</div>
)
})}
</nav>
{/* Bottom section */}
<div className="mt-auto">
<div className="relative group">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => { }}
className="w-12 h-12 rounded-full flex items-center justify-center bg-linear-to-b from-[#225999] to-[#56A5FF] transition-all duration-200"
disabled
>
<User className="h-6 w-6 text-white" />
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={12}>
<p>Account</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</aside>
)
}

View File

@@ -0,0 +1,89 @@
"use client"
import * as React from "react"
import { ChevronsUpDown, Plus } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function TeamSwitcher({
teams,
}: {
teams: {
name: string
logo: React.ElementType
plan: string
}[]
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam.name}
</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -0,0 +1,11 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@@ -0,0 +1,53 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@@ -1,39 +1,39 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
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'
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-full gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-full px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
},
}
)
function Button({
@@ -42,11 +42,11 @@ function Button({
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
const Comp = asChild ? Slot : "button"
return (
<Comp

View File

@@ -1,81 +1,81 @@
import * as React from 'react'
import * as React from "react"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<'div'>) {
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-md border-none py-6 shadow-none hover:shadow-none',
className,
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn('leading-none font-semibold', className)}
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn('text-muted-foreground text-sm', className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn('px-6', className)}
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)

View File

@@ -0,0 +1,33 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -1,10 +1,11 @@
'use client'
"use client"
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
function Dialog({
...props
@@ -38,8 +39,8 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
'fixed inset-0 z-50 bg-black/50',
className,
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
)}
{...props}
/>
@@ -60,8 +61,8 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
'bg-dialog fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg',
className,
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
className
)}
{...props}
>
@@ -69,7 +70,7 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
@@ -80,26 +81,40 @@ function DialogContent({
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className,
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
@@ -110,7 +125,7 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn('text-lg leading-none font-semibold', className)}
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
@@ -123,7 +138,7 @@ function DialogDescription({
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn('text-muted-foreground text-sm', className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)

View File

@@ -1,15 +1,15 @@
'use client'
"use client";
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
@@ -17,7 +17,7 @@ function DropdownMenuPortal({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
);
}
function DropdownMenuTrigger({
@@ -28,7 +28,7 @@ function DropdownMenuTrigger({
data-slot="dropdown-menu-trigger"
{...props}
/>
)
);
}
function DropdownMenuContent({
@@ -42,13 +42,13 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
);
}
function DropdownMenuGroup({
@@ -56,17 +56,17 @@ function DropdownMenuGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
);
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: 'default' | 'destructive'
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
@@ -75,11 +75,11 @@ function DropdownMenuItem({
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
/>
)
);
}
function DropdownMenuCheckboxItem({
@@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
checked={checked}
{...props}
@@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
);
}
function DropdownMenuRadioGroup({
@@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
);
}
function DropdownMenuRadioItem({
@@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
@@ -140,7 +140,7 @@ function DropdownMenuRadioItem({
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
);
}
function DropdownMenuLabel({
@@ -148,19 +148,19 @@ function DropdownMenuLabel({
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
);
}
function DropdownMenuSeparator({
@@ -170,32 +170,32 @@ function DropdownMenuSeparator({
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
@@ -204,7 +204,7 @@ function DropdownMenuSubTrigger({
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
@@ -212,14 +212,14 @@ function DropdownMenuSubTrigger({
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
);
}
function DropdownMenuSubContent({
@@ -230,12 +230,12 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
);
}
export {
@@ -254,4 +254,4 @@ export {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
};

View File

@@ -1,17 +1,17 @@
import * as React from 'react'
import * as React from "react"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className,
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>

View File

@@ -1,13 +1,13 @@
'use client'
"use client"
import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = 'horizontal',
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
@@ -17,8 +17,8 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>

View File

@@ -1,10 +1,10 @@
'use client'
"use client"
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
@@ -36,8 +36,8 @@ function SheetOverlay({
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
'fixed inset-0 z-50 bg-black/50',
className,
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
@@ -47,10 +47,10 @@ function SheetOverlay({
function SheetContent({
className,
children,
side = 'right',
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: 'top' | 'right' | 'bottom' | 'left'
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
@@ -58,16 +58,16 @@ function SheetContent({
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
'bg-background fixed z-50 flex flex-col gap-4 shadow-lg',
side === 'right' &&
'inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left' &&
'inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top' &&
'inset-x-0 top-0 h-auto border-b',
side === 'bottom' &&
'inset-x-0 bottom-0 h-auto border-t',
className,
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
@@ -81,21 +81,21 @@ function SheetContent({
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn('flex flex-col gap-1.5 p-4', className)}
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
@@ -108,7 +108,7 @@ function SheetTitle({
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn('text-foreground font-semibold', className)}
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
@@ -121,7 +121,7 @@ function SheetDescription({
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn('text-muted-foreground text-sm', className)}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)

View File

@@ -1,39 +1,39 @@
'use client'
"use client"
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, VariantProps } from 'class-variance-authority'
import { PanelLeftIcon } from 'lucide-react'
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from '@/hooks/use-mobile'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { Skeleton } from '@/components/ui/skeleton'
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = 'sidebar_state'
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = '16rem'
const SIDEBAR_WIDTH_MOBILE = '18rem'
const SIDEBAR_WIDTH_ICON = '3rem'
const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: 'expanded' | 'collapsed'
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
@@ -47,7 +47,7 @@ const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider.')
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
@@ -61,7 +61,7 @@ function SidebarProvider({
style,
children,
...props
}: React.ComponentProps<'div'> & {
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
@@ -75,7 +75,7 @@ function SidebarProvider({
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === 'function' ? value(open) : value
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
@@ -85,7 +85,7 @@ function SidebarProvider({
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open],
[setOpenProp, open]
)
// Helper to toggle the sidebar.
@@ -105,13 +105,13 @@ function SidebarProvider({
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed'
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
@@ -123,7 +123,7 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
@@ -133,14 +133,14 @@ function SidebarProvider({
data-slot="sidebar-wrapper"
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
className,
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
@@ -152,26 +152,26 @@ function SidebarProvider({
}
function Sidebar({
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<'div'> & {
side?: 'left' | 'right'
variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: 'offcanvas' | 'icon' | 'none'
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === 'none') {
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
className,
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
@@ -190,7 +190,7 @@ function Sidebar({
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
@@ -209,7 +209,7 @@ function Sidebar({
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
@@ -218,26 +218,26 @@ function Sidebar({
<div
data-slot="sidebar-gap"
className={cn(
'relative w-(--sidebar-width) bg-transparent',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
className,
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
@@ -266,7 +266,7 @@ function SidebarTrigger({
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn('size-7', className)}
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
@@ -279,7 +279,7 @@ function SidebarTrigger({
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
@@ -291,27 +291,27 @@ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex',
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className,
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
'bg-background relative flex w-full flex-1 flex-col',
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
className,
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
@@ -326,29 +326,29 @@ function SidebarInput({
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn('bg-background h-8 w-full shadow-none', className)}
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn('flex flex-col gap-2 p-2', className)}
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn('flex flex-col gap-2 p-2', className)}
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
@@ -362,32 +362,32 @@ function SidebarSeparator({
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn('bg-sidebar-border mx-2 w-auto', className)}
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className,
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
@@ -397,17 +397,17 @@ function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<'div'> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'div'
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className,
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
@@ -418,19 +418,19 @@ function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<'button'> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'button'
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 md:after:hidden',
'group-data-[collapsible=icon]:hidden',
className,
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
@@ -440,75 +440,75 @@ function SidebarGroupAction({
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<'div'>) {
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn('w-full text-sm', className)}
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn('group/menu-item relative', className)}
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
},
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = 'default',
size = 'default',
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<'button'> & {
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : 'button'
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
@@ -526,7 +526,7 @@ function SidebarMenuButton({
return button
}
if (typeof tooltip === 'string') {
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
@@ -538,7 +538,7 @@ function SidebarMenuButton({
<TooltipContent
side="right"
align="center"
hidden={state !== 'collapsed' || isMobile}
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
@@ -550,27 +550,27 @@ function SidebarMenuAction({
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<'button'> & {
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : 'button'
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 md:after:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
className,
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
@@ -580,19 +580,19 @@ function SidebarMenuAction({
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<'div'>) {
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
className,
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
@@ -603,17 +603,19 @@ function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<'div'> & {
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Fixed width to prevent hydration mismatch.
const width = "70%"
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
@@ -627,7 +629,7 @@ function SidebarMenuSkeleton({
data-sidebar="menu-skeleton-text"
style={
{
'--skeleton-width': width,
"--skeleton-width": width,
} as React.CSSProperties
}
/>
@@ -635,15 +637,15 @@ function SidebarMenuSkeleton({
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
className,
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
@@ -653,12 +655,12 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<'li'>) {
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn('group/menu-sub-item relative', className)}
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
@@ -666,16 +668,16 @@ function SidebarMenuSubItem({
function SidebarMenuSubButton({
asChild = false,
size = 'md',
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<'a'> & {
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: 'sm' | 'md'
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot : 'a'
const Comp = asChild ? Slot : "a"
return (
<Comp
@@ -684,12 +686,12 @@ function SidebarMenuSubButton({
data-size={size}
data-active={isActive}
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
className,
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>

View File

@@ -1,10 +1,10 @@
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn('bg-muted animate-pulse rounded-md', className)}
className={cn("animate-pulse rounded-md bg-accent", className)}
{...props}
/>
)

View File

@@ -1,10 +1,10 @@
'use client'
"use client"
import * as React from 'react'
import * as React from "react"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<'table'>) {
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
@@ -12,79 +12,79 @@ function Table({ className, ...props }: React.ComponentProps<'table'>) {
>
<table
data-slot="table"
className={cn('w-full caption-bottom text-sm', className)}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn('[&_tr]:border-b', className)}
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn('[&_tr:last-child]:border-0', className)}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className,
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b',
className,
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
@@ -94,11 +94,11 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
function TableCaption({
className,
...props
}: React.ComponentProps<'caption'>) {
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn('text-muted-foreground mt-4 text-sm', className)}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)

View File

@@ -1,9 +1,9 @@
'use client'
"use client"
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
@@ -46,8 +46,8 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
'bg-foreground text-white z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
className,
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>

View File

@@ -1,19 +0,0 @@
import * as React from 'react'
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener('change', onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener('change', onChange)
}, [])
return !!isMobile
}

View File

@@ -213,7 +213,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
}
return (
<Card className="bg-white/20 backdrop-blur-md">
<Card>
<CardHeader>
<CardTitle>Upload Video</CardTitle>
<CardDescription>
@@ -222,8 +222,6 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* Video File Input kept in 2-col row */}
{/* File Input */}
<div className="space-y-2">
<Label htmlFor="video-file">Video File</Label>
<div className="flex gap-2">
@@ -247,7 +245,6 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
)}
</div>
{/* JSON File Input */}
<div className="space-y-2">
<Label htmlFor="json-file">GPS JSON File</Label>
<div className="flex gap-2">
@@ -270,12 +267,9 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
</p>
)}
</div>
</div>
{/* Detection Type, Speed, and Method - 3 column row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Detection Type Selector */}
<div className="space-y-2">
<Label htmlFor="detection-type">Detection Type</Label>
<Select
@@ -289,16 +283,13 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
<SelectContent>
{DETECTION_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
<div className="flex items-center gap-2">
<span>{type.label}</span>
</div>
<span>{type.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Speed Input */}
<div className="space-y-2">
<Label htmlFor="speed">Vehicle Speed (km/h)</Label>
<Input
@@ -312,7 +303,6 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
/>
</div>
{/* Select Method */}
<div className="space-y-2">
<Label htmlFor="select-method">Select Method</Label>
<Select
@@ -326,9 +316,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
<SelectContent>
{DETECTION_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
<div className="flex items-center gap-2">
<span>{method.label}</span>
</div>
<span>{method.label}</span>
</SelectItem>
))}
</SelectContent>
@@ -336,7 +324,6 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
</div>
</div>
{/* Error Display */}
{error && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
<AlertCircle className="h-5 w-5 mt-0.5 shrink-0" />
@@ -344,12 +331,10 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
</div>
)}
{/* Upload Button */}
<Button
onClick={handleUpload}
disabled={!file || uploading}
className="w-full btn-blue-gradient rounded-full"
className="w-full"
size="lg"
>
{uploading ? (
@@ -365,7 +350,6 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
)}
</Button>
{/* Progress Section */}
{uploading && (
<div className="space-y-3">
<Progress value={progress} className="h-3" />

File diff suppressed because it is too large Load Diff