refactor: remove static routes and stop multiple token api call in sse

This commit is contained in:
2026-06-23 22:48:34 +05:30
parent b282fdf56d
commit 5e4cf61d6e
25 changed files with 129 additions and 2086 deletions

View File

@@ -1,143 +0,0 @@
'use client';
import * as React from 'react';
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { Loader2 } from 'lucide-react';
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
interface ChainageData {
name: string;
defected_sign_board: number;
pothole: number;
road_crack: number;
damaged_road_marking: number;
drain_issue: number;
defective_culvert: number;
total: number;
}
interface ChainageBarChartProps {
data: ChainageData[];
isLoading?: boolean;
}
const chartConfig = {
pothole: {
label: DETECTION_TYPES.pothole.label + 's',
color: DETECTION_TYPES.pothole.color,
},
defected_sign_board: {
label: DETECTION_TYPES.defected_sign_board.label + 's',
color: DETECTION_TYPES.defected_sign_board.color,
},
road_crack: {
label: DETECTION_TYPES.road_crack.label + 's',
color: DETECTION_TYPES.road_crack.color,
},
damaged_road_marking: {
label: DETECTION_TYPES.damaged_road_marking.label + 's',
color: DETECTION_TYPES.damaged_road_marking.color,
},
drain_issue: {
label: DETECTION_TYPES.drain_issue.label + 's',
color: DETECTION_TYPES.drain_issue.color,
},
defective_culvert: {
label: DETECTION_TYPES.defective_culvert.label + 's',
color: DETECTION_TYPES.defective_culvert.color,
},
} satisfies ChartConfig;
export function ChainageBarChart({
data,
isLoading = false,
}: ChainageBarChartProps) {
if (isLoading) {
return (
<div className="h-[250px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
</div>
);
}
if (data.length === 0) {
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No segment data available</p>
<p className="text-xs mt-1">
Process videos to see detections by segment
</p>
</div>
);
}
// Filter out good signboards for a more "damage-focused" standard chart as per multiple bar example
const chartData = data.map((item) => ({
name: item.name,
pothole: item.pothole,
defected_sign_board: item.defected_sign_board,
road_crack: item.road_crack,
damaged_road_marking: item.damaged_road_marking,
drain_issue: item.drain_issue,
defective_culvert: item.defective_culvert,
}));
return (
<div className="h-[250px] w-full">
<ChartContainer config={chartConfig} className="h-full w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} strokeOpacity={0.1} />
<XAxis
dataKey="name"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) =>
value.length > 8 ? `${value.slice(0, 8)}...` : value
}
fontSize={12}
/>
<YAxis
tickLine={false}
axisLine={false}
fontSize={12}
tickMargin={10}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dashed" />}
/>
<Bar dataKey="pothole" fill="var(--color-pothole)" radius={4} />
<Bar
dataKey="defected_sign_board"
fill="var(--color-defected_sign_board)"
radius={4}
/>
<Bar dataKey="road_crack" fill="var(--color-road_crack)" radius={4} />
<Bar
dataKey="damaged_road_marking"
fill="var(--color-damaged_road_marking)"
radius={4}
/>
<Bar
dataKey="drain_issue"
fill="var(--color-drain_issue)"
radius={4}
/>
<Bar
dataKey="defective_culvert"
fill="var(--color-defective_culvert)"
radius={4}
/>
</BarChart>
</ChartContainer>
</div>
);
}

View File

@@ -1,166 +0,0 @@
'use client';
import { useEffect } from 'react';
import {
MapContainer,
TileLayer,
CircleMarker,
Popup,
useMap,
} from 'react-leaflet';
import { LatLngBounds, LatLng } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { Detection } from '@/types';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
interface DashboardMapContentProps {
detections: Detection[];
}
// Component to auto-fit map bounds to show all markers
function FitBounds({ bounds }: { bounds: LatLngBounds }) {
const map = useMap();
useEffect(() => {
if (bounds.isValid()) {
map.fitBounds(bounds, { padding: [50, 50] });
}
}, [bounds, map]);
return null;
}
export default function DashboardMapContent({
detections,
}: DashboardMapContentProps) {
if (detections.length === 0) {
return (
<div className="h-full w-full flex items-center justify-center bg-muted/20">
<p className="text-muted-foreground">No detections to display</p>
</div>
);
}
// Calculate bounds to fit all markers
const firstDetection = detections[0];
const bounds = new LatLngBounds(
new LatLng(firstDetection.latitude!, firstDetection.longitude!),
new LatLng(firstDetection.latitude!, firstDetection.longitude!),
);
detections.forEach((d) => {
if (d.latitude && d.longitude) {
bounds.extend(new LatLng(d.latitude, d.longitude));
}
});
// Center point
const center: [number, number] = [
(bounds.getNorth() + bounds.getSouth()) / 2,
(bounds.getEast() + bounds.getWest()) / 2,
];
// Get marker color based on detection type
const getMarkerColor = (typeId: string) => {
const config = DETECTION_TYPES[typeId.toLowerCase()];
if (config) {
return {
fill: config.color,
stroke: '#ffffff', // White stroke for better visibility on the map
};
}
return { fill: '#64748b', stroke: '#475569' }; // Default Slate
};
// Get display name for detection type
const getTypeName = (typeId: string) => {
const config = DETECTION_TYPES[typeId.toLowerCase()];
return config ? config.label : typeId.replace(/_/g, ' ');
};
return (
<MapContainer
center={center}
zoom={13}
className="h-full w-full"
scrollWheelZoom={true}
zoomAnimation={false}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* Detection markers */}
{detections.map((detection, idx) => {
const colors = getMarkerColor(detection.type);
const typeName = getTypeName(detection.type);
return (
<CircleMarker
key={`${detection.id}-${idx}`}
center={[detection.latitude!, detection.longitude!]}
radius={10}
fillColor={colors.fill}
color={colors.stroke}
weight={2}
opacity={1}
fillOpacity={0.8}
>
<Popup>
<div className="text-sm min-w-[200px] py-1">
<div className="font-bold text-base border-b border-border pb-2 mb-3 leading-none">
{typeName}
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">Class</span>
<span className="font-medium text-right capitalize">
{detection.class.replace(/_/g, ' ')}
</span>
</div>
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">
Confidence
</span>
<span className="font-medium text-right">
{(detection.confidence * 100).toFixed(1)}%
</span>
</div>
</div>
<div className="pt-2 border-t border-border/50">
<div className="text-[10px] uppercase tracking-wider font-semibold text-muted-foreground mb-2">
Location Details
</div>
<div className="grid grid-cols-2 gap-3 bg-muted/40 p-2.5 rounded-md font-mono text-[11px]">
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">
Latitude
</span>
<span className="font-medium tracking-tighter">
{detection.latitude!.toFixed(6)}
</span>
</div>
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">
Longitude
</span>
<span className="font-medium tracking-tighter">
{detection.longitude!.toFixed(6)}
</span>
</div>
</div>
</div>
</div>
</Popup>
</CircleMarker>
);
})}
{/* Auto-fit bounds */}
<FitBounds bounds={bounds} />
</MapContainer>
);
}

View File

@@ -1,216 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2, Milestone, Maximize2, Minimize2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Detection } from '@/types';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
// Dynamically import the map to avoid SSR issues with Leaflet
const DashboardMapContent = dynamic(() => import('./dashboard-map-content'), {
ssr: false,
loading: () => (
<div className="h-full w-full flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary" />
</div>
),
});
interface DashboardMapProps {
className?: string;
selectedProjectId?: string | null;
selectedPackageId?: string | null;
selectedChainageId?: string | null;
projectSummary?: any;
}
export function DashboardMap({
className,
selectedProjectId,
selectedPackageId,
selectedChainageId,
projectSummary,
}: DashboardMapProps) {
const [detections, setDetections] = useState<Detection[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isMaximized, setIsMaximized] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
useEffect(() => {
if (!projectSummary) {
setDetections([]);
setIsLoading(false);
return;
}
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)) {
if (!pkg) continue;
const chainagesToProcess =
selectedChainageId && selectedChainageId !== 'all'
? {
[selectedChainageId]: (pkg as any).chainages?.[
selectedChainageId
],
}
: (pkg as any).chainages || {};
for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
if (!chn) continue;
const chainageDetections = ((chn as any).detections || []).filter(
(d: any) =>
(d.type || d.class || '').toLowerCase() !== 'good_sign_board',
);
filteredDetections.push(...chainageDetections);
}
}
setDetections(filteredDetections);
} catch (err) {
console.error('Failed to extract detections:', err);
setError('Failed to load detection data');
} finally {
setIsLoading(false);
}
}, [projectSummary, selectedPackageId, selectedChainageId]);
// Handle detection type toggle
const toggleType = (typeId: string) => {
setSelectedTypes((prev) => {
if (prev.includes(typeId)) {
const next = prev.filter((id) => id !== typeId);
return next.length === 0 ? [] : next;
} else {
return [...prev, typeId];
}
});
};
// Filter detections with valid GPS coordinates and selected types
const validDetections = detections.filter((d) => {
const hasGps = d.latitude && d.longitude;
if (!hasGps) return false;
if (selectedTypes.length === 0) return true;
const typeId = (d.type || d.class || '').toLowerCase();
return selectedTypes.includes(typeId);
});
return (
<Card
className={`overflow-hidden transition-all duration-300 p-2 rounded-lg ${
isMaximized
? 'fixed inset-0 z-100 m-0 rounded-none bg-background'
: className
}`}
>
<CardContent className="p-0 relative rounded-lg">
{/* Top Right Actions */}
<div className="absolute top-4 right-4 z-1000 pointer-events-none">
<div className="pointer-events-auto">
<Button
variant="default"
size="icon"
onClick={() => setIsMaximized(!isMaximized)}
title={isMaximized ? 'Exit Fullscreen' : 'Maximize Map'}
>
{isMaximized ? (
<Minimize2 className="h-5 w-5" />
) : (
<Maximize2 className="h-5 w-5" />
)}
</Button>
</div>
</div>
{/* Bottom Left Legend */}
<div className="absolute bottom-4 left-4 z-1000 pointer-events-none">
<div className="pointer-events-auto bg-zinc-950/80 backdrop-blur-xl border rounded-md p-2">
<div className="flex flex-wrap items-center gap-2 max-w-4xl">
{Object.values(DETECTION_TYPES).map((type) => {
const typeId = type.id.toLowerCase();
const count = detections.filter(
(d) =>
(d.type || d.class || '').toLowerCase() === typeId &&
d.latitude &&
d.longitude,
).length;
if (
count === 0 &&
(type.id.includes('culvert') || type.id === 'drain_issue')
)
return null;
if (type.id === 'good_sign_board') return null;
const isSelected =
selectedTypes.length === 0 || selectedTypes.includes(typeId);
return (
<button
key={type.id}
onClick={() => toggleType(typeId)}
className={`flex items-center gap-2 px-2 py-1.5 text-zinc-100 rounded-full transition-all text-xs ${
isSelected ? 'bg-zinc-800/80 ' : 'bg-transparent '
}`}
>
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: type.color }}
/>
<span>
{type.label} ({count})
</span>
</button>
);
})}
</div>
</div>
</div>
<div
className={`${isMaximized ? 'h-screen' : 'h-[600px]'} w-full relative transition-all`}
>
{isLoading ? (
<div className="h-full w-full flex items-center justify-center bg-muted/50">
<Loader2 className="h-8 w-8 text-primary animate-spin" />
</div>
) : error ? (
<div className="h-full w-full flex items-center justify-center bg-muted/50">
<p className="text-muted-foreground">{error}</p>
</div>
) : validDetections.length === 0 ? (
<div className="h-full w-full flex items-center justify-center bg-muted/50">
<div className="text-center">
<p className="text-muted-foreground">
No detections with GPS coordinates found
</p>
<p className="text-sm text-muted-foreground mt-1 opacity-70">
Process some videos to see detections on the map
</p>
</div>
</div>
) : (
<DashboardMapContent detections={validDetections} />
)}
{/* Bottom Left Label Overlay */}
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,102 +0,0 @@
'use client';
import { Skeleton } from '@/components/ui/skeleton';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
export function DashboardSkeleton() {
return (
<div className="space-y-6">
{/* Stats Cards Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="py-6 px-4">
<CardContent className="p-0 flex items-center justify-between gap-6">
<div className="flex flex-col gap-1 min-w-0 flex-1">
<Skeleton className="h-4 w-24 mb-1" />
<div className="flex flex-col gap-1">
<Skeleton className="h-9 w-20" />
<Skeleton className="h-3 w-32" />
</div>
</div>
<Skeleton className="w-12 h-12 rounded-xl shrink-0" />
</CardContent>
</Card>
))}
</div>
{/* Charts Row Skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{/* Donut Chart Skeleton */}
<Card className="h-full">
<CardHeader className="items-center pb-2">
<Skeleton className="h-6 w-48 mb-2" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent className="h-[300px] flex items-center justify-center">
<Skeleton className="h-56 w-56 rounded-full border-20 border-muted" />
</CardContent>
</Card>
{/* Bar Chart Skeleton */}
<Card className="h-full">
<CardHeader className="items-center pb-2">
<Skeleton className="h-6 w-40 mb-2" />
<Skeleton className="h-4 w-52" />
</CardHeader>
<CardContent className="h-[300px] flex items-end justify-between gap-2 px-6 pb-8 pt-4">
{[...Array(8)].map((_, i) => {
const heights = [
'60%',
'40%',
'75%',
'50%',
'65%',
'35%',
'80%',
'45%',
];
return (
<div
key={i}
className="flex flex-col gap-1 items-center flex-1"
>
<Skeleton
className="w-full rounded-t-sm"
style={{ height: heights[i % heights.length] }}
/>
<Skeleton className="h-2 w-full mt-2" />
</div>
);
})}
</CardContent>
</Card>
</div>
{/* Map Skeleton */}
<Card className="overflow-hidden">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Skeleton className="w-9 h-9 rounded-md" />
<div className="flex flex-col gap-1">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-3 w-48" />
</div>
</div>
<div className="flex gap-2">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center gap-1">
<Skeleton className="w-2.5 h-2.5 rounded-full" />
<Skeleton className="h-3 w-16" />
</div>
))}
</div>
</div>
</CardHeader>
<CardContent className="p-2">
<Skeleton className="h-[500px] w-full rounded-md" />
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,176 +0,0 @@
'use client';
import * as React from 'react';
import { Loader2 } from 'lucide-react';
import { Label, Pie, PieChart } from 'recharts';
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
interface DetectionDonutChartProps {
defectedSignboard: number;
pothole: number;
roadCrack: number;
damagedRoadMarking: number;
// goodSignboard: number;
drainIssue: number;
defectiveCulvert: number;
isLoading?: boolean;
}
const chartConfig = {
pothole: {
label: DETECTION_TYPES.pothole.label + 's',
color: DETECTION_TYPES.pothole.color,
},
defectedSignboard: {
label: DETECTION_TYPES.defected_sign_board.label + 's',
color: DETECTION_TYPES.defected_sign_board.color,
},
roadCrack: {
label: DETECTION_TYPES.road_crack.label + 's',
color: DETECTION_TYPES.road_crack.color,
},
damagedRoadMarking: {
label: DETECTION_TYPES.damaged_road_marking.label + 's',
color: DETECTION_TYPES.damaged_road_marking.color,
},
drainIssue: {
label: DETECTION_TYPES.drain_issue.label + 's',
color: DETECTION_TYPES.drain_issue.color,
},
defectiveCulvert: {
label: DETECTION_TYPES.defective_culvert.label + 's',
color: DETECTION_TYPES.defective_culvert.color,
},
} satisfies ChartConfig;
export function DetectionDonutChart({
defectedSignboard,
pothole,
roadCrack,
damagedRoadMarking,
// goodSignboard,
drainIssue,
defectiveCulvert,
isLoading = false,
}: DetectionDonutChartProps) {
const chartData = React.useMemo(
() =>
[
{ type: 'pothole', count: pothole, fill: 'var(--color-pothole)' },
{
type: 'defectedSignboard',
count: defectedSignboard,
fill: 'var(--color-defectedSignboard)',
},
{ type: 'roadCrack', count: roadCrack, fill: 'var(--color-roadCrack)' },
{
type: 'damagedRoadMarking',
count: damagedRoadMarking,
fill: 'var(--color-damagedRoadMarking)',
},
/* {
type: 'goodSignboard',
count: goodSignboard,
fill: 'var(--color-goodSignboard)',
}, */
{
type: 'drainIssue',
count: drainIssue,
fill: 'var(--color-drainIssue)',
},
{
type: 'defectiveCulvert',
count: defectiveCulvert,
fill: 'var(--color-defectiveCulvert)',
},
].filter((item) => item.count > 0),
[
pothole,
defectedSignboard,
roadCrack,
damagedRoadMarking,
// goodSignboard,
drainIssue,
defectiveCulvert,
],
);
const totalDetections = React.useMemo(() => {
return chartData.reduce((acc, curr) => acc + curr.count, 0);
}, [chartData]);
if (isLoading) {
return (
<div className="h-[250px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
</div>
);
}
if (totalDetections === 0) {
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No detections found</p>
<p className="text-xs mt-1">Process videos to see data</p>
</div>
);
}
return (
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie
data={chartData}
dataKey="count"
nameKey="type"
innerRadius={60}
strokeWidth={5}
>
<Label
content={({ viewBox }) => {
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-3xl font-bold"
>
{totalDetections.toLocaleString()}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
Total
</tspan>
</text>
);
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
);
}

View File

@@ -1,131 +0,0 @@
'use client';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { FolderOpen, Package, Milestone } from 'lucide-react';
import { Project } from '@/types';
interface FilterSelectorProps {
projects: Project[];
selectedProjectId: string | null;
selectedPackageId: string | null;
selectedChainageId: string | null;
onProjectChange: (projectId: string) => void;
onPackageChange: (packageId: string) => void;
onChainageChange: (chainageId: string) => void;
packages: Array<{ id: string; name: string }>;
chainages: Array<{ id: string; name: string }>;
isLoading?: boolean;
}
export function FilterSelector({
projects,
selectedProjectId,
selectedPackageId,
selectedChainageId,
onProjectChange,
onPackageChange,
onChainageChange,
packages,
chainages,
isLoading = false,
}: FilterSelectorProps) {
return (
<div className="flex flex-col gap-5 p-5">
{/* Project Dropdown */}
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
<FolderOpen className="h-4 w-4" />
</div>
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">
Project
</span>
</div>
<Select
value={selectedProjectId || ''}
onValueChange={onProjectChange}
disabled={isLoading || projects.length === 0}
>
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Package Dropdown */}
{selectedProjectId && (
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
<Package className="h-4 w-4" />
</div>
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">
Package
</span>
</div>
<Select
value={selectedPackageId || 'all'}
onValueChange={onPackageChange}
disabled={isLoading}
>
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60">
<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>
)}
{/* Chainage Dropdown */}
{selectedPackageId && selectedPackageId !== 'all' && (
<div className="space-y-3">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
<Milestone className="h-4 w-4" />
</div>
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">
Segment
</span>
</div>
<Select
value={selectedChainageId || 'all'}
onValueChange={onChainageChange}
disabled={isLoading}
>
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60">
<SelectValue placeholder="All segments" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Segments</SelectItem>
{chainages.map((chn) => (
<SelectItem key={chn.id} value={chn.id}>
{chn.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
);
}

View File

@@ -1,57 +0,0 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import { LucideIcon } from 'lucide-react';
import { motion } from 'motion/react';
interface StatsCardProps {
title: string;
subtitle?: string;
value: number | string;
icon: LucideIcon;
isLoading?: boolean;
}
export function StatsCard({
title,
subtitle,
value,
icon: Icon,
isLoading = false,
}: StatsCardProps) {
return (
<motion.div
whileHover={{ y: -4, scale: 1.02 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="h-full"
>
<Card className="h-full py-6 px-4 transition-colors cursor-pointer">
<CardContent className="p-0 flex items-center justify-between gap-6">
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
{title}
</h3>
<div className="flex flex-col">
{isLoading ? (
<div className="h-10 w-24 bg-muted rounded-md mt-2 animate-pulse" />
) : (
<>
<p className="text-3xl font-bold tracking-tight">{value}</p>
{subtitle && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
{subtitle}
</p>
)}
</>
)}
</div>
</div>
<div className="p-3 rounded-xl bg-primary/10 text-primary shrink-0 group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
<Icon className="h-6 w-6" />
</div>
</CardContent>
</Card>
</motion.div>
);
}

View File

@@ -1,217 +0,0 @@
'use client';
import { useEffect } from 'react';
import {
MapContainer,
TileLayer,
Polyline,
CircleMarker,
Popup,
useMap,
} from 'react-leaflet';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { LatLngBounds, LatLng } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import {
getDetectionModeConfig,
DETECTION_TYPES,
} from '@/constants/detectionModeConfig';
import { DetectionType } from '@/types';
type Detection = {
id: number;
type: string;
class: string;
confidence: number;
latitude: number;
longitude: number;
frame_number: number;
};
type MapModalProps = {
open: boolean;
onClose: () => void;
detections: Detection[];
detectionType: DetectionType | string;
};
// Component to auto-fit map bounds to show all markers
function FitBounds({ bounds }: { bounds: LatLngBounds }) {
const map = useMap();
useEffect(() => {
if (!map || !bounds.isValid()) return;
const timer = setTimeout(() => {
map.invalidateSize();
map.fitBounds(bounds, {
padding: [50, 50],
maxZoom: 16,
animate: true,
});
}, 200);
return () => clearTimeout(timer);
}, [bounds, map]);
return null;
}
export default function MapModal({
open,
onClose,
detections,
detectionType,
}: MapModalProps) {
const validDetections = detections.filter((d) => d.latitude && d.longitude);
if (validDetections.length === 0) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl h-[600px]">
<DialogHeader>
<DialogTitle>Detection Map</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-center h-full text-muted-foreground text-sm font-medium">
No GPS data available for detections.
</div>
</DialogContent>
</Dialog>
);
}
const routeCoordinates: [number, number][] = validDetections.map((d) => [
d.latitude,
d.longitude,
]);
const bounds = new LatLngBounds(
new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]),
new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]),
);
routeCoordinates.forEach((coord) =>
bounds.extend(new LatLng(coord[0], coord[1])),
);
const center: [number, number] = [
(bounds.getNorth() + bounds.getSouth()) / 2,
(bounds.getEast() + bounds.getWest()) / 2,
];
const modeConfig = getDetectionModeConfig(detectionType);
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-6xl h-[85vh] p-0 flex flex-col overflow-hidden border-none shadow-2xl">
<DialogHeader className="px-6 py-4 border-b shrink-0">
<DialogTitle className="text-xl font-bold">
{modeConfig.label} Map
</DialogTitle>
<p className="text-xs text-muted-foreground font-medium">
{validDetections.length} points of interest identified with GPS data
</p>
</DialogHeader>
<div className="flex-1 w-full relative bg-muted/20">
<MapContainer
center={center}
zoom={13}
className="h-full w-full"
scrollWheelZoom={true}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Polyline
positions={routeCoordinates}
color="#3b82f6"
weight={4}
opacity={0.6}
/>
{validDetections.map((detection, idx) => {
const type = (detection.type || '').toLowerCase();
const typeConfig = DETECTION_TYPES[type];
const color = typeConfig
? { fill: typeConfig.color, stroke: typeConfig.color }
: { fill: '#64748b', stroke: '#475569' };
return (
<CircleMarker
key={`${detection.id}-${idx}`}
center={[detection.latitude, detection.longitude]}
radius={7}
fillColor={color.fill}
color={color.stroke}
weight={2}
opacity={1}
fillOpacity={0.9}
>
<Popup>
<div className="text-sm min-w-[200px] py-1">
<div className="font-bold text-base border-b border-border pb-2 mb-3 leading-none capitalize">
{(detection.type || '').replace(/_/g, ' ')}{' '}
<span className="text-muted-foreground font-medium text-sm ml-1">
#{detection.id}
</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">
Frame
</span>
<span className="font-medium text-right font-mono">
{detection.frame_number}
</span>
</div>
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">
Confidence
</span>
<span className="font-medium text-right">
{(detection.confidence * 100).toFixed(1)}%
</span>
</div>
</div>
<div className="pt-2 border-t border-border/50">
<div className="text-[10px] uppercase tracking-wider font-semibold text-muted-foreground mb-2">
Location Details
</div>
<div className="grid grid-cols-2 gap-3 bg-muted/40 p-2.5 rounded-md font-mono text-[11px]">
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">
Latitude
</span>
<span className="font-medium tracking-tighter">
{detection.latitude.toFixed(6)}
</span>
</div>
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">
Longitude
</span>
<span className="font-medium tracking-tighter">
{detection.longitude.toFixed(6)}
</span>
</div>
</div>
</div>
</div>
</Popup>
</CircleMarker>
);
})}
<FitBounds bounds={bounds} />
</MapContainer>
</div>
</DialogContent>
</Dialog>
);
}