refactor: redesign table module

This commit is contained in:
2026-03-18 01:03:58 +05:30
parent caf527f584
commit 344f37719e
24 changed files with 991 additions and 815 deletions

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { import {
AlertCircle, AlertCircle,
Activity, Activity,
@@ -11,6 +11,7 @@ import {
Zap, Zap,
PencilLine, PencilLine,
CheckCircle2, CheckCircle2,
Settings2,
} from "lucide-react"; } from "lucide-react";
import { StatsCard } from "@/components/dashboard/stats-card"; import { StatsCard } from "@/components/dashboard/stats-card";
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"; import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector";
@@ -26,9 +27,9 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Filter } from "lucide-react";
import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton" import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton"
import { PoweredBy } from "@/components/powered-by"; import { PoweredBy } from "@/components/powered-by";
import { toast } from "sonner";
const API_URL = process.env.NEXT_PUBLIC_API_URL; const API_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -207,14 +208,12 @@ export default function DashboardPage() {
totalRoadDamage: 0, totalRoadDamage: 0,
locationData: [], locationData: [],
}); });
const [error, setError] = useState<string | null>(null);
// Load projects on mount // Load projects on mount
useEffect(() => { useEffect(() => {
const loadProjects = async () => { const loadProjects = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null);
const projectsData = await fetchProjects(); const projectsData = await fetchProjects();
setProjects(projectsData); setProjects(projectsData);
@@ -223,9 +222,9 @@ export default function DashboardPage() {
} }
} catch (err) { } catch (err) {
console.error("Failed to load projects:", err); console.error("Failed to load projects:", err);
setError( toast.error("Failed to load projects", {
"Failed to load projects. Please check if the backend is running.", description: "Please check if the backend is running."
); });
} finally { } finally {
setTimeout(() => { setTimeout(() => {
setIsLoading(false); setIsLoading(false);
@@ -274,7 +273,6 @@ export default function DashboardPage() {
const loadProjectSummary = async () => { const loadProjectSummary = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null);
const response = await fetch( const response = await fetch(
`${API_URL}/summary/projects/${selectedProjectId}`, `${API_URL}/summary/projects/${selectedProjectId}`,
@@ -294,7 +292,7 @@ export default function DashboardPage() {
setProjectSummary(summary); setProjectSummary(summary);
} catch (err) { } catch (err) {
console.error("Failed to load project summary:", err); console.error("Failed to load project summary:", err);
setError("Failed to load project summary."); toast.error("Failed to load project summary");
setProjectSummary(null); setProjectSummary(null);
} finally { } finally {
setTimeout(() => { setTimeout(() => {
@@ -422,10 +420,10 @@ export default function DashboardPage() {
}, [projectSummary, selectedPackageId, selectedLocationId]); }, [projectSummary, selectedPackageId, selectedLocationId]);
return ( return (
<div className="min-h-screen text-gray-900 dark:text-gray-100"> <>
{/* Main Content */} {/* Main Content */}
<main className="min-h-screen"> <main>
<div className="p-4 px-8 max-w-340 mx-auto"> <div>
{/* Header */} {/* Header */}
<div className="mb-8 flex items-center justify-between"> <div className="mb-8 flex items-center justify-between">
<PageHeader <PageHeader
@@ -437,14 +435,14 @@ export default function DashboardPage() {
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" className="gap-2 font-semibold"> <Button variant="outline" className="gap-2 font-semibold">
<Filter className="h-4 w-4" /> <Settings2 className="h-4 w-4" />
<span>Filters</span> <span>Filters</span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[320px] p-0 overflow-hidden" align="end"> <PopoverContent className="w-[320px] p-0 overflow-hidden" align="end">
<div className="p-4 border-b bg-muted/30"> <div className="p-4 border-b bg-muted/30">
<h3 className="font-bold text-sm flex items-center gap-2 text-foreground"> <h3 className="font-bold text-sm flex items-center gap-2 text-foreground">
<Filter className="h-4 w-4 text-primary" /> <Settings2 className="h-4 w-4 text-primary" />
Filter Analysis Filter Analysis
</h3> </h3>
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or location</p> <p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or location</p>
@@ -467,14 +465,6 @@ export default function DashboardPage() {
</Popover> </Popover>
</div> </div>
{/* Error Display */}
{error && (
<div className="mb-4 flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-sm">
<AlertCircle className="h-4 w-4 shrink-0" />
<p>{error}</p>
</div>
)}
{isLoading && !projectSummary ? ( {isLoading && !projectSummary ? (
<DashboardSkeleton /> <DashboardSkeleton />
) : ( ) : (
@@ -522,18 +512,12 @@ export default function DashboardPage() {
{/* Charts Row - Side by Side */} {/* Charts Row - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{/* Left Chart - Detection Distribution */} {/* Left Chart - Detection Distribution */}
<Card> <Card className="flex flex-col">
<CardHeader className="pb-2"> <CardHeader className="items-center pb-0">
<CardTitle className="text-base font-bold flex items-center gap-2"> <CardTitle>Detection Distribution</CardTitle>
<div className="p-2 rounded-md bg-secondary text-secondary-foreground"> <CardDescription>Breakdown of all detected road conditions</CardDescription>
<BarChart3 className="h-5 w-5" />
</div>
<span className="text-xl">
Detection Distribution
</span>
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-4"> <CardContent className="flex-1 pb-0">
<DetectionDonutChart <DetectionDonutChart
defectedSignboard={stats.totalDefectedSignboard} defectedSignboard={stats.totalDefectedSignboard}
pothole={stats.totalPothole} pothole={stats.totalPothole}
@@ -542,19 +526,20 @@ export default function DashboardPage() {
goodSignboard={stats.totalGoodSignboard} goodSignboard={stats.totalGoodSignboard}
/> />
</CardContent> </CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground">
Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
</div>
</CardFooter>
</Card> </Card>
{/* Right Chart - Location Bar Chart */} {/* Right Chart - Detections by Location */}
<Card> <Card className="flex flex-col">
<CardHeader className="pb-2"> <CardHeader className="items-center pb-0">
<CardTitle className="text-base font-bold flex items-center gap-2"> <CardTitle>Detections by Location</CardTitle>
<div className="p-2 rounded-md bg-secondary text-secondary-foreground"> <CardDescription>Frequency of road issues across different map segments</CardDescription>
<MapPinIcon className="h-5 w-5" />
</div>
<span className="text-xl">Detections by Location</span>
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-4"> <CardContent className="flex-1 pb-0">
<LocationBarChart <LocationBarChart
data={stats.locationData.map((loc) => ({ data={stats.locationData.map((loc) => ({
name: loc.name, name: loc.name,
@@ -562,11 +547,15 @@ export default function DashboardPage() {
pothole: loc.pothole, pothole: loc.pothole,
road_crack: loc.road_crack, road_crack: loc.road_crack,
damaged_road_marking: loc.damaged_road_marking, damaged_road_marking: loc.damaged_road_marking,
good_sign_board: loc.good_sign_board,
total: loc.total, total: loc.total,
}))} }))}
/> />
</CardContent> </CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground">
Analysis based on latest processed sequence
</div>
</CardFooter>
</Card> </Card>
</div> </div>
@@ -585,6 +574,6 @@ export default function DashboardPage() {
<PoweredBy /> <PoweredBy />
</div> </div>
</main> </main>
</div> </>
); );
} }

View File

@@ -1,6 +1,8 @@
import { BreadcrumbBasic } from "@/components/app-breadcrumb";
import { AppSidebar } from "@/components/app-sidebar"; import { AppSidebar } from "@/components/app-sidebar";
import { ModeToggle } from "@/components/mode-toogle"; import { ModeToggle } from "@/components/mode-toogle";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import React from "react"; import React from "react";
const ModulesLayout = ({ const ModulesLayout = ({
@@ -11,15 +13,23 @@ const ModulesLayout = ({
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
<main className="flex flex-1 flex-col p-4 pt-0 w-full h-screen overflow-hidden"> <main className="flex flex-1 flex-col w-full h-screen overflow-hidden">
<div className="flex gap-3 items-center mt-2 sticky top-0 bg-background z-50 py-2"> <div className="flex-1 overflow-auto">
<SidebarTrigger /> {/* Centering container wrapper */}
<ModeToggle /> <div className="max-w-380 mx-auto w-full flex flex-col min-h-full">
<div className="flex gap-3 items-center sticky top-0 bg-background/50 backdrop-blur-md z-30 py-3 px-6 border-b border-border/10">
<SidebarTrigger />
<ModeToggle />
<Separator orientation="vertical" className="mx-2 h-4" />
<BreadcrumbBasic />
</div>
<div className="p-6 flex-1">{children}</div>
</div>
</div> </div>
<div className="flex-1 py-2 overflow-auto">{children}</div>
</main> </main>
</SidebarProvider> </SidebarProvider>
); );
}; };
export default ModulesLayout; export default ModulesLayout;

View File

@@ -27,6 +27,8 @@ import {
} from "@/lib/api" } from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table" import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner" import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
export default function LocationPage() { export default function LocationPage() {
const [locations, setLocations] = useState<Location[]>([]) const [locations, setLocations] = useState<Location[]>([])
@@ -168,7 +170,9 @@ export default function LocationPage() {
end_lng: parseFloat(endLng), end_lng: parseFloat(endLng),
} }
await updateLocation(currentLocation.id, data) await updateLocation(currentLocation.id, data)
toast.success("Location updated successfully!") toast.success("Location Updated", {
description: `${segmentName} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else { } else {
const data: LocationCreate = { const data: LocationCreate = {
package_id: selectedPackageId, package_id: selectedPackageId,
@@ -181,7 +185,9 @@ export default function LocationPage() {
end_lng: parseFloat(endLng), end_lng: parseFloat(endLng),
} }
await createLocation(data) await createLocation(data)
toast.success("Location created successfully!") toast.success("Location Created", {
description: `${segmentName} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
} }
// Refresh locations list // Refresh locations list
@@ -191,7 +197,11 @@ export default function LocationPage() {
setIsModalOpen(false) setIsModalOpen(false)
resetForm() resetForm()
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} location`) const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} location`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@@ -224,10 +234,15 @@ export default function LocationPage() {
try { try {
setIsLoading(true) setIsLoading(true)
await deleteLocation(location.id) await deleteLocation(location.id)
toast.success("Location deleted successfully!") toast.success("Location Deleted", {
description: `${location.segment_name} has been removed from the system.`,
})
await loadLocations() await loadLocations()
} catch (err) { } catch (err) {
setError("Failed to delete location") setError("Failed to delete location")
toast.error("Deletion Failed", {
description: "The location could not be removed. Please try again.",
})
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -262,6 +277,50 @@ export default function LocationPage() {
return project?.name || "—" return project?.name || "—"
} }
}, },
{
id: "project_state",
header: "Project State",
cell: ({ row }) => {
const location = row.original
const pkg = allPackages.find(p => p.id === location.package_id)
const project = projects.find(p => p.id === pkg?.project_id)
if (!project?.state) return <span className="text-gray-400"></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span className="text-gray-400"></span>
const firstState = states[0]
const remainingStates = states.slice(1)
return (
<div className="flex items-center gap-1.5">
<Badge variant="secondary">
{firstState}
</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge key={idx} variant="secondary">
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
)
}
},
{ {
accessorKey: "chainage", accessorKey: "chainage",
header: "Chainage (km)", header: "Chainage (km)",
@@ -269,9 +328,10 @@ export default function LocationPage() {
const location = row.original const location = row.original
if (location.chainage_start_km !== null && location.chainage_end_km !== null) { if (location.chainage_start_km !== null && location.chainage_end_km !== null) {
return ( return (
<span className="px-2 py-0.5 rounded-full bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 font-semibold border border-amber-100 dark:border-amber-800 whitespace-nowrap"> <Badge variant="outline" className="flex items-center gap-1.5 border-amber-500/50 text-amber-500 font-bold whitespace-nowrap">
<Milestone className="h-3 w-3" />
{location.chainage_start_km} - {location.chainage_end_km} {location.chainage_start_km} - {location.chainage_end_km}
</span> </Badge>
) )
} }
return "—" return "—"
@@ -281,44 +341,44 @@ export default function LocationPage() {
accessorKey: "start_gps", accessorKey: "start_gps",
header: "Start GPS", header: "Start GPS",
cell: ({ row }) => ( cell: ({ row }) => (
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-100 dark:border-blue-800 whitespace-nowrap"> <Badge variant="outline" className="flex items-center gap-1.5 border-blue-500/50 text-blue-500 font-bold whitespace-nowrap">
<MapPin className="h-3 w-3" />
{row.original.start_lat.toFixed(4)}, {row.original.start_lng.toFixed(4)} {row.original.start_lat.toFixed(4)}, {row.original.start_lng.toFixed(4)}
</span> </Badge>
) )
}, },
{ {
accessorKey: "end_gps", accessorKey: "end_gps",
header: "End GPS", header: "End GPS",
cell: ({ row }) => ( cell: ({ row }) => (
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-100 dark:border-blue-800 whitespace-nowrap"> <Badge variant="outline" className="flex items-center gap-1.5 border-blue-500/50 text-blue-500 font-bold whitespace-nowrap">
<MapPin className="h-3 w-3" />
{row.original.end_lat.toFixed(4)}, {row.original.end_lng.toFixed(4)} {row.original.end_lat.toFixed(4)}, {row.original.end_lng.toFixed(4)}
</span> </Badge>
) )
}, },
] ]
return ( return (
<div className="min-h-screen text-gray-900 dark:text-gray-100"> <>
<main className="min-h-screen relative overflow-hidden"> <main className="relative z-10">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
{/* Refined Header */} {/* Refined Header */}
<div className="mb-8"> <div className="mb-8">
<PageHeader <PageHeader
title="Location Management" title="Location Management"
description="Manage road segment locations" description="Manage road segment locations"
icon={MapPin} icon={MapPin}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<MapPin className="mr-2 h-4 w-4" />
Add New Location
</Button>
}
/> />
</div> </div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
{/* Data Table */} {/* Data Table */}
<div> <div>
@@ -326,10 +386,8 @@ export default function LocationPage() {
title="Locations" title="Locations"
data={locations} data={locations}
columns={columns} columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}
addButtonText="Add New Location"
isLoading={isLoading} isLoading={isLoading}
pagination={{ pagination={{
skip, skip,
@@ -344,8 +402,7 @@ export default function LocationPage() {
</div> </div>
<PoweredBy /> <PoweredBy />
</div> </main>
</main>
{/* Modal Dialog */} {/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}> <Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
@@ -365,15 +422,6 @@ export default function LocationPage() {
</DialogHeader> </DialogHeader>
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{!isEditing && ( {!isEditing && (
@@ -607,6 +655,6 @@ export default function LocationPage() {
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </>
) )
} }

View File

@@ -23,6 +23,8 @@ import {
} from "@/lib/api" } from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table" import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner" import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by" import { PoweredBy } from "@/components/powered-by"
export default function PackagePage() { export default function PackagePage() {
@@ -108,7 +110,9 @@ export default function PackagePage() {
region: region.trim() || null, region: region.trim() || null,
} }
await updatePackage(currentPackage.id, data) await updatePackage(currentPackage.id, data)
toast.success("Package updated successfully!") toast.success("Package Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else { } else {
const data: PackageCreate = { const data: PackageCreate = {
project_id: selectedProjectId, project_id: selectedProjectId,
@@ -116,7 +120,9 @@ export default function PackagePage() {
region: region.trim() || null, region: region.trim() || null,
} }
await createPackage(data) await createPackage(data)
toast.success("Package created successfully!") toast.success("Package Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
} }
// Refresh packages list // Refresh packages list
@@ -126,7 +132,11 @@ export default function PackagePage() {
setIsModalOpen(false) setIsModalOpen(false)
resetForm() resetForm()
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} package`) const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} package`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@@ -147,10 +157,15 @@ export default function PackagePage() {
try { try {
setIsLoading(true) setIsLoading(true)
await deletePackage(pkg.id) await deletePackage(pkg.id)
toast.success("Package deleted successfully!") toast.success("Package Deleted", {
description: `${pkg.name} has been removed from the system.`,
})
await loadPackages() await loadPackages()
} catch (err) { } catch (err) {
setError("Failed to delete package") setError("Failed to delete package")
toast.error("Deletion Failed", {
description: "The package could not be removed. Please try again.",
})
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -180,16 +195,38 @@ export default function PackagePage() {
const pkg = row.original const pkg = row.original
const project = projects.find(p => p.id === pkg.project_id) const project = projects.find(p => p.id === pkg.project_id)
if (!project?.state) return <span className="text-gray-400"></span> if (!project?.state) return <span className="text-gray-400"></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span className="text-gray-400"></span>
const firstState = states[0]
const remainingStates = states.slice(1)
return ( return (
<div className="flex flex-wrap gap-1"> <div className="flex items-center gap-1.5">
{project.state.split(',').map((item, idx) => ( <Badge variant="secondary">
<span {firstState}
key={idx} </Badge>
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-semibold border border-blue-100 dark:border-blue-800"
> {remainingStates.length > 0 && (
{item.trim()} <Popover>
</span> <PopoverTrigger asChild>
))} <button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge key={idx} variant="secondary">
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div> </div>
) )
} }
@@ -201,27 +238,26 @@ export default function PackagePage() {
] ]
return ( return (
<div className="min-h-screen text-gray-900 dark:text-gray-100"> <>
<main className="min-h-screen relative overflow-hidden">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10"> <main className="relative z-10">
{/* Refined Header */} {/* Refined Header */}
<div className="mb-8"> <div className="mb-8">
<PageHeader <PageHeader
title="Package Management" title="Package Management"
description="Manage project packages" description="Manage project packages"
icon={Package} icon={Package}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<Package className="mr-2 h-4 w-4" />
Add New Package
</Button>
}
/> />
</div> </div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
{/* Data Table */} {/* Data Table */}
<div> <div>
@@ -229,10 +265,8 @@ export default function PackagePage() {
title="Packages" title="Packages"
data={packages} data={packages}
columns={columns} columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}
addButtonText="Add New Package"
isLoading={isLoading} isLoading={isLoading}
pagination={{ pagination={{
skip, skip,
@@ -247,8 +281,7 @@ export default function PackagePage() {
</div> </div>
<PoweredBy /> <PoweredBy />
</div> </main>
</main>
{/* Modal Dialog */} {/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}> <Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
@@ -269,15 +302,6 @@ export default function PackagePage() {
</DialogHeader> </DialogHeader>
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-8"> <form onSubmit={handleSubmit} className="space-y-8">
{/* Step 1: Select Project */} {/* Step 1: Select Project */}
@@ -371,7 +395,7 @@ export default function PackagePage() {
<Button <Button
type="submit" type="submit"
disabled={isSubmitting || !name.trim() || !selectedProjectId} disabled={isSubmitting || !name.trim() || !selectedProjectId}
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20" className="flex-1 h-12 font-bold uppercase tracking-wider text-xs"
> >
{isSubmitting ? ( {isSubmitting ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -389,6 +413,6 @@ export default function PackagePage() {
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </>
) )
} }

View File

@@ -12,6 +12,8 @@ import { PageHeader } from "@/components/page-header"
import { createProject, fetchProjects, updateProject, deleteProject, type ProjectCreate, type Project, type ProjectUpdate } from "@/lib/api" import { createProject, fetchProjects, updateProject, deleteProject, type ProjectCreate, type Project, type ProjectUpdate } from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table" import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner" import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by" import { PoweredBy } from "@/components/powered-by"
export default function ProjectPage() { export default function ProjectPage() {
@@ -91,7 +93,9 @@ export default function ProjectPage() {
end_lng: endLng ? parseFloat(endLng) : null, end_lng: endLng ? parseFloat(endLng) : null,
} }
await updateProject(currentProject.id, data) await updateProject(currentProject.id, data)
toast.success("Project updated successfully!") toast.success("Project Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else { } else {
const data: ProjectCreate = { const data: ProjectCreate = {
name: name.trim(), name: name.trim(),
@@ -103,7 +107,9 @@ export default function ProjectPage() {
end_lng: endLng ? parseFloat(endLng) : null, end_lng: endLng ? parseFloat(endLng) : null,
} }
await createProject(data) await createProject(data)
toast.success("Project created successfully!") toast.success("Project Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
} }
// Refresh projects list // Refresh projects list
@@ -113,7 +119,11 @@ export default function ProjectPage() {
setIsModalOpen(false) setIsModalOpen(false)
resetForm() resetForm()
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} project`) const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} project`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@@ -138,10 +148,15 @@ export default function ProjectPage() {
try { try {
setIsLoading(true) setIsLoading(true)
await deleteProject(project.id) await deleteProject(project.id)
toast.success("Project deleted successfully!") toast.success("Project Deleted", {
description: `${project.name} has been removed from the system.`,
})
await loadProjects() await loadProjects()
} catch (err) { } catch (err) {
setError("Failed to delete project") setError("Failed to delete project")
toast.error("Deletion Failed", {
description: "The project could not be removed. Please try again or check your permissions.",
})
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -152,7 +167,7 @@ export default function ProjectPage() {
accessorKey: "name", accessorKey: "name",
header: "Project Name", header: "Project Name",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.name}</div> <div className="font-semibold">{row.original.name}</div>
) )
}, },
{ {
@@ -160,17 +175,45 @@ export default function ProjectPage() {
header: "State", header: "State",
cell: ({ row }) => { cell: ({ row }) => {
const project = row.original const project = row.original
if (!project.state) return <span className="text-gray-400"></span> if (!project.state) return <span></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span></span>
const firstState = states[0]
const remainingStates = states.slice(1)
return ( return (
<div className="flex flex-wrap gap-1.5"> <div className="flex items-center gap-1.5">
{project.state.split(',').map((item, idx) => ( <Badge
<span variant="secondary"
key={idx}
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-semibold border border-blue-100 dark:border-blue-800" >
> {firstState}
{item.trim()} </Badge>
</span>
))} {remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge
key={idx}
variant="secondary"
>
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div> </div>
) )
} }
@@ -182,27 +225,26 @@ export default function ProjectPage() {
] ]
return ( return (
<div className="min-h-screen"> <>
<main className="min-h-screen relative overflow-hidden">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10"> <main className="relative z-10">
{/* Refined Header */} {/* Refined Header */}
<div className="mb-8"> <div className="mb-8">
<PageHeader <PageHeader
title="Project Management" title="Project Management"
description="Manage road infrastructure projects" description="Manage road infrastructure projects"
icon={FolderPlus} icon={FolderPlus}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<FolderPlus className="mr-2 h-5 w-5" />
Add New Project
</Button>
}
/> />
</div> </div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
{/* Data Table */} {/* Data Table */}
<div> <div>
@@ -210,10 +252,8 @@ export default function ProjectPage() {
title="Projects" title="Projects"
data={projects} data={projects}
columns={columns} columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}
addButtonText="Add New Project"
isLoading={isLoading} isLoading={isLoading}
pagination={{ pagination={{
skip, skip,
@@ -228,8 +268,8 @@ export default function ProjectPage() {
</div> </div>
<PoweredBy /> <PoweredBy />
</div> </main>
</main>
{/* Modal Dialog */} {/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}> <Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
@@ -250,15 +290,6 @@ export default function ProjectPage() {
</DialogHeader> </DialogHeader>
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Project Name */} {/* Project Name */}
@@ -409,6 +440,6 @@ export default function ProjectPage() {
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </>
) )
} }

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -149,6 +149,29 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
/* Modern Scrollbar Styles */
::-webkit-scrollbar {
width: 8px; /* Slightly wider for better accessibility */
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--muted-foreground);
border-radius: 20px;
border: 2px solid transparent;
background-clip: content-box;
opacity: 0.5; /* More visible default */
}
/* Firefox Support */
* {
/* scrollbar-width: ; */
scrollbar-color: var(--muted-foreground) transparent;
}
} }

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner"
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -35,6 +36,7 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
{children} {children}
<Toaster position="top-center" />
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,57 @@
"use client";
import React from "react";
import { usePathname } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
export function BreadcrumbBasic() {
const pathname = usePathname();
const segments = pathname.split("/").filter((segment) => segment !== "");
// Helper to format segment (e.g., "new-analysis" -> "New Analysis")
const formatSegment = (segment: string) => {
return segment
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
{segments.length > 0 && <BreadcrumbSeparator />}
{segments.map((segment, index) => {
const href = `/${segments.slice(0, index + 1).join("/")}`;
const isLast = index === segments.length - 1;
// Skip segments that represent module groups or generic IDs if needed
// For now, mapping all segments
return (
<React.Fragment key={href}>
<BreadcrumbItem>
{isLast ? (
<BreadcrumbPage>{formatSegment(segment)}</BreadcrumbPage>
) : (
<BreadcrumbLink href={href}>{formatSegment(segment)}</BreadcrumbLink>
)}
</BreadcrumbItem>
{!isLast && <BreadcrumbSeparator />}
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
}

View File

@@ -78,7 +78,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarMenu> <SidebarMenu>
{data.navMain.map((item) => ( {data.navMain.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>

View File

@@ -1,159 +1,156 @@
"use client" "use client"
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts" import * as React from "react"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { Label, Pie, PieChart } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
interface DetectionDonutChartProps { interface DetectionDonutChartProps {
defectedSignboard: number defectedSignboard: number
pothole: number pothole: number
roadCrack: number roadCrack: number
damagedRoadMarking: number damagedRoadMarking: number
goodSignboard: number goodSignboard: number
isLoading?: boolean isLoading?: boolean
} }
const COLORS = { const chartConfig = {
defectedSignboard: "#3b82f6", // Blue pothole: {
pothole: "#ef4444", // Red label: "Potholes",
roadCrack: "#f59e0b", // Amber/Orange color: "var(--chart-1)",
damagedRoadMarking: "#6366f1", // Indigo },
goodSignboard: "#10b981" // Emerald defectedSignboard: {
} label: "Defected Signboards",
color: "var(--chart-2)",
},
roadCrack: {
label: "Road Cracks",
color: "var(--chart-3)",
},
damagedRoadMarking: {
label: "Damaged Markings",
color: "var(--chart-4)",
},
goodSignboard: {
label: "Good Signboards",
color: "var(--chart-5)",
},
} satisfies ChartConfig
export function DetectionDonutChart({ export function DetectionDonutChart({
defectedSignboard, defectedSignboard,
pothole, pothole,
roadCrack, roadCrack,
damagedRoadMarking, damagedRoadMarking,
goodSignboard, goodSignboard,
isLoading = false isLoading = false,
}: DetectionDonutChartProps) { }: DetectionDonutChartProps) {
if (isLoading) { const chartData = React.useMemo(
return ( () =>
<div className="h-[250px] flex items-center justify-center"> [
<Loader2 className="h-8 w-8 text-primary/50" /> { type: "pothole", count: pothole, fill: "var(--color-pothole)" },
</div> {
) type: "defectedSignboard",
} count: defectedSignboard,
fill: "var(--color-defectedSignboard)",
},
{ type: "roadCrack", count: roadCrack, fill: "var(--color-roadCrack)" },
{
type: "damagedRoadMarking",
count: damagedRoadMarking,
fill: "var(--color-damagedRoadMarking)",
},
{
type: "goodSignboard",
count: goodSignboard,
fill: "var(--color-goodSignboard)",
},
].filter((item) => item.count > 0),
[
pothole,
defectedSignboard,
roadCrack,
damagedRoadMarking,
goodSignboard,
]
)
const total = defectedSignboard + pothole + roadCrack + damagedRoadMarking + goodSignboard const totalDetections = React.useMemo(() => {
return chartData.reduce((acc, curr) => acc + curr.count, 0)
if (total === 0) { }, [chartData])
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No detections found</p>
<p className="text-xs mt-1">Process videos to see data</p>
</div>
)
}
const data = [
{ name: "Defected Signboards", value: defectedSignboard, color: COLORS.defectedSignboard },
{ name: "Potholes", value: pothole, color: COLORS.pothole },
{ name: "Road Cracks", value: roadCrack, color: COLORS.roadCrack },
{ name: "Damaged Markings", value: damagedRoadMarking, color: COLORS.damagedRoadMarking },
{ name: "Good Signboards", value: goodSignboard, color: COLORS.goodSignboard }
].filter(item => item.value > 0)
if (isLoading) {
return ( return (
<div className="h-[220px] relative"> <div className="h-[250px] flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%"> <Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
<PieChart> </div>
<defs>
<linearGradient id="gradPothole" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ff8a8a" />
<stop offset="100%" stopColor="#ef4444" />
</linearGradient>
<linearGradient id="gradDefectedSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#60a5fa" />
<stop offset="100%" stopColor="#3b82f6" />
</linearGradient>
<linearGradient id="gradCrack" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fbbf24" />
<stop offset="100%" stopColor="#f59e0b" />
</linearGradient>
<linearGradient id="gradMarking" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#818cf8" />
<stop offset="100%" stopColor="#6366f1" />
</linearGradient>
<linearGradient id="gradGoodSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#34d399" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
</defs>
<Pie
data={data}
cx="50%"
cy="45%"
innerRadius={50}
outerRadius={70}
paddingAngle={5}
dataKey="value"
strokeWidth={0}
isAnimationActive={false}
>
{data.map((entry, index) => {
const gradId = entry.name === "Potholes" ? "gradPothole" :
entry.name === "Defected Signboards" ? "gradDefectedSign" :
entry.name === "Road Cracks" ? "gradCrack" :
entry.name === "Damaged Markings" ? "gradMarking" : "gradGoodSign"
return (
<Cell
key={`cell-${index}`}
fill={`url(#${gradId})`}
fillOpacity={1}
className="hover:fill-opacity-80"
/>
)
})}
</Pie>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0]
return (
<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>
</p>
<p className="text-xs text-muted-foreground">
{((Number(data.value) / total) * 100).toFixed(1)}% of total
</p>
</div>
)
}
return null
}}
/>
<Legend
verticalAlign="bottom"
height={40}
content={({ payload }) => (
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mt-2">
{payload?.map((entry, index) => (
<div key={`legend-${index}`} className="flex items-center gap-1.5">
<div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
{entry.value}: {data[index].value}
</span>
</div>
))}
</div>
)}
/>
</PieChart>
</ResponsiveContainer>
{/* Center label */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none" style={{ marginTop: '-55px' }}>
<div className="text-center">
<p className="text-xl font-extrabold text-[#2563eb]">{total}</p>
<p className="text-[9px] uppercase tracking-wider text-muted-foreground">Total</p>
</div>
</div>
</div>
) )
}
if (totalDetections === 0) {
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No detections found</p>
<p className="text-xs mt-1">Process videos to see data</p>
</div>
)
}
return (
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie
data={chartData}
dataKey="count"
nameKey="type"
innerRadius={60}
strokeWidth={5}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-3xl font-bold"
>
{totalDetections.toLocaleString()}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
Total
</tspan>
</text>
)
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
)
} }

View File

@@ -1,179 +1,105 @@
"use client" "use client"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" import * as React from "react"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
interface LocationData { interface LocationData {
name: string name: string
defected_sign_board: number defected_sign_board: number
pothole: number pothole: number
road_crack: number road_crack: number
damaged_road_marking: number damaged_road_marking: number
good_sign_board: number total: number
total: number
} }
interface LocationBarChartProps { interface LocationBarChartProps {
data: LocationData[] data: LocationData[]
isLoading?: boolean isLoading?: boolean
} }
const COLORS = { const chartConfig = {
defected_sign_board: "#60a5fa", // Lighter Blue pothole: {
pothole: "#ff8a8a", // Lighter Red label: "Potholes",
road_crack: "#fbbf24", // Lighter Orange color: "var(--chart-1)",
damaged_road_marking: "#818cf8", // Lighter Indigo },
good_sign_board: "#34d399" // Lighter Emerald defected_sign_board: {
} label: "Defected Signboards",
color: "var(--chart-2)",
},
road_crack: {
label: "Road Cracks",
color: "var(--chart-3)",
},
damaged_road_marking: {
label: "Damaged Markings",
color: "var(--chart-4)",
},
} satisfies ChartConfig
export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) { export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) {
if (isLoading) { if (isLoading) {
return (
<div className="h-[300px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50" />
</div>
)
}
if (data.length === 0) {
return (
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No location data available</p>
<p className="text-xs mt-1">Process videos to see detections by location</p>
</div>
)
}
return ( return (
<div className="h-[200px]"> <div className="h-[250px] flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%"> <Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
<BarChart </div>
data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 20 }}
barCategoryGap="10%"
barGap={2}
>
<defs>
<linearGradient id="barPothole" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ff8a8a" />
<stop offset="100%" stopColor="#ef4444" />
</linearGradient>
<linearGradient id="barDefectedSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#60a5fa" />
<stop offset="100%" stopColor="#3b82f6" />
</linearGradient>
<linearGradient id="barCrack" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fbbf24" />
<stop offset="100%" stopColor="#f59e0b" />
</linearGradient>
<linearGradient id="barMarking" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#818cf8" />
<stop offset="100%" stopColor="#6366f1" />
</linearGradient>
<linearGradient id="barGoodSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#34d399" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke="hsl(var(--muted-foreground) / 0.1)"
/>
<XAxis
dataKey="name"
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false}
axisLine={false}
angle={-45}
textAnchor="end"
height={50}
interval={0}
/>
<YAxis
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false}
axisLine={false}
width={25}
/>
<Tooltip
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<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">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-muted-foreground">{(entry.name as string).replace(/_/g, ' ')}:</span>
<span className="font-semibold">{entry.value}</span>
</div>
))}
</div>
)
}
return null
}}
/>
<Legend
verticalAlign="top"
height={30}
content={({ payload }) => (
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mb-2">
{payload?.map((entry, index) => (
<div key={`legend-${index}`} className="flex items-center gap-1.5">
<div
className="w-2.5 h-2.5 rounded-sm"
style={{ backgroundColor: entry.color }}
/>
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
{(entry.value as string).replace(/_/g, ' ')}
</span>
</div>
))}
</div>
)}
/>
<Bar
dataKey="pothole"
name="Pothole"
fill="url(#barPothole)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="defected_sign_board"
name="Defected Signboard"
fill="url(#barDefectedSign)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="road_crack"
name="Road Crack"
fill="url(#barCrack)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="damaged_road_marking"
name="Damaged Marking"
fill="url(#barMarking)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="good_sign_board"
name="Good Signboard"
fill="url(#barGoodSign)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
</BarChart>
</ResponsiveContainer>
</div>
) )
}
if (data.length === 0) {
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No location data available</p>
<p className="text-xs mt-1">Process videos to see detections by location</p>
</div>
)
}
// Filter out good signboards for a more "damage-focused" standard chart as per multiple bar example
const chartData = data.map(item => ({
name: item.name,
pothole: item.pothole,
defected_sign_board: item.defected_sign_board,
road_crack: item.road_crack,
damaged_road_marking: item.damaged_road_marking,
}))
return (
<div className="h-[250px] w-full">
<ChartContainer config={chartConfig} className="h-full w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} strokeOpacity={0.1} />
<XAxis
dataKey="name"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.length > 8 ? `${value.slice(0, 8)}...` : value}
fontSize={12}
/>
<YAxis
tickLine={false}
axisLine={false}
fontSize={12}
tickMargin={10}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dashed" />}
/>
<Bar dataKey="pothole" fill="var(--color-pothole)" radius={4} />
<Bar dataKey="defected_sign_board" fill="var(--color-defected_sign_board)" radius={4} />
<Bar dataKey="road_crack" fill="var(--color-road_crack)" radius={4} />
<Bar dataKey="damaged_road_marking" fill="var(--color-damaged_road_marking)" radius={4} />
</BarChart>
</ChartContainer>
</div>
)
} }

View File

@@ -19,7 +19,7 @@ export function StatsCard({
isLoading = false isLoading = false
}: StatsCardProps) { }: StatsCardProps) {
return ( return (
<Card className="py-6 px-6"> <Card className="py-6 px-6 hover:scale-105 transition-all duration-300 ease-in-out">
<CardContent className="p-0 flex items-center justify-between gap-6"> <CardContent className="p-0 flex items-center justify-between gap-6">
<div className="flex flex-col gap-1 min-w-0"> <div className="flex flex-col gap-1 min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider"> <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">

View File

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

View File

@@ -3,15 +3,15 @@ import { SearchIcon } from "lucide-react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
const SearchBar = () => { const SearchBar = () => {
return ( return (
<div className="relative w-full"> <div className="relative w-full">
<SearchIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70" />
<Input <Input
placeholder="Search..." placeholder="Search resources..."
className="pl-8 h-9 text-sm" className="pl-9 h-10 text-sm bg-muted/20 border-border/40 focus:bg-background transition-all"
/> />
</div> </div>
); );
}; };
export default SearchBar; export default SearchBar;

View File

@@ -1,45 +1,47 @@
"use client"; "use client";
import React from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import { PlusIcon } from "lucide-react"; import { FolderPlus, Settings2, SlidersHorizontal } from "lucide-react";
import { Badge } from "@/components/ui/badge";
interface TopHeaderProps { interface TopHeaderProps {
title?: string; title?: string;
itemCount?: number; itemCount?: number;
onAddNew?: () => void; onAddNew?: () => void;
addButtonText?: string; addButtonText?: string;
} }
const TopHeader = ({ title, itemCount, onAddNew, addButtonText = "Add New" }: TopHeaderProps) => { const TopHeader = ({ title, itemCount, onAddNew, addButtonText = "Add New" }: TopHeaderProps) => {
return ( return (
<div className="flex flex-col gap-4 p-4"> <div className="flex flex-col gap-1 p-3 w-full">
<div className="flex items-center justify-between"> {/* Connected Summary Block */}
<div className="flex items-center gap-2"> <div className="w-full bg-muted/30 border border-border/50 rounded-lg p-4 flex items-center">
<h2 className="text-xl font-semibold tracking-tight">{title}</h2> <div className="flex items-center gap-2 text-muted-foreground font-semibold tracking-tight">
{itemCount !== undefined && ( <span className="text-base text-foreground/80">Total {title || "Items"} :</span>
<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"> <span className="text-primary font-bold text-lg">{itemCount || 0}</span>
{itemCount} </div>
</span> </div>
)}
<div className="flex items-center justify-between gap-4">
{/* Search Bar Hidden for now as per requirement */}
{/* <div className="flex w-full max-w-sm">
<SearchBar />
</div> */}
{/* Add New Button moved to PageHeader */}
{/* {onAddNew && (
<Button
onClick={onAddNew}
className="h-11 px-6 rounded-xl bg-primary hover:bg-primary/90 text-primary-foreground font-bold text-sm shadow-md shadow-primary/20 transition-all hover:-translate-y-0.5"
>
<FolderPlus className="mr-2 h-5 w-5" />
{addButtonText}
</Button>
)} */}
</div>
</div> </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; export default TopHeader;

View File

@@ -1,33 +1,38 @@
"use client"; "use client";
import { flexRender } from "@tanstack/react-table"; import { flexRender } from "@tanstack/react-table";
import { import {
TableHead, TableHead,
TableHeader as ShadTableHeader, TableHeader as ShadTableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Table } from "@tanstack/react-table"; import { Table } from "@tanstack/react-table";
import { ChevronDown, ChevronsUpDown } from "lucide-react";
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => { const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
return ( return (
<ShadTableHeader> <ShadTableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id} className="hover:bg-transparent border-b border-border/30">
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead key={header.id} className="h-14 px-6 text-muted-foreground border-b border-border/30 font-medium text-sm">
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : (
header.column.columnDef.header, <div className="flex items-center gap-2 group cursor-pointer select-none">
header.getContext() {flexRender(
)} header.column.columnDef.header,
</TableHead> header.getContext()
); )}
})} </div>
</TableRow> )}
))} </TableHead>
</ShadTableHeader> );
); })}
</TableRow>
))}
</ShadTableHeader>
);
}; };
export default TableHeader; export default TableHeader;

View File

@@ -1,20 +1,19 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { import {
ColumnDef, ColumnDef,
SortingState, SortingState,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
getSortedRowModel, getSortedRowModel,
getPaginationRowModel, getPaginationRowModel,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Edit3, MoreHorizontal, Trash2 } from "lucide-react";
import { Edit2, Trash2 } from "lucide-react";
import TopHeader from "./Header"; import TopHeader from "./Header";
import TableHeader from "./TableHeader"; import TableHeader from "./TableHeader";
@@ -22,165 +21,169 @@ import { TableFooter } from "./Footer";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface DataTableProps<TData, TValue> { export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
data: TData[]; data: TData[];
title?: string; title?: string;
onAddNew?: () => void; onAddNew?: () => void;
addButtonText?: string; addButtonText?: string;
isLoading?: boolean; isLoading?: boolean;
onEdit?: (item: TData) => void; onEdit?: (item: TData) => void;
onDelete?: (item: TData) => void; onDelete?: (item: TData) => void;
pagination?: { pagination?: {
skip: number; skip: number;
limit: number; limit: number;
totalItems?: number; totalItems?: number;
onPageChange: (newSkip: number) => void; onPageChange: (newSkip: number) => void;
onLimitChange: (newLimit: number) => void; onLimitChange: (newLimit: number) => void;
}; };
} }
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
columns: initialColumns, 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, data,
columns, title,
getCoreRowModel: getCoreRowModel(), onAddNew,
onRowSelectionChange: setRowSelection, addButtonText,
onSortingChange: setSorting, isLoading = false,
getSortedRowModel: getSortedRowModel(), onEdit,
getPaginationRowModel: getPaginationRowModel(), onDelete,
manualPagination: !!pagination, pagination,
pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1, }: DataTableProps<TData, TValue>) {
state: { const [rowSelection, setRowSelection] = React.useState({});
rowSelection, const [sorting, setSorting] = React.useState<SortingState>([]);
sorting,
pagination: pagination ? { const columns = React.useMemo(() => {
pageIndex: Math.floor(pagination.skip / pagination.limit), const cols: ColumnDef<TData, TValue>[] = [
pageSize: pagination.limit, ...initialColumns,
} : undefined, ];
},
onPaginationChange: (updater) => { if (onEdit || onDelete) {
if (typeof updater === 'function' && pagination) { cols.push({
const newState = updater({ id: "actions",
header: () => <div className="text-right px-4">Action</div>,
cell: ({ row }) => {
const item = row.original;
return (
<div className="flex justify-end gap-3 px-4">
{onEdit && (
<button
onClick={(e) => {
e.stopPropagation();
onEdit(item);
}}
className="flex items-center justify-center h-8 w-8 rounded-md text-primary hover:bg-foreground/10 transition-all duration-200"
>
<Edit3 className="h-4.5 w-4.5" />
</button>
)}
{onDelete && (
<button
onClick={(e) => {
e.stopPropagation();
onDelete(item);
}}
className="flex items-center justify-center h-8 w-8 rounded-md text-red-500 hover:bg-foreground/10 transition-all duration-200"
>
<Trash2 className="h-4.5 w-4.5" />
</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), pageIndex: Math.floor(pagination.skip / pagination.limit),
pageSize: pagination.limit, pageSize: pagination.limit,
}); } : undefined,
pagination.onLimitChange(newState.pageSize); },
pagination.onPageChange(newState.pageIndex * newState.pageSize); onPaginationChange: (updater) => {
} if (typeof updater === 'function' && pagination) {
}, const newState = updater({
initialState: { pageIndex: Math.floor(pagination.skip / pagination.limit),
pagination: { pageSize: pagination.limit,
pageSize: 10, });
pageIndex: 0, pagination.onLimitChange(newState.pageSize);
}, pagination.onPageChange(newState.pageIndex * newState.pageSize);
}, }
}); },
});
return ( return (
<div className="rounded-md border"> <div className="rounded-xl border border-border/50 bg-muted/20 p-1 space-y-6">
<TopHeader <div className="bg-background rounded-xl border border-border/50 overflow-hidden transition-all duration-300">
title={title} <TopHeader
itemCount={data.length} title={title}
onAddNew={onAddNew} itemCount={data.length}
addButtonText={addButtonText} onAddNew={onAddNew}
/> addButtonText={addButtonText}
/>
<div className="relative"> <div className="px-6 py-2">
<Table> <Table containerClassName="max-h-[calc(100vh-300px)] overflow-y-auto scrollbar-thin" className="border-separate border-spacing-0">
<TableHeader table={table} /> <TableHeader table={table} />
<TableBody className="bg-transparent"> <TableBody>
{isLoading ? ( {isLoading ? (
Array.from({ length: 5 }).map((_, idx) => ( Array.from({ length: 5 }).map((_, idx) => (
<TableRow key={idx}> <TableRow key={idx} className="border-b border-border/30 last:border-0 hover:bg-muted/5">
{columns.map((_, colIdx) => ( {columns.map((_, colIdx) => (
<TableCell key={colIdx} className="px-8 py-4"> <TableCell key={colIdx} className="px-6 py-6 border-b border-border/30">
<Skeleton className="h-4 w-full max-w-[120px]" /> <Skeleton className="h-4 w-full max-w-[140px] opacity-20" />
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
)) ))
) : table.getRowModel().rows?.length ? ( ) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
key={row.id} key={row.id}
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
> className="group hover:bg-muted/10 transition-colors"
{row.getVisibleCells().map((cell) => ( >
<TableCell key={cell.id}> {row.getVisibleCells().map((cell) => (
{flexRender( <TableCell key={cell.id} className="px-4 py-3 border-b border-border/50 align-middle text-muted-foreground text-sm font-medium group-hover:text-foreground">
cell.column.columnDef.cell, {flexRender(
cell.getContext() cell.column.columnDef.cell,
)} cell.getContext()
</TableCell> )}
))} </TableCell>
</TableRow> ))}
)) </TableRow>
) : ( ))
<TableRow> ) : (
<TableCell <TableRow>
colSpan={columns.length} <TableCell
className="h-32 text-center text-sm" colSpan={columns.length}
> className="h-48 text-center"
No results found. >
</TableCell> <div className="flex flex-col items-center justify-center text-muted-foreground gap-1">
</TableRow> <p className="font-bold text-sm tracking-tight">No results found.</p>
)} <p className="text-xs opacity-60 font-medium">Try adjusting your filters or search terms.</p>
</TableBody> </div>
</Table> </TableCell>
</div> </TableRow>
)}
<TableFooter table={table} /> </TableBody>
</div> </Table>
); </div>
<TableFooter table={table} />
</div>
</div>
);
} }

View File

@@ -7,23 +7,31 @@ interface PageHeaderProps {
description: string description: string
icon?: LucideIcon icon?: LucideIcon
children?: React.ReactNode children?: React.ReactNode
actions?: React.ReactNode
} }
export function PageHeader({ title, description, icon: Icon, children }: PageHeaderProps) { export function PageHeader({ title, description, icon: Icon, children, actions }: PageHeaderProps) {
return ( return (
<div className="flex items-center gap-6"> <div className="flex items-center justify-between w-full">
<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"> <div className="flex items-center gap-6">
{Icon && <Icon className="h-7 w-7" />} {/* <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">
{children} {Icon && <Icon className="h-7 w-7" />}
</div> {children}
<div className="flex flex-col"> </div> */}
<h1 className="text-3xl font-extrabold tracking-tight "> <div className="flex flex-col">
{title} <h1 className="text-3xl font-extrabold tracking-tight ">
</h1> {title}
<p className="text-muted-foreground mt-1 text-sm font-semibold tracking-wide opacity-80"> </h1>
{description} <p className="text-muted-foreground mt-1 text-sm font-semibold tracking-wide opacity-80">
</p> {description}
</p>
</div>
</div> </div>
{actions && (
<div className="flex items-center gap-4">
{actions}
</div>
)}
</div> </div>
) )
} }

View File

@@ -1,42 +1,44 @@
import * as React from 'react' import * as React from "react"
import { Slot } from '@radix-ui/react-slot' import { cva, type VariantProps } from "class-variance-authority"
import { cva, type VariantProps } from 'class-variance-authority' import { Slot } from "radix-ui"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const badgeVariants = cva( const badgeVariants = cva(
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden', "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
{ {
variants: { variants: {
variant: { variant: {
default: default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary: secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive: destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
outline: outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
link: "text-primary underline-offset-4 [a&]:hover:underline",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
}, },
}, }
) )
function Badge({ function Badge({
className, className,
variant, variant = "default",
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<'span'> & }: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) { VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span' const Comp = asChild ? Slot.Root : "span"
return ( return (
<Comp <Comp
data-slot="badge" data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)} className={cn(badgeVariants({ variant }), className)}
{...props} {...props}
/> />

View File

@@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronRight, MoreHorizontal } from "lucide-react"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -13,7 +13,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
<ol <ol
data-slot="breadcrumb-list" data-slot="breadcrumb-list"
className={cn( className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5", "flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground sm:gap-2.5",
className className
)} )}
{...props} {...props}
@@ -38,12 +38,12 @@ function BreadcrumbLink({
}: React.ComponentProps<"a"> & { }: React.ComponentProps<"a"> & {
asChild?: boolean asChild?: boolean
}) { }) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot.Root : "a"
return ( return (
<Comp <Comp
data-slot="breadcrumb-link" data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)} className={cn("transition-colors hover:text-foreground", className)}
{...props} {...props}
/> />
) )
@@ -56,7 +56,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
role="link" role="link"
aria-disabled="true" aria-disabled="true"
aria-current="page" aria-current="page"
className={cn("text-foreground font-normal", className)} className={cn("font-normal text-foreground", className)}
{...props} {...props}
/> />
) )

View File

@@ -1,9 +1,9 @@
'use client' "use client"
import * as React from 'react' import * as React from "react"
import * as PopoverPrimitive from '@radix-ui/react-popover' import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
function Popover({ function Popover({
...props ...props
@@ -19,7 +19,7 @@ function PopoverTrigger({
function PopoverContent({ function PopoverContent({
className, className,
align = 'center', align = "center",
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) { }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
@@ -30,8 +30,8 @@ function PopoverContent({
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-popover text-popover-foreground z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden', "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden 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 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",
className, className
)} )}
{...props} {...props}
/> />
@@ -45,4 +45,45 @@ function PopoverAnchor({
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} /> return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
} }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-1 text-sm", className)}
{...props}
/>
)
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<div
data-slot="popover-title"
className={cn("font-medium", className)}
{...props}
/>
)
}
function PopoverDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
)
}
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverAnchor,
PopoverHeader,
PopoverTitle,
PopoverDescription,
}

View File

@@ -1,9 +1,9 @@
'use client' "use client"
import * as React from 'react' import * as React from "react"
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
function ScrollArea({ function ScrollArea({
className, className,
@@ -13,12 +13,12 @@ function ScrollArea({
return ( return (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
data-slot="scroll-area" data-slot="scroll-area"
className={cn('relative', className)} className={cn("relative", className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:outline-1" className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
> >
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
@@ -30,7 +30,7 @@ function ScrollArea({
function ScrollBar({ function ScrollBar({
className, className,
orientation = 'vertical', orientation = "vertical",
...props ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return ( return (
@@ -38,18 +38,18 @@ function ScrollBar({
data-slot="scroll-area-scrollbar" data-slot="scroll-area-scrollbar"
orientation={orientation} orientation={orientation}
className={cn( className={cn(
'flex touch-none p-px select-none', "flex touch-none p-px transition-colors select-none",
orientation === 'vertical' && orientation === "vertical" &&
'h-full w-2.5 border-l border-l-transparent', "h-full w-2.5 border-l border-l-transparent",
orientation === 'horizontal' && orientation === "horizontal" &&
'h-2.5 flex-col border-t border-t-transparent', "h-2.5 flex-col border-t border-t-transparent",
className, className
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb <ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb" data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full" className="relative flex-1 rounded-full bg-border"
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
) )

View File

@@ -4,11 +4,15 @@ 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,
containerClassName,
...props
}: React.ComponentProps<"table"> & { containerClassName?: string }) {
return ( return (
<div <div
data-slot="table-container" data-slot="table-container"
className="relative w-full overflow-x-auto" className={cn("relative w-full overflow-x-auto", containerClassName)}
> >
<table <table
data-slot="table" data-slot="table"