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

View File

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

View File

@@ -27,6 +27,8 @@ import {
} from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
export default function LocationPage() {
const [locations, setLocations] = useState<Location[]>([])
@@ -168,7 +170,9 @@ export default function LocationPage() {
end_lng: parseFloat(endLng),
}
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 {
const data: LocationCreate = {
package_id: selectedPackageId,
@@ -181,7 +185,9 @@ export default function LocationPage() {
end_lng: parseFloat(endLng),
}
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
@@ -191,7 +197,11 @@ export default function LocationPage() {
setIsModalOpen(false)
resetForm()
} 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 {
setIsSubmitting(false)
}
@@ -224,10 +234,15 @@ export default function LocationPage() {
try {
setIsLoading(true)
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()
} catch (err) {
setError("Failed to delete location")
toast.error("Deletion Failed", {
description: "The location could not be removed. Please try again.",
})
} finally {
setIsLoading(false)
}
@@ -262,6 +277,50 @@ export default function LocationPage() {
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",
header: "Chainage (km)",
@@ -269,9 +328,10 @@ export default function LocationPage() {
const location = row.original
if (location.chainage_start_km !== null && location.chainage_end_km !== null) {
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}
</span>
</Badge>
)
}
return "—"
@@ -281,44 +341,44 @@ export default function LocationPage() {
accessorKey: "start_gps",
header: "Start GPS",
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)}
</span>
</Badge>
)
},
{
accessorKey: "end_gps",
header: "End GPS",
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)}
</span>
</Badge>
)
},
]
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 */}
<div className="mb-8">
<PageHeader
title="Location Management"
description="Manage road segment locations"
icon={MapPin}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<MapPin className="mr-2 h-4 w-4" />
Add New Location
</Button>
}
/>
</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 */}
<div>
@@ -326,10 +386,8 @@ export default function LocationPage() {
title="Locations"
data={locations}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit}
onDelete={handleDelete}
addButtonText="Add New Location"
isLoading={isLoading}
pagination={{
skip,
@@ -344,7 +402,6 @@ export default function LocationPage() {
</div>
<PoweredBy />
</div>
</main>
{/* Modal Dialog */}
@@ -365,15 +422,6 @@ export default function LocationPage() {
</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">
{!isEditing && (
@@ -607,6 +655,6 @@ export default function LocationPage() {
</form>
</DialogContent>
</Dialog>
</div>
</>
)
}

View File

@@ -23,6 +23,8 @@ import {
} from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by"
export default function PackagePage() {
@@ -108,7 +110,9 @@ export default function PackagePage() {
region: region.trim() || null,
}
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 {
const data: PackageCreate = {
project_id: selectedProjectId,
@@ -116,7 +120,9 @@ export default function PackagePage() {
region: region.trim() || null,
}
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
@@ -126,7 +132,11 @@ export default function PackagePage() {
setIsModalOpen(false)
resetForm()
} 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 {
setIsSubmitting(false)
}
@@ -147,10 +157,15 @@ export default function PackagePage() {
try {
setIsLoading(true)
await deletePackage(pkg.id)
toast.success("Package deleted successfully!")
toast.success("Package Deleted", {
description: `${pkg.name} has been removed from the system.`,
})
await loadPackages()
} catch (err) {
setError("Failed to delete package")
toast.error("Deletion Failed", {
description: "The package could not be removed. Please try again.",
})
} finally {
setIsLoading(false)
}
@@ -180,17 +195,39 @@ export default function PackagePage() {
const pkg = row.original
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 flex-wrap gap-1">
{project.state.split(',').map((item, idx) => (
<span
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"
>
{item.trim()}
</span>
<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>
)
}
},
@@ -201,27 +238,26 @@ export default function PackagePage() {
]
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 */}
<div className="mb-8">
<PageHeader
title="Package Management"
description="Manage project packages"
icon={Package}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<Package className="mr-2 h-4 w-4" />
Add New Package
</Button>
}
/>
</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 */}
<div>
@@ -229,10 +265,8 @@ export default function PackagePage() {
title="Packages"
data={packages}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit}
onDelete={handleDelete}
addButtonText="Add New Package"
isLoading={isLoading}
pagination={{
skip,
@@ -247,7 +281,6 @@ export default function PackagePage() {
</div>
<PoweredBy />
</div>
</main>
{/* Modal Dialog */}
@@ -269,15 +302,6 @@ export default function PackagePage() {
</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">
{/* Step 1: Select Project */}
@@ -371,7 +395,7 @@ export default function PackagePage() {
<Button
type="submit"
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 ? (
<div className="flex items-center gap-2">
@@ -389,6 +413,6 @@ export default function PackagePage() {
</form>
</DialogContent>
</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 { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by"
export default function ProjectPage() {
@@ -91,7 +93,9 @@ export default function ProjectPage() {
end_lng: endLng ? parseFloat(endLng) : null,
}
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 {
const data: ProjectCreate = {
name: name.trim(),
@@ -103,7 +107,9 @@ export default function ProjectPage() {
end_lng: endLng ? parseFloat(endLng) : null,
}
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
@@ -113,7 +119,11 @@ export default function ProjectPage() {
setIsModalOpen(false)
resetForm()
} 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 {
setIsSubmitting(false)
}
@@ -138,10 +148,15 @@ export default function ProjectPage() {
try {
setIsLoading(true)
await deleteProject(project.id)
toast.success("Project deleted successfully!")
toast.success("Project Deleted", {
description: `${project.name} has been removed from the system.`,
})
await loadProjects()
} catch (err) {
setError("Failed to delete project")
toast.error("Deletion Failed", {
description: "The project could not be removed. Please try again or check your permissions.",
})
} finally {
setIsLoading(false)
}
@@ -152,7 +167,7 @@ export default function ProjectPage() {
accessorKey: "name",
header: "Project Name",
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,18 +175,46 @@ export default function ProjectPage() {
header: "State",
cell: ({ row }) => {
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 (
<div className="flex flex-wrap gap-1.5">
{project.state.split(',').map((item, idx) => (
<span
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"
<div className="flex items-center gap-1.5">
<Badge
variant="secondary"
>
{item.trim()}
</span>
{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>
)
}
},
@@ -182,27 +225,26 @@ export default function ProjectPage() {
]
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 */}
<div className="mb-8">
<PageHeader
title="Project Management"
description="Manage road infrastructure projects"
icon={FolderPlus}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<FolderPlus className="mr-2 h-5 w-5" />
Add New Project
</Button>
}
/>
</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 */}
<div>
@@ -210,10 +252,8 @@ export default function ProjectPage() {
title="Projects"
data={projects}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit}
onDelete={handleDelete}
addButtonText="Add New Project"
isLoading={isLoading}
pagination={{
skip,
@@ -228,9 +268,9 @@ export default function ProjectPage() {
</div>
<PoweredBy />
</div>
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
<DialogContent
@@ -250,15 +290,6 @@ export default function ProjectPage() {
</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">
{/* Project Name */}
@@ -409,6 +440,6 @@ export default function ProjectPage() {
</form>
</DialogContent>
</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 {
@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 "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner"
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -35,6 +36,7 @@ export default function RootLayout({
disableTransitionOnChange
>
{children}
<Toaster position="top-center" />
</ThemeProvider>
</body>
</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>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarMenu>
{data.navMain.map((item) => (
<SidebarMenuItem key={item.title}>

View File

@@ -1,7 +1,15 @@
"use client"
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
import * as React from "react"
import { Loader2 } from "lucide-react"
import { Label, Pie, PieChart } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
interface DetectionDonutChartProps {
defectedSignboard: number
@@ -12,13 +20,28 @@ interface DetectionDonutChartProps {
isLoading?: boolean
}
const COLORS = {
defectedSignboard: "#3b82f6", // Blue
pothole: "#ef4444", // Red
roadCrack: "#f59e0b", // Amber/Orange
damagedRoadMarking: "#6366f1", // Indigo
goodSignboard: "#10b981" // Emerald
}
const chartConfig = {
pothole: {
label: "Potholes",
color: "var(--chart-1)",
},
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({
defectedSignboard,
@@ -26,19 +49,51 @@ export function DetectionDonutChart({
roadCrack,
damagedRoadMarking,
goodSignboard,
isLoading = false
isLoading = false,
}: DetectionDonutChartProps) {
const chartData = React.useMemo(
() =>
[
{ type: "pothole", count: pothole, fill: "var(--color-pothole)" },
{
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 totalDetections = React.useMemo(() => {
return chartData.reduce((acc, curr) => acc + curr.count, 0)
}, [chartData])
if (isLoading) {
return (
<div className="h-[250px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50" />
<Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
</div>
)
}
const total = defectedSignboard + pothole + roadCrack + damagedRoadMarking + goodSignboard
if (total === 0) {
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>
@@ -47,113 +102,55 @@ export function DetectionDonutChart({
)
}
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)
return (
<div className="h-[220px] relative">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<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}
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
{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"
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
)
})}
</Pie>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0]
<Pie
data={chartData}
dataKey="count"
nameKey="type"
innerRadius={60}
strokeWidth={5}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
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>
<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>
)
}
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>
)}
/>
</Pie>
</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>
</ChartContainer>
)
}

View File

@@ -1,15 +1,22 @@
"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 {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
interface LocationData {
name: string
defected_sign_board: number
pothole: number
road_crack: number
damaged_road_marking: number
good_sign_board: number
total: number
}
@@ -18,162 +25,81 @@ interface LocationBarChartProps {
isLoading?: boolean
}
const COLORS = {
defected_sign_board: "#60a5fa", // Lighter Blue
pothole: "#ff8a8a", // Lighter Red
road_crack: "#fbbf24", // Lighter Orange
damaged_road_marking: "#818cf8", // Lighter Indigo
good_sign_board: "#34d399" // Lighter Emerald
}
const chartConfig = {
pothole: {
label: "Potholes",
color: "var(--chart-1)",
},
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) {
if (isLoading) {
return (
<div className="h-[300px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50" />
<div className="h-[250px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
</div>
)
}
if (data.length === 0) {
return (
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground">
<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-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
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)"
/>
<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"
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false}
tickMargin={10}
axisLine={false}
angle={-45}
textAnchor="end"
height={50}
interval={0}
tickFormatter={(value) => value.length > 8 ? `${value.slice(0, 8)}...` : value}
fontSize={12}
/>
<YAxis
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false}
axisLine={false}
width={25}
fontSize={12}
tickMargin={10}
/>
<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}
<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>
</ResponsiveContainer>
</ChartContainer>
</div>
)
}

View File

@@ -19,7 +19,7 @@ export function StatsCard({
isLoading = false
}: StatsCardProps) {
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">
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">

View File

@@ -12,53 +12,58 @@ 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 justify-between px-6 py-4 border-t border-border/40 bg-muted/5">
<div className="flex items-center gap-6">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<p className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectTrigger className="h-8 w-[70px] bg-transparent border-border/40 text-xs font-semibold">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
<SelectContent side="top" className="min-w-[70px]">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
<SelectItem key={pageSize} value={`${pageSize}`} className="text-xs">
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</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 className="flex items-center gap-8">
<div className="flex items-center text-[11px] font-bold text-muted-foreground uppercase tracking-widest gap-1">
<span className="text-foreground">Page {table.getState().pagination.pageIndex + 1}</span>
<span className="opacity-40">/</span>
<span>{table.getPageCount() || 1}</span>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="h-8 w-8 p-0"
className="h-8 w-8 p-0 border-border/40 bg-transparent hover:bg-muted/50 transition-colors"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="h-4 w-4" />
<ChevronLeft className="h-4 w-4 opacity-70" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
className="h-8 w-8 p-0 border-border/40 bg-transparent hover:bg-muted/50 transition-colors"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="h-4 w-4" />
<ChevronRight className="h-4 w-4 opacity-70" />
</Button>
</div>
</div>
</div>
);
}

View File

@@ -5,10 +5,10 @@ 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" />
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70" />
<Input
placeholder="Search..."
className="pl-8 h-9 text-sm"
placeholder="Search resources..."
className="pl-9 h-10 text-sm bg-muted/20 border-border/40 focus:bg-background transition-all"
/>
</div>
);

View File

@@ -1,7 +1,9 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import SearchBar from "./SearchBar";
import { PlusIcon } from "lucide-react";
import { FolderPlus, Settings2, SlidersHorizontal } from "lucide-react";
import { Badge } from "@/components/ui/badge";
interface TopHeaderProps {
title?: string;
@@ -12,31 +14,31 @@ interface TopHeaderProps {
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 className="flex flex-col gap-1 p-3 w-full">
{/* Connected Summary Block */}
<div className="w-full bg-muted/30 border border-border/50 rounded-lg p-4 flex items-center">
<div className="flex items-center gap-2 text-muted-foreground font-semibold tracking-tight">
<span className="text-base text-foreground/80">Total {title || "Items"} :</span>
<span className="text-primary font-bold text-lg">{itemCount || 0}</span>
</div>
</div>
{onAddNew && (
<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}
size="sm"
className="flex items-center gap-2"
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"
>
<PlusIcon className="w-4 h-4" />
<FolderPlus className="mr-2 h-5 w-5" />
{addButtonText}
</Button>
)}
</div>
<div className="flex w-full max-w-sm ml-auto">
<SearchBar />
)} */}
</div>
</div>
);

View File

@@ -6,21 +6,26 @@ import {
TableRow,
} from "@/components/ui/table";
import { Table } from "@tanstack/react-table";
import { ChevronDown, ChevronsUpDown } from "lucide-react";
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
return (
<ShadTableHeader>
{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) => {
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
? null
: flexRender(
: (
<div className="flex items-center gap-2 group cursor-pointer select-none">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</div>
)}
</TableHead>
);
})}

View File

@@ -13,8 +13,7 @@ import {
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 { Edit3, MoreHorizontal, Trash2 } from "lucide-react";
import TopHeader from "./Header";
import TableHeader from "./TableHeader";
@@ -54,34 +53,39 @@ export function DataTable<TData, TValue>({
const [sorting, setSorting] = React.useState<SortingState>([]);
const columns = React.useMemo(() => {
const cols = [...initialColumns];
const cols: ColumnDef<TData, TValue>[] = [
...initialColumns,
];
if (onEdit || onDelete) {
cols.push({
id: "actions",
header: () => <div className="text-right">Actions</div>,
header: () => <div className="text-right px-4">Action</div>,
cell: ({ row }) => {
const item = row.original;
return (
<div className="flex justify-end gap-2">
<div className="flex justify-end gap-3 px-4">
{onEdit && (
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(item)}
className="h-8 w-8"
<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"
>
<Edit2 className="h-4 w-4" />
</Button>
<Edit3 className="h-4.5 w-4.5" />
</button>
)}
{onDelete && (
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(item)}
className="h-8 w-8 text-destructive hover:text-destructive"
<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 w-4" />
</Button>
<Trash2 className="h-4.5 w-4.5" />
</button>
)}
</div>
);
@@ -119,16 +123,11 @@ export function DataTable<TData, TValue>({
pagination.onPageChange(newState.pageIndex * newState.pageSize);
}
},
initialState: {
pagination: {
pageSize: 10,
pageIndex: 0,
},
},
});
return (
<div className="rounded-md border">
<div className="rounded-xl border border-border/50 bg-muted/20 p-1 space-y-6">
<div className="bg-background rounded-xl border border-border/50 overflow-hidden transition-all duration-300">
<TopHeader
title={title}
itemCount={data.length}
@@ -136,16 +135,16 @@ export function DataTable<TData, TValue>({
addButtonText={addButtonText}
/>
<div className="relative">
<Table>
<div className="px-6 py-2">
<Table containerClassName="max-h-[calc(100vh-300px)] overflow-y-auto scrollbar-thin" className="border-separate border-spacing-0">
<TableHeader table={table} />
<TableBody className="bg-transparent">
<TableBody>
{isLoading ? (
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) => (
<TableCell key={colIdx} className="px-8 py-4">
<Skeleton className="h-4 w-full max-w-[120px]" />
<TableCell key={colIdx} className="px-6 py-6 border-b border-border/30">
<Skeleton className="h-4 w-full max-w-[140px] opacity-20" />
</TableCell>
))}
</TableRow>
@@ -155,9 +154,10 @@ export function DataTable<TData, TValue>({
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="group hover:bg-muted/10 transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<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">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
@@ -170,17 +170,20 @@ export function DataTable<TData, TValue>({
<TableRow>
<TableCell
colSpan={columns.length}
className="h-32 text-center text-sm"
className="h-48 text-center"
>
No results found.
<div className="flex flex-col items-center justify-center text-muted-foreground gap-1">
<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>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<TableFooter table={table} />
</div>
</div>
);
}

View File

@@ -7,15 +7,17 @@ interface PageHeaderProps {
description: string
icon?: LucideIcon
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 (
<div className="flex items-center justify-between w-full">
<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">
{/* <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> */}
<div className="flex flex-col">
<h1 className="text-3xl font-extrabold tracking-tight ">
{title}
@@ -25,5 +27,11 @@ export function PageHeader({ title, description, icon: Icon, children }: PageHea
</p>
</div>
</div>
{actions && (
<div className="flex items-center gap-4">
{actions}
</div>
)}
</div>
)
}

View File

@@ -1,42 +1,44 @@
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 { 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(
'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: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
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:
'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: {
variant: 'default',
},
variant: "default",
},
}
)
function Badge({
className,
variant,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<'span'> &
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span'
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>

View File

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

View File

@@ -1,9 +1,9 @@
'use client'
"use client"
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import * as React from "react"
import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function Popover({
...props
@@ -19,7 +19,7 @@ function PopoverTrigger({
function PopoverContent({
className,
align = 'center',
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
@@ -30,8 +30,8 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
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',
className,
"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
)}
{...props}
/>
@@ -45,4 +45,45 @@ function PopoverAnchor({
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 ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function ScrollArea({
className,
@@ -13,12 +13,12 @@ function ScrollArea({
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn('relative', className)}
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.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}
</ScrollAreaPrimitive.Viewport>
@@ -30,7 +30,7 @@ function ScrollArea({
function ScrollBar({
className,
orientation = 'vertical',
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
@@ -38,18 +38,18 @@ function ScrollBar({
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
'flex touch-none p-px select-none',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent',
className,
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)

View File

@@ -4,11 +4,15 @@ import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
function Table({
className,
containerClassName,
...props
}: React.ComponentProps<"table"> & { containerClassName?: string }) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
className={cn("relative w-full overflow-x-auto", containerClassName)}
>
<table
data-slot="table"