refactor: refactor the color schema
This commit is contained in:
@@ -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">
|
||||
25
src/app/(modules)/layout.tsx
Normal file
25
src/app/(modules)/layout.tsx
Normal 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;
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
150
src/app/(modules)/results/page.tsx
Normal file
150
src/app/(modules)/results/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user