setup husky prettier eslint

This commit is contained in:
2026-03-18 19:39:52 +05:30
parent 42624cae94
commit b675dd857b
93 changed files with 11269 additions and 6011 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
import { BreadcrumbBasic } from "@/components/app-breadcrumb";
import { AppSidebar } from "@/components/app-sidebar";
import { ModeToggle } from "@/components/mode-toogle";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import React from "react";
import { BreadcrumbBasic } from '@/components/app-breadcrumb';
import { AppSidebar } from '@/components/app-sidebar';
import { ModeToggle } from '@/components/mode-toogle';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { Separator } from '@/components/ui/separator';
import React from 'react';
const ModulesLayout = ({
children,
@@ -31,5 +31,4 @@ const ModulesLayout = ({
);
};
export default ModulesLayout;

View File

@@ -1,59 +1,55 @@
"use client"
'use client';
import { useRouter } from "next/navigation"
import dynamic from "next/dynamic"
import { sessionService } from "@/services/api"
import { SessionContext } from "@/types"
import { PowerCircle, TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import { ROUTES } from "@/utils/routes"
import { Reveal } from "@/components/ui/reveal"
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
import { sessionService } from '@/services/api';
import { SessionContext } from '@/types';
import { PowerCircle, TrendingUp } from 'lucide-react';
import { PageHeader } from '@/components/page-header';
import { PoweredBy } from '@/components/powered-by';
import { ROUTES } from '@/utils/routes';
import { Reveal } from '@/components/ui/reveal';
const ProjectSelectionSection = dynamic(
() => import("@/components/project-selection-section").then(mod => mod.ProjectSelectionSection),
{ ssr: false }
)
() => import('@/components/project-selection-section').then((mod) => mod.ProjectSelectionSection),
{ ssr: false },
);
export default function NewAnalysisPage() {
const router = useRouter()
const router = useRouter();
const handleSelectionComplete = (session: SessionContext) => {
// Save session to storage and navigate to upload page
sessionService.saveSession(session)
router.push(ROUTES.UPLOAD)
}
const handleSelectionComplete = (session: SessionContext) => {
// Save session to storage and navigate to upload page
sessionService.saveSession(session);
router.push(ROUTES.UPLOAD);
};
return (
<div className="min-h-screen">
{/* Main Content */}
<main className="min-h-screen">
return (
<div className="min-h-screen">
{/* Main Content */}
<main className="min-h-screen">
<div className="container mx-auto px-6 py-10 max-w-340">
{/* Refined Left-Aligned Header */}
<div className="mb-8">
<PageHeader
title="VisionRoad Detection System"
description="Select project details to begin your AI-powered road infrastructure analysis"
icon={TrendingUp}
/>
</div>
<div className="container mx-auto px-6 py-10 max-w-340">
{/* Refined Left-Aligned Header */}
<div className="mb-8">
<PageHeader
title="VisionRoad Detection System"
description="Select project details to begin your AI-powered road infrastructure analysis"
icon={TrendingUp}
/>
</div>
{/* Project Selection Section */}
<Reveal delay={0.2} direction="up">
<div>
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
</div>
</Reveal>
{/* Project Selection Section */}
<Reveal delay={0.2} direction="up">
<div>
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
</div>
</Reveal>
<Reveal delay={0.4}>
<PoweredBy />
</Reveal>
</div>
</main>
<Reveal delay={0.4}>
<PoweredBy />
</Reveal>
</div>
)
</main>
</div>
);
}

View File

@@ -1,470 +1,513 @@
"use client"
'use client';
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
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 { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header"
import { projectService, packageService } from "@/services/api"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Project,
Package as PackageType,
PackageCreate,
PackageUpdate
} from "@/types"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by"
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 { DataTable } from '@/components/data-table';
import { PageHeader } from '@/components/page-header';
import { projectService, packageService } from '@/services/api';
import { Project, Package as PackageType, PackageCreate, PackageUpdate } from '@/types';
import { ColumnDef } from '@tanstack/react-table';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { PoweredBy } from '@/components/powered-by';
export default function PackagePage() {
const [packages, setPackages] = useState<PackageType[]>([])
const [totalItems, setTotalItems] = useState(0)
const [projects, setProjects] = useState<Project[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [loadingProjects, setLoadingProjects] = useState(false)
const [packages, setPackages] = useState<PackageType[]>([]);
const [totalItems, setTotalItems] = useState(0);
const [projects, setProjects] = useState<Project[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loadingProjects, setLoadingProjects] = useState(false);
// Pagination state
const [skip, setSkip] = useState(0)
const [limit, setLimit] = useState(10)
// Pagination state
const [skip, setSkip] = useState(0);
const [limit, setLimit] = useState(10);
// Editing state
const [isEditing, setIsEditing] = useState(false)
const [currentPackage, setCurrentPackage] = useState<PackageType | null>(null)
// Editing state
const [isEditing, setIsEditing] = useState(false);
const [currentPackage, setCurrentPackage] = useState<PackageType | null>(null);
// Form fields
const [selectedProjectId, setSelectedProjectId] = useState("")
const [name, setName] = useState("")
const [region, setRegion] = useState("")
const [chainageStartKm, setChainageStartKm] = useState<string>("")
const [chainageEndKm, setChainageEndKm] = useState<string>("")
// Form fields
const [selectedProjectId, setSelectedProjectId] = useState('');
const [name, setName] = useState('');
const [region, setRegion] = useState('');
const [chainageStartKm, setChainageStartKm] = useState<string>('');
const [chainageEndKm, setChainageEndKm] = useState<string>('');
// Load packages and projects
const loadPackages = async (currentSkip = skip, currentLimit = limit) => {
try {
setIsLoading(true)
setError(null)
const data = await packageService.getPackages({ skip: currentSkip, limit: currentLimit })
setPackages(data.items)
setTotalItems(data.totalItems)
} catch (err) {
setError("Failed to load packages. Please check if the backend is running.")
} finally {
// Add a small delay for animation stability
setTimeout(() => {
setIsLoading(false)
}, 800)
}
// Load packages and projects
const loadPackages = async (currentSkip = skip, currentLimit = limit) => {
try {
setIsLoading(true);
setError(null);
const data = await packageService.getPackages({ skip: currentSkip, limit: currentLimit });
setPackages(data.items);
setTotalItems(data.totalItems);
} catch (err) {
setError('Failed to load packages. Please check if the backend is running.');
} finally {
// Add a small delay for animation stability
setTimeout(() => {
setIsLoading(false);
}, 800);
}
};
const loadProjects = async () => {
try {
setLoadingProjects(true);
const data = await projectService.getProjects({ skip: 0, limit: 1000 }); // Load all projects for selector
setProjects(data.items);
} catch (err) {
setError('Failed to load projects.');
} finally {
setLoadingProjects(false);
}
};
useEffect(() => {
loadPackages(skip, limit);
loadProjects();
}, [skip, limit]);
const resetForm = () => {
setSelectedProjectId('');
setName('');
setRegion('');
setChainageStartKm('');
setChainageEndKm('');
setError(null);
setIsEditing(false);
setCurrentPackage(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedProjectId) {
setError('Please select a project first');
return;
}
if (!name.trim()) {
setError('Package name is required');
return;
}
const loadProjects = async () => {
try {
setLoadingProjects(true)
const data = await projectService.getProjects({ skip: 0, limit: 1000 }) // Load all projects for selector
setProjects(data.items)
} catch (err) {
setError("Failed to load projects.")
} finally {
setLoadingProjects(false)
}
setIsSubmitting(true);
setError(null);
try {
if (isEditing && currentPackage) {
const data: PackageUpdate = {
name: name.trim(),
region: region.trim() || null,
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : 0,
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : 0,
};
await packageService.updatePackage(currentPackage.id, data);
toast.success('Package Updated', {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
});
} else {
const data: PackageCreate = {
project_id: selectedProjectId,
name: name.trim(),
region: region.trim() || null,
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : 0,
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : 0,
};
await packageService.createPackage(data);
toast.success('Package Created', {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
});
}
// Refresh packages list
await loadPackages();
// Close modal and reset form immediately
setIsModalOpen(false);
resetForm();
} catch (err) {
const message =
err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} package`;
setError(message);
toast.error('Operation Failed', {
description: message,
});
} finally {
setIsSubmitting(false);
}
};
useEffect(() => {
loadPackages(skip, limit)
loadProjects()
}, [skip, limit])
const handleEdit = (pkg: PackageType) => {
setIsEditing(true);
setCurrentPackage(pkg);
setSelectedProjectId(pkg.project_id);
setName(pkg.name || '');
setRegion(pkg.region || '');
setChainageStartKm(pkg.chainage_start_km?.toString() || '0');
setChainageEndKm(pkg.chainage_end_km?.toString() || '0');
setIsModalOpen(true);
};
const resetForm = () => {
setSelectedProjectId("")
setName("")
setRegion("")
setChainageStartKm("")
setChainageEndKm("")
setError(null)
setIsEditing(false)
setCurrentPackage(null)
const handleDelete = async (pkg: PackageType) => {
if (!confirm(`Are you sure you want to delete package "${pkg.name}"?`)) return;
try {
setIsLoading(true);
await packageService.deletePackage(pkg.id);
toast.success('Package Deleted', {
description: `${pkg.name} has been removed from the system.`,
});
await loadPackages();
} catch (err) {
setError('Failed to delete package');
toast.error('Deletion Failed', {
description: 'The package could not be removed. Please try again.',
});
} finally {
setIsLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!selectedProjectId) {
setError("Please select a project first")
return
}
if (!name.trim()) {
setError("Package name is required")
return
}
const getProjectName = (projectId: string) => {
return projects.find((p) => p.id === projectId)?.name || projectId;
};
setIsSubmitting(true)
setError(null)
const columns: ColumnDef<PackageType>[] = [
{
accessorKey: 'name',
header: 'Package Name',
cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.name}</div>
),
},
{
accessorKey: 'project_id',
header: 'Project',
cell: ({ row }) => getProjectName(row.original.project_id),
},
{
accessorKey: 'project_state',
header: 'Project State',
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>;
try {
if (isEditing && currentPackage) {
const data: PackageUpdate = {
name: name.trim(),
region: region.trim() || null,
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : 0,
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : 0,
}
await packageService.updatePackage(currentPackage.id, data)
toast.success("Package Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else {
const data: PackageCreate = {
project_id: selectedProjectId,
name: name.trim(),
region: region.trim() || null,
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : 0,
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : 0,
}
await packageService.createPackage(data)
toast.success("Package Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
const states = project.state
.split(',')
.map((s) => s.trim())
.filter(Boolean);
if (states.length === 0) return <span className="text-gray-400"></span>;
const firstState = states[0];
const remainingStates = states.slice(1);
return (
<div className="flex items-center gap-1.5">
<Badge variant="secondary">{firstState}</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">
Other States
</p>
{remainingStates.map((item, idx) => (
<Badge key={idx} variant="secondary">
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
);
},
},
{
accessorKey: 'region',
header: 'Region',
},
{
accessorKey: 'chainage_start_km',
header: 'Start (km)',
cell: ({ row }) => row.original.chainage_start_km?.toFixed(2) ?? '0.00',
},
{
accessorKey: 'chainage_end_km',
header: 'End (km)',
cell: ({ row }) => row.original.chainage_end_km?.toFixed(2) ?? '0.00',
},
];
return (
<>
<main className="relative z-10">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title="Package Management"
description="Manage project packages"
icon={Package}
actions={
<Button
onClick={() => {
setIsEditing(false);
setIsModalOpen(true);
}}
>
<Package className="mr-2 h-4 w-4" />
Add New Package
</Button>
}
/>
</div>
// Refresh packages list
await loadPackages()
{/* Data Table */}
<div>
<DataTable
title="Packages"
data={packages}
columns={columns}
onEdit={handleEdit}
onDelete={handleDelete}
isLoading={isLoading}
pagination={{
skip,
limit,
totalItems,
onPageChange: setSkip,
onLimitChange: (newLimit) => {
setLimit(newLimit);
setSkip(0); // Reset skip when limit changes
},
}}
/>
</div>
// Close modal and reset form immediately
setIsModalOpen(false)
resetForm()
} catch (err) {
const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} package`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally {
setIsSubmitting(false)
}
}
<PoweredBy />
</main>
const handleEdit = (pkg: PackageType) => {
setIsEditing(true)
setCurrentPackage(pkg)
setSelectedProjectId(pkg.project_id)
setName(pkg.name || "")
setRegion(pkg.region || "")
setChainageStartKm(pkg.chainage_start_km?.toString() || "0")
setChainageEndKm(pkg.chainage_end_km?.toString() || "0")
setIsModalOpen(true)
}
{/* 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 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 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>
const handleDelete = async (pkg: PackageType) => {
if (!confirm(`Are you sure you want to delete package "${pkg.name}"?`)) return
<form onSubmit={handleSubmit} className="space-y-8">
{/* Step 1: Select Project */}
{!isEditing && (
<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-[11px] font-black text-muted-foreground uppercase tracking-widest">
Select Parent Project
</p>
</div>
<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-sm">Loading...</span>
</div>
) : (
<SelectValue placeholder="Choose a project..." />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id} className="py-3">
<span className="font-semibold">{project.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
try {
setIsLoading(true)
await packageService.deletePackage(pkg.id)
toast.success("Package Deleted", {
description: `${pkg.name} has been removed from the system.`,
})
await loadPackages()
} catch (err) {
setError("Failed to delete package")
toast.error("Deletion Failed", {
description: "The package could not be removed. Please try again.",
})
} finally {
setIsLoading(false)
}
}
{/* 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.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-[11px] font-black text-muted-foreground uppercase tracking-widest">
Package Information
</p>
</div>
)}
const getProjectName = (projectId: string) => {
return projects.find(p => p.id === projectId)?.name || projectId
}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-2">
<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="e.g. Package 01"
className="h-11 bg-muted/20 border-border/60"
required
/>
</div>
const columns: ColumnDef<PackageType>[] = [
{
accessorKey: "name",
header: "Package Name",
cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.name}</div>
)
},
{
accessorKey: "project_id",
header: "Project",
cell: ({ row }) => getProjectName(row.original.project_id)
},
{
accessorKey: "project_state",
header: "Project State",
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>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span className="text-gray-400"></span>
const firstState = states[0]
const remainingStates = states.slice(1)
<div className="space-y-2">
<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="e.g. North Zone"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
return (
<div className="flex items-center gap-1.5">
<Badge variant="secondary">
{firstState}
</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge key={idx} variant="secondary">
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
)
}
},
{
accessorKey: "region",
header: "Region",
},
{
accessorKey: "chainage_start_km",
header: "Start (km)",
cell: ({ row }) => row.original.chainage_start_km?.toFixed(2) ?? "0.00"
},
{
accessorKey: "chainage_end_km",
header: "End (km)",
cell: ({ row }) => row.original.chainage_end_km?.toFixed(2) ?? "0.00"
},
]
<div className="space-y-2">
<Label
htmlFor="chainage-start"
className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider"
>
Chainage Start (km)
</Label>
<Input
id="chainage-start"
type="number"
step="0.01"
value={chainageStartKm}
onChange={(e) => setChainageStartKm(e.target.value)}
placeholder="0.00"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
return (
<>
<main className="relative z-10">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title="Package Management"
description="Manage project packages"
icon={Package}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<Package className="mr-2 h-4 w-4" />
Add New Package
</Button>
}
/>
</div>
<div className="space-y-2">
<Label
htmlFor="chainage-end"
className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider"
>
Chainage End (km)
</Label>
<Input
id="chainage-end"
type="number"
step="0.01"
value={chainageEndKm}
onChange={(e) => setChainageEndKm(e.target.value)}
placeholder="0.00"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
</div>
</div>
{/* Data Table */}
<div>
<DataTable
title="Packages"
data={packages}
columns={columns}
onEdit={handleEdit}
onDelete={handleDelete}
isLoading={isLoading}
pagination={{
skip,
limit,
totalItems,
onPageChange: setSkip,
onLimitChange: (newLimit) => {
setLimit(newLimit);
setSkip(0); // Reset skip when limit changes
}
}}
/>
</div>
<PoweredBy />
</main>
{/* 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 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 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>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Step 1: Select Project */}
{!isEditing && (
<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-[11px] font-black text-muted-foreground uppercase tracking-widest">Select Parent Project</p>
</div>
<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-sm">Loading...</span>
</div>
) : (
<SelectValue placeholder="Choose a project..." />
)}
</SelectTrigger>
<SelectContent>
{projects.map(project => (
<SelectItem key={project.id} value={project.id} className="py-3">
<span className="font-semibold">{project.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 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.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-[11px] font-black text-muted-foreground uppercase tracking-widest">Package Information</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-2">
<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="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-[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="e.g. North Zone"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
<div className="space-y-2">
<Label htmlFor="chainage-start" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Chainage Start (km)
</Label>
<Input
id="chainage-start"
type="number"
step="0.01"
value={chainageStartKm}
onChange={(e) => setChainageStartKm(e.target.value)}
placeholder="0.00"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
<div className="space-y-2">
<Label htmlFor="chainage-end" className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">
Chainage End (km)
</Label>
<Input
id="chainage-end"
type="number"
step="0.01"
value={chainageEndKm}
onChange={(e) => setChainageEndKm(e.target.value)}
placeholder="0.00"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-4 pt-4">
<Button
type="button"
variant="outline"
onClick={() => {
setIsModalOpen(false)
resetForm()
}}
disabled={isSubmitting}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !name.trim() || !selectedProjectId}
className="flex-1"
>
{isSubmitting ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>{isEditing ? 'Updating...' : 'Creating...'}</span>
</div>
) : (
<div className="flex items-center gap-2">
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <Package className="h-4 w-4" />}
<span>{isEditing ? 'Update Package' : 'Create Package'}</span>
</div>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
)
{/* Submit Button */}
<div className="flex gap-4 pt-4">
<Button
type="button"
variant="outline"
onClick={() => {
setIsModalOpen(false);
resetForm();
}}
disabled={isSubmitting}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !name.trim() || !selectedProjectId}
className="flex-1"
>
{isSubmitting ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>{isEditing ? 'Updating...' : 'Creating...'}</span>
</div>
) : (
<div className="flex items-center gap-2">
{isEditing ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<Package className="h-4 w-4" />
)}
<span>{isEditing ? 'Update Package' : 'Create Package'}</span>
</div>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { redirect } from "next/navigation"
import { ROUTES } from "@/utils/routes"
import { redirect } from 'next/navigation';
import { ROUTES } from '@/utils/routes';
export default function HomePage() {
redirect(ROUTES.DASHBOARD)
redirect(ROUTES.DASHBOARD);
}

View File

@@ -1,452 +1,497 @@
"use client"
'use client';
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
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, Layers, MapPin, Building2, Route, X } from "lucide-react"
import { DataTable } from "@/components/data-table"
import { PageHeader } from "@/components/page-header"
import { projectService } from "@/services/api"
import { ProjectCreate, Project, ProjectUpdate } from "@/types"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
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, Layers, MapPin, Building2, Route, X } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { PageHeader } from '@/components/page-header';
import { projectService } from '@/services/api';
import { ProjectCreate, Project, ProjectUpdate } from '@/types';
import { ColumnDef } from '@tanstack/react-table';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { PoweredBy } from '@/components/powered-by';
export default function ProjectPage() {
const [projects, setProjects] = useState<Project[]>([])
const [totalItems, setTotalItems] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [projects, setProjects] = useState<Project[]>([]);
const [totalItems, setTotalItems] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// Pagination state
const [skip, setSkip] = useState(0)
const [limit, setLimit] = useState(10)
// Pagination state
const [skip, setSkip] = useState(0);
const [limit, setLimit] = useState(10);
// Editing state
const [isEditing, setIsEditing] = useState(false)
const [currentProject, setCurrentProject] = useState<Project | null>(null)
// Editing state
const [isEditing, setIsEditing] = useState(false);
const [currentProject, setCurrentProject] = useState<Project | null>(null);
// Form fields
const [name, setName] = useState("")
const [state, setState] = useState("")
const [corridorName, setCorridorName] = useState("")
const [startLat, setStartLat] = useState("")
const [startLng, setStartLng] = useState("")
const [endLat, setEndLat] = useState("")
const [endLng, setEndLng] = useState("")
// Form fields
const [name, setName] = useState('');
const [state, setState] = useState('');
const [corridorName, setCorridorName] = useState('');
const [startLat, setStartLat] = useState('');
const [startLng, setStartLng] = useState('');
const [endLat, setEndLat] = useState('');
const [endLng, setEndLng] = useState('');
// Load projects
const loadProjects = async (currentSkip = skip, currentLimit = limit) => {
try {
setIsLoading(true)
setError(null)
const data = await projectService.getProjects({ skip: currentSkip, limit: currentLimit })
setProjects(data.items)
setTotalItems(data.totalItems)
} catch (err) {
setError("Failed to load projects. Please check if the backend is running.")
} finally {
// Add a small delay for animation stability
setTimeout(() => {
setIsLoading(false)
}, 800)
}
// Load projects
const loadProjects = async (currentSkip = skip, currentLimit = limit) => {
try {
setIsLoading(true);
setError(null);
const data = await projectService.getProjects({ skip: currentSkip, limit: currentLimit });
setProjects(data.items);
setTotalItems(data.totalItems);
} catch (err) {
setError('Failed to load projects. Please check if the backend is running.');
} finally {
// Add a small delay for animation stability
setTimeout(() => {
setIsLoading(false);
}, 800);
}
};
useEffect(() => {
loadProjects(skip, limit);
}, [skip, limit]);
const resetForm = () => {
setName('');
setState('');
setCorridorName('');
setStartLat('');
setStartLng('');
setEndLat('');
setEndLng('');
setError(null);
setIsEditing(false);
setCurrentProject(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
setError('Project name is required');
return;
}
useEffect(() => {
loadProjects(skip, limit)
}, [skip, limit])
setIsSubmitting(true);
setError(null);
const resetForm = () => {
setName("")
setState("")
setCorridorName("")
setStartLat("")
setStartLng("")
setEndLat("")
setEndLng("")
setError(null)
setIsEditing(false)
setCurrentProject(null)
try {
if (isEditing && currentProject) {
const data: ProjectUpdate = {
name: name.trim(),
state: state.trim() || null,
corridor_name: corridorName.trim() || null,
start_lat: startLat ? parseFloat(startLat) : null,
start_lng: startLng ? parseFloat(startLng) : null,
end_lat: endLat ? parseFloat(endLat) : null,
end_lng: endLng ? parseFloat(endLng) : null,
};
await projectService.updateProject(currentProject.id, data);
toast.success('Project Updated', {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
});
} else {
const data: ProjectCreate = {
name: name.trim(),
state: state.trim() || null,
corridor_name: corridorName.trim() || null,
start_lat: startLat ? parseFloat(startLat) : null,
start_lng: startLng ? parseFloat(startLng) : null,
end_lat: endLat ? parseFloat(endLat) : null,
end_lng: endLng ? parseFloat(endLng) : null,
};
await projectService.createProject(data);
toast.success('Project Created', {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
});
}
// Refresh projects list
await loadProjects();
// Close modal and reset form immediately
setIsModalOpen(false);
resetForm();
} catch (err) {
const message =
err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} project`;
setError(message);
toast.error('Operation Failed', {
description: message,
});
} finally {
setIsSubmitting(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
setError("Project name is required")
return
}
const handleEdit = (project: Project) => {
setIsEditing(true);
setCurrentProject(project);
setName(project.name || '');
setState(project.state || '');
setCorridorName(project.corridor_name || '');
setStartLat(project.start_lat?.toString() || '');
setStartLng(project.start_lng?.toString() || '');
setEndLat(project.end_lat?.toString() || '');
setEndLng(project.end_lng?.toString() || '');
setIsModalOpen(true);
};
setIsSubmitting(true)
setError(null)
const handleDelete = async (project: Project) => {
if (!confirm(`Are you sure you want to delete project "${project.name}"?`)) return;
try {
if (isEditing && currentProject) {
const data: ProjectUpdate = {
name: name.trim(),
state: state.trim() || null,
corridor_name: corridorName.trim() || null,
start_lat: startLat ? parseFloat(startLat) : null,
start_lng: startLng ? parseFloat(startLng) : null,
end_lat: endLat ? parseFloat(endLat) : null,
end_lng: endLng ? parseFloat(endLng) : null,
}
await projectService.updateProject(currentProject.id, data)
toast.success("Project Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else {
const data: ProjectCreate = {
name: name.trim(),
state: state.trim() || null,
corridor_name: corridorName.trim() || null,
start_lat: startLat ? parseFloat(startLat) : null,
start_lng: startLng ? parseFloat(startLng) : null,
end_lat: endLat ? parseFloat(endLat) : null,
end_lng: endLng ? parseFloat(endLng) : null,
}
await projectService.createProject(data)
toast.success("Project Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
try {
setIsLoading(true);
await projectService.deleteProject(project.id);
toast.success('Project Deleted', {
description: `${project.name} has been removed from the system.`,
});
await loadProjects();
} catch (err) {
setError('Failed to delete project');
toast.error('Deletion Failed', {
description:
'The project could not be removed. Please try again or check your permissions.',
});
} finally {
setIsLoading(false);
}
};
const columns: ColumnDef<Project>[] = [
{
accessorKey: 'name',
header: 'Project Name',
cell: ({ row }) => <div className="font-semibold">{row.original.name}</div>,
},
{
accessorKey: 'state',
header: 'State',
cell: ({ row }) => {
const project = row.original;
if (!project.state) return <span></span>;
const states = project.state
.split(',')
.map((s) => s.trim())
.filter(Boolean);
if (states.length === 0) return <span></span>;
const firstState = states[0];
const remainingStates = states.slice(1);
return (
<div className="flex items-center gap-1.5">
<Badge variant="secondary">{firstState}</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">
Other States
</p>
{remainingStates.map((item, idx) => (
<Badge key={idx} variant="secondary">
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
);
},
},
{
accessorKey: 'corridor_name',
header: 'Corridor',
},
];
return (
<>
<main className="relative z-10">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title="Project Management"
description="Manage road infrastructure projects"
icon={Layers}
actions={
<Button
onClick={() => {
setIsEditing(false);
setIsModalOpen(true);
}}
>
<Layers className="mr-2 h-5 w-5" />
Add New Project
</Button>
}
/>
</div>
// Refresh projects list
await loadProjects()
{/* Data Table */}
<div>
<DataTable
title="Projects"
data={projects}
columns={columns}
onEdit={handleEdit}
onDelete={handleDelete}
isLoading={isLoading}
pagination={{
skip,
limit,
totalItems,
onPageChange: setSkip,
onLimitChange: (newLimit) => {
setLimit(newLimit);
setSkip(0); // Reset skip when limit changes
},
}}
/>
</div>
// Close modal and reset form immediately
setIsModalOpen(false)
resetForm()
} catch (err) {
const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} project`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally {
setIsSubmitting(false)
}
}
<PoweredBy />
</main>
const handleEdit = (project: Project) => {
setIsEditing(true)
setCurrentProject(project)
setName(project.name || "")
setState(project.state || "")
setCorridorName(project.corridor_name || "")
setStartLat(project.start_lat?.toString() || "")
setStartLng(project.start_lng?.toString() || "")
setEndLat(project.end_lat?.toString() || "")
setEndLng(project.end_lng?.toString() || "")
setIsModalOpen(true)
}
{/* 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 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">
<Layers className="h-5 w-5" />
</div>
{isEditing ? 'Edit Project Details' : 'Create New Project'}
</DialogTitle>
<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>
const handleDelete = async (project: Project) => {
if (!confirm(`Are you sure you want to delete project "${project.name}"?`)) return
<form onSubmit={handleSubmit} className="space-y-6">
{/* Project Name */}
<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="Enter a descriptive project name..."
className="h-11 bg-muted/20 border-border/60"
required
/>
</div>
try {
setIsLoading(true)
await projectService.deleteProject(project.id)
toast.success("Project Deleted", {
description: `${project.name} has been removed from the system.`,
})
await loadProjects()
} catch (err) {
setError("Failed to delete project")
toast.error("Deletion Failed", {
description: "The project could not be removed. Please try again or check your permissions.",
})
} finally {
setIsLoading(false)
}
}
const columns: ColumnDef<Project>[] = [
{
accessorKey: "name",
header: "Project Name",
cell: ({ row }) => (
<div className="font-semibold">{row.original.name}</div>
)
},
{
accessorKey: "state",
header: "State",
cell: ({ row }) => {
const project = row.original
if (!project.state) return <span></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span></span>
const firstState = states[0]
const remainingStates = states.slice(1)
return (
<div className="flex items-center gap-1.5">
<Badge
variant="secondary"
>
{firstState}
</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge
key={idx}
variant="secondary"
>
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
)
}
},
{
accessorKey: "corridor_name",
header: "Corridor",
},
]
return (
<>
<main className="relative z-10">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title="Project Management"
description="Manage road infrastructure projects"
icon={Layers}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<Layers className="mr-2 h-5 w-5" />
Add New Project
</Button>
}
/>
</div>
{/* Data Table */}
<div>
<DataTable
title="Projects"
data={projects}
columns={columns}
onEdit={handleEdit}
onDelete={handleDelete}
isLoading={isLoading}
pagination={{
skip,
limit,
totalItems,
onPageChange: setSkip,
onLimitChange: (newLimit) => {
setLimit(newLimit);
setSkip(0); // Reset skip when limit changes
}
}}
/>
</div>
<PoweredBy />
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
<DialogContent
className="max-w-2xl"
onOpenAutoFocus={(e) => e.preventDefault()}
{/* State & Corridor Row */}
<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"
>
<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">
<Layers className="h-5 w-5" />
</div>
{isEditing ? 'Edit Project Details' : 'Create New Project'}
</DialogTitle>
<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>
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="e.g. Maharashtra"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
<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="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-8 pt-2">
{/* 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-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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
<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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
</div>
</div>
{/* 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-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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
<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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
</div>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Project Name */}
<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="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-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="e.g. Maharashtra"
className="h-11 bg-muted/20 border-border/60"
/>
</div>
<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="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-8 pt-2">
{/* 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-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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
<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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
</div>
</div>
{/* 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-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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
<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="0.0000"
className="h-10 bg-muted/10 border-border/40 text-xs font-mono"
/>
</div>
</div>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-4 pt-2">
<Button
type="button"
variant="outline"
onClick={() => {
setIsModalOpen(false)
resetForm()
}}
disabled={isSubmitting}
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 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20"
>
{isSubmitting ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>{isEditing ? 'Updating...' : 'Creating...'}</span>
</div>
) : (
<div className="flex items-center gap-2">
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <Layers className="h-4 w-4" />}
<span>{isEditing ? 'Update Project' : 'Create Project'}</span>
</div>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
)
{/* Submit Button */}
<div className="flex gap-4 pt-2">
<Button
type="button"
variant="outline"
onClick={() => {
setIsModalOpen(false);
resetForm();
}}
disabled={isSubmitting}
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 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20"
>
{isSubmitting ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>{isEditing ? 'Updating...' : 'Creating...'}</span>
</div>
) : (
<div className="flex items-center gap-2">
{isEditing ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<Layers className="h-4 w-4" />
)}
<span>{isEditing ? 'Update Project' : 'Create Project'}</span>
</div>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -1,163 +1,175 @@
"use client"
'use client';
import { useState, useEffect } from "react"
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 { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import { sessionService, videoService } from "@/services/api"
import { SessionContext, DetectionData, DetectionType } from "@/types"
import { getVideoFile, clearVideoFile } from "@/lib/video-storage"
import { ROUTES } from "@/utils/routes"
import { Card } from "@/components/ui/card"
import { useState, useEffect } from 'react';
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 { PageHeader } from '@/components/page-header';
import { PoweredBy } from '@/components/powered-by';
import { sessionService, videoService } from '@/services/api';
import { SessionContext, DetectionData, DetectionType } from '@/types';
import { getVideoFile, clearVideoFile } from '@/lib/video-storage';
import { ROUTES } from '@/utils/routes';
import { Card } from '@/components/ui/card';
const API_URL = process.env.NEXT_PUBLIC_API_URL
const API_URL = process.env.NEXT_PUBLIC_API_URL;
export default function VideoResultsPage() {
const router = useRouter()
const { videoId } = useParams() as { videoId: string }
const [session, setSession] = useState<SessionContext | null>(null)
const [detectionData, setDetectionData] = useState<DetectionData | null>(null)
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
const [videoFile, setVideoFile] = useState<File | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const router = useRouter();
const { videoId } = useParams() as { videoId: string };
const [session, setSession] = useState<SessionContext | null>(null);
const [detectionData, setDetectionData] = useState<DetectionData | null>(null);
const [detectionType, setDetectionType] = useState<DetectionType>('pothole-detection');
const [videoFile, setVideoFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const storedSession = sessionService.loadSession()
setSession(storedSession)
useEffect(() => {
const storedSession = sessionService.loadSession();
setSession(storedSession);
const fetchResults = async () => {
try {
// Fetch detection data from backend
const data = await videoService.getVideoResults(videoId);
setDetectionData(data as any)
const fetchResults = async () => {
try {
// Fetch detection data from backend
const data = await videoService.getVideoResults(videoId);
setDetectionData(data as any);
// Try to infer detection type from results if possible
if (data.summary?.unique_signboards !== undefined && data.summary?.unique_signboards > 0) {
setDetectionType("sign-board-detection")
} else if (data.summary?.unique_potholes !== undefined && data.summary?.unique_potholes > 0) {
setDetectionType("pothole-detection")
}
// Retrieve video file from IndexedDB
const storedVideoFile = await getVideoFile(videoId)
if (storedVideoFile) {
setVideoFile(storedVideoFile)
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load results")
} finally {
setIsLoading(false)
}
// Try to infer detection type from results if possible
if (data.summary?.unique_signboards !== undefined && data.summary?.unique_signboards > 0) {
setDetectionType('sign-board-detection');
} else if (
data.summary?.unique_potholes !== undefined &&
data.summary?.unique_potholes > 0
) {
setDetectionType('pothole-detection');
}
if (videoId) {
fetchResults()
// Retrieve video file from IndexedDB
const storedVideoFile = await getVideoFile(videoId);
if (storedVideoFile) {
setVideoFile(storedVideoFile);
}
}, [videoId])
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load results');
} finally {
setIsLoading(false);
}
};
const handleNewAnalysis = async () => {
if (videoId) {
try {
await clearVideoFile(videoId)
} catch (err) {
console.error("Failed to clear video file:", err)
}
}
sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS)
if (videoId) {
fetchResults();
}
}, [videoId]);
const getTitle = () => {
if (detectionType === "pothole-detection") return "Pothole Detection Results"
if (detectionType === "sign-board-detection") return "Signboard Detection Results"
return "Pothole & Signboard Detection Results"
const handleNewAnalysis = async () => {
if (videoId) {
try {
await clearVideoFile(videoId);
} catch (err) {
console.error('Failed to clear video file:', err);
}
}
sessionService.clearSession();
router.push(ROUTES.NEW_ANALYSIS);
};
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 detection 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>
<div className="flex gap-4">
<Button onClick={() => router.push(ROUTES.UPLOAD)} variant="outline">Back to Upload</Button>
<Button onClick={handleNewAnalysis}>New Analysis</Button>
</div>
</Card>
</div>
)
}
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">
<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={`Video ID: ${videoId}`}
icon={TrendingUp}
/>
<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 detection 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>
<div className="flex gap-4">
<Button onClick={() => router.push(ROUTES.UPLOAD)} variant="outline">
Back to Upload
</Button>
<Button onClick={handleNewAnalysis}>New Analysis</Button>
</div>
</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={`Video ID: ${videoId}`} 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>
{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">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</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 && (
<VideoPlayerSection
data={detectionData}
videoId={videoId}
videoFile={videoFile}
detectionType={detectionType}
projectId={session?.projectId || undefined}
/>
)}
<PoweredBy />
<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">
Chainage
</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</span>
</div>
</div>
<Button
onClick={handleNewAnalysis}
variant="outline"
size="sm"
className="font-semibold px-6 shrink-0 h-9"
>
Start New Analysis
</Button>
</div>
</main>
</Card>
</div>
)}
{detectionData && (
<VideoPlayerSection
data={detectionData}
videoId={videoId}
videoFile={videoFile}
detectionType={detectionType}
projectId={session?.projectId || undefined}
/>
)}
<PoweredBy />
</div>
)
</main>
</div>
);
}

View File

@@ -1,144 +1,155 @@
"use client"
'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 { sessionService } from "@/services/api"
import { SessionContext, DetectionData, DetectionType } from "@/types"
import { clearVideoFile } from "@/lib/video-storage"
import { ROUTES } from "@/utils/routes"
import { Card } from "@/components/ui/card"
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 { sessionService } from '@/services/api';
import { SessionContext, DetectionData, DetectionType } from '@/types';
import { clearVideoFile } from '@/lib/video-storage';
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)
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 = sessionService.loadSession()
const videoData = sessionService.loadVideoData()
// Load session and video data on mount
useEffect(() => {
const storedSession = sessionService.loadSession();
const videoData = sessionService.loadVideoData();
if (!sessionService.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)
}
}
sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS)
if (!sessionService.isSessionValid(storedSession) || !videoData) {
router.replace(ROUTES.NEW_ANALYSIS);
return;
}
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 we have a videoId, redirect to the dynamic results page
if (videoData.videoId) {
router.replace(`${ROUTES.RESULTS}/${videoData.videoId}`);
return;
}
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>
)
}
setSession(storedSession);
}, [router]);
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>
)
const handleNewAnalysis = async () => {
// Clear video from IndexedDB
if (videoId) {
try {
await clearVideoFile(videoId);
} catch (err) {
console.error('Failed to clear video file:', err);
}
}
sessionService.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">
<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 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>
{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">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</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 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">
Chainage
</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</span>
</div>
</div>
<Button
onClick={handleNewAnalysis}
variant="outline"
size="sm"
className="font-semibold px-6 shrink-0 h-9"
>
Start New Analysis
</Button>
</div>
</main>
</Card>
</div>
)}
{detectionData && videoId && (
<VideoPlayerSection
data={detectionData}
videoId={videoId}
videoFile={videoFile}
detectionType={detectionType}
projectId={session?.projectId || undefined}
/>
)}
<PoweredBy />
</div>
)
</main>
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
'use client';
import { motion } from "motion/react";
import React from "react";
import { motion } from 'motion/react';
import React from 'react';
export default function Template({ children }: { children: React.ReactNode }) {
return (

View File

@@ -1,208 +1,210 @@
"use client"
'use client';
import { useState, useEffect, useCallback } from "react"
import { useRouter, useParams } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, TrendingUp } from "lucide-react"
import { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import { sessionService, videoService } from "@/services/api"
import { SessionContext } from "@/types"
import { ROUTES } from "@/utils/routes"
import { Button } from "@/components/ui/button"
import { useState, useEffect, useCallback } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2, TrendingUp } from 'lucide-react';
import { PageHeader } from '@/components/page-header';
import { PoweredBy } from '@/components/powered-by';
import { sessionService, videoService } from '@/services/api';
import { SessionContext } from '@/types';
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://")
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const WS_URL = API_URL?.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
export default function VideoProcessingPage() {
const router = useRouter()
const { videoId } = useParams() as { videoId: string }
const [session, setSession] = useState<SessionContext | null>(null)
const [isLoading, setIsLoading] = useState(true)
const router = useRouter();
const { videoId } = useParams() as { videoId: string };
const [session, setSession] = useState<SessionContext | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Processing states
const [progress, setProgress] = useState(0)
const [statusMessage, setStatusMessage] = useState("Initializing...")
const [error, setError] = useState<string | null>(null)
// Processing states
const [progress, setProgress] = useState(0);
const [statusMessage, setStatusMessage] = useState('Initializing...');
const [error, setError] = useState<string | null>(null);
const connectWebSocket = useCallback((vid: string) => {
const ws = new WebSocket(`${WS_URL}/ws/${vid}`)
const connectWebSocket = useCallback(
(vid: string) => {
const ws = new WebSocket(`${WS_URL}/ws/${vid}`);
ws.onmessage = async (event) => {
const data = JSON.parse(event.data)
ws.onmessage = async (event) => {
const data = JSON.parse(event.data);
if (data.type === "progress" || data.progress !== undefined) {
setProgress(data.progress || 0)
let message = data.message || "Processing..."
if (data.unique_potholes !== undefined) {
message += ` | Unique: ${data.unique_potholes} | Total: ${data.total_detections || 0}`
} else if (data.unique_signboards !== undefined) {
message += ` | Unique: ${data.unique_signboards} | Total: ${data.total_detections || 0}`
}
setStatusMessage(message)
}
if (data.type === "complete" || data.status === "completed") {
setStatusMessage("Processing completed! Finalizing...")
ws.close()
// Navigate to results
setTimeout(() => router.push(`/results/${vid}`), 1000)
}
if (data.type === "error") {
setError("Error: " + data.message)
setStatusMessage("")
ws.close()
}
if (data.type === 'progress' || data.progress !== undefined) {
setProgress(data.progress || 0);
let message = data.message || 'Processing...';
if (data.unique_potholes !== undefined) {
message += ` | Unique: ${data.unique_potholes} | Total: ${data.total_detections || 0}`;
} else if (data.unique_signboards !== undefined) {
message += ` | Unique: ${data.unique_signboards} | Total: ${data.total_detections || 0}`;
}
setStatusMessage(message);
}
ws.onerror = () => {
setStatusMessage("Connection lost. Reconnecting...")
setTimeout(() => connectWebSocket(vid), 3000)
if (data.type === 'complete' || data.status === 'completed') {
setStatusMessage('Processing completed! Finalizing...');
ws.close();
// Navigate to results
setTimeout(() => router.push(`/results/${vid}`), 1000);
}
return ws
}, [router])
if (data.type === 'error') {
setError('Error: ' + data.message);
setStatusMessage('');
ws.close();
}
};
useEffect(() => {
const storedSession = sessionService.loadSession()
setSession(storedSession)
ws.onerror = () => {
setStatusMessage('Connection lost. Reconnecting...');
setTimeout(() => connectWebSocket(vid), 3000);
};
const checkStatus = async () => {
try {
const statusData = await videoService.getVideoStatus(videoId);
return ws;
},
[router],
);
if (statusData.status === "completed") {
router.replace(`/results/${videoId}`)
return
}
useEffect(() => {
const storedSession = sessionService.loadSession();
setSession(storedSession);
if (statusData.status === "error") {
setError(statusData.message || "An error occurred during processing.")
setIsLoading(false)
return
}
const checkStatus = async () => {
try {
const statusData = await videoService.getVideoStatus(videoId);
// If processing, start WebSocket
setProgress(statusData.progress || 0)
setStatusMessage(statusData.message || "Resuming processing...")
connectWebSocket(videoId)
setIsLoading(false)
} catch (err) {
console.error("Status check failed:", err)
setError("Failed to connect to server.")
setIsLoading(false)
}
if (statusData.status === 'completed') {
router.replace(`/results/${videoId}`);
return;
}
if (videoId) {
checkStatus()
if (statusData.status === 'error') {
setError(statusData.message || 'An error occurred during processing.');
setIsLoading(false);
return;
}
}, [videoId, router, connectWebSocket])
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</Card>
</div>
)
// If processing, start WebSocket
setProgress(statusData.progress || 0);
setStatusMessage(statusData.message || 'Resuming processing...');
connectWebSocket(videoId);
setIsLoading(false);
} catch (err) {
console.error('Status check failed:', err);
setError('Failed to connect to server.');
setIsLoading(false);
}
};
if (videoId) {
checkStatus();
}
}, [videoId, router, connectWebSocket]);
if (isLoading) {
return (
<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"
description={`Real-time analysis progress for video ID: ${videoId}`}
icon={TrendingUp}
/>
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</Card>
</div>
);
}
return (
<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"
description={`Real-time analysis progress for video ID: ${videoId}`}
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 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>
{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 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">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</span>
</div>
</div>
</div>
</Card>
</div>
)}
<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-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-muted-foreground text-xs uppercase tracking-widest">Progress</span>
<span className="font-bold text-primary text-lg">{progress}%</span>
</div>
<div className="h-4 rounded-full bg-secondary overflow-hidden border">
<div
className="h-full bg-primary transition-all duration-1000 ease-in-out"
style={{ width: `${progress}%` }}
/>
</div>
<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-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()}
>
Retry Connection
</Button>
</div>
)}
</div>
</Card>
<PoweredBy />
<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">
Chainage
</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</span>
</div>
</div>
</div>
</main>
</Card>
</div>
)}
<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-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-muted-foreground text-xs uppercase tracking-widest">
Progress
</span>
<span className="font-bold text-primary text-lg">{progress}%</span>
</div>
<div className="h-4 rounded-full bg-secondary overflow-hidden border">
<div
className="h-full bg-primary transition-all duration-1000 ease-in-out"
style={{ width: `${progress}%` }}
/>
</div>
<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-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()}>
Retry Connection
</Button>
</div>
)}
</div>
</Card>
<PoweredBy />
</div>
)
</main>
</div>
);
}

View File

@@ -1,336 +1,341 @@
"use client"
'use client';
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
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 { PageHeader } from "@/components/page-header"
import { PoweredBy } from "@/components/powered-by"
import { sessionService, videoService } from "@/services/api"
import { SessionContext } from "@/types"
import { ROUTES } from "@/utils/routes"
import { storeVideoFile } from "@/lib/video-storage"
import { cn } from "@/lib/utils"
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
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 { PageHeader } from '@/components/page-header';
import { PoweredBy } from '@/components/powered-by';
import { sessionService, videoService } from '@/services/api';
import { SessionContext } from '@/types';
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 API_URL = process.env.NEXT_PUBLIC_API_URL;
const DETECTION_TYPES = [
{ value: "pothole-detection", label: "Pothole Detection" },
{ value: "sign-board-detection", label: "Signboard Detection" },
{ value: "pot-sign-detection", label: "Pothole & Signboard Detection" },
] as const
{ value: 'pothole-detection', label: 'Pothole Detection' },
{ value: 'sign-board-detection', label: 'Signboard Detection' },
{ value: 'pot-sign-detection', label: 'Pothole & Signboard Detection' },
] as const;
const DETECTION_METHODS = [
{ value: "yolo", label: "YOLO Detection Model" },
{ value: "yolo_vl", label: "YOLO with Vision-Language Model" },
{ value: "sam3", label: "OpenAI SAM 3 Segmentation Model" },
{ value: "yoloe", label: "YOLOE Open-Vocabulary Detection" },
{ value: "yoloe_trained_vl", label: "YOLOE With Vision Language Model" },
] as const
{ value: 'yolo', label: 'YOLO Detection Model' },
{ value: 'yolo_vl', label: 'YOLO with Vision-Language Model' },
{ value: 'sam3', label: 'OpenAI SAM 3 Segmentation Model' },
{ value: 'yoloe', label: 'YOLOE Open-Vocabulary Detection' },
{ value: 'yoloe_trained_vl', label: 'YOLOE With Vision Language Model' },
] as const;
type DetectionType = "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
type DetectionType = 'pothole-detection' | 'sign-board-detection' | 'pot-sign-detection';
export default function UploadPage() {
const router = useRouter()
const [session, setSession] = useState<SessionContext | null>(null)
const [isLoading, setIsLoading] = useState(true)
const router = useRouter();
const [session, setSession] = useState<SessionContext | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Form states
const [file, setFile] = useState<File | null>(null)
const [jsonFile, setJsonFile] = useState<File | null>(null)
const [speed, setSpeed] = useState(30)
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
const [selectMethod, setSelectMethod] = useState("yolo_vl")
// Form states
const [file, setFile] = useState<File | null>(null);
const [jsonFile, setJsonFile] = useState<File | null>(null);
const [speed, setSpeed] = useState(30);
const [detectionType, setDetectionType] = useState<DetectionType>('pothole-detection');
const [selectMethod, setSelectMethod] = useState('yolo_vl');
// Upload states
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const [statusMessage, setStatusMessage] = useState("")
const [error, setError] = useState<string | null>(null)
// Upload states
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [statusMessage, setStatusMessage] = useState('');
const [error, setError] = useState<string | null>(null);
// Load session on mount
useEffect(() => {
const storedSession = sessionService.loadSession()
if (!sessionService.isSessionValid(storedSession)) {
router.replace(ROUTES.NEW_ANALYSIS)
return
}
setSession(storedSession)
setIsLoading(false)
}, [router])
// Load session on mount
useEffect(() => {
const storedSession = sessionService.loadSession();
if (!sessionService.isSessionValid(storedSession)) {
router.replace(ROUTES.NEW_ANALYSIS);
return;
}
setSession(storedSession);
setIsLoading(false);
}, [router]);
const handleUpload = async () => {
if (!file) {
setError("Please select a video file")
return
}
const handleUpload = async () => {
if (!file) {
setError('Please select a video file');
return;
}
const formData = new FormData()
formData.append("file", file)
formData.append("detection_type", detectionType)
formData.append("speed_kmh", speed.toString())
formData.append("detection_mode", selectMethod)
if (jsonFile) {
formData.append("json_file", jsonFile)
}
const formData = new FormData();
formData.append('file', file);
formData.append('detection_type', detectionType);
formData.append('speed_kmh', speed.toString());
formData.append('detection_mode', selectMethod);
if (jsonFile) {
formData.append('json_file', jsonFile);
}
setUploading(true)
setProgress(0)
setStatusMessage("Uploading...")
setError(null)
setUploading(true);
setProgress(0);
setStatusMessage('Uploading...');
setError(null);
try {
const result = await videoService.uploadVideo(formData);
// Store file locally for potential recovery/results display
if (file) {
try {
const result = await videoService.uploadVideo(formData);
// Store file locally for potential recovery/results display
if (file) {
try {
await storeVideoFile(result.video_id, file)
} catch (err) {
console.error("Failed to store video file:", err)
}
}
sessionService.saveVideoData({ videoId: result.video_id, detectionType })
// Redirect to the dynamic processing page
router.push(`/upload/${result.video_id}`)
await storeVideoFile(result.video_id, file);
} catch (err) {
let errorMessage = "Upload failed"
if (err instanceof TypeError && err.message === "Failed to fetch") {
errorMessage = "Cannot connect to server. Please check if backend is running."
} else if (err instanceof Error) {
errorMessage = err.message
}
setError(errorMessage)
setStatusMessage("")
setUploading(false)
setProgress(0)
console.error('Failed to store video file:', err);
}
}
}
const handleBackToSelection = () => {
sessionService.clearSession()
router.push(ROUTES.NEW_ANALYSIS)
}
sessionService.saveVideoData({ videoId: result.video_id, detectionType });
const getTitle = () => {
if (detectionType === "pothole-detection") return "Pothole Detection"
if (detectionType === "sign-board-detection") return "Signboard Detection"
return "Pothole & Signboard Detection"
// Redirect to the dynamic processing page
router.push(`/upload/${result.video_id}`);
} catch (err) {
let errorMessage = 'Upload failed';
if (err instanceof TypeError && err.message === 'Failed to fetch') {
errorMessage = 'Cannot connect to server. Please check if backend is running.';
} else if (err instanceof Error) {
errorMessage = err.message;
}
setError(errorMessage);
setStatusMessage('');
setUploading(false);
setProgress(0);
}
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</Card>
</div>
)
}
const handleBackToSelection = () => {
sessionService.clearSession();
router.push(ROUTES.NEW_ANALYSIS);
};
const getTitle = () => {
if (detectionType === 'pothole-detection') return 'Pothole Detection';
if (detectionType === 'sign-board-detection') return 'Signboard Detection';
return 'Pothole & Signboard Detection';
};
if (isLoading) {
return (
<div className="min-h-screen">
{/* Main Content */}
<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()}
description="Upload video file and fill in required details to start the road analysis"
icon={TrendingUp}
/>
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</Card>
</div>
);
}
return (
<div className="min-h-screen">
{/* Main Content */}
<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()}
description="Upload video file and fill in required details to start the road analysis"
icon={TrendingUp}
/>
</div>
{/* Compact Session Info Bar */}
{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>
{/* Compact Session Info Bar */}
{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">Chainage</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</span>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleBackToSelection}
className="font-semibold px-6 shrink-0 h-9"
>
Change Selection
</Button>
</div>
</Card>
</div>
)}
{/* Upload Card */}
<Card className="flex-1">
<CardHeader className="pb-4">
<CardTitle className="text-xl font-bold">
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-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">
Video File
</Label>
<Input
id="video-file"
type="file"
accept="video/*"
onChange={(e) => {
setFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
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>
{/* JSON File Input */}
<div className="space-y-2">
<Label htmlFor="json-file" className="text-sm font-semibold">
GPS JSON File
</Label>
<Input
id="json-file"
type="file"
accept=".json,application/json"
onChange={(e) => {
setJsonFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
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-6">
{/* Detection Type */}
<div className="space-y-2">
<Label htmlFor="detection-type" className="text-sm font-semibold">
Detection Type
</Label>
<Select
value={detectionType}
onValueChange={(v) => setDetectionType(v as DetectionType)}
disabled={uploading}
>
<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}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Speed Input */}
<div className="space-y-2">
<Label htmlFor="speed" className="text-sm font-semibold">
Vehicle Speed (km/h)
</Label>
<Input
id="speed"
type="number"
min={1}
max={200}
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
disabled={uploading}
className="h-11"
/>
</div>
{/* Select Method */}
<div className="space-y-2">
<Label htmlFor="select-method" className="text-sm font-semibold">
Select Method
</Label>
<Select
value={selectMethod}
onValueChange={setSelectMethod}
disabled={uploading}
>
<SelectTrigger id="select-method" className="h-11">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{DETECTION_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
{method.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Error Display */}
{error && (
<div className="p-4 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm leading-relaxed whitespace-pre-line">
{error}
</div>
)}
{/* Upload Button */}
<Button
onClick={handleUpload}
disabled={!file || uploading}
className="w-full h-14 text-base font-bold uppercase tracking-wide"
size="lg"
>
{uploading ? (
<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>
<PoweredBy />
<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">
Chainage
</span>
<span className="text-sm font-semibold text-muted-foreground leading-tight">
{session.chainageName}
</span>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleBackToSelection}
className="font-semibold px-6 shrink-0 h-9"
>
Change Selection
</Button>
</div>
</main>
</Card>
</div>
)}
{/* Upload Card */}
<Card className="flex-1">
<CardHeader className="pb-4">
<CardTitle className="text-xl font-bold">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-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">
Video File
</Label>
<Input
id="video-file"
type="file"
accept="video/*"
onChange={(e) => {
setFile(e.target.files?.[0] || null);
setError(null);
}}
disabled={uploading}
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>
{/* JSON File Input */}
<div className="space-y-2">
<Label htmlFor="json-file" className="text-sm font-semibold">
GPS JSON File
</Label>
<Input
id="json-file"
type="file"
accept=".json,application/json"
onChange={(e) => {
setJsonFile(e.target.files?.[0] || null);
setError(null);
}}
disabled={uploading}
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-6">
{/* Detection Type */}
<div className="space-y-2">
<Label htmlFor="detection-type" className="text-sm font-semibold">
Detection Type
</Label>
<Select
value={detectionType}
onValueChange={(v) => setDetectionType(v as DetectionType)}
disabled={uploading}
>
<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}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Speed Input */}
<div className="space-y-2">
<Label htmlFor="speed" className="text-sm font-semibold">
Vehicle Speed (km/h)
</Label>
<Input
id="speed"
type="number"
min={1}
max={200}
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
disabled={uploading}
className="h-11"
/>
</div>
{/* Select Method */}
<div className="space-y-2">
<Label htmlFor="select-method" className="text-sm font-semibold">
Select Method
</Label>
<Select value={selectMethod} onValueChange={setSelectMethod} disabled={uploading}>
<SelectTrigger id="select-method" className="h-11">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{DETECTION_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
{method.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Error Display */}
{error && (
<div className="p-4 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm leading-relaxed whitespace-pre-line">
{error}
</div>
)}
{/* Upload Button */}
<Button
onClick={handleUpload}
disabled={!file || uploading}
className="w-full h-14 text-base font-bold uppercase tracking-wide"
size="lg"
>
{uploading ? (
<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>
<PoweredBy />
</div>
)
</main>
</div>
);
}

View File

@@ -1,5 +1,5 @@
@import "tailwindcss";
@import "tw-animate-css";
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@@ -72,10 +72,9 @@
--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;
--font-sans: 'Geist', 'Geist Fallback', system-ui, sans-serif;
--font-mono: 'Geist Mono', 'Geist Mono Fallback', monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -124,7 +123,7 @@
@apply bg-transparent text-foreground;
min-height: 100vh;
}
/* Modern Scrollbar Styles */
::-webkit-scrollbar {
width: 8px; /* Slightly wider for better accessibility */
@@ -148,4 +147,3 @@
scrollbar-color: var(--muted-foreground) transparent;
}
}

View File

@@ -1,23 +1,23 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner"
import { BackgroundGradient } from "@/components/background-gradient";
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/sonner';
import { BackgroundGradient } from '@/components/background-gradient';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
@@ -27,9 +27,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<ThemeProvider
attribute="class"
defaultTheme="system"
@@ -40,7 +38,7 @@ export default function RootLayout({
<BackgroundGradient />
{children}
</div>
<Toaster position="top-center" />
<Toaster position="top-center" />
</ThemeProvider>
</body>
</html>