changed full frontend,added new features, and components.

This commit is contained in:
sumona-banerjeee
2026-02-11 11:17:22 +05:30
parent ae16cec5ff
commit 3c3aef7d82
24 changed files with 2144 additions and 379 deletions

View File

@@ -0,0 +1,543 @@
"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, MapPin, Navigation, Milestone } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { DataTable } from "@/components/data-table"
import {
fetchProjects,
fetchPackagesByProject,
fetchAllLocations,
fetchAllPackages,
createLocation,
type Project,
type Package as PackageType,
type Location,
type LocationCreate
} from "@/lib/api"
export default function CreateLocationPage() {
const [locations, setLocations] = useState<Location[]>([])
const [projects, setProjects] = useState<Project[]>([])
const [packages, setPackages] = useState<PackageType[]>([])
const [allPackages, setAllPackages] = useState<PackageType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState<string | null>(null)
const [loadingProjects, setLoadingProjects] = useState(false)
const [loadingPackages, setLoadingPackages] = useState(false)
// Form fields
const [selectedProjectId, setSelectedProjectId] = useState("")
const [selectedPackageId, setSelectedPackageId] = useState("")
const [segmentName, setSegmentName] = useState("")
const [chainageStartKm, setChainageStartKm] = useState("")
const [chainageEndKm, setChainageEndKm] = useState("")
const [startLat, setStartLat] = useState("")
const [startLng, setStartLng] = useState("")
const [endLat, setEndLat] = useState("")
const [endLng, setEndLng] = useState("")
// Load locations and projects
const loadLocations = async () => {
try {
setIsLoading(true)
setError(null)
const data = await fetchAllLocations()
setLocations(data)
} catch (err) {
setError("Failed to load locations. Please check if the backend is running.")
} finally {
setIsLoading(false)
}
}
const loadProjects = async () => {
try {
setLoadingProjects(true)
const data = await fetchProjects()
setProjects(data)
} catch (err) {
setError("Failed to load projects.")
} finally {
setLoadingProjects(false)
}
}
const loadAllPackages = async () => {
try {
const data = await fetchAllPackages()
setAllPackages(data)
} catch (err) {
console.error("Failed to load all packages")
}
}
useEffect(() => {
loadLocations()
loadProjects()
loadAllPackages()
}, [])
// Load packages when project changes
useEffect(() => {
if (!selectedProjectId) {
setPackages([])
setSelectedPackageId("")
return
}
const loadPackages = async () => {
try {
setLoadingPackages(true)
setSelectedPackageId("")
const data = await fetchPackagesByProject(selectedProjectId)
setPackages(data)
} catch (err) {
setError("Failed to load packages for the selected project.")
} finally {
setLoadingPackages(false)
}
}
loadPackages()
}, [selectedProjectId])
const resetForm = () => {
setSelectedProjectId("")
setSelectedPackageId("")
setSegmentName("")
setChainageStartKm("")
setChainageEndKm("")
setStartLat("")
setStartLng("")
setEndLat("")
setEndLng("")
setError(null)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!selectedPackageId) {
setError("Please select a project and package first")
return
}
if (!segmentName.trim()) {
setError("Segment name is required")
return
}
if (!startLat || !startLng || !endLat || !endLng) {
setError("All GPS coordinates are required for locations")
return
}
setIsSubmitting(true)
setError(null)
try {
const data: LocationCreate = {
package_id: selectedPackageId,
segment_name: segmentName.trim(),
chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : null,
chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : null,
start_lat: parseFloat(startLat),
start_lng: parseFloat(startLng),
end_lat: parseFloat(endLat),
end_lng: parseFloat(endLng),
}
await createLocation(data)
setSuccess(true)
// Refresh locations list
await loadLocations()
setTimeout(() => {
resetForm()
setSuccess(false)
setIsModalOpen(false)
}, 2000)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create location")
} finally {
setIsSubmitting(false)
}
}
const getPackageName = (packageId: string) => {
return allPackages.find(p => p.id === packageId)?.name || packageId
}
const selectedProject = projects.find(p => p.id === selectedProjectId)
const selectedPackage = packages.find(p => p.id === selectedPackageId)
const isFormComplete = selectedPackageId && segmentName.trim() && startLat && startLng && endLat && endLng
const columns = [
{
key: "segment_name",
header: "Segment Name",
render: (location: Location) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{location.segment_name}</div>
)
},
{
key: "package_id",
header: "Package",
render: (location: Location) => getPackageName(location.package_id)
},
{
key: "chainage",
header: "Chainage (km)",
render: (location: Location) => {
if (location.chainage_start_km && location.chainage_end_km) {
return `${location.chainage_start_km} - ${location.chainage_end_km}`
}
return "—"
}
},
{
key: "created_at",
header: "Created",
render: (location: Location) => new Date(location.created_at).toLocaleDateString()
},
]
return (
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-16 min-h-screen relative overflow-hidden">
<div className="mx-auto px-4 py-8 max-w-6xl relative z-10">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-left duration-700">
<div className="flex items-center gap-6">
<div className="flex-shrink-0 w-16 h-16 rounded-2xl bg-white shadow-xl border-2 border-[#1e40af] transition-transform duration-300 hover:scale-110 flex items-center justify-center">
<MapPin className="h-8 w-8 text-[#2563eb]" />
</div>
<div className="flex flex-col">
<h1 className="text-3xl md:text-4xl font-extrabold text-[#2563eb] tracking-tight">
Locations
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium italic">
Manage road segment locations
</p>
</div>
</div>
</div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
{/* Data Table */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-150">
<DataTable
title="All Locations"
data={locations}
columns={columns}
onAddNew={() => setIsModalOpen(true)}
addButtonText="Add New Location"
isLoading={isLoading}
/>
</div>
{/* Footer */}
<div className="mt-8 text-center animate-in fade-in duration-700 delay-300">
<p className="text-xs text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MapPin className="h-5 w-5 text-amber-500" />
Create New Location
</DialogTitle>
<DialogDescription>
Select project & package, then fill in the location details
</DialogDescription>
</DialogHeader>
{/* Success Message in Modal */}
{success && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30 border border-amber-200 dark:border-amber-800 animate-in fade-in slide-in-from-top duration-300">
<CheckCircle2 className="h-5 w-5 text-amber-500 flex-shrink-0" />
<p className="text-sm font-medium text-amber-700 dark:text-amber-400">Location created successfully!</p>
</div>
)}
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Step 1: Select Project */}
<div className="p-4 rounded-xl bg-gradient-to-r from-amber-50/50 to-orange-50/50 dark:from-amber-950/20 dark:to-orange-950/20 border border-amber-100 dark:border-amber-900/50">
<div className="flex items-center gap-2 mb-3">
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
<span className="text-white text-xs font-bold">1</span>
</div>
<p className="text-sm font-bold text-amber-700 dark:text-amber-400">Select Project</p>
</div>
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
<SelectTrigger className="h-11 bg-white dark:bg-gray-800 border-amber-200 dark:border-amber-800 focus:border-amber-400">
{loadingProjects ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-amber-500" />
<span className="text-gray-400">Loading projects...</span>
</div>
) : (
<SelectValue placeholder="Choose a project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map(project => (
<SelectItem key={project.id} value={project.id}>
<span className="font-medium">{project.name}</span>
{project.state && <span className="text-gray-400 ml-2">({project.state})</span>}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedProject && (
<div className="mt-2 px-3 py-1.5 rounded-lg bg-amber-100/50 dark:bg-amber-900/20 text-xs text-amber-600 dark:text-amber-400">
Selected: <span className="font-semibold">{selectedProject.name}</span>
</div>
)}
</div>
{/* Step 2: Select Package */}
<div className={`p-4 rounded-xl bg-gradient-to-r from-orange-50/50 to-rose-50/50 dark:from-orange-950/20 dark:to-rose-950/20 border border-orange-100 dark:border-orange-900/50 transition-opacity duration-300 ${selectedProjectId ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
<div className="flex items-center gap-2 mb-3">
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${selectedProjectId ? 'bg-gradient-to-br from-orange-500 to-rose-600' : 'bg-gray-300 dark:bg-gray-600'}`}>
<span className="text-white text-xs font-bold">2</span>
</div>
<p className="text-sm font-bold text-orange-700 dark:text-orange-400">Select Package</p>
</div>
<Select value={selectedPackageId} onValueChange={setSelectedPackageId} disabled={!selectedProjectId}>
<SelectTrigger className="h-11 bg-white dark:bg-gray-800 border-orange-200 dark:border-orange-800 focus:border-orange-400">
{loadingPackages ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-orange-500" />
<span className="text-gray-400">Loading packages...</span>
</div>
) : (
<SelectValue placeholder={selectedProjectId ? "Choose a package" : "Select project first"} />
)}
</SelectTrigger>
<SelectContent>
{packages.map(pkg => (
<SelectItem key={pkg.id} value={pkg.id}>
<span className="font-medium">{pkg.name}</span>
{pkg.region && <span className="text-gray-400 ml-2">({pkg.region})</span>}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedPackage && (
<div className="mt-2 px-3 py-1.5 rounded-lg bg-orange-100/50 dark:bg-orange-900/20 text-xs text-orange-600 dark:text-orange-400">
Selected: <span className="font-semibold">{selectedPackage.name}</span>
{selectedPackage.region && `${selectedPackage.region}`}
</div>
)}
</div>
{/* Step 3: Location Details */}
<div className={`space-y-5 transition-opacity duration-300 ${selectedPackageId ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
<div className="flex items-center gap-2">
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${selectedPackageId ? 'bg-gradient-to-br from-rose-500 to-red-600' : 'bg-gray-300 dark:bg-gray-600'}`}>
<span className="text-white text-xs font-bold">3</span>
</div>
<p className="text-sm font-bold text-gray-700 dark:text-gray-300">Location Information</p>
</div>
{/* Segment Name */}
<div className="space-y-2">
<Label htmlFor="segment" className="text-sm font-semibold flex items-center gap-1">
Segment Name <span className="text-red-500">*</span>
</Label>
<Input
id="segment"
value={segmentName}
onChange={(e) => setSegmentName(e.target.value)}
placeholder="e.g., KM 120 to KM 135"
className="h-11"
required
/>
</div>
{/* Chainage */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="ch-start" className="text-sm font-semibold flex items-center gap-1">
<Milestone className="h-3.5 w-3.5 text-gray-400" />
Chainage Start (km) <span className="text-gray-400 font-normal text-xs">(Opt)</span>
</Label>
<Input
id="ch-start"
type="number"
step="any"
min="0"
value={chainageStartKm}
onChange={(e) => setChainageStartKm(e.target.value)}
placeholder="0"
className="h-10 text-sm"
/>
</div>
<div className="space-y-2">
<Label htmlFor="ch-end" className="text-sm font-semibold flex items-center gap-1">
<Milestone className="h-3.5 w-3.5 text-gray-400" />
Chainage End (km) <span className="text-gray-400 font-normal text-xs">(Opt)</span>
</Label>
<Input
id="ch-end"
type="number"
step="any"
min="0"
value={chainageEndKm}
onChange={(e) => setChainageEndKm(e.target.value)}
placeholder="0"
className="h-10 text-sm"
/>
</div>
</div>
{/* GPS Coordinates */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 shadow-md shadow-amber-500/20">
<MapPin className="h-4 w-4 text-white" />
</div>
<h3 className="text-sm font-bold text-gray-700 dark:text-gray-300">GPS Coordinates</h3>
<span className="text-xs text-red-500 font-medium">Required</span>
</div>
{/* Start Point */}
<div className="p-4 rounded-xl bg-gradient-to-r from-amber-50/50 to-yellow-50/50 dark:from-amber-950/20 dark:to-yellow-950/20 border border-amber-100 dark:border-amber-900/50">
<p className="text-xs font-semibold text-amber-600 dark:text-amber-400 mb-3 uppercase tracking-wide">Start Point</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label htmlFor="s-lat" className="text-xs text-gray-500">Latitude *</Label>
<Input
id="s-lat"
type="number"
step="any"
value={startLat}
onChange={(e) => setStartLat(e.target.value)}
placeholder="-90 to 90"
className="h-10 text-sm"
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="s-lng" className="text-xs text-gray-500">Longitude *</Label>
<Input
id="s-lng"
type="number"
step="any"
value={startLng}
onChange={(e) => setStartLng(e.target.value)}
placeholder="-180 to 180"
className="h-10 text-sm"
required
/>
</div>
</div>
</div>
{/* End Point */}
<div className="p-4 rounded-xl bg-gradient-to-r from-orange-50/50 to-red-50/50 dark:from-orange-950/20 dark:to-red-950/20 border border-orange-100 dark:border-orange-900/50">
<p className="text-xs font-semibold text-orange-600 dark:text-orange-400 mb-3 uppercase tracking-wide">End Point</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label htmlFor="e-lat" className="text-xs text-gray-500">Latitude *</Label>
<Input
id="e-lat"
type="number"
step="any"
value={endLat}
onChange={(e) => setEndLat(e.target.value)}
placeholder="-90 to 90"
className="h-10 text-sm"
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="e-lng" className="text-xs text-gray-500">Longitude *</Label>
<Input
id="e-lng"
type="number"
step="any"
value={endLng}
onChange={(e) => setEndLng(e.target.value)}
placeholder="-180 to 180"
className="h-10 text-sm"
required
/>
</div>
</div>
</div>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={() => {
setIsModalOpen(false)
resetForm()
}}
disabled={isSubmitting}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !isFormComplete}
className="flex-1 bg-gradient-to-r from-amber-500 via-orange-500 to-red-600 hover:from-amber-600 hover:via-orange-600 hover:to-red-700 text-white"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</span>
) : (
<span className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
Create Location
</span>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
)
}

340
app/create-package/page.tsx Normal file
View File

@@ -0,0 +1,340 @@
"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 { SidebarNavigation } from "@/components/sidebar-navigation"
import { DataTable } from "@/components/data-table"
import {
fetchProjects,
fetchAllPackages,
createPackage,
type Project,
type Package as PackageType,
type PackageCreate
} from "@/lib/api"
export default function CreatePackagePage() {
const [packages, setPackages] = useState<PackageType[]>([])
const [projects, setProjects] = useState<Project[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState<string | null>(null)
const [loadingProjects, setLoadingProjects] = useState(false)
// Form fields
const [selectedProjectId, setSelectedProjectId] = useState("")
const [name, setName] = useState("")
const [region, setRegion] = useState("")
// Load packages and projects
const loadPackages = async () => {
try {
setIsLoading(true)
setError(null)
const data = await fetchAllPackages()
setPackages(data)
} catch (err) {
setError("Failed to load packages. Please check if the backend is running.")
} finally {
setIsLoading(false)
}
}
const loadProjects = async () => {
try {
setLoadingProjects(true)
const data = await fetchProjects()
setProjects(data)
} catch (err) {
setError("Failed to load projects.")
} finally {
setLoadingProjects(false)
}
}
useEffect(() => {
loadPackages()
loadProjects()
}, [])
const resetForm = () => {
setSelectedProjectId("")
setName("")
setRegion("")
setError(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
}
setIsSubmitting(true)
setError(null)
try {
const data: PackageCreate = {
project_id: selectedProjectId,
name: name.trim(),
region: region.trim() || null,
}
await createPackage(data)
setSuccess(true)
// Refresh packages list
await loadPackages()
setTimeout(() => {
resetForm()
setSuccess(false)
setIsModalOpen(false)
}, 2000)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create package")
} finally {
setIsSubmitting(false)
}
}
const getProjectName = (projectId: string) => {
return projects.find(p => p.id === projectId)?.name || projectId
}
const selectedProject = projects.find(p => p.id === selectedProjectId)
const columns = [
{
key: "name",
header: "Package Name",
render: (pkg: PackageType) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{pkg.name}</div>
)
},
{
key: "project_id",
header: "Project",
render: (pkg: PackageType) => getProjectName(pkg.project_id)
},
{
key: "region",
header: "Region",
},
{
key: "created_at",
header: "Created",
render: (pkg: PackageType) => new Date(pkg.created_at).toLocaleDateString()
},
]
return (
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-16 min-h-screen relative overflow-hidden">
<div className="mx-auto px-4 py-8 max-w-6xl relative z-10">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-left duration-700">
<div className="flex items-center gap-6">
<div className="flex-shrink-0 w-16 h-16 rounded-2xl bg-white shadow-xl border-2 border-[#1e40af] transition-transform duration-300 hover:scale-110 flex items-center justify-center">
<Package className="h-8 w-8 text-[#2563eb]" />
</div>
<div className="flex flex-col">
<h1 className="text-3xl md:text-4xl font-extrabold text-[#2563eb] tracking-tight">
Packages
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium italic">
Manage project packages
</p>
</div>
</div>
</div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
{/* Data Table */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-150">
<DataTable
title="All Packages"
data={packages}
columns={columns}
onAddNew={() => setIsModalOpen(true)}
addButtonText="Add New Package"
isLoading={isLoading}
/>
</div>
{/* Footer */}
<div className="mt-8 text-center animate-in fade-in duration-700 delay-300">
<p className="text-xs text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package className="h-5 w-5 text-violet-500" />
Create New Package
</DialogTitle>
<DialogDescription>
Select a project and fill in the package details
</DialogDescription>
</DialogHeader>
{/* Success Message in Modal */}
{success && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-gradient-to-r from-violet-50 to-purple-50 dark:from-violet-950/30 dark:to-purple-950/30 border border-violet-200 dark:border-violet-800 animate-in fade-in slide-in-from-top duration-300">
<CheckCircle2 className="h-5 w-5 text-violet-500 flex-shrink-0" />
<p className="text-sm font-medium text-violet-700 dark:text-violet-400">Package created successfully!</p>
</div>
)}
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Step 1: Select Project */}
<div className="p-4 rounded-xl bg-gradient-to-r from-violet-50/50 to-purple-50/50 dark:from-violet-950/20 dark:to-purple-950/20 border border-violet-100 dark:border-violet-900/50">
<div className="flex items-center gap-2 mb-3">
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
<span className="text-white text-xs font-bold">1</span>
</div>
<p className="text-sm font-bold text-violet-700 dark:text-violet-400">Select Project</p>
</div>
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
<SelectTrigger className="h-11 bg-white dark:bg-gray-800 border-violet-200 dark:border-violet-800 focus:border-violet-400">
{loadingProjects ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-violet-500" />
<span className="text-gray-400">Loading projects...</span>
</div>
) : (
<SelectValue placeholder="Choose a project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map(project => (
<SelectItem key={project.id} value={project.id}>
<span className="font-medium">{project.name}</span>
{project.state && <span className="text-gray-400 ml-2">({project.state})</span>}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedProject && (
<div className="mt-2 px-3 py-1.5 rounded-lg bg-violet-100/50 dark:bg-violet-900/20 text-xs text-violet-600 dark:text-violet-400">
Selected: <span className="font-semibold">{selectedProject.name}</span>
{selectedProject.corridor_name && `${selectedProject.corridor_name}`}
</div>
)}
</div>
{/* Step 2: Package Info */}
<div className={`space-y-4 transition-opacity duration-300 ${selectedProjectId ? 'opacity-100' : 'opacity-40 pointer-events-none'}`}>
<div className="flex items-center gap-2">
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${selectedProjectId ? 'bg-gradient-to-br from-violet-500 to-purple-600' : 'bg-gray-300 dark:bg-gray-600'}`}>
<span className="text-white text-xs font-bold">2</span>
</div>
<p className="text-sm font-bold text-gray-700 dark:text-gray-300">Package Information</p>
</div>
<div className="space-y-2">
<Label htmlFor="pkg-name" className="text-sm font-semibold flex items-center gap-1">
Package Name <span className="text-red-500">*</span>
</Label>
<Input
id="pkg-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Package A - Section 1"
className="h-11"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="region" className="text-sm font-semibold flex items-center gap-1">
<Globe className="h-3.5 w-3.5 text-gray-400" />
Region <span className="text-gray-400 font-normal text-xs">(Optional)</span>
</Label>
<Input
id="region"
value={region}
onChange={(e) => setRegion(e.target.value)}
placeholder="e.g., Delhi, Haryana, Punjab"
className="h-11"
/>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-3">
<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 bg-gradient-to-r from-violet-500 via-purple-500 to-fuchsia-600 hover:from-violet-600 hover:via-purple-600 hover:to-fuchsia-700 text-white"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</span>
) : (
<span className="flex items-center gap-2">
<Package className="h-4 w-4" />
Create Package
</span>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
)
}

376
app/create-project/page.tsx Normal file
View File

@@ -0,0 +1,376 @@
"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, FolderPlus, MapPin, Building2, Route, X } from "lucide-react"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { DataTable } from "@/components/data-table"
import { createProject, fetchProjects, type ProjectCreate, type Project } from "@/lib/api"
export default function CreateProjectPage() {
const [projects, setProjects] = useState<Project[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState<string | 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("")
// Load projects
const loadProjects = async () => {
try {
setIsLoading(true)
setError(null)
const data = await fetchProjects()
setProjects(data)
} catch (err) {
setError("Failed to load projects. Please check if the backend is running.")
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadProjects()
}, [])
const resetForm = () => {
setName("")
setState("")
setCorridorName("")
setStartLat("")
setStartLng("")
setEndLat("")
setEndLng("")
setError(null)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
setError("Project name is required")
return
}
setIsSubmitting(true)
setError(null)
try {
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 createProject(data)
setSuccess(true)
// Refresh projects list
await loadProjects()
setTimeout(() => {
resetForm()
setSuccess(false)
setIsModalOpen(false)
}, 2000)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create project")
} finally {
setIsSubmitting(false)
}
}
const columns = [
{
key: "name",
header: "Project Name",
render: (project: Project) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{project.name}</div>
)
},
{
key: "state",
header: "State",
render: (project: Project) => {
if (!project.state) return <span className="text-gray-400"></span>
return (
<div className="flex flex-wrap gap-1.5">
{project.state.split(',').map((item, idx) => (
<span
key={idx}
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-[11px] font-semibold border border-blue-100 dark:border-blue-800 shadow-sm"
>
{item.trim()}
</span>
))}
</div>
)
}
},
{
key: "corridor_name",
header: "Corridor",
},
{
key: "created_at",
header: "Created",
render: (project: Project) => new Date(project.created_at).toLocaleDateString()
},
]
return (
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
<SidebarNavigation />
<main className="ml-16 min-h-screen relative overflow-hidden">
<div className="mx-auto px-4 py-8 max-w-6xl relative z-10">
{/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-left duration-700">
<div className="flex items-center gap-6">
<div className="flex-shrink-0 w-16 h-16 rounded-2xl bg-white shadow-xl border-2 border-blue-800 transition-transform duration-300 hover:scale-110 flex items-center justify-center">
<FolderPlus className="h-8 w-8 text-blue-600" />
</div>
<div className="flex flex-col">
<h1 className="text-3xl md:text-4xl font-extrabold text-blue-600 tracking-tight">
Projects
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium italic">
Manage road infrastructure projects
</p>
</div>
</div>
</div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
{/* Data Table */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-150">
<DataTable
title="All Projects"
data={projects}
columns={columns}
onAddNew={() => setIsModalOpen(true)}
addButtonText="Add New Project"
isLoading={isLoading}
/>
</div>
{/* Footer */}
<div className="mt-8 text-center animate-in fade-in duration-700 delay-300">
<p className="text-xs text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderPlus className="h-5 w-5 text-blue-500" />
Create New Project
</DialogTitle>
<DialogDescription>
Fill in the details for your new road infrastructure project
</DialogDescription>
</DialogHeader>
{/* Success Message in Modal */}
{success && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30 border border-blue-200 dark:border-blue-800 animate-in fade-in slide-in-from-top duration-300">
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
<p className="text-sm font-medium text-blue-700 dark:text-blue-400">Project created successfully!</p>
</div>
)}
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 animate-in fade-in slide-in-from-top duration-300">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Project Name */}
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-semibold flex items-center gap-1">
Project Name <span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Delhi-Chandigarh Highway"
className="h-11"
required
/>
</div>
{/* State & Corridor Row */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="state" className="text-sm font-semibold">
State <span className="text-gray-400 font-normal text-xs">(Optional)</span>
</Label>
<Input
id="state"
value={state}
onChange={(e) => setState(e.target.value)}
placeholder="e.g., Haryana"
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="corridor" className="text-sm font-semibold flex items-center gap-1">
<Route className="h-3.5 w-3.5 text-gray-400" />
Corridor Name <span className="text-gray-400 font-normal text-xs">(Optional)</span>
</Label>
<Input
id="corridor"
value={corridorName}
onChange={(e) => setCorridorName(e.target.value)}
placeholder="e.g., National Highway 44"
className="h-11"
/>
</div>
</div>
{/* GPS Coordinates Section */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-blue-400 to-indigo-500 shadow-md shadow-blue-500/20">
<MapPin className="h-4 w-4 text-white" />
</div>
<h3 className="text-sm font-bold text-gray-700 dark:text-gray-300">GPS Coordinates</h3>
<span className="text-xs text-gray-400">(Optional)</span>
</div>
{/* Start Point */}
<div className="p-4 rounded-xl bg-gradient-to-r from-blue-50/50 to-indigo-50/50 dark:from-blue-950/20 dark:to-indigo-950/20 border border-blue-100 dark:border-blue-900/50">
<p className="text-xs font-semibold text-blue-600 dark:text-blue-400 mb-3 uppercase tracking-wide">Start Point</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label htmlFor="start-lat" className="text-xs text-gray-500">Latitude</Label>
<Input
id="start-lat"
type="number"
step="any"
value={startLat}
onChange={(e) => setStartLat(e.target.value)}
placeholder="-90 to 90"
className="h-10 text-sm"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="start-lng" className="text-xs text-gray-500">Longitude</Label>
<Input
id="start-lng"
type="number"
step="any"
value={startLng}
onChange={(e) => setStartLng(e.target.value)}
placeholder="-180 to 180"
className="h-10 text-sm"
/>
</div>
</div>
</div>
{/* End Point */}
<div className="p-4 rounded-xl bg-gradient-to-r from-blue-50/50 to-indigo-50/50 dark:from-blue-950/20 dark:to-indigo-950/20 border border-blue-100 dark:border-blue-900/50">
<p className="text-xs font-semibold text-blue-600 dark:text-blue-400 mb-3 uppercase tracking-wide">End Point</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label htmlFor="end-lat" className="text-xs text-gray-500">Latitude</Label>
<Input
id="end-lat"
type="number"
step="any"
value={endLat}
onChange={(e) => setEndLat(e.target.value)}
placeholder="-90 to 90"
className="h-10 text-sm"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="end-lng" className="text-xs text-gray-500">Longitude</Label>
<Input
id="end-lng"
type="number"
step="any"
value={endLng}
onChange={(e) => setEndLng(e.target.value)}
placeholder="-180 to 180"
className="h-10 text-sm"
/>
</div>
</div>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={() => {
setIsModalOpen(false)
resetForm()
}}
disabled={isSubmitting}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !name.trim()}
className="flex-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-blue-700 hover:from-blue-600 hover:via-indigo-600 hover:to-blue-800 text-white"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</span>
) : (
<span className="flex items-center gap-2">
<FolderPlus className="h-4 w-4" />
Create Project
</span>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,12 +1,8 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { import {
Plus,
Loader2,
AlertCircle, AlertCircle,
TrendingUp, TrendingUp,
MapPin as MapPinIcon, MapPin as MapPinIcon,
@@ -17,6 +13,7 @@ import {
import { SidebarNavigation } from "@/components/sidebar-navigation" import { SidebarNavigation } from "@/components/sidebar-navigation"
import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card" import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card"
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector" import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"
import { FilterSelector } from "@/components/dashboard/filter-selector"
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart" import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"
import { LocationBarChart } from "@/components/dashboard/location-bar-chart" import { LocationBarChart } from "@/components/dashboard/location-bar-chart"
import { DashboardMap } from "@/components/dashboard/dashboard-map" import { DashboardMap } from "@/components/dashboard/dashboard-map"
@@ -114,7 +111,6 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
} }
export default function DashboardPage() { export default function DashboardPage() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [projects, setProjects] = useState<Project[]>([]) const [projects, setProjects] = useState<Project[]>([])
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null) const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
@@ -150,11 +146,31 @@ export default function DashboardPage() {
loadProjects() loadProjects()
}, []) }, [])
const selectedProject = projects.find(p => p.id === selectedProjectId)
// Extract packages from project summary
const packages = projectSummary
? Object.keys(projectSummary.packages || {}).map(pkgName => ({
id: pkgName,
name: pkgName
}))
: []
// Extract locations from selected package
const [selectedPackageId, setSelectedPackageId] = useState<string | null>(null)
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(null)
const locations = projectSummary && selectedPackageId && selectedPackageId !== "all"
? Object.keys(projectSummary.packages[selectedPackageId]?.locations || {}).map(locName => ({
id: locName,
name: locName
}))
: []
// Load project summary when project changes // Load project summary when project changes
useEffect(() => { useEffect(() => {
if (!selectedProjectId) { if (!selectedProjectId) {
setProjectSummary(null) setProjectSummary(null)
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
return return
} }
@@ -176,12 +192,10 @@ export default function DashboardPage() {
const summary: ProjectSummary = await response.json() const summary: ProjectSummary = await response.json()
setProjectSummary(summary) setProjectSummary(summary)
setStats(calculateStats(summary))
} catch (err) { } catch (err) {
console.error("Failed to load project summary:", err) console.error("Failed to load project summary:", err)
setError("Failed to load project summary.") setError("Failed to load project summary.")
setProjectSummary(null) setProjectSummary(null)
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -190,14 +204,73 @@ export default function DashboardPage() {
loadProjectSummary() loadProjectSummary()
}, [selectedProjectId]) }, [selectedProjectId])
const handleNewAnalysis = () => { // Reset package and location when project changes
router.push("/") useEffect(() => {
} setSelectedPackageId(null)
setSelectedLocationId(null)
}, [selectedProjectId])
const selectedProject = projects.find(p => p.id === selectedProjectId) // Reset location when package changes
useEffect(() => {
setSelectedLocationId(null)
}, [selectedPackageId])
// Filter stats based on selections
useEffect(() => {
if (!projectSummary) {
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
return
}
let totalPotholes = 0
let totalSignboards = 0
const locationData: DetectionStats["locationData"] = []
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
: projectSummary.packages || {}
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
const locationsToProcess = selectedLocationId && selectedLocationId !== "all"
? { [selectedLocationId]: pkg.locations[selectedLocationId] }
: pkg.locations || {}
for (const [locName, loc] of Object.entries(locationsToProcess)) {
let locPotholes = 0
let locSignboards = 0
for (const detection of loc.detections || []) {
if (detection.type.toLowerCase().includes("pothole")) {
locPotholes++
totalPotholes++
} else {
locSignboards++
totalSignboards++
}
}
if (locPotholes > 0 || locSignboards > 0) {
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
locationData.push({
name: shortName,
potholes: locPotholes,
signboards: locSignboards,
total: locPotholes + locSignboards
})
}
}
}
setStats({
totalPotholes,
totalSignboards,
totalDetections: totalPotholes + totalSignboards,
locationData
})
}, [projectSummary, selectedPackageId, selectedLocationId])
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950"> <div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */} {/* Sidebar Navigation */}
<SidebarNavigation /> <SidebarNavigation />
@@ -206,22 +279,13 @@ export default function DashboardPage() {
<div className="p-6 max-w-[1600px] mx-auto"> <div className="p-6 max-w-[1600px] mx-auto">
{/* Header */} {/* Header */}
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500"> <div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
<div className="flex items-center justify-between"> <div>
<div> <h1 className="text-2xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
<h1 className="text-2xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent"> VisionRoad Analytics Dashboard
VisionRoad Analytics Dashboard </h1>
</h1> <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1"> A Comprehensive Overview Of Your Road Infrastructure Analysis
A Comprehensive Overview Of Your Road Infrastructure Analysis </p>
</p>
</div>
<Button
onClick={handleNewAnalysis}
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white shadow-lg shadow-indigo-500/25 hover:shadow-indigo-500/40 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
New Analysis
</Button>
</div> </div>
</div> </div>
@@ -261,67 +325,74 @@ export default function DashboardPage() {
/> />
</div> </div>
{/* Project Selector - Above main content */} {/* Filter Selector - Above main content */}
<div className="mb-6 animate-in fade-in slide-in-from-bottom duration-500 delay-100"> <div className="mb-6 animate-in fade-in slide-in-from-bottom duration-500 delay-100">
<CompactProjectSelector <FilterSelector
projects={projects} projects={projects}
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
selectedPackageId={selectedPackageId}
selectedLocationId={selectedLocationId}
onProjectChange={setSelectedProjectId} onProjectChange={setSelectedProjectId}
selectedProject={selectedProject} onPackageChange={setSelectedPackageId}
onLocationChange={setSelectedLocationId}
packages={packages}
locations={locations}
isLoading={isLoading} isLoading={isLoading}
/> />
</div> </div>
{/* Main Content Grid - Map Left, Charts Right */} {/* Charts Row - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 animate-in fade-in slide-in-from-bottom duration-500 delay-200"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4 animate-in fade-in slide-in-from-bottom duration-500 delay-200">
{/* Left Side - Map */} {/* Left Chart - Detection Distribution */}
<div> <Card className="rounded-xl overflow-hidden">
<DashboardMap className="h-auto" /> <CardHeader className="pb-2 border-b border-[var(--border)]">
</div> <CardTitle className="text-base font-bold flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-[#2563eb]/10 shadow-sm">
<BarChart3 className="h-4 w-4 text-[#2563eb]" />
</div>
<span className="text-[#2563eb]">
Detection Distribution
</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<DetectionDonutChart
potholes={stats.totalPotholes}
signboards={stats.totalSignboards}
isLoading={isLoading}
/>
</CardContent>
</Card>
{/* Right Side - Stacked Charts */} {/* Right Chart - Location Bar Chart */}
<div className="space-y-4"> <Card className="rounded-xl overflow-hidden">
{/* Detection Distribution */} <CardHeader className="pb-2 border-b border-[var(--border)]">
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden"> <CardTitle className="text-base font-bold flex items-center gap-2">
<CardHeader className="pb-2 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/30 dark:to-emerald-950/30"> <div className="p-1.5 rounded-lg bg-[#2563eb]/10 shadow-sm">
<CardTitle className="text-base font-bold flex items-center gap-2"> <MapPinIcon className="h-4 w-4 text-[#2563eb]" />
<div className="p-1.5 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 shadow-md shadow-emerald-500/30"> </div>
<BarChart3 className="h-4 w-4 text-white" /> <span className="text-[#2563eb]">
</div> Detections by Location
<span className="bg-gradient-to-r from-emerald-600 via-teal-500 to-emerald-600 dark:from-emerald-400 dark:via-teal-400 dark:to-emerald-400 bg-clip-text text-transparent"> </span>
Detection Distribution </CardTitle>
</span> </CardHeader>
</CardTitle> <CardContent className="pt-4">
</CardHeader> <LocationBarChart
<CardContent className="pt-4"> data={stats.locationData}
<DetectionDonutChart isLoading={isLoading}
potholes={stats.totalPotholes} />
signboards={stats.totalSignboards} </CardContent>
isLoading={isLoading} </Card>
/> </div>
</CardContent>
</Card>
{/* Location Bar Chart */} {/* Map - Full Width Below Charts */}
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden"> <div className="animate-in fade-in slide-in-from-bottom duration-500 delay-300">
<CardHeader className="pb-2 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30"> <DashboardMap
<CardTitle className="text-base font-bold flex items-center gap-2"> selectedProjectId={selectedProjectId}
<div className="p-1.5 rounded-lg bg-gradient-to-br from-blue-400 to-indigo-500 shadow-md shadow-blue-500/30"> selectedPackageId={selectedPackageId}
<MapPinIcon className="h-4 w-4 text-white" /> selectedLocationId={selectedLocationId}
</div> projectSummary={projectSummary}
<span className="bg-gradient-to-r from-blue-600 via-indigo-500 to-blue-600 dark:from-blue-400 dark:via-indigo-400 dark:to-blue-400 bg-clip-text text-transparent"> />
Detections by Location
</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<LocationBarChart
data={stats.locationData}
isLoading={isLoading}
/>
</CardContent>
</Card>
</div>
</div> </div>
{/* Footer */} {/* Footer */}

View File

@@ -9,11 +9,11 @@
--primary-foreground: oklch(0.98 0.01 264); --primary-foreground: oklch(0.98 0.01 264);
/* Background & Surfaces */ /* Background & Surfaces */
--background: oklch(0.98 0.01 280); --background: hsla(210, 100%, 98%, 1);
--foreground: oklch(0.15 0.02 280); --foreground: oklch(0.15 0.02 280);
--card: oklch(0.99 0.005 280); --card: oklch(1 0 0);
--card-foreground: oklch(0.15 0.02 280); --card-foreground: oklch(0.15 0.02 280);
--popover: oklch(0.99 0.005 280); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.15 0.02 280); --popover-foreground: oklch(0.15 0.02 280);
/* Secondary */ /* Secondary */
@@ -33,9 +33,13 @@
--destructive-foreground: oklch(0.98 0.01 25); --destructive-foreground: oklch(0.98 0.01 25);
/* Borders & Inputs */ /* Borders & Inputs */
--border: oklch(0.90 0.02 280); --border: hsla(210, 100%, 85%, 1);
--input: oklch(0.92 0.02 280); --input: oklch(0.92 0.02 280);
--ring: oklch(0.55 0.24 264); --ring: hsla(210, 100%, 85%, 1);
/* Card Glow */
--card-glow: hsla(210, 100%, 85%, 0.3);
--card-hover-border: hsla(210, 100%, 75%, 0.8);
/* Chart Colors - Vibrant Palette */ /* Chart Colors - Vibrant Palette */
--chart-1: oklch(0.55 0.24 264); --chart-1: oklch(0.55 0.24 264);
@@ -154,6 +158,7 @@
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
@@ -162,19 +167,12 @@
/* Premium Background with Animated Mesh Gradient */ /* Premium Background with Animated Mesh Gradient */
@layer utilities { @layer utilities {
.bg-mesh-gradient { .bg-mesh-gradient {
background: background: hsla(210, 100%, 98%, 1);
radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.75 0.15 264 / 0.15), transparent), background: linear-gradient(135deg, hsla(210, 100%, 98%, 1) 0%, hsla(220, 100%, 95%, 1) 100%);
radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.70 0.12 200 / 0.12), transparent),
radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.65 0.10 320 / 0.08), transparent),
var(--background);
} }
.dark .bg-mesh-gradient { .dark .bg-mesh-gradient {
background: background: hsla(210, 30%, 15%, 1);
radial-gradient(ellipse 80% 50% at 20% 20%, oklch(0.40 0.18 264 / 0.20), transparent),
radial-gradient(ellipse 60% 40% at 80% 80%, oklch(0.35 0.15 200 / 0.15), transparent),
radial-gradient(ellipse 50% 30% at 50% 50%, oklch(0.30 0.12 320 / 0.10), transparent),
var(--background);
} }
/* Glassmorphism Card */ /* Glassmorphism Card */
@@ -266,8 +264,13 @@
} }
@keyframes progress-shimmer { @keyframes progress-shimmer {
0% { background-position: 200% 0; } 0% {
100% { background-position: -200% 0; } background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
} }
/* Subtle Float Animation */ /* Subtle Float Animation */
@@ -276,8 +279,15 @@
} }
@keyframes float-subtle { @keyframes float-subtle {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); } 0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
} }
/* Input Focus Glow */ /* Input Focus Glow */

View File

@@ -9,13 +9,6 @@ export default function MapPage() {
{/* Navigation Menu */} {/* Navigation Menu */}
<NavigationMenu /> <NavigationMenu />
{/* Decorative background elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -left-40 w-80 h-80 bg-primary/10 rounded-full blur-3xl float-subtle" />
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-accent/10 rounded-full blur-3xl float-subtle" style={{ animationDelay: '-3s' }} />
<div className="absolute top-1/2 left-1/4 w-64 h-64 bg-primary/5 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 py-8 max-w-7xl relative z-10"> <div className="container mx-auto px-4 py-8 max-w-7xl relative z-10">
{/* Header */} {/* Header */}
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700"> <div className="mb-8 animate-in fade-in slide-in-from-top duration-700">

50
app/new-analysis/page.tsx Normal file
View File

@@ -0,0 +1,50 @@
"use client"
import { useRouter } from "next/navigation"
import { ProjectSelectionSection } from "@/components/project-selection-section"
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { type SessionContext, saveSession } from "@/lib/api"
export default function NewAnalysisPage() {
const router = useRouter()
const handleSelectionComplete = (session: SessionContext) => {
// Save session to storage and navigate to upload page
saveSession(session)
router.push("/upload")
}
return (
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Main Content */}
<main className="ml-16 min-h-screen relative overflow-hidden">
<div className="container mx-auto px-4 py-12 max-w-5xl relative z-10">
{/* Premium Header */}
<div className="mb-10 animate-in fade-in slide-in-from-top duration-700">
<div className="text-center max-w-2xl mx-auto">
<h1 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
VisionRoad Detection System
</h1>
</div>
</div>
{/* Project Selection Section */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-150">
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
</div>
{/* Footer */}
<div className="mt-12 text-center animate-in fade-in duration-700 delay-300">
<p className="text-sm text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</main>
</div>
)
}

View File

@@ -1,56 +1,5 @@
"use client" import { redirect } from "next/navigation"
import { useRouter } from "next/navigation" export default function HomePage() {
import { ProjectSelectionSection } from "@/components/project-selection-section" redirect("/dashboard")
import { SidebarNavigation } from "@/components/sidebar-navigation"
import { type SessionContext, saveSession } from "@/lib/api"
export default function SelectionPage() {
const router = useRouter()
const handleSelectionComplete = (session: SessionContext) => {
// Save session to storage and navigate to upload page
saveSession(session)
router.push("/upload")
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
{/* Sidebar Navigation */}
<SidebarNavigation />
{/* Main Content */}
<main className="ml-16 min-h-screen relative overflow-hidden">
{/* Decorative background elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -left-40 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl" />
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-blue-500/5 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 py-12 max-w-5xl relative z-10">
{/* Premium Header */}
<div className="mb-10 animate-in fade-in slide-in-from-top duration-700">
<div className="text-center max-w-2xl mx-auto">
<h1 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
VisionRoad Detection System
</h1>
</div>
</div>
{/* Project Selection Section */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-150">
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
</div>
{/* Footer */}
<div className="mt-12 text-center animate-in fade-in duration-700 delay-300">
<p className="text-sm text-gray-400 dark:text-gray-500">
Sentient Geeks Pvt. Ltd.
</p>
</div>
</div>
</main>
</div>
)
} }

View File

@@ -95,7 +95,7 @@ export default function ResultsPage() {
const videoData = loadVideoData() const videoData = loadVideoData()
if (!isSessionValid(storedSession) || !videoData) { if (!isSessionValid(storedSession) || !videoData) {
router.replace("/") router.replace("/new-analysis")
return return
} }
@@ -143,7 +143,7 @@ export default function ResultsPage() {
} }
} }
clearSession() clearSession()
router.push("/") router.push("/new-analysis")
} }
const handleBackToUpload = () => { const handleBackToUpload = () => {
@@ -158,7 +158,7 @@ export default function ResultsPage() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center"> <div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg"> <div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" /> <Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
<p className="text-gray-500">Loading detection results...</p> <p className="text-gray-500">Loading detection results...</p>
@@ -169,7 +169,7 @@ export default function ResultsPage() {
if (error) { if (error) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center"> <div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg"> <div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<p className="text-red-500">{error}</p> <p className="text-red-500">{error}</p>
<Button onClick={handleNewAnalysis} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white">Start New Analysis</Button> <Button onClick={handleNewAnalysis} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white">Start New Analysis</Button>
@@ -179,7 +179,7 @@ export default function ResultsPage() {
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950"> <div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */} {/* Sidebar Navigation */}
<SidebarNavigation /> <SidebarNavigation />
@@ -199,7 +199,7 @@ export default function ResultsPage() {
{/* Session Info Bar */} {/* Session Info Bar */}
{session && ( {session && (
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500"> <div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
<div className="flex items-center justify-between p-4 rounded-xl bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50"> <div className="flex items-center justify-between p-4 rounded-xl bg-card border border-[var(--border)] shadow-sm">
<div className="flex items-center gap-6 text-sm"> <div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-indigo-400 to-purple-500 shadow-md shadow-indigo-500/30"> <div className="p-1.5 rounded-lg bg-gradient-to-br from-indigo-400 to-purple-500 shadow-md shadow-indigo-500/30">
@@ -209,11 +209,11 @@ export default function ResultsPage() {
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 bg-clip-text text-transparent">{session.projectName}</span> <span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 bg-clip-text text-transparent">{session.projectName}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 shadow-md shadow-emerald-500/30"> <div className="p-1.5 rounded-lg bg-gradient-to-br from-blue-400 to-indigo-500 shadow-md shadow-blue-500/30">
<Package className="h-4 w-4 text-white" /> <Package className="h-4 w-4 text-white" />
</div> </div>
<span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Package:</span> <span className="text-gray-500 dark:text-gray-400 text-xs uppercase font-semibold tracking-wide">Package:</span>
<span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-emerald-600 to-teal-600 dark:from-emerald-400 dark:to-teal-400 bg-clip-text text-transparent">{session.packageName}</span> <span className="font-bold text-gray-900 dark:text-white bg-gradient-to-r from-blue-600 to-indigo-600 dark:from-blue-400 dark:to-indigo-400 bg-clip-text text-transparent">{session.packageName}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-purple-400 to-pink-500 shadow-md shadow-purple-500/30"> <div className="p-1.5 rounded-lg bg-gradient-to-br from-purple-400 to-pink-500 shadow-md shadow-purple-500/30">

View File

@@ -44,7 +44,7 @@ export default function UploadPage() {
useEffect(() => { useEffect(() => {
const storedSession = loadSession() const storedSession = loadSession()
if (!isSessionValid(storedSession)) { if (!isSessionValid(storedSession)) {
router.replace("/") router.replace("/new-analysis")
return return
} }
setSession(storedSession) setSession(storedSession)
@@ -147,7 +147,7 @@ export default function UploadPage() {
const handleBackToSelection = () => { const handleBackToSelection = () => {
clearSession() clearSession()
router.push("/") router.push("/new-analysis")
} }
const getTitle = () => { const getTitle = () => {
@@ -158,7 +158,7 @@ export default function UploadPage() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 flex items-center justify-center"> <div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg"> <div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" /> <Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
</div> </div>
@@ -167,17 +167,12 @@ export default function UploadPage() {
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950"> <div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
{/* Sidebar Navigation */} {/* Sidebar Navigation */}
<SidebarNavigation /> <SidebarNavigation />
{/* Main Content */} {/* Main Content */}
<main className="ml-16 min-h-screen relative overflow-hidden flex flex-col"> <main className="ml-16 min-h-screen relative overflow-hidden flex flex-col">
{/* Decorative background elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -left-40 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl" />
<div className="absolute -bottom-40 -right-40 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
</div>
<div className="flex-1 container mx-auto px-4 py-6 max-w-6xl relative z-10 flex flex-col"> <div className="flex-1 container mx-auto px-4 py-6 max-w-6xl relative z-10 flex flex-col">
{/* Compact Header */} {/* Compact Header */}
@@ -224,7 +219,7 @@ export default function UploadPage() {
)} )}
{/* Upload Card */} {/* Upload Card */}
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden animate-in fade-in slide-in-from-bottom duration-700 delay-150 flex-1"> <Card className="rounded-xl overflow-hidden animate-in fade-in slide-in-from-bottom duration-700 delay-150 flex-1">
<CardHeader className="pb-4 border-b border-gray-100 dark:border-gray-700 bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-950/30 dark:to-purple-950/30"> <CardHeader className="pb-4 border-b border-gray-100 dark:border-gray-700 bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-950/30 dark:to-purple-950/30">
<CardTitle className="text-xl font-bold"> <CardTitle className="text-xl font-bold">
<span className="bg-gradient-to-r from-indigo-600 via-purple-500 to-indigo-600 dark:from-indigo-400 dark:via-purple-400 dark:to-indigo-400 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-indigo-600 via-purple-500 to-indigo-600 dark:from-indigo-400 dark:via-purple-400 dark:to-indigo-400 bg-clip-text text-transparent">

View File

@@ -21,30 +21,59 @@ const DashboardMapContent = dynamic(
interface DashboardMapProps { interface DashboardMapProps {
className?: string className?: string
selectedProjectId?: string | null
selectedPackageId?: string | null
selectedLocationId?: string | null
projectSummary?: any
} }
export function DashboardMap({ className }: DashboardMapProps) { export function DashboardMap({
className,
selectedProjectId,
selectedPackageId,
selectedLocationId,
projectSummary
}: DashboardMapProps) {
const [detections, setDetections] = useState<Detection[]>([]) const [detections, setDetections] = useState<Detection[]>([])
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
const loadDetections = async () => { if (!projectSummary) {
try { setDetections([])
setIsLoading(true) setIsLoading(false)
setError(null) return
const data = await fetchAllDetections()
setDetections(data)
} catch (err) {
console.error("Failed to load detections:", err)
setError("Failed to load detection data")
} finally {
setIsLoading(false)
}
} }
loadDetections() try {
}, []) setIsLoading(true)
setError(null)
const filteredDetections: Detection[] = []
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
: projectSummary.packages || {}
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
const locationsToProcess = selectedLocationId && selectedLocationId !== "all"
? { [selectedLocationId]: (pkg as any).locations[selectedLocationId] }
: (pkg as any).locations || {}
for (const [locName, loc] of Object.entries(locationsToProcess)) {
const locationDetections = (loc as any).detections || []
filteredDetections.push(...locationDetections)
}
}
setDetections(filteredDetections)
} catch (err) {
console.error("Failed to extract detections:", err)
setError("Failed to load detection data")
} finally {
setIsLoading(false)
}
}, [projectSummary, selectedPackageId, selectedLocationId])
// Filter detections with valid GPS coordinates // Filter detections with valid GPS coordinates
const validDetections = detections.filter(d => d.latitude && d.longitude) const validDetections = detections.filter(d => d.latitude && d.longitude)
@@ -57,18 +86,16 @@ export function DashboardMap({ className }: DashboardMapProps) {
const signboardCount = validDetections.length - potholeCount const signboardCount = validDetections.length - potholeCount
return ( return (
<Card className={`bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden ${className}`}> <Card className={`overflow-hidden ${className}`}>
<CardHeader className="pb-2 bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-950/30 dark:to-purple-950/30"> <CardHeader className="pb-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-indigo-400 to-purple-500 shadow-md shadow-indigo-500/30"> <div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center shadow-lg border-2 border-[#1e40af] transition-transform duration-300 hover:scale-110">
<MapPin className="h-4 w-4 text-white" /> <MapPin className="h-5 w-5 text-[#2563eb]" />
</div> </div>
<div> <div>
<CardTitle className="text-base font-bold"> <CardTitle className="text-base font-bold text-[#2563eb]">
<span className="bg-gradient-to-r from-indigo-600 via-purple-500 to-indigo-600 dark:from-indigo-400 dark:via-purple-400 dark:to-indigo-400 bg-clip-text text-transparent"> Detection Map
Detection Map
</span>
</CardTitle> </CardTitle>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
{isLoading ? "Loading..." : `${validDetections.length} detections with GPS coordinates`} {isLoading ? "Loading..." : `${validDetections.length} detections with GPS coordinates`}

View File

@@ -14,9 +14,9 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha
const signboardPercent = total > 0 ? (signboards / total) * 100 : 50 const signboardPercent = total > 0 ? (signboards / total) * 100 : 50
return ( return (
<Card className="glass-card card-glow border-0 overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-lg font-semibold">Detection Distribution</CardTitle> <CardTitle className="text-lg font-bold text-blue-600">Detection Distribution</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
{isLoading ? ( {isLoading ? (
@@ -73,8 +73,8 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha
<stop offset="100%" stopColor="#ea580c" /> <stop offset="100%" stopColor="#ea580c" />
</linearGradient> </linearGradient>
<linearGradient id="signboardGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <linearGradient id="signboardGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#06b6d4" /> <stop offset="0%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#0891b2" /> <stop offset="100%" stopColor="#2563eb" />
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
@@ -98,9 +98,9 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha
<p className="text-xs text-muted-foreground">{potholePercent.toFixed(0)}%</p> <p className="text-xs text-muted-foreground">{potholePercent.toFixed(0)}%</p>
</div> </div>
</div> </div>
<div className="flex items-center justify-between p-3 rounded-lg bg-cyan-500/10 border border-cyan-500/20"> <div className="flex items-center justify-between p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-gradient-to-r from-cyan-500 to-cyan-600" /> <div className="w-3 h-3 rounded-full bg-gradient-to-r from-blue-500 to-blue-600" />
<span className="text-sm font-medium">Signboards</span> <span className="text-sm font-medium">Signboards</span>
</div> </div>
<div className="text-right"> <div className="text-right">

View File

@@ -40,16 +40,16 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
] ]
return ( return (
<div className="h-[250px]"> <div className="h-[200px] relative">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
data={data} data={data}
cx="50%" cx="50%"
cy="50%" cy="50%"
innerRadius={55} innerRadius={50}
outerRadius={80} outerRadius={70}
paddingAngle={3} paddingAngle={5}
dataKey="value" dataKey="value"
strokeWidth={0} strokeWidth={0}
> >
@@ -57,7 +57,8 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
<Cell <Cell
key={`cell-${index}`} key={`cell-${index}`}
fill={entry.color} fill={entry.color}
className="transition-opacity hover:opacity-80" fillOpacity={0.8}
className="transition-all duration-300 hover:fill-opacity-100"
/> />
))} ))}
</Pie> </Pie>
@@ -91,7 +92,7 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
className="w-3 h-3 rounded-full" className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }} style={{ backgroundColor: entry.color }}
/> />
<span className="text-xs text-muted-foreground"> <span className="text-[10px] text-muted-foreground">
{entry.value}: {data[index].value} {entry.value}: {data[index].value}
</span> </span>
</div> </div>
@@ -104,8 +105,8 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
{/* Center label */} {/* Center label */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none" style={{ marginTop: '-40px' }}> <div className="absolute inset-0 flex items-center justify-center pointer-events-none" style={{ marginTop: '-40px' }}>
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold">{total}</p> <p className="text-xl font-extrabold text-[#2563eb]">{total}</p>
<p className="text-xs text-muted-foreground">Total</p> <p className="text-[9px] uppercase tracking-wider text-muted-foreground">Total</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,126 @@
"use client"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { FolderOpen, Package, MapPin } from "lucide-react"
import { type Project } from "@/lib/api"
interface FilterSelectorProps {
projects: Project[]
selectedProjectId: string | null
selectedPackageId: string | null
selectedLocationId: string | null
onProjectChange: (projectId: string) => void
onPackageChange: (packageId: string) => void
onLocationChange: (locationId: string) => void
packages: Array<{ id: string; name: string }>
locations: Array<{ id: string; name: string }>
isLoading?: boolean
}
export function FilterSelector({
projects,
selectedProjectId,
selectedPackageId,
selectedLocationId,
onProjectChange,
onPackageChange,
onLocationChange,
packages,
locations,
isLoading = false
}: FilterSelectorProps) {
return (
<div className="flex flex-wrap items-center gap-3 p-3 rounded-xl bg-card border border-[var(--border)] shadow-sm">
{/* Project Dropdown */}
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-indigo-100 dark:bg-indigo-900/50">
<FolderOpen className="h-4 w-4 text-indigo-500" />
</div>
<Select
value={selectedProjectId || ""}
onValueChange={onProjectChange}
disabled={isLoading || projects.length === 0}
>
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Divider */}
{selectedProjectId && packages.length > 0 && (
<div className="h-5 w-px bg-gray-300 dark:bg-gray-600" />
)}
{/* Package Dropdown */}
{selectedProjectId && packages.length > 0 && (
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-emerald-100 dark:bg-emerald-900/50">
<Package className="h-4 w-4 text-emerald-500" />
</div>
<Select
value={selectedPackageId || "all"}
onValueChange={onPackageChange}
disabled={isLoading}
>
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
<SelectValue placeholder="All packages" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Packages</SelectItem>
{packages.map((pkg) => (
<SelectItem key={pkg.id} value={pkg.id}>
{pkg.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Divider */}
{selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && (
<div className="h-5 w-px bg-gray-300 dark:bg-gray-600" />
)}
{/* Location Dropdown */}
{selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && (
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-purple-100 dark:bg-purple-900/50">
<MapPin className="h-4 w-4 text-purple-500" />
</div>
<Select
value={selectedLocationId || "all"}
onValueChange={onLocationChange}
disabled={isLoading}
>
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
<SelectValue placeholder="All locations" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Locations</SelectItem>
{locations.map((loc) => (
<SelectItem key={loc.id} value={loc.id}>
{loc.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)
}

View File

@@ -14,38 +14,38 @@ interface GradientStatsCardProps {
const gradientStyles = { const gradientStyles = {
green: { green: {
background: "bg-gradient-to-br from-emerald-50 via-emerald-50 to-teal-100 dark:from-emerald-950/40 dark:via-emerald-950/30 dark:to-teal-950/40", background: "bg-card",
border: "border-l-4 border-l-emerald-500", border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-emerald-400 to-teal-500", iconBg: "bg-[#60a5fa]",
iconShadow: "shadow-lg shadow-emerald-500/30", iconShadow: "shadow-lg shadow-[#60a5fa]/20",
valueGradient: "bg-gradient-to-r from-emerald-600 via-teal-500 to-emerald-600 dark:from-emerald-400 dark:via-teal-400 dark:to-emerald-400" valueGradient: "text-gray-900 dark:text-white"
}, },
coral: { coral: {
background: "bg-gradient-to-br from-red-50 via-red-50 to-orange-100 dark:from-red-950/40 dark:via-red-950/30 dark:to-orange-950/40", background: "bg-card",
border: "border-l-4 border-l-red-500", border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-red-400 to-orange-500", iconBg: "bg-[#60a5fa]",
iconShadow: "shadow-lg shadow-red-500/30", iconShadow: "shadow-lg shadow-[#60a5fa]/20",
valueGradient: "bg-gradient-to-r from-red-600 via-orange-500 to-red-600 dark:from-red-400 dark:via-orange-400 dark:to-red-400" valueGradient: "text-gray-900 dark:text-white"
}, },
blue: { blue: {
background: "bg-gradient-to-br from-blue-50 via-blue-50 to-indigo-100 dark:from-blue-950/40 dark:via-blue-950/30 dark:to-indigo-950/40", background: "bg-card",
border: "border-l-4 border-l-blue-500", border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-blue-400 to-indigo-500", iconBg: "bg-[#60a5fa]",
iconShadow: "shadow-lg shadow-blue-500/30", iconShadow: "shadow-lg shadow-[#60a5fa]/20",
valueGradient: "bg-gradient-to-r from-blue-600 via-indigo-500 to-blue-600 dark:from-blue-400 dark:via-indigo-400 dark:to-blue-400" valueGradient: "text-gray-900 dark:text-white"
}, },
purple: { purple: {
background: "bg-gradient-to-br from-purple-50 via-purple-50 to-pink-100 dark:from-purple-950/40 dark:via-purple-950/30 dark:to-pink-950/40", background: "bg-card",
border: "border-l-4 border-l-purple-500", border: "border-l-4 border-l-[var(--border)]",
iconBg: "bg-gradient-to-br from-purple-400 to-pink-500", iconBg: "bg-[#60a5fa]",
iconShadow: "shadow-lg shadow-purple-500/30", iconShadow: "shadow-lg shadow-[#60a5fa]/20",
valueGradient: "bg-gradient-to-r from-purple-600 via-pink-500 to-purple-600 dark:from-purple-400 dark:via-pink-400 dark:to-purple-400" valueGradient: "text-gray-900 dark:text-white"
} }
} }
export function GradientStatsCard({ export function GradientStatsCard({
title, title,
subtitle = "Work level distribution", subtitle,
value, value,
icon: Icon, icon: Icon,
gradient, gradient,
@@ -54,41 +54,37 @@ export function GradientStatsCard({
const styles = gradientStyles[gradient] const styles = gradientStyles[gradient]
return ( return (
<Card className={` <Card className="bg-white dark:bg-gray-900 border-none shadow-xl shadow-blue-500/5 overflow-hidden py-8 px-6 transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/10 group">
${styles.background} ${styles.border} <CardContent className="p-0 flex items-center justify-between gap-6">
border-0 rounded-xl overflow-hidden <div className="flex flex-col gap-1 min-w-0">
shadow-md hover:shadow-lg <h3 className="text-sm font-bold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-widest">
transition-all duration-300 ease-out {title}
hover:-translate-y-1 hover:scale-[1.02] </h3>
`}> <div className="flex flex-col">
<CardContent className="p-4">
<div className="flex items-center gap-4">
{/* Large gradient icon */}
<div className={`
w-14 h-14 rounded-xl flex items-center justify-center
${styles.iconBg} ${styles.iconShadow}
`}>
<Icon className="h-7 w-7 text-white" />
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 truncate">
{title}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate">
{subtitle}
</p>
{isLoading ? ( {isLoading ? (
<div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded mt-1 animate-pulse" /> <div className="h-14 w-24 bg-gray-100 dark:bg-gray-800 rounded-xl animate-pulse mt-2" />
) : ( ) : (
<p className={`text-3xl font-extrabold mt-1 ${styles.valueGradient} bg-clip-text text-transparent`}> <>
{value} <p className="text-5xl md:text-6xl font-black tracking-tighter text-gray-900 dark:text-white leading-tight">
</p> {value}
</p>
{subtitle && (
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mt-1 opacity-80 italic">
{subtitle}
</p>
)}
</>
)} )}
</div> </div>
</div> </div>
<div className={`
w-20 h-20 rounded-3xl flex items-center justify-center flex-shrink-0
${styles.iconBg} shadow-[0_10px_30px_rgba(37,99,235,0.2)]
transition-all duration-500 group-hover:scale-110 group-hover:rotate-6
`}>
<Icon className="h-10 w-10 text-white" />
</div>
</CardContent> </CardContent>
</Card> </Card>
) )

View File

@@ -39,13 +39,23 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
} }
return ( return (
<div className="h-[300px]"> <div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart
data={data} data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 40 }} margin={{ top: 10, right: 10, left: 0, bottom: 20 }}
barCategoryGap="20%" barCategoryGap="25%"
> >
<defs>
<linearGradient id="barGradientPothole" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.8} />
<stop offset="100%" stopColor="#ef4444" stopOpacity={0.4} />
</linearGradient>
<linearGradient id="barGradientSignboard" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.8} />
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0.4} />
</linearGradient>
</defs>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
vertical={false} vertical={false}
@@ -53,28 +63,28 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
/> />
<XAxis <XAxis
dataKey="name" dataKey="name"
tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }} tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
angle={-45} angle={-45}
textAnchor="end" textAnchor="end"
height={60} height={50}
interval={0} interval={0}
/> />
<YAxis <YAxis
tick={{ fontSize: 11, fill: 'hsl(var(--muted-foreground))' }} tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
width={35} width={25}
/> />
<Tooltip <Tooltip
content={({ active, payload, label }) => { content={({ active, payload, label }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg"> <div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg">
<p className="font-medium text-sm mb-1">{label}</p> <p className="font-medium text-xs mb-1">{label}</p>
{payload.map((entry, index) => ( {payload.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-sm"> <div key={index} className="flex items-center gap-2 text-xs">
<div <div
className="w-2 h-2 rounded-full" className="w-2 h-2 rounded-full"
style={{ backgroundColor: entry.color }} style={{ backgroundColor: entry.color }}
@@ -91,16 +101,16 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
/> />
<Legend <Legend
verticalAlign="top" verticalAlign="top"
height={36} height={30}
content={({ payload }) => ( content={({ payload }) => (
<div className="flex items-center justify-center gap-6 mb-2"> <div className="flex items-center justify-center gap-6 mb-2">
{payload?.map((entry, index) => ( {payload?.map((entry, index) => (
<div key={`legend-${index}`} className="flex items-center gap-2"> <div key={`legend-${index}`} className="flex items-center gap-2">
<div <div
className="w-3 h-3 rounded" className="w-3 h-3 rounded-sm"
style={{ backgroundColor: entry.color }} style={{ backgroundColor: entry.color }}
/> />
<span className="text-xs text-muted-foreground"> <span className="text-[10px] text-muted-foreground">
{entry.value} {entry.value}
</span> </span>
</div> </div>
@@ -111,13 +121,13 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
<Bar <Bar
dataKey="potholes" dataKey="potholes"
name="Potholes" name="Potholes"
fill={COLORS.pothole} fill="url(#barGradientPothole)"
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
/> />
<Bar <Bar
dataKey="signboards" dataKey="signboards"
name="Signboards" name="Signboards"
fill={COLORS.signboard} fill="url(#barGradientSignboard)"
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
/> />
</BarChart> </BarChart>

View File

@@ -43,13 +43,13 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent
if (type === "pothole-detection") { if (type === "pothole-detection") {
return <Badge className="bg-orange-500/20 text-orange-600 border-orange-500/30">Pothole</Badge> return <Badge className="bg-orange-500/20 text-orange-600 border-orange-500/30">Pothole</Badge>
} }
return <Badge className="bg-cyan-500/20 text-cyan-600 border-cyan-500/30">Signboard</Badge> return <Badge className="bg-blue-500/20 text-blue-600 border-blue-500/30">Signboard</Badge>
} }
return ( return (
<Card className="glass-card card-glow border-0 overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="pb-3 border-b border-border/50"> <CardHeader className="pb-3 border-b border-border/50">
<CardTitle className="text-lg font-semibold">Recent Analyses</CardTitle> <CardTitle className="text-lg font-bold text-blue-600">Recent Analyses</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
{isLoading ? ( {isLoading ? (

102
components/data-table.tsx Normal file
View File

@@ -0,0 +1,102 @@
"use client"
import { Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
interface Column<T> {
key: string
header: string
render?: (item: T) => React.ReactNode
}
interface DataTableProps<T> {
title: string
data: T[]
columns: Column<T>[]
onAddNew: () => void
addButtonText: string
isLoading?: boolean
}
export function DataTable<T extends Record<string, any>>({
title,
data,
columns,
onAddNew,
addButtonText,
isLoading = false
}: DataTableProps<T>) {
return (
<div>
{/* Header with Title and Add Button */}
<div className="mb-4 flex justify-between items-center bg-card p-3 rounded-xl border border-[var(--border)] shadow-sm">
<div>
<h2 className="text-lg font-bold tracking-tight text-blue-600">{title}</h2>
<p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase font-medium">Total items: {data.length}</p>
</div>
<Button
onClick={onAddNew}
className="bg-blue-600 hover:bg-blue-800 text-white shadow-md transition-all duration-300 hover:scale-105 active:scale-95 border-none h-8 px-4 text-xs font-semibold rounded-lg"
>
<Plus className="h-3.5 w-3.5 mr-1.5 stroke-[3px]" />
{addButtonText}
</Button>
</div>
{/* Table */}
<div className="overflow-hidden rounded-2xl border border-[var(--border)] bg-card shadow-2xl shadow-black/5">
<table className="min-w-full divide-y divide-gray-200/50 dark:divide-gray-700/50 border-collapse">
<thead className="bg-[#f8fafc] dark:bg-gray-900/50">
<tr>
{columns.map((col) => (
<th
key={col.key}
className="px-6 py-4 text-left text-[11px] font-black text-gray-500 dark:text-gray-400 uppercase tracking-[0.1em] border-b border-[var(--border)]"
>
{col.header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-100/30 dark:divide-gray-800/30">
{isLoading ? (
<tr>
<td colSpan={columns.length} className="px-6 py-16 text-center">
<div className="flex flex-col items-center justify-center gap-3">
<div className="h-8 w-8 border-3 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">Loading records...</span>
</div>
</td>
</tr>
) : data.length === 0 ? (
<tr>
<td colSpan={columns.length} className="px-6 py-16 text-center">
<div className="flex flex-col items-center justify-center gap-2">
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-2">
<Plus className="w-6 h-6 text-gray-400" />
</div>
<p className="text-gray-500 dark:text-gray-400 font-medium">No data available</p>
<p className="text-xs text-gray-400 dark:text-gray-500">Click "{addButtonText}" to get started</p>
</div>
</td>
</tr>
) : (
data.map((item, idx) => (
<tr
key={idx}
className="hover:bg-blue-50/30 dark:hover:bg-blue-900/10 transition-colors duration-200"
>
{columns.map((col) => (
<td key={col.key} className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300 font-medium">
{col.render ? col.render(item) : item[col.key] || "—"}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -14,7 +14,10 @@ import {
Menu, Menu,
LayoutDashboard, LayoutDashboard,
ChevronRight, ChevronRight,
Home, PlusCircle,
FolderPlus,
Package,
MapPin,
Map Map
} from "lucide-react" } from "lucide-react"
@@ -26,18 +29,36 @@ interface NavItem {
} }
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{
title: "Home",
description: "Project selection and analysis setup",
href: "/",
icon: Home
},
{ {
title: "Dashboard", title: "Dashboard",
description: "View analytics, statistics, and recent analyses", description: "View analytics, statistics, and recent analyses",
href: "/dashboard", href: "/dashboard",
icon: LayoutDashboard icon: LayoutDashboard
}, },
{
title: "New Analysis",
description: "Start a new road analysis project",
href: "/new-analysis",
icon: PlusCircle
},
{
title: "Create Project",
description: "Create a new infrastructure project",
href: "/create-project",
icon: FolderPlus
},
{
title: "Create Package",
description: "Add a package under an existing project",
href: "/create-package",
icon: Package
},
{
title: "Create Location",
description: "Add a location under a project & package",
href: "/create-location",
icon: MapPin
},
{ {
title: "Show Map", title: "Show Map",
description: "View all detections on an interactive map", description: "View all detections on an interactive map",
@@ -94,8 +115,8 @@ export function NavigationMenu() {
}`} }`}
> >
<div className={`p-2.5 rounded-lg transition-all duration-300 ${isActive <div className={`p-2.5 rounded-lg transition-all duration-300 ${isActive
? "bg-gradient-to-br from-primary to-accent text-white" ? "bg-[#60a5fa] text-white"
: "bg-white/5 text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary" : "bg-white/5 text-muted-foreground group-hover:bg-[#60a5fa]/10 group-hover:text-[#60a5fa]"
}`}> }`}>
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
</div> </div>

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { useRouter, usePathname } from "next/navigation" import { useRouter, usePathname } from "next/navigation"
import { Home, LayoutDashboard, User } from "lucide-react" import { LayoutDashboard, Plus, User, FolderPlus, Package, MapPin } from "lucide-react"
interface NavItem { interface NavItem {
title: string title: string
@@ -12,18 +12,30 @@ interface NavItem {
} }
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{
title: "Home",
href: "/",
icon: Home,
gradient: "from-emerald-400 to-teal-500"
},
{ {
title: "Dashboard", title: "Dashboard",
href: "/dashboard", href: "/dashboard",
icon: LayoutDashboard, icon: LayoutDashboard,
gradient: "from-blue-400 to-indigo-500" gradient: "from-blue-400 to-indigo-500"
}, },
{
title: "Create Project",
href: "/create-project",
icon: FolderPlus,
gradient: "from-blue-400 to-blue-600"
},
{
title: "Create Package",
href: "/create-package",
icon: Package,
gradient: "from-violet-400 to-purple-500"
},
{
title: "Create Location",
href: "/create-location",
icon: MapPin,
gradient: "from-amber-400 to-orange-500"
},
{ {
title: "Account", title: "Account",
href: "/account", href: "/account",
@@ -42,54 +54,112 @@ export function SidebarNavigation() {
router.push(item.href) router.push(item.href)
} }
const isNewAnalysisActive = pathname === "/new-analysis"
return ( return (
<aside className="fixed left-0 top-0 h-screen w-16 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl border-r border-gray-200/50 dark:border-gray-700/50 z-50 flex flex-col items-center py-6 shadow-lg"> <aside className="fixed left-0 top-0 h-screen w-20 bg-[#2563eb] border-r border-[#2563eb]/20 z-50 flex flex-col items-center py-8 shadow-2xl">
{/* Logo */} {/* Logo */}
<div className="mb-8"> <div className="mb-6">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-indigo-500/30"> <div className="w-12 h-12 rounded-xl bg-white flex items-center justify-center shadow-lg border-2 border-[#1e40af] transition-transform duration-300 hover:scale-110">
<span className="text-white font-bold text-lg">V</span> <span className="text-[#2563eb] font-black text-xl">V</span>
</div> </div>
</div> </div>
{/* Navigation Items */} {/* Navigation Items */}
<nav className="flex-1 flex flex-col items-center gap-3"> <nav className="flex-1 flex flex-col items-center gap-4">
{navItems.map((item) => { {/* Dashboard - first item */}
{navItems.slice(0, 1).map((item) => {
const isActive = pathname === item.href const isActive = pathname === item.href
const Icon = item.icon const Icon = item.icon
return ( return (
<div key={item.href} className="relative group"> <div key={item.href} className="relative group">
<button <button
onClick={() => handleNavigation(item)} onClick={() => handleNavigation(item)}
disabled={item.disabled}
className={` className={`
relative w-11 h-11 rounded-xl flex items-center justify-center relative w-12 h-12 rounded-xl flex items-center justify-center
bg-white text-[#2563eb] shadow-lg border-2
${isActive ? 'border-[#2563eb] ring-2 ring-white/30' : 'border-[#1e40af]'}
transition-all duration-300 ease-out transition-all duration-300 ease-out
${item.disabled hover:scale-110 hover:shadow-xl
? "text-gray-300 dark:text-gray-600 cursor-not-allowed opacity-50" active:scale-95
: isActive
? `bg-gradient-to-br ${item.gradient} text-white shadow-lg`
: "text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300"
}
`} `}
style={isActive && !item.disabled ? { boxShadow: `0 8px 20px -4px rgba(99, 102, 241, 0.4)` } : {}}
> >
<Icon className="h-5 w-5" /> <Icon className={`h-6 w-6 ${isActive ? 'stroke-[2.5]' : 'stroke-[2]'}`} />
</button> </button>
{/* Tooltip */}
<div className=" <div className="
absolute left-full ml-3 top-1/2 -translate-y-1/2 absolute left-full ml-3 top-1/2 -translate-y-1/2
px-3 py-1.5 rounded-lg px-3 py-1.5 rounded-lg
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
opacity-0 invisible group-hover:opacity-100 group-hover:visible opacity-0 invisible group-hover:opacity-100 group-hover:visible
transition-all duration-200 ease-out transition-all duration-200 ease-out
whitespace-nowrap whitespace-nowrap shadow-lg pointer-events-none
shadow-lg
pointer-events-none
"> ">
{item.title}{item.disabled && " (Coming Soon)"} {item.title}
{/* Arrow */} <div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
</div>
</div>
)
})}
{/* New Analysis - Special eye-catchy button */}
<div className="relative group">
<button
onClick={() => router.push("/new-analysis")}
className={`
relative w-12 h-12 rounded-xl flex items-center justify-center
bg-white text-[#2563eb] shadow-lg border-2
${isNewAnalysisActive ? 'border-[#2563eb] ring-2 ring-white/30' : 'border-[#1e40af]'}
transition-all duration-300 ease-out
hover:scale-110 hover:shadow-xl
active:scale-95
`}
>
<Plus className={`h-6 w-6 ${isNewAnalysisActive ? 'stroke-[2.5]' : 'stroke-[2]'}`} />
</button>
<div className="
absolute left-full ml-3 top-1/2 -translate-y-1/2
px-3 py-1.5 rounded-lg
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
opacity-0 invisible group-hover:opacity-100 group-hover:visible
transition-all duration-200 ease-out
whitespace-nowrap shadow-lg pointer-events-none
">
Start New Analysis
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
</div>
</div>
{/* Divider */}
<div className="w-8 h-px bg-white/20 my-1" />
{/* Create items */}
{navItems.slice(1, 4).map((item) => {
const isActive = pathname === item.href
const Icon = item.icon
return (
<div key={item.href} className="relative group">
<button
onClick={() => handleNavigation(item)}
className={`
relative w-12 h-12 rounded-xl flex items-center justify-center
bg-white text-[#2563eb] shadow-lg border-2
${isActive ? 'border-[#2563eb] ring-2 ring-white/30' : 'border-[#1e40af]'}
transition-all duration-300 ease-out
hover:scale-110 hover:shadow-xl
active:scale-95
`}
>
<Icon className={`h-6 w-6 ${isActive ? 'stroke-[2.5]' : 'stroke-[2]'}`} />
</button>
<div className="
absolute left-full ml-3 top-1/2 -translate-y-1/2
px-3 py-1.5 rounded-lg
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
opacity-0 invisible group-hover:opacity-100 group-hover:visible
transition-all duration-200 ease-out
whitespace-nowrap shadow-lg pointer-events-none
">
{item.title}
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" /> <div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
</div> </div>
</div> </div>
@@ -99,8 +169,25 @@ export function SidebarNavigation() {
{/* Bottom section */} {/* Bottom section */}
<div className="mt-auto"> <div className="mt-auto">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-700 dark:to-gray-800 flex items-center justify-center"> <div className="relative group">
<User className="h-5 w-5 text-gray-500 dark:text-gray-400" /> <button
onClick={() => { }}
className="w-12 h-12 rounded-full bg-white flex items-center justify-center cursor-not-allowed opacity-80 border-2 border-[#1e40af] transition-transform duration-300 hover:scale-110"
disabled
>
<User className="h-6 w-6 text-[#2563eb]" />
</button>
<div className="
absolute left-full ml-3 top-1/2 -translate-y-1/2
px-3 py-1.5 rounded-lg
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
opacity-0 invisible group-hover:opacity-100 group-hover:visible
transition-all duration-200 ease-out
whitespace-nowrap shadow-lg pointer-events-none
">
Account (Coming Soon)
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
</div>
</div> </div>
</div> </div>
</aside> </aside>

View File

@@ -49,8 +49,8 @@ export function SummarySection({ data }: SummarySectionProps) {
label: "Resolution", label: "Resolution",
value: `${data.video_info.width}×${data.video_info.height}`, value: `${data.video_info.width}×${data.video_info.height}`,
icon: Monitor, icon: Monitor,
color: "text-cyan-500", color: "text-blue-500",
bgColor: "bg-cyan-50 dark:bg-cyan-950/30", bgColor: "bg-blue-50 dark:bg-blue-950/30",
}, },
] ]

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm transition-all duration-300 hover:shadow-md hover:border-[var(--card-hover-border)] hover:shadow-[var(--card-glow)] hover:-translate-y-0.5',
className, className,
)} )}
{...props} {...props}

View File

@@ -198,15 +198,15 @@ function DetailedSummarySection({
<div className="space-y-4"> <div className="space-y-4">
{/* Location-based Summary */} {/* Location-based Summary */}
<Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden"> <Card className="bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm border-0 shadow-lg shadow-gray-200/50 dark:shadow-gray-900/50 rounded-xl overflow-hidden">
<CardHeader className="pb-3 bg-gradient-to-r from-cyan-50 to-blue-50 dark:from-cyan-950/30 dark:to-blue-950/30"> <CardHeader className="pb-3 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-gradient-to-br from-cyan-400 to-blue-500 shadow-md shadow-cyan-500/30"> <div className="p-1.5 rounded-lg bg-gradient-to-br from-blue-400 to-indigo-500 shadow-md shadow-blue-500/30">
<MapIcon className="h-4 w-4 text-white" /> <MapIcon className="h-4 w-4 text-white" />
</div> </div>
<div> <div>
<CardTitle className="text-base font-bold"> <CardTitle className="text-base font-bold">
<span className="bg-gradient-to-r from-cyan-600 via-blue-500 to-cyan-600 dark:from-cyan-400 dark:via-blue-400 dark:to-cyan-400 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-blue-600 via-indigo-500 to-blue-600 dark:from-blue-400 dark:via-indigo-400 dark:to-blue-400 bg-clip-text text-transparent">
Detection Locations Detection Locations
</span> </span>
</CardTitle> </CardTitle>
@@ -219,9 +219,9 @@ function DetailedSummarySection({
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setShowMap(true)} onClick={() => setShowMap(true)}
className="gap-2 border-cyan-200 dark:border-cyan-800 hover:bg-cyan-50 dark:hover:bg-cyan-900/50" className="gap-2 border-blue-200 dark:border-blue-800 hover:bg-blue-50 dark:hover:bg-blue-900/50"
> >
<MapIcon className="h-4 w-4 text-cyan-500" /> <MapIcon className="h-4 w-4 text-blue-500" />
Show Map Show Map
</Button> </Button>
</div> </div>
@@ -362,8 +362,8 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh
label: "Resolution", label: "Resolution",
value: `${data.video_info.width}×${data.video_info.height}`, value: `${data.video_info.width}×${data.video_info.height}`,
icon: Monitor, icon: Monitor,
color: "text-cyan-500", color: "text-blue-500",
bgColor: "bg-cyan-50 dark:bg-cyan-950/30", bgColor: "bg-blue-50 dark:bg-blue-950/30",
}, },
] ]

View File

@@ -42,7 +42,7 @@ export interface Location {
updated_at: string updated_at: string
} }
// Helper function for API requests // Helper function for GET API requests
async function apiRequest<T>(endpoint: string): Promise<T> { async function apiRequest<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, { const response = await fetch(`${API_URL}${endpoint}`, {
headers: { headers: {
@@ -58,6 +58,74 @@ async function apiRequest<T>(endpoint: string): Promise<T> {
return response.json() return response.json()
} }
// Helper function for POST API requests
async function apiPostRequest<T>(endpoint: string, body: unknown): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true"
},
body: JSON.stringify(body)
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API Error: ${response.status} - ${errorText}`)
}
return response.json()
}
// Create request body types
export interface ProjectCreate {
name: string
state?: string | null
corridor_name?: string | null
start_lat?: number | null
start_lng?: number | null
end_lat?: number | null
end_lng?: number | null
}
export interface PackageCreate {
project_id: string
name: string
region?: string | null
}
export interface LocationCreate {
package_id: string
segment_name: string
chainage_start_km?: number | null
chainage_end_km?: number | null
start_lat: number
start_lng: number
end_lat: number
end_lng: number
}
/**
* Create a new project
*/
export async function createProject(data: ProjectCreate): Promise<Project> {
return apiPostRequest<Project>("/projects/", data)
}
/**
* Create a new package in a project
*/
export async function createPackage(data: PackageCreate): Promise<Package> {
return apiPostRequest<Package>("/packages/", data)
}
/**
* Create a new location in a package
*/
export async function createLocation(data: LocationCreate): Promise<Location> {
return apiPostRequest<Location>("/locations/", data)
}
// API Functions // API Functions
/** /**