refactor: refactor the color schema

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

View File

@@ -12,8 +12,7 @@ import {
PencilLine,
CheckCircle2,
} from "lucide-react";
import { SidebarNavigation } from "@/components/sidebar-navigation";
import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card";
import { StatsCard } from "@/components/dashboard/stats-card";
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector";
import { FilterSelector } from "@/components/dashboard/filter-selector";
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart";
@@ -424,54 +423,33 @@ export default function DashboardPage() {
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Main Content - offset by sidebar width */}
<main className="ml-20 min-h-screen">
{/* Main Content */}
<main className="min-h-screen">
<div className="p-4 px-8 max-w-340 mx-auto">
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<PageHeader
title="Dashboard"
description="A Comprehensive Overview Of Your Road Infrastructure Analysis"
>
<svg
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8 text-white relative z-10"
>
<path
d="M50 20L85 80H15L50 20Z"
stroke="currentColor"
strokeWidth="6"
strokeLinejoin="round"
/>
<path
d="M40 80L50 55L60 80"
stroke="currentColor"
strokeWidth="6"
/>
</svg>
</PageHeader>
icon={BarChart3}
/>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="bg-white/70 backdrop-blur-md shadow-none rounded-md hover:bg-white/50 border-none transition-all duration-300 gap-2 font-semibold">
<Button variant="outline" className="gap-2 font-semibold">
<Filter className="h-4 w-4" />
<span>Filters</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-0 border-none shadow-2xl rounded-md overflow-hidden" align="end">
<div className="bg-linear-to-b from-[#225999] to-[#56A5FF] p-4 text-white">
<h3 className="font-bold text-sm flex items-center gap-2">
<Filter className="h-4 w-4" />
<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" />
Filter Analysis
</h3>
<p className="text-xs text-blue-100 mt-1 opacity-80">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>
</div>
<div className="p-1 bg-white">
<div>
<FilterSelector
projects={projects}
selectedProjectId={selectedProjectId}
@@ -503,60 +481,54 @@ export default function DashboardPage() {
<>
{/* Stats Cards - Top Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<GradientStatsCard
<StatsCard
title="Total Road Damage"
subtitle="Combined damage detections"
value={stats.totalRoadDamage}
icon={Activity}
gradient="purple"
/>
<GradientStatsCard
<StatsCard
title="Potholes"
subtitle="Surface depressions"
value={stats.totalPothole}
icon={AlertCircle}
gradient="green"
/>
<GradientStatsCard
<StatsCard
title="Defected Signboards"
subtitle="Damaged traffic signs"
value={stats.totalDefectedSignboard}
icon={AlertTriangle}
gradient="blue"
/>
<GradientStatsCard
<StatsCard
title="Road Cracks"
subtitle="Surface fissures"
value={stats.totalRoadCrack}
icon={Zap}
gradient="orange"
/>
<GradientStatsCard
<StatsCard
title="Damaged Markings"
subtitle="Worn road lines"
value={stats.totalDamagedRoadMarking}
icon={PencilLine}
gradient="indigo"
/>
<GradientStatsCard
<StatsCard
title="Good Signboards"
subtitle="Informational markers"
value={stats.totalGoodSignboard}
icon={CheckCircle2}
gradient="emerald"
/>
</div>
{/* Charts Row - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{/* Left Chart - Detection Distribution */}
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden">
<CardHeader className="pb-2 border-none">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold flex items-center gap-2">
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
<BarChart3 className="h-5 w-5 text-white" />
<div className="p-2 rounded-md bg-secondary text-secondary-foreground">
<BarChart3 className="h-5 w-5" />
</div>
<span className="heading-blue-gradient text-xl">
<span className="text-xl">
Detection Distribution
</span>
</CardTitle>
@@ -573,13 +545,13 @@ export default function DashboardPage() {
</Card>
{/* Right Chart - Location Bar Chart */}
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold flex items-center gap-2">
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
<MapPinIcon className="h-5 w-5 text-white" />
<div className="p-2 rounded-md bg-secondary text-secondary-foreground">
<MapPinIcon className="h-5 w-5" />
</div>
<span className="heading-blue-gradient text-xl">Detections by Location</span>
<span className="text-xl">Detections by Location</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-4">

View File

@@ -0,0 +1,25 @@
import { AppSidebar } from "@/components/app-sidebar";
import { ModeToggle } from "@/components/mode-toogle";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import React from "react";
const ModulesLayout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
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">
<SidebarTrigger />
<ModeToggle />
</div>
<div className="flex-1 py-2 overflow-auto">{children}</div>
</main>
</SidebarProvider>
);
};
export default ModulesLayout;

View File

@@ -8,7 +8,6 @@ import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Loader2, CheckCircle2, MapPin, Navigation, Milestone } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
@@ -26,6 +25,7 @@ import {
type LocationCreate,
type LocationUpdate
} from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
export default function LocationPage() {
@@ -239,32 +239,34 @@ export default function LocationPage() {
const isFormComplete = selectedPackageId && segmentName.trim() && startLat && startLng && endLat && endLng
const columns = [
const columns: ColumnDef<Location>[] = [
{
key: "segment_name",
accessorKey: "segment_name",
header: "Segment Name",
render: (location: Location) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{location.segment_name}</div>
cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.segment_name}</div>
)
},
{
key: "package_id",
accessorKey: "package_id",
header: "Package",
render: (location: Location) => getPackageName(location.package_id)
cell: ({ row }) => getPackageName(row.original.package_id)
},
{
key: "project",
accessorKey: "project",
header: "Project",
render: (location: Location) => {
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)
return project?.name || "—"
}
},
{
key: "chainage",
accessorKey: "chainage",
header: "Chainage (km)",
render: (location: Location) => {
cell: ({ row }) => {
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">
@@ -276,20 +278,20 @@ export default function LocationPage() {
}
},
{
key: "start_gps",
accessorKey: "start_gps",
header: "Start GPS",
render: (location: Location) => (
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">
{location.start_lat.toFixed(4)}, {location.start_lng.toFixed(4)}
{row.original.start_lat.toFixed(4)}, {row.original.start_lng.toFixed(4)}
</span>
)
},
{
key: "end_gps",
accessorKey: "end_gps",
header: "End GPS",
render: (location: Location) => (
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">
{location.end_lat.toFixed(4)}, {location.end_lng.toFixed(4)}
{row.original.end_lat.toFixed(4)}, {row.original.end_lng.toFixed(4)}
</span>
)
},
@@ -297,8 +299,7 @@ export default function LocationPage() {
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-20 min-h-screen relative overflow-hidden">
<main className="min-h-screen relative overflow-hidden">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
{/* Refined Header */}
<div className="mb-8">
@@ -322,7 +323,7 @@ export default function LocationPage() {
{/* Data Table */}
<div>
<DataTable
title="All Locations"
title="Locations"
data={locations}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
@@ -349,16 +350,17 @@ export default function LocationPage() {
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
<DialogContent
className="max-w-2xl"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MapPin className="h-5 w-5 text-blue-500" />
{isEditing ? 'Edit Location' : 'Create New Location'}
<DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3 text-xl">
<div className="p-2 rounded-lg bg-primary text-primary-foreground shadow-sm">
<MapPin className="h-5 w-5" />
</div>
{isEditing ? 'Edit Location Details' : 'Create New Location'}
</DialogTitle>
<DialogDescription>
{isEditing ? 'Update disclosure details for your road infrastructure segment' : 'Select project & package, then fill in the location details'}
<DialogDescription className="text-sm">
{isEditing ? 'Update the technical specifications for your road segment location.' : 'Select a project & package, then provide the essential location data.'}
</DialogDescription>
</DialogHeader>
@@ -374,97 +376,97 @@ export default function LocationPage() {
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Step 1: Select Project */}
{!isEditing && (
<div className="p-3 rounded-xl bg-blue-50/80 dark:bg-blue-900/40 border border-blue-200 dark:border-blue-800 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<div className="w-5 h-5 rounded-full bg-blue-600 flex items-center justify-center">
<span className="text-white text-[10px] font-bold">1</span>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Step 1: Select Project */}
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="w-5 h-5 rounded-full bg-primary/10 text-primary flex items-center justify-center text-[10px] font-bold uppercase">
01
</div>
<p className="text-[11px] font-black text-muted-foreground uppercase tracking-widest">Select Project</p>
</div>
<p className="text-xs font-bold text-blue-700 dark:text-blue-400">Select Project</p>
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
<SelectTrigger className="h-11 bg-muted/10 border-border/40 focus:ring-primary/20">
{loadingProjects ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-muted-foreground text-xs">Loading...</span>
</div>
) : (
<SelectValue placeholder="Choose a project..." />
)}
</SelectTrigger>
<SelectContent>
{projects.map(project => (
<SelectItem key={project.id} value={project.id} className="py-2.5">
<span className="font-semibold">{project.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
<SelectTrigger className="h-11 bg-white dark:bg-gray-800 border-blue-200 dark:border-blue-800 focus:border-blue-400">
{loadingProjects ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
<span className="text-gray-400">Loading projects...</span>
</div>
) : (
<SelectValue placeholder="Choose a project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map(project => (
<SelectItem key={project.id} value={project.id}>
<span className="font-medium">{project.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Step 2: Select Package */}
{!isEditing && (
<div className={`p-3 rounded-xl bg-blue-50/80 dark:bg-blue-900/40 border border-blue-200 dark:border-blue-800 shadow-sm ${selectedProjectId ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
<div className="flex items-center gap-2 mb-2">
<div className={`w-5 h-5 rounded-full flex items-center justify-center ${selectedProjectId ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}`}>
<span className="text-white text-[10px] font-bold">2</span>
{/* Step 2: Select Package */}
<div className={`space-y-3 ${selectedProjectId ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
<div className="flex items-center gap-2.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold uppercase ${selectedProjectId ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
02
</div>
<p className="text-[11px] font-black text-muted-foreground uppercase tracking-widest">Select Package</p>
</div>
<p className="text-xs font-bold text-blue-700 dark:text-blue-400">Select Package</p>
<Select value={selectedPackageId} onValueChange={setSelectedPackageId} disabled={!selectedProjectId}>
<SelectTrigger className="h-11 bg-muted/10 border-border/40 focus:ring-primary/20">
{loadingPackages ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-muted-foreground text-xs">Loading...</span>
</div>
) : (
<SelectValue placeholder={selectedProjectId ? "Choose a package..." : "Select project first"} />
)}
</SelectTrigger>
<SelectContent>
{packages.map(pkg => (
<SelectItem key={pkg.id} value={pkg.id} className="py-2.5">
<span className="font-semibold">{pkg.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select value={selectedPackageId} onValueChange={setSelectedPackageId} disabled={!selectedProjectId}>
<SelectTrigger className="h-11 bg-white dark:bg-gray-800 border-blue-200 dark:border-blue-800 focus:border-blue-400">
{loadingPackages ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
<span className="text-gray-400">Loading packages...</span>
</div>
) : (
<SelectValue placeholder={selectedProjectId ? "Choose a package" : "Select project first"} />
)}
</SelectTrigger>
<SelectContent>
{packages.map(pkg => (
<SelectItem key={pkg.id} value={pkg.id}>
<span className="font-medium">{pkg.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Step 3: Location Details */}
<div className={`space-y-4 ${selectedPackageId || isEditing ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
{!isEditing && (
<div className="flex items-center gap-2">
<div className={`w-5 h-5 rounded-full flex items-center justify-center ${selectedPackageId ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}`}>
<span className="text-white text-[10px] font-bold">3</span>
<div className="flex items-center gap-2.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold uppercase ${selectedPackageId ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
03
</div>
<p className="text-xs font-bold text-gray-700 dark:text-gray-300">Location Information</p>
<p className="text-[11px] font-black text-muted-foreground uppercase tracking-widest">Location Information</p>
</div>
)}
{/* Segment Name & Chainage Row */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="segment" className="text-xs font-semibold flex items-center gap-1">
Segment Name <span className="text-red-500">*</span>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-2">
<Label htmlFor="segment" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Segment Name <span className="text-destructive">*</span>
</Label>
<Input
id="segment"
value={segmentName}
onChange={(e) => setSegmentName(e.target.value)}
placeholder="Location to Location"
className="h-9 text-sm"
placeholder="e.g. Mumbai to Pune"
className="h-11 bg-muted/20 border-border/60"
required
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1.5">
<Label htmlFor="ch-start" className="text-[10px] font-semibold flex items-center gap-1">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="ch-start" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Start (km)
</Label>
<Input
@@ -474,12 +476,12 @@ export default function LocationPage() {
min="0"
value={chainageStartKm}
onChange={(e) => setChainageStartKm(e.target.value)}
placeholder="0"
className="h-9 text-sm"
placeholder="0.0"
className="h-11 bg-muted/20 border-border/60 text-sm font-mono"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="ch-end" className="text-[10px] font-semibold flex items-center gap-1">
<div className="space-y-2">
<Label htmlFor="ch-end" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
End (km)
</Label>
<Input
@@ -489,44 +491,44 @@ export default function LocationPage() {
min="0"
value={chainageEndKm}
onChange={(e) => setChainageEndKm(e.target.value)}
placeholder="0"
className="h-9 text-sm"
placeholder="1.0"
className="h-11 bg-muted/20 border-border/60 text-sm font-mono"
/>
</div>
</div>
</div>
{/* GPS Coordinates Grid */}
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-2 gap-8 pt-4">
{/* Start Point */}
<div className="p-3 rounded-xl bg-blue-50/50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-900/50">
<p className="text-[10px] font-bold text-blue-600 dark:text-blue-400 mb-2 uppercase tracking-wide flex items-center gap-1">
<MapPin className="h-3 w-3" /> Start Point
<div className="space-y-4">
<p className="text-[11px] font-black text-muted-foreground tracking-widest flex items-center gap-2">
<MapPin className="h-3.5 w-3.5 opacity-60" /> START POINT
</p>
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="s-lat" className="text-[10px] text-gray-500 w-8">Lat</Label>
<div className="grid grid-cols-1 gap-3">
<div className="flex items-center gap-3">
<Label htmlFor="s-lat" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70 w-8">Lat</Label>
<Input
id="s-lat"
type="number"
step="any"
value={startLat}
onChange={(e) => setStartLat(e.target.value)}
placeholder="Lat"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
required
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="s-lng" className="text-[10px] text-gray-500 w-8">Lng</Label>
<div className="flex items-center gap-3">
<Label htmlFor="s-lng" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70 w-8">Lng</Label>
<Input
id="s-lng"
type="number"
step="any"
value={startLng}
onChange={(e) => setStartLng(e.target.value)}
placeholder="Lng"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
required
/>
</div>
@@ -534,34 +536,34 @@ export default function LocationPage() {
</div>
{/* End Point */}
<div className="p-3 rounded-xl bg-blue-50/50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-900/50">
<p className="text-[10px] font-bold text-blue-600 dark:text-blue-400 mb-2 uppercase tracking-wide flex items-center gap-1">
<MapPin className="h-3 w-3" /> End Point
<div className="space-y-4">
<p className="text-[11px] font-black text-muted-foreground tracking-widest flex items-center gap-2">
<MapPin className="h-3.5 w-3.5 opacity-60" /> END POINT
</p>
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="e-lat" className="text-[10px] text-gray-500 w-8">Lat</Label>
<div className="grid grid-cols-1 gap-3">
<div className="flex items-center gap-3">
<Label htmlFor="e-lat" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70 w-8">Lat</Label>
<Input
id="e-lat"
type="number"
step="any"
value={endLat}
onChange={(e) => setEndLat(e.target.value)}
placeholder="Lat"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
required
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="e-lng" className="text-[10px] text-gray-500 w-8">Lng</Label>
<div className="flex items-center gap-3">
<Label htmlFor="e-lng" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70 w-8">Lng</Label>
<Input
id="e-lng"
type="number"
step="any"
value={endLng}
onChange={(e) => setEndLng(e.target.value)}
placeholder="Lng"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
required
/>
</div>
@@ -571,7 +573,7 @@ export default function LocationPage() {
</div>
{/* Submit Button */}
<div className="flex gap-3">
<div className="flex gap-4 pt-4">
<Button
type="button"
variant="outline"
@@ -580,25 +582,25 @@ export default function LocationPage() {
resetForm()
}}
disabled={isSubmitting}
className="flex-1"
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs border-border/80 hover:bg-muted/50"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !isFormComplete}
className="flex-1 btn-blue-gradient"
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{isEditing ? 'Updating...' : 'Creating...'}
</span>
<span>{isEditing ? 'Updating...' : 'Creating...'}</span>
</div>
) : (
<span className="flex items-center gap-2">
<div className="flex items-center gap-2">
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <MapPin className="h-4 w-4" />}
{isEditing ? 'Update Location' : 'Create Location'}
</span>
<span>{isEditing ? 'Update Location' : 'Create Location'}</span>
</div>
)}
</Button>
</div>

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation"
import dynamic from "next/dynamic"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { type SessionContext, saveSession } from "@/lib/api"
import { TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header"
@@ -25,14 +24,11 @@ export default function NewAnalysisPage() {
}
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */}
<SidebarNavigation />
<div className="min-h-screen">
{/* Main Content */}
<main className="ml-20 min-h-screen relative overflow-hidden">
<main className="min-h-screen">
<div className="container mx-auto px-6 py-10 max-w-340 relative z-10">
<div className="container mx-auto px-6 py-10 max-w-340">
{/* Refined Left-Aligned Header */}
<div className="mb-8">
<PageHeader

View File

@@ -8,7 +8,6 @@ import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Loader2, CheckCircle2, Package, FolderKanban, Globe } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header"
import {
@@ -22,6 +21,7 @@ import {
type PackageCreate,
type PackageUpdate
} from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { PoweredBy } from "@/components/powered-by"
@@ -160,23 +160,24 @@ export default function PackagePage() {
return projects.find(p => p.id === projectId)?.name || projectId
}
const columns = [
const columns: ColumnDef<PackageType>[] = [
{
key: "name",
accessorKey: "name",
header: "Package Name",
render: (pkg: PackageType) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{pkg.name}</div>
cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.name}</div>
)
},
{
key: "project_id",
accessorKey: "project_id",
header: "Project",
render: (pkg: PackageType) => getProjectName(pkg.project_id)
cell: ({ row }) => getProjectName(row.original.project_id)
},
{
key: "project_state",
accessorKey: "project_state",
header: "Project State",
render: (pkg: PackageType) => {
cell: ({ row }) => {
const pkg = row.original
const project = projects.find(p => p.id === pkg.project_id)
if (!project?.state) return <span className="text-gray-400"></span>
return (
@@ -194,15 +195,14 @@ export default function PackagePage() {
}
},
{
key: "region",
accessorKey: "region",
header: "Region",
},
]
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-20 min-h-screen relative overflow-hidden">
<main className="min-h-screen relative overflow-hidden">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
{/* Refined Header */}
<div className="mb-8">
@@ -226,7 +226,7 @@ export default function PackagePage() {
{/* Data Table */}
<div>
<DataTable
title="All Packages"
title="Packages"
data={packages}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
@@ -256,13 +256,15 @@ export default function PackagePage() {
className="max-w-2xl"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package className="h-5 w-5 text-blue-500" />
{isEditing ? 'Edit Package' : 'Create New Package'}
<DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3 text-xl">
<div className="p-2 rounded-lg bg-primary text-primary-foreground shadow-sm">
<Package className="h-5 w-5" />
</div>
{isEditing ? 'Edit Package Details' : 'Create New Package'}
</DialogTitle>
<DialogDescription>
{isEditing ? 'Update disclosure details for your road infrastructure package' : 'Select a project and fill in the package details'}
<DialogDescription className="text-sm">
{isEditing ? 'Update the technical specifications for your road infrastructure package.' : 'Select a project and provide the essential data to establish a new package.'}
</DialogDescription>
</DialogHeader>
@@ -280,28 +282,28 @@ export default function PackagePage() {
<form onSubmit={handleSubmit} className="space-y-8">
{/* Step 1: Select Project */}
{!isEditing && (
<div className="p-4 rounded-xl bg-blue-50/80 dark:bg-blue-900/40 border border-blue-200 dark:border-blue-800 shadow-sm">
<div className="flex items-center gap-2 mb-3">
<div className="w-6 h-6 rounded-full bg-blue-600 flex items-center justify-center">
<span className="text-white text-xs font-bold">1</span>
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center text-[10px] font-bold uppercase">
01
</div>
<p className="text-sm font-bold text-blue-700 dark:text-blue-400">Select Project</p>
<p className="text-[11px] font-black text-muted-foreground uppercase tracking-widest">Select Parent Project</p>
</div>
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
<SelectTrigger className="h-11 bg-white dark:bg-gray-800 border-blue-200 dark:border-blue-800 focus:border-blue-400">
<SelectTrigger className="h-11 bg-muted/10 border-border/40 focus:ring-primary/20">
{loadingProjects ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
<span className="text-gray-400">Loading projects...</span>
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-muted-foreground text-sm">Loading...</span>
</div>
) : (
<SelectValue placeholder="Choose a project" />
<SelectValue placeholder="Choose a project..." />
)}
</SelectTrigger>
<SelectContent>
{projects.map(project => (
<SelectItem key={project.id} value={project.id}>
<span className="font-medium">{project.name}</span>
<SelectItem key={project.id} value={project.id} className="py-3">
<span className="font-semibold">{project.name}</span>
</SelectItem>
))}
</SelectContent>
@@ -313,47 +315,47 @@ export default function PackagePage() {
{/* Step 2: Package Info */}
<div className={`space-y-4 ${selectedProjectId || isEditing ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
{!isEditing && (
<div className="flex items-center gap-2">
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${selectedProjectId ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}`}>
<span className="text-white text-xs font-bold">2</span>
<div className="flex items-center gap-2.5">
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold uppercase ${selectedProjectId ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
02
</div>
<p className="text-sm font-bold text-gray-700 dark:text-gray-300">Package Information</p>
<p className="text-[11px] font-black text-muted-foreground uppercase tracking-widest">Package Information</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-2">
<Label htmlFor="pkg-name" className="text-sm font-semibold flex items-center gap-1">
Package Name <span className="text-red-500">*</span>
<Label htmlFor="pkg-name" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Package Name <span className="text-destructive">*</span>
</Label>
<Input
id="pkg-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Package Name"
className="h-10 text-sm"
placeholder="e.g. Package 01"
className="h-11 bg-muted/20 border-border/60"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="region" className="text-sm font-semibold flex items-center gap-1">
<Globe className="h-3.5 w-3.5 text-gray-400" />
Region <span className="text-gray-400 font-normal text-xs">(Optional)</span>
<Label htmlFor="region" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider flex items-center gap-2">
<Globe className="h-3.5 w-3.5 opacity-60" />
Region <span className="text-[10px] lowercase font-normal opacity-70">(Optional)</span>
</Label>
<Input
id="region"
value={region}
onChange={(e) => setRegion(e.target.value)}
placeholder="Region"
className="h-10 text-sm focus-visible:ring-blue-500"
placeholder="e.g. North Zone"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-3">
<div className="flex gap-4 pt-4">
<Button
type="button"
variant="outline"
@@ -362,25 +364,25 @@ export default function PackagePage() {
resetForm()
}}
disabled={isSubmitting}
className="flex-1"
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs border-border/80 hover:bg-muted/50"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !name.trim() || !selectedProjectId}
className="flex-1 btn-blue-gradient text-white"
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{isEditing ? 'Updating...' : 'Creating...'}
</span>
<span>{isEditing ? 'Updating...' : 'Creating...'}</span>
</div>
) : (
<span className="flex items-center gap-2">
<div className="flex items-center gap-2">
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <Package className="h-4 w-4" />}
{isEditing ? 'Update Package' : 'Create Package'}
</span>
<span>{isEditing ? 'Update Package' : 'Create Package'}</span>
</div>
)}
</Button>
</div>

View File

@@ -7,10 +7,10 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Loader2, CheckCircle2, FolderPlus, MapPin, Building2, Route, X } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { DataTable } from "@/components/data-table"
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 { PoweredBy } from "@/components/powered-by"
@@ -147,18 +147,19 @@ export default function ProjectPage() {
}
}
const columns = [
const columns: ColumnDef<Project>[] = [
{
key: "name",
accessorKey: "name",
header: "Project Name",
render: (project: Project) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{project.name}</div>
cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.name}</div>
)
},
{
key: "state",
accessorKey: "state",
header: "State",
render: (project: Project) => {
cell: ({ row }) => {
const project = row.original
if (!project.state) return <span className="text-gray-400"></span>
return (
<div className="flex flex-wrap gap-1.5">
@@ -175,15 +176,14 @@ export default function ProjectPage() {
}
},
{
key: "corridor_name",
accessorKey: "corridor_name",
header: "Corridor",
},
]
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-20 min-h-screen relative overflow-hidden">
<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">
{/* Refined Header */}
<div className="mb-8">
@@ -207,7 +207,7 @@ export default function ProjectPage() {
{/* Data Table */}
<div>
<DataTable
title="All Projects"
title="Projects"
data={projects}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
@@ -237,13 +237,15 @@ export default function ProjectPage() {
className="max-w-2xl"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderPlus className="h-5 w-5 text-blue-500" />
{isEditing ? 'Edit Project' : 'Create New Project'}
<DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3 text-xl">
<div className="p-2 rounded-lg bg-primary text-primary-foreground shadow-sm">
<FolderPlus className="h-5 w-5" />
</div>
{isEditing ? 'Edit Project Details' : 'Create New Project'}
</DialogTitle>
<DialogDescription>
{isEditing ? 'Update disclosure details for your road infrastructure project' : 'Fill in the details for your new road infrastructure project'}
<DialogDescription className="text-sm">
{isEditing ? 'Update the technical specifications for your road infrastructure project.' : 'Provide the essential road data to establish a new analysis project.'}
</DialogDescription>
</DialogHeader>
@@ -260,112 +262,112 @@ export default function ProjectPage() {
<form onSubmit={handleSubmit} className="space-y-6">
{/* Project Name */}
<div className="space-y-1.5">
<Label htmlFor="name" className="text-sm font-semibold flex items-center gap-1">
Project Name <span className="text-red-500">*</span>
<div className="space-y-2">
<Label htmlFor="name" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Project Name <span className="text-destructive">*</span>
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Project Name"
className="h-10 text-sm"
placeholder="Enter a descriptive project name..."
className="h-11 bg-muted/20 border-border/60"
required
/>
</div>
{/* State & Corridor Row */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="state" className="text-sm font-semibold">
State <span className="text-gray-400 font-normal text-xs">(Optional)</span>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-2">
<Label htmlFor="state" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
State <span className="text-[10px] lowercase font-normal opacity-70">(Optional)</span>
</Label>
<Input
id="state"
value={state}
onChange={(e) => setState(e.target.value)}
placeholder="State"
className="h-10 text-sm"
placeholder="e.g. Maharashtra"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="corridor" className="text-sm font-semibold flex items-center gap-1">
<Route className="h-3.5 w-3.5 text-gray-400" />
Corridor Name <span className="text-gray-400 font-normal text-xs">(Optional)</span>
<div className="space-y-2">
<Label htmlFor="corridor" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider flex items-center gap-2">
<Route className="h-3.5 w-3.5 opacity-60" />
Corridor Name <span className="text-[10px] lowercase font-normal opacity-70">(Optional)</span>
</Label>
<Input
id="corridor"
value={corridorName}
onChange={(e) => setCorridorName(e.target.value)}
placeholder="Corridor Name"
className="h-10 text-sm"
placeholder="e.g. Mumbai-Goa Highway"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
</div>
{/* GPS Coordinates Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-2">
{/* Start Point */}
<div className="p-3 rounded-xl bg-blue-50/80 dark:bg-blue-900/40 border border-blue-200 dark:border-blue-800 shadow-sm">
<p className="text-[10px] font-bold text-blue-600 dark:text-blue-400 mb-2 uppercase tracking-wide flex items-center gap-1">
<MapPin className="h-3 w-3" /> Start Point
<div className="space-y-4">
<p className="text-[11px] font-black text-muted-foreground tracking-widest flex items-center gap-2">
<MapPin className="h-3.5 w-3.5 opacity-60" /> START POINT
</p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label htmlFor="start-lat" className="text-[10px] text-gray-500">Lat</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="start-lat" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70">Lat</Label>
<Input
id="start-lat"
type="number"
step="any"
value={startLat}
onChange={(e) => setStartLat(e.target.value)}
placeholder="Lat"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
<div className="space-y-1">
<Label htmlFor="start-lng" className="text-[10px] text-gray-500">Lng</Label>
<div className="space-y-2">
<Label htmlFor="start-lng" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70">Lng</Label>
<Input
id="start-lng"
type="number"
step="any"
value={startLng}
onChange={(e) => setStartLng(e.target.value)}
placeholder="Lng"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
</div>
</div>
{/* End Point */}
<div className="p-3 rounded-xl bg-blue-50/80 dark:bg-blue-900/40 border border-blue-200 dark:border-blue-800 shadow-sm">
<p className="text-[10px] font-bold text-blue-600 dark:text-blue-400 mb-2 uppercase tracking-wide flex items-center gap-1">
<MapPin className="h-3 w-3" /> End Point
<div className="space-y-4">
<p className="text-[11px] font-black text-muted-foreground tracking-widest flex items-center gap-2">
<MapPin className="h-3.5 w-3.5 opacity-60" /> END POINT
</p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label htmlFor="end-lat" className="text-[10px] text-gray-500">Lat</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="end-lat" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70">Lat</Label>
<Input
id="end-lat"
type="number"
step="any"
value={endLat}
onChange={(e) => setEndLat(e.target.value)}
placeholder="Lat"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
<div className="space-y-1">
<Label htmlFor="end-lng" className="text-[10px] text-gray-500">Lng</Label>
<div className="space-y-2">
<Label htmlFor="end-lng" className="text-[10px] font-bold text-muted-foreground uppercase opacity-70">Lng</Label>
<Input
id="end-lng"
type="number"
step="any"
value={endLng}
onChange={(e) => setEndLng(e.target.value)}
placeholder="Lng"
className="h-8 text-[11px]"
placeholder="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
</div>
@@ -373,7 +375,7 @@ export default function ProjectPage() {
</div>
{/* Submit Button */}
<div className="flex gap-3">
<div className="flex gap-4 pt-2">
<Button
type="button"
variant="outline"
@@ -382,25 +384,25 @@ export default function ProjectPage() {
resetForm()
}}
disabled={isSubmitting}
className="flex-1"
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs border-border/80 hover:bg-muted/50"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !name.trim()}
className="flex-1 bg-linear-to-r from-blue-500 via-indigo-500 to-blue-700 hover:bg-blue-600 text-white"
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{isEditing ? 'Updating...' : 'Creating...'}
</span>
<span>{isEditing ? 'Updating...' : 'Creating...'}</span>
</div>
) : (
<span className="flex items-center gap-2">
<div className="flex items-center gap-2">
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <FolderPlus className="h-4 w-4" />}
{isEditing ? 'Update Project' : 'Create Project'}
</span>
<span>{isEditing ? 'Update Project' : 'Create Project'}</span>
</div>
)}
</Button>
</div>

View File

@@ -5,7 +5,6 @@ import { useRouter, useParams } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Loader2, TrendingUp } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
@@ -16,6 +15,7 @@ import {
import { getVideoFile, clearVideoFile } from "@/lib/video-storage"
import { DetectionData, DetectionType } from "@/lib/types"
import { ROUTES } from "@/utils/routes"
import { Card } from "@/components/ui/card"
const API_URL = process.env.NEXT_PUBLIC_API_URL
@@ -95,10 +95,10 @@ export default function VideoResultsPage() {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
<p className="text-gray-500">Loading detection results...</p>
</div>
<Card className="flex flex-col items-center gap-4 p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">Loading detection results...</p>
</Card>
</div>
)
}
@@ -106,22 +106,21 @@ export default function VideoResultsPage() {
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg text-center">
<p className="text-red-500 font-medium">{error}</p>
<div className="flex gap-4 mt-4">
<Card className="flex flex-col items-center gap-6 p-8 text-center max-w-md">
<p className="text-destructive font-medium">{error}</p>
<div className="flex gap-4">
<Button onClick={() => router.push(ROUTES.UPLOAD)} variant="outline">Back to Upload</Button>
<Button onClick={handleNewAnalysis} className="bg-linear-to-r from-indigo-500 to-purple-600 text-white">New Analysis</Button>
<Button onClick={handleNewAnalysis}>New Analysis</Button>
</div>
</div>
</Card>
</div>
)
}
return (
<div className="min-h-screen max-w-340 mx-auto text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-20 min-h-screen">
<div className="container mx-auto px-6 py-8 max-w-full">
<div className="min-h-screen">
<main className="min-h-screen">
<div className="container mx-auto px-6 py-10 max-w-[1600px]">
<div className="mb-8">
<PageHeader
title={getTitle()}
@@ -132,31 +131,33 @@ export default function VideoResultsPage() {
{session && (
<div className="mb-6">
<div className="flex items-center justify-between rounded-md px-4 py-3 bg-white/60 backdrop-blur-sm">
<div className="flex items-center gap-12">
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Project</span>
<span className="text-base font-bold text-gray-900 dark:text-white leading-tight">
{session.projectName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Package</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
{session.packageName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Location</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
{session.locationName}
</span>
<Card className="p-0 border shadow-sm overflow-hidden">
<div className="flex flex-col md:flex-row md:items-center justify-between py-4 px-6 gap-6 bg-card">
<div className="flex flex-wrap items-center gap-x-12 gap-y-4">
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Project</span>
<span className="text-base font-bold leading-tight">
{session.projectName}
</span>
</div>
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Package</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.packageName}
</span>
</div>
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName}
</span>
</div>
</div>
<Button onClick={handleNewAnalysis} variant="outline" size="sm" className="font-semibold px-6 shrink-0 h-9">
Start New Analysis
</Button>
</div>
<Button onClick={handleNewAnalysis} variant="outline" size="sm" className="btn-blue-gradient">
Start New
</Button>
</div>
</Card>
</div>
)}

View File

@@ -0,0 +1,150 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Loader2, TrendingUp } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
type SessionContext,
loadSession,
loadVideoData,
isSessionValid,
clearSession
} from "@/lib/api"
import { clearVideoFile } from "@/lib/video-storage"
import { DetectionData, DetectionType } from "@/lib/types"
import { ROUTES } from "@/utils/routes"
import { Card } from "@/components/ui/card"
export default function ResultsPage() {
const router = useRouter()
const [session, setSession] = useState<SessionContext | null>(null)
const [detectionData] = useState<DetectionData | null>(null)
const [detectionType] = useState<DetectionType>("pothole-detection")
const [videoId] = useState<string | null>(null)
const [videoFile] = useState<File | null>(null)
const [isLoading] = useState(true)
const [error] = useState<string | null>(null)
// Load session and video data on mount
useEffect(() => {
const storedSession = loadSession()
const videoData = loadVideoData()
if (!isSessionValid(storedSession) || !videoData) {
router.replace(ROUTES.NEW_ANALYSIS)
return
}
// If we have a videoId, redirect to the dynamic results page
if (videoData.videoId) {
router.replace(`${ROUTES.RESULTS}/${videoData.videoId}`)
return
}
setSession(storedSession)
}, [router])
const handleNewAnalysis = async () => {
// Clear video from IndexedDB
if (videoId) {
try {
await clearVideoFile(videoId)
} catch (err) {
console.error("Failed to clear video file:", err)
}
}
clearSession()
router.push(ROUTES.NEW_ANALYSIS)
}
const getTitle = () => {
if (detectionType === "pothole-detection") return "Pothole Detection Results"
if (detectionType === "sign-board-detection") return "Signboard Detection Results"
return "Pothole & Signboard Detection Results"
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="flex flex-col items-center gap-4 p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">Loading results...</p>
</Card>
</div>
)
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="flex flex-col items-center gap-6 p-8 text-center max-w-md">
<p className="text-destructive font-medium">{error}</p>
<Button onClick={handleNewAnalysis}>Start New Analysis</Button>
</Card>
</div>
)
}
return (
<div className="min-h-screen">
<main className="min-h-screen">
<div className="container mx-auto px-6 py-10 max-w-[1600px]">
<div className="mb-8">
<PageHeader
title={getTitle()}
description="View your AI-powered road analysis results"
icon={TrendingUp}
/>
</div>
{session && (
<div className="mb-6">
<Card className="p-0 border shadow-sm overflow-hidden">
<div className="flex flex-col md:flex-row md:items-center justify-between py-4 px-6 gap-6 bg-card">
<div className="flex flex-wrap items-center gap-x-12 gap-y-4">
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Project</span>
<span className="text-base font-bold leading-tight">
{session.projectName}
</span>
</div>
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Package</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.packageName}
</span>
</div>
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName}
</span>
</div>
</div>
<Button onClick={handleNewAnalysis} variant="outline" size="sm" className="font-semibold px-6 shrink-0 h-9">
Start New Analysis
</Button>
</div>
</Card>
</div>
)}
{detectionData && videoId && (
<VideoPlayerSection
data={detectionData}
videoId={videoId}
videoFile={videoFile}
detectionType={detectionType}
projectId={session?.projectId || undefined}
/>
)}
<PoweredBy />
</div>
</main>
</div>
)
}

View File

@@ -2,18 +2,16 @@
import { useState, useEffect, useCallback } from "react"
import { useRouter, useParams } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, TrendingUp } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
type SessionContext,
loadSession,
} from "@/lib/api"
import { getVideoFile } from "@/lib/video-storage"
import { storeVideoFile } from "@/lib/video-storage"
import { ROUTES } from "@/utils/routes"
import { Button } from "@/components/ui/button"
const API_URL = process.env.NEXT_PUBLIC_API_URL
const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://")
@@ -28,7 +26,6 @@ export default function VideoProcessingPage() {
const [progress, setProgress] = useState(0)
const [statusMessage, setStatusMessage] = useState("Initializing...")
const [error, setError] = useState<string | null>(null)
const [detectionType, setDetectionType] = useState<string>("pothole-detection")
const connectWebSocket = useCallback((vid: string) => {
const ws = new WebSocket(`${WS_URL}/ws/${vid}`)
@@ -123,18 +120,17 @@ export default function VideoProcessingPage() {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
</div>
<Card className="p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</Card>
</div>
)
}
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-20 min-h-screen relative overflow-hidden flex flex-col">
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 relative z-10 flex flex-col">
<div className="min-h-screen">
<main className="min-h-screen flex flex-col">
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 flex flex-col">
<div className="mb-6">
<PageHeader
title="Processing Analysis"
@@ -144,85 +140,78 @@ export default function VideoProcessingPage() {
</div>
{session && (
<div className="mb-4">
<div className="rounded-md px-4 py-3 bg-white/60 backdrop-blur-sm">
<div className="flex items-center gap-12">
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Project</span>
<span className="text-base font-bold text-gray-900 dark:text-white leading-tight">
{session.projectName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Package</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
{session.packageName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Location</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
{session.locationName}
</span>
<div className="mb-6">
<Card className="p-0 border shadow-sm overflow-hidden">
<div className="flex flex-col md:flex-row md:items-center py-4 px-6 gap-6 bg-card">
<div className="flex flex-wrap items-center gap-x-12 gap-y-4 flex-1">
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Project</span>
<span className="text-base font-bold leading-tight">
{session.projectName}
</span>
</div>
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Package</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.packageName}
</span>
</div>
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName}
</span>
</div>
</div>
</div>
</div>
</Card>
</div>
)}
<Card className="rounded-md overflow-hidden bg-white/60 backdrop-blur-xl flex flex-col items-center justify-center py-12 px-8 min-h-[450px]">
<div className="flex flex-col items-center justify-center w-full max-w-4xl space-y-8">
{/* Visual Spinner Area */}
<div className="relative">
<div className="h-24 w-24 rounded-full bg-blue-50 dark:bg-blue-900/20 flex items-center justify-center border-4 border-blue-100 dark:border-blue-800/30">
<Loader2 className="h-10 w-10 text-blue-500 animate-spin" />
</div>
</div>
<div className="text-center space-y-2">
<h2 className="text-3xl font-extrabold text-gray-900 dark:text-white tracking-tight">
Processing Video
<Card className="flex-1 flex flex-col items-center justify-center py-12 px-8 min-h-[450px]">
<div className="flex flex-col items-center justify-center w-full max-w-2xl space-y-10">
<div className="text-center space-y-4">
<h2 className="text-3xl font-bold tracking-tight">
Processing Analysis
</h2>
<p className="text-sm font-medium text-gray-400 dark:text-gray-500 font-mono tracking-wider">
<p className="text-sm font-mono text-muted-foreground tracking-widest">
ID: {videoId}
</p>
</div>
<div className="w-full space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="font-bold text-gray-500 dark:text-gray-400 text-xs uppercase tracking-widest">Processing Progress</span>
<span className="font-black text-blue-600 dark:text-blue-400 text-base">{progress}%</span>
<span className="font-bold text-muted-foreground text-xs uppercase tracking-widest">Progress</span>
<span className="font-bold text-primary text-lg">{progress}%</span>
</div>
<div className="h-3 rounded-full bg-blue-100 dark:bg-gray-800 overflow-hidden p-0.5 border border-blue-50 dark:border-gray-700">
<div className="h-4 rounded-full bg-secondary overflow-hidden border">
<div
className="h-full bg-gradient-to-r from-blue-500 to-indigo-600 rounded-full transition-all duration-1000 ease-in-out shadow-[0_0_15px_rgba(59,130,246,0.5)]"
className="h-full bg-primary transition-all duration-1000 ease-in-out"
style={{ width: `${progress}%` }}
/>
</div>
<div className="text-center">
<p className="text-sm font-bold text-gray-500 dark:text-gray-400 animate-pulse">
{statusMessage}
</p>
<div className="text-center pt-2">
<div className="inline-flex items-center gap-3 px-4 py-2 rounded-full border bg-secondary/30">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<p className="text-sm font-semibold">
{statusMessage}
</p>
</div>
</div>
</div>
{error && (
<div className="w-full p-4 rounded-xl bg-red-50/50 dark:bg-red-950/20 border border-red-200/50 dark:border-red-800/50 text-center">
<p className="text-red-600 dark:text-red-400 text-sm font-medium">{error}</p>
<button
<div className="w-full p-6 rounded-md bg-destructive/10 border border-destructive/20 text-center space-y-3">
<p className="text-destructive font-medium">{error}</p>
<Button
variant="outline"
size="sm"
onClick={() => window.location.reload()}
className="mt-3 text-xs font-bold text-red-700 dark:text-red-300 underline underline-offset-4"
>
Retry Connection
</button>
</Button>
</div>
)}
<div className="pt-8 text-center">
{/* <p className="text-xs font-medium text-gray-400 dark:text-gray-500/60 max-w-sm mx-auto leading-relaxed">
You can safely refresh this page progress will resume automatically.
</p> */}
</div>
</div>
</Card>

View File

@@ -8,7 +8,6 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Loader2, TrendingUp } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import {
@@ -20,9 +19,9 @@ import {
} from "@/lib/api"
import { ROUTES } from "@/utils/routes"
import { storeVideoFile } from "@/lib/video-storage"
import { cn } from "@/lib/utils"
const API_URL = process.env.NEXT_PUBLIC_API_URL
const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://")
const DETECTION_TYPES = [
{ value: "pothole-detection", label: "Pothole Detection" },
@@ -40,8 +39,6 @@ const DETECTION_METHODS = [
type DetectionType = "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
export default function UploadPage() {
const router = useRouter()
const [session, setSession] = useState<SessionContext | null>(null)
@@ -71,8 +68,6 @@ export default function UploadPage() {
setIsLoading(false)
}, [router])
const handleUpload = async () => {
if (!file) {
setError("Please select a video file")
@@ -149,23 +144,19 @@ export default function UploadPage() {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
</div>
<Card className="p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</Card>
</div>
)
}
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */}
<SidebarNavigation />
<div className="min-h-screen">
{/* Main Content */}
<main className="ml-20 min-h-screen relative overflow-hidden flex flex-col">
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 relative z-10 flex flex-col">
{/* Refined Header */}
<main className="min-h-screen flex flex-col">
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 flex flex-col">
{/* Header */}
<div className="mb-6">
<PageHeader
title={getTitle()}
@@ -176,56 +167,54 @@ export default function UploadPage() {
{/* Compact Session Info Bar */}
{session && (
<div className="mb-4">
<div className="rounded-md px-4 py-3 bg-white/60 backdrop-blur-sm ">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-12">
<div className="mb-6">
<Card className="p-0 border shadow-sm overflow-hidden">
<div className="flex flex-col md:flex-row md:items-center justify-between py-4 px-6 gap-6 bg-card">
<div className="flex flex-wrap items-center gap-x-12 gap-y-4">
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Project</span>
<span className="text-base font-bold text-gray-900 dark:text-white leading-tight">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Project</span>
<span className="text-base font-bold leading-tight">
{session.projectName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Package</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Package</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.packageName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Location</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
<div className="flex flex-col border-l pl-12 border-border/60">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-1.5">Location</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.locationName}
</span>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleBackToSelection}
className="btn-blue-gradient"
className="font-semibold px-6 shrink-0 h-9"
>
Change Selection
</Button>
</div>
</div>
</Card>
</div>
)}
{/* Upload Card */}
<Card className="bg-white/60 backdrop-blur-md rounded-xl overflow-hidden flex-1">
<CardHeader className="pb-4 ">
<Card className="flex-1">
<CardHeader className="pb-4">
<CardTitle className="text-xl font-bold">
<span className="heading-gradient">
Upload Video
</span>
Upload Video
</CardTitle>
<CardDescription className="text-sm">
Select video file, detection type, vehicle speed, and method for analysis
</CardDescription>
</CardHeader>
<CardContent className="pt-4 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<CardContent className="pt-4 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
{/* Video File Input */}
<div className="space-y-2">
<Label htmlFor="video-file" className="text-sm font-semibold">
@@ -240,7 +229,7 @@ export default function UploadPage() {
setError(null)
}}
disabled={uploading}
className="h-10 bg-gray-50 dark:bg-gray-800 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-blue-100 dark:file:bg-blue-900/50 file:text-blue-600 dark:file:text-blue-400 file:font-medium file:text-xs hover:file:bg-blue-200"
className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
/>
</div>
@@ -258,13 +247,13 @@ export default function UploadPage() {
setError(null)
}}
disabled={uploading}
className="h-10 bg-gray-50 dark:bg-gray-800 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-blue-100 dark:file:bg-blue-900/50 file:text-blue-600 dark:file:text-blue-400 file:font-medium file:text-xs hover:file:bg-blue-200"
className="h-11 bg-muted/20 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-primary/10 file:text-primary file:font-medium file:text-xs hover:file:bg-primary/20"
/>
</div>
</div>
{/* Detection, Speed, and Method Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Detection Type */}
<div className="space-y-2">
<Label htmlFor="detection-type" className="text-sm font-semibold">
@@ -275,13 +264,13 @@ export default function UploadPage() {
onValueChange={(v) => setDetectionType(v as DetectionType)}
disabled={uploading}
>
<SelectTrigger id="detection-type" className="h-10 bg-gray-50 dark:bg-gray-800">
<SelectTrigger id="detection-type" className="h-11">
<SelectValue placeholder="Select detection type" />
</SelectTrigger>
<SelectContent>
{DETECTION_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
<span className="font-medium">{type.label}</span>
{type.label}
</SelectItem>
))}
</SelectContent>
@@ -301,7 +290,7 @@ export default function UploadPage() {
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
disabled={uploading}
className="h-10 bg-gray-50 dark:bg-gray-800"
className="h-11"
/>
</div>
@@ -315,13 +304,13 @@ export default function UploadPage() {
onValueChange={setSelectMethod}
disabled={uploading}
>
<SelectTrigger id="select-method" className="h-10 bg-gray-50 dark:bg-gray-800">
<SelectTrigger id="select-method" className="h-11">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{DETECTION_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
<span className="font-medium">{method.label}</span>
{method.label}
</SelectItem>
))}
</SelectContent>
@@ -331,11 +320,8 @@ export default function UploadPage() {
{/* Error Display */}
{error && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-4 h-4 rounded-full bg-red-100 dark:bg-red-900/50 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-[10px] font-bold text-red-500">!</span>
</div>
<p className="text-xs text-red-600 dark:text-red-400 leading-relaxed whitespace-pre-line">{error}</p>
<div className="p-4 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm leading-relaxed whitespace-pre-line">
{error}
</div>
)}
@@ -343,23 +329,18 @@ export default function UploadPage() {
<Button
onClick={handleUpload}
disabled={!file || uploading}
className={`w-full h-12 text-sm font-semibold transition-all rounded-full ${file && !uploading
? 'btn-blue-gradient'
: ''
}`}
className="w-full h-14 text-base font-bold uppercase tracking-wide"
size="lg"
>
{uploading ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Processing...
</span>
<div className="flex items-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span>Processing...</span>
</div>
) : (
"Upload and Process"
)}
</Button>
</CardContent>
</Card>

View File

@@ -4,116 +4,75 @@
@custom-variant dark (&:is(.dark *));
:root {
/* Primary - Indigo */
--primary: oklch(0.55 0.24 264);
--primary-foreground: oklch(0.98 0.01 264);
/* Background & Surfaces */
--background: hsla(210, 40%, 98%, 0.2);
--foreground: oklch(0.15 0.02 280);
--dialog: white;
--card: white;
--card-foreground: oklch(0.15 0.02 280);
--popover: white;
--popover-foreground: oklch(0.15 0.02 280);
/* Secondary */
--secondary: oklch(0.95 0.02 264);
--secondary-foreground: oklch(0.25 0.05 264);
/* Muted */
--muted: oklch(0.94 0.02 280);
--muted-foreground: oklch(0.45 0.03 280);
/* Accent - Cyan */
--accent: oklch(0.92 0.04 200);
--accent-foreground: oklch(0.25 0.08 200);
/* Destructive */
--destructive: oklch(0.55 0.22 25);
--destructive-foreground: oklch(0.98 0.01 25);
/* Borders & Inputs */
--border: hsla(210, 100%, 85%, 1);
--input: oklch(0.92 0.02 280);
--ring: hsla(210, 100%, 85%, 1);
/* Card Glow */
--card-glow: hsla(210, 100%, 85%, 0.3);
--card-hover-border: hsla(210, 100%, 75%, 0.8);
/* Chart Colors - Vibrant Palette */
--chart-1: oklch(0.55 0.24 264);
--chart-2: oklch(0.65 0.2 200);
--chart-3: oklch(0.6 0.18 160);
--chart-4: oklch(0.7 0.2 85);
--chart-5: oklch(0.6 0.22 320);
--radius: 0.75rem;
/* Sidebar */
--sidebar: #ffffff;
--sidebar-foreground: #64748b;
--sidebar-primary: #2563eb;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f1f5f9;
--sidebar-accent-foreground: #0f172a;
--sidebar-border: #e2e8f0;
--sidebar-ring: #3b82f6;
--radius: 0.65rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.541 0.281 293.009);
--primary-foreground: oklch(0.969 0.016 293.756);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.702 0.183 293.541);
--chart-1: oklch(0.811 0.111 293.571);
--chart-2: oklch(0.606 0.25 292.717);
--chart-3: oklch(0.541 0.281 293.009);
--chart-4: oklch(0.491 0.27 292.581);
--chart-5: oklch(0.432 0.232 292.759);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.541 0.281 293.009);
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.702 0.183 293.541);
}
.dark {
/* Primary - Light Indigo */
--primary: oklch(0.7 0.2 264);
--primary-foreground: oklch(0.15 0.02 264);
--dialog: white;
/* Background & Surfaces */
--background: hsla(280, 20%, 12%, 0.3);
--foreground: oklch(0.95 0.01 280);
--card: hsla(280, 20%, 16%, 0.4);
--card-foreground: oklch(0.95 0.01 280);
--popover: hsla(280, 20%, 16%, 0.7);
--popover-foreground: oklch(0.95 0.01 280);
/* Secondary */
--secondary: oklch(0.22 0.03 264);
--secondary-foreground: oklch(0.9 0.02 264);
/* Muted */
--muted: oklch(0.2 0.02 280);
--muted-foreground: oklch(0.65 0.02 280);
/* Accent */
--accent: oklch(0.22 0.04 200);
--accent-foreground: oklch(0.85 0.08 200);
/* Destructive */
--destructive: oklch(0.45 0.18 25);
--destructive-foreground: oklch(0.9 0.08 25);
/* Borders & Inputs */
--border: oklch(0.25 0.02 280);
--input: oklch(0.22 0.02 280);
--ring: oklch(0.7 0.2 264);
/* Chart Colors */
--chart-1: oklch(0.65 0.22 264);
--chart-2: oklch(0.7 0.18 200);
--chart-3: oklch(0.65 0.16 160);
--chart-4: oklch(0.75 0.18 85);
--chart-5: oklch(0.65 0.2 320);
/* Sidebar */
--sidebar: oklch(0.14 0.02 280);
--sidebar-foreground: oklch(0.95 0.01 280);
--sidebar-primary: oklch(0.65 0.22 264);
--sidebar-primary-foreground: oklch(0.95 0.01 264);
--sidebar-accent: oklch(0.22 0.03 264);
--sidebar-accent-foreground: oklch(0.9 0.02 264);
--sidebar-border: oklch(0.25 0.02 280);
--sidebar-ring: oklch(0.7 0.2 264);
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.606 0.25 292.717);
--primary-foreground: oklch(0.969 0.016 293.756);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.38 0.189 293.745);
--chart-1: oklch(0.811 0.111 293.571);
--chart-2: oklch(0.606 0.25 292.717);
--chart-3: oklch(0.541 0.281 293.009);
--chart-4: oklch(0.491 0.27 292.581);
--chart-5: oklch(0.432 0.232 292.759);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.606 0.25 292.717);
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.38 0.189 293.745);
}
@theme inline {
--font-sans: "Geist", "Geist Fallback", system-ui, sans-serif;
--font-mono: "Geist Mono", "Geist Mono Fallback", monospace;
@@ -157,7 +116,7 @@
}
/* Base styles */
@layer base {
/* @layer base {
* {
@apply border-border outline-ring/50;
}
@@ -181,54 +140,15 @@
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
}
} */
/*
Definitive Layout Stability Reset
Prevents Radix UI / Shadcn from "sliding" content when scroll-locking occurs
*/
:root {
--removed-body-scroll-bar-size: 0px;
}
html {
/* scrollbar-gutter: stable; */
scroll-behavior: smooth;
}
body[data-scroll-locked] {
padding-right: 0 !important;
margin-right: 0 !important;
overflow: hidden !important;
}
/* Fix for fixed position elements like sidebars/headers */
[data-radix-scroll-area-viewport] {
scrollbar-width: none;
-ms-overflow-style: none;
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
.heading-blue-gradient {
@apply bg-linear-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent;
}
.powered-blue-gradient {
@apply bg-linear-to-b from-[#225999] to-[#56A5FF] bg-clip-text text-transparent;
}
.btn-blue-gradient {
@apply text-white bg-linear-to-r from-blue-400 to-blue-600 hover:bg-linear-to-bl focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium transition-all duration-200;
}
.bg-blue-gradient {
background: linear-gradient(271.19deg, #86B8F1 1.02%, #3895FF 191.34%);
}
.badge-blue {
@apply flex items-center justify-center bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400 text-sm font-bold rounded-full border border-blue-200/50;
}

View File

@@ -1,53 +1,42 @@
import type React from "react"
import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import { Analytics } from "@vercel/analytics/next"
import "./globals.css"
import "leaflet/dist/leaflet.css"
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
const _geist = Geist({ subsets: ["latin"] })
const _geistMono = Geist_Mono({ subsets: ["latin"] })
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Pothole Detection System",
description: "AI-powered pothole detection and tracking system",
generator: "v0.app",
icons: {
icon: [
{
url: "/icon-light-32x32.png",
media: "(prefers-color-scheme: light)",
},
{
url: "/icon-dark-32x32.png",
media: "(prefers-color-scheme: dark)",
},
{
url: "/icon.svg",
type: "image/svg+xml",
},
],
apple: "/apple-icon.png",
},
}
import { Toaster } from "@/components/ui/sonner"
import { TooltipProvider } from "@/components/ui/tooltip"
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) {
return (
<html lang="en" data-scroll-behavior="smooth">
<body className={`font-sans antialiased`} suppressHydrationWarning>
<TooltipProvider>
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster />
</TooltipProvider>
<Analytics />
</ThemeProvider>
</body>
</html>
)
}
);
}

View File

@@ -1,160 +0,0 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Loader2, ArrowLeft, MapPin, Package, FolderKanban, RotateCcw, TrendingUp } from "lucide-react"
import VideoPlayerSection from "@/components/video-player-section"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { PageHeader } from "@/components/page-header"
import {
type SessionContext,
loadSession,
loadVideoData,
isSessionValid,
clearSession
} from "@/lib/api"
import { getVideoFile, clearVideoFile } from "@/lib/video-storage"
import { DetectionData, DetectionType } from "@/lib/types"
const API_URL = process.env.NEXT_PUBLIC_API_URL
import { ROUTES } from "@/utils/routes"
export default function ResultsPage() {
const router = useRouter()
const [session, setSession] = useState<SessionContext | null>(null)
const [detectionData, setDetectionData] = useState<DetectionData | null>(null)
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
const [videoId, setVideoId] = useState<string | null>(null)
const [videoFile, setVideoFile] = useState<File | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Load session and video data on mount
useEffect(() => {
const storedSession = loadSession()
const videoData = loadVideoData()
if (!isSessionValid(storedSession) || !videoData) {
router.replace(ROUTES.NEW_ANALYSIS)
return
}
// If we have a videoId, redirect to the dynamic results page
if (videoData.videoId) {
router.replace(`${ROUTES.RESULTS}/${videoData.videoId}`)
return
}
setSession(storedSession)
}, [router])
const handleNewAnalysis = async () => {
// Clear video from IndexedDB
if (videoId) {
try {
await clearVideoFile(videoId)
} catch (err) {
console.error("Failed to clear video file:", err)
}
}
clearSession()
router.push(ROUTES.NEW_ANALYSIS)
}
const handleBackToUpload = () => {
router.push(ROUTES.UPLOAD)
}
const getTitle = () => {
if (detectionType === "pothole-detection") return "Pothole Detection Results"
if (detectionType === "sign-board-detection") return "Signboard Detection Results"
return "Pothole & Signboard Detection Results"
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
<p className="text-gray-500">Loading detection results...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<p className="text-red-500">{error}</p>
<Button onClick={handleNewAnalysis} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white">Start New Analysis</Button>
</div>
</div>
)
}
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Main Content */}
<main className="ml-20 min-h-screen">
<div className="container mx-auto px-6 py-8 max-w-full">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title={getTitle()}
description="View your AI-powered road analysis results"
icon={TrendingUp}
/>
</div>
{/* Session Info Bar */}
{session && (
<div className="mb-6">
<div className="flex items-center justify-between p-4 rounded-xl bg-card border border-[var(--border)] shadow-sm">
<div className="flex items-center gap-12">
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Project</span>
<span className="text-base font-bold text-gray-900 dark:text-white leading-tight">
{session.projectName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Package</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
{session.packageName}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1.5">Location</span>
<span className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-tight">
{session.locationName}
</span>
</div>
</div>
</div>
</div>
)}
{/* Video Player Section */}
{detectionData && videoId && (
<div>
<VideoPlayerSection
data={detectionData}
videoId={videoId}
videoFile={videoFile}
detectionType={detectionType}
projectId={session?.projectId || undefined}
/>
</div>
)}
</div>
</main>
</div>
)
}