refactor: remove unused files

This commit is contained in:
2026-03-18 19:39:13 +05:30
parent 598098d7e8
commit 42624cae94
10 changed files with 156 additions and 1092 deletions

BIN
public/avatars/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,7 +1,14 @@
"use client"; 'use client';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
CardFooter,
} from '@/components/ui/card';
import { import {
AlertCircle, AlertCircle,
Activity, Activity,
@@ -13,26 +20,21 @@ import {
PencilLine, PencilLine,
CheckCircle2, CheckCircle2,
Settings2, Settings2,
} from "lucide-react"; } from 'lucide-react';
import { StatsCard } from "@/components/dashboard/stats-card"; import { StatsCard } from '@/components/dashboard/stats-card';
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"; import { FilterSelector } from '@/components/dashboard/filter-selector';
import { FilterSelector } from "@/components/dashboard/filter-selector"; import { DetectionDonutChart } from '@/components/dashboard/detection-donut-chart';
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"; import { ChainageBarChart } from '@/components/dashboard/chainage-bar-chart';
import { ChainageBarChart } from "@/components/dashboard/chainage-bar-chart" import { DashboardMap } from '@/components/dashboard/dashboard-map';
import { DashboardMap } from "@/components/dashboard/dashboard-map"; import { PageHeader } from '@/components/page-header';
import { PageHeader } from "@/components/page-header"; import { projectService } from '@/services/api';
import { projectService } from "@/services/api"; import { Project } from '@/types';
import { Project } from "@/types"; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { import { Button } from '@/components/ui/button';
Popover, import { DashboardSkeleton } from '@/components/dashboard/dashboard-skeleton';
PopoverContent, import { PoweredBy } from '@/components/powered-by';
PopoverTrigger, import { toast } from 'sonner';
} from "@/components/ui/popover"; import { Reveal, StaggerContainer, StaggerItem } from '@/components/ui/reveal';
import { Button } from "@/components/ui/button";
import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton"
import { PoweredBy } from "@/components/powered-by";
import { toast } from "sonner";
import { Reveal, StaggerContainer, StaggerItem } from "@/components/ui/reveal";
const API_URL = process.env.NEXT_PUBLIC_API_URL; const API_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -103,41 +105,40 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
let total_damaged_road_marking = 0; let total_damaged_road_marking = 0;
let total_good_sign_board = 0; let total_good_sign_board = 0;
let total_road_damage = 0; let total_road_damage = 0;
const chainageData: DetectionStats["chainageData"] = []; const chainageData: DetectionStats['chainageData'] = [];
for (const pkg of Object.values(summary.packages || {})) { for (const pkg of Object.values(summary.packages || {})) {
for (const [chnName, chn] of Object.entries(pkg.chainages || {})) { // Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages' for (const [chnName, chn] of Object.entries(pkg.chainages || {})) {
// Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages'
let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard' let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard'
let chnPothole = 0; // Changed 'locPothole' to 'chnPothole' let chnPothole = 0; // Changed 'locPothole' to 'chnPothole'
let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack' let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack'
let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking' let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking'
let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard' let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard'
for (const detection of chn.detections || []) { // Changed 'loc.detections' to 'chn.detections' for (const detection of chn.detections || []) {
// Changed 'loc.detections' to 'chn.detections'
const type = detection.type.toLowerCase(); const type = detection.type.toLowerCase();
if (type === "defected_sign_board") { if (type === 'defected_sign_board') {
chnDefectedSignboard++; chnDefectedSignboard++;
total_defected_sign_board++; total_defected_sign_board++;
} else if (type === "pothole") { } else if (type === 'pothole') {
chnPothole++; chnPothole++;
total_pothole++; total_pothole++;
} else if (type === "road_crack") { } else if (type === 'road_crack') {
chnRoadCrack++; chnRoadCrack++;
total_road_crack++; total_road_crack++;
} else if (type === "damaged_road_marking") { } else if (type === 'damaged_road_marking') {
chnDamagedRoadMarking++; chnDamagedRoadMarking++;
total_damaged_road_marking++; total_damaged_road_marking++;
} else if (type === "good_sign_board") { } else if (type === 'good_sign_board') {
chnGoodSignboard++; chnGoodSignboard++;
total_good_sign_board++; total_good_sign_board++;
} }
} }
const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage' const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage'
chnDefectedSignboard + chnDefectedSignboard + chnPothole + chnRoadCrack + chnDamagedRoadMarking;
chnPothole +
chnRoadCrack +
chnDamagedRoadMarking;
if ( if (
chnDefectedSignboard > 0 || chnDefectedSignboard > 0 ||
@@ -146,9 +147,9 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
chnDamagedRoadMarking > 0 || chnDamagedRoadMarking > 0 ||
chnGoodSignboard > 0 chnGoodSignboard > 0
) { ) {
const shortName = const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName;
chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName; chainageData.push({
chainageData.push({ // Changed 'locationData.push' to 'chainageData.push' // Changed 'locationData.push' to 'chainageData.push'
name: shortName, name: shortName,
defected_sign_board: chnDefectedSignboard, defected_sign_board: chnDefectedSignboard,
pothole: chnPothole, pothole: chnPothole,
@@ -167,10 +168,7 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
} }
total_road_damage = total_road_damage =
total_defected_sign_board + total_defected_sign_board + total_pothole + total_road_crack + total_damaged_road_marking;
total_pothole +
total_road_crack +
total_damaged_road_marking;
return { return {
totalDefectedSignboard: total_defected_sign_board, totalDefectedSignboard: total_defected_sign_board,
@@ -186,12 +184,8 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
export default function DashboardPage() { export default function DashboardPage() {
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>( const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
null, const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(null);
);
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(
null,
);
const [stats, setStats] = useState<DetectionStats>({ const [stats, setStats] = useState<DetectionStats>({
totalDefectedSignboard: 0, totalDefectedSignboard: 0,
totalPothole: 0, totalPothole: 0,
@@ -214,9 +208,9 @@ export default function DashboardPage() {
setSelectedProjectId(projectsData.items[0].id); setSelectedProjectId(projectsData.items[0].id);
} }
} catch (err) { } catch (err) {
console.error("Failed to load projects:", err); console.error('Failed to load projects:', err);
toast.error("Failed to load projects", { toast.error('Failed to load projects', {
description: "Please check if the backend is running." description: 'Please check if the backend is running.',
}); });
} finally { } finally {
setTimeout(() => { setTimeout(() => {
@@ -239,18 +233,12 @@ export default function DashboardPage() {
: []; : [];
// Extract chainages from selected package // Extract chainages from selected package
const [selectedPackageId, setSelectedPackageId] = useState<string | null>( const [selectedPackageId, setSelectedPackageId] = useState<string | null>(null);
null, const [selectedChainageId, setSelectedChainageId] = useState<string | null>(null); // Changed 'selectedLocationId' to 'selectedChainageId'
);
const [selectedChainageId, setSelectedChainageId] = useState<string | null>( // Changed 'selectedLocationId' to 'selectedChainageId'
null,
);
const chainages = const chainages =
projectSummary && selectedPackageId && selectedPackageId !== "all" projectSummary && selectedPackageId && selectedPackageId !== 'all'
? Object.keys( ? Object.keys(projectSummary.packages[selectedPackageId]?.chainages || {}).map((chnName) => ({
projectSummary.packages[selectedPackageId]?.chainages || {},
).map((chnName) => ({
id: chnName, id: chnName,
name: chnName, name: chnName,
})) }))
@@ -270,8 +258,8 @@ export default function DashboardPage() {
const summary: ProjectSummary = await projectService.getProjectSummary(selectedProjectId); const summary: ProjectSummary = await projectService.getProjectSummary(selectedProjectId);
setProjectSummary(summary); setProjectSummary(summary);
} catch (err) { } catch (err) {
console.error("Failed to load project summary:", err); console.error('Failed to load project summary:', err);
toast.error("Failed to load project summary"); toast.error('Failed to load project summary');
setProjectSummary(null); setProjectSummary(null);
} finally { } finally {
setTimeout(() => { setTimeout(() => {
@@ -314,16 +302,16 @@ export default function DashboardPage() {
let totalRoadCrack = 0; let totalRoadCrack = 0;
let totalDamagedRoadMarking = 0; let totalDamagedRoadMarking = 0;
let totalGoodSignboard = 0; let totalGoodSignboard = 0;
const chainageData: DetectionStats["chainageData"] = []; const chainageData: DetectionStats['chainageData'] = [];
const packagesToProcess = const packagesToProcess =
selectedPackageId && selectedPackageId !== "all" selectedPackageId && selectedPackageId !== 'all'
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] } ? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
: projectSummary.packages || {}; : projectSummary.packages || {};
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) { for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
const chainagesToProcess = const chainagesToProcess =
selectedChainageId && selectedChainageId !== "all" selectedChainageId && selectedChainageId !== 'all'
? { [selectedChainageId]: pkg.chainages[selectedChainageId] } ? { [selectedChainageId]: pkg.chainages[selectedChainageId] }
: pkg.chainages || {}; : pkg.chainages || {};
@@ -338,19 +326,19 @@ export default function DashboardPage() {
for (const detection of chn.detections || []) { for (const detection of chn.detections || []) {
const type = detection.type.toLowerCase(); const type = detection.type.toLowerCase();
if (type === "defected_sign_board") { if (type === 'defected_sign_board') {
chnDefectedSignboard++; chnDefectedSignboard++;
totalDefectedSignboard++; totalDefectedSignboard++;
} else if (type === "pothole") { } else if (type === 'pothole') {
chnPothole++; chnPothole++;
totalPothole++; totalPothole++;
} else if (type === "road_crack") { } else if (type === 'road_crack') {
chnRoadCrack++; chnRoadCrack++;
totalRoadCrack++; totalRoadCrack++;
} else if (type === "damaged_road_marking") { } else if (type === 'damaged_road_marking') {
chnDamagedRoadMarking++; chnDamagedRoadMarking++;
totalDamagedRoadMarking++; totalDamagedRoadMarking++;
} else if (type === "good_sign_board") { } else if (type === 'good_sign_board') {
chnGoodSignboard++; chnGoodSignboard++;
totalGoodSignboard++; totalGoodSignboard++;
} }
@@ -363,8 +351,7 @@ export default function DashboardPage() {
chnDamagedRoadMarking > 0 || chnDamagedRoadMarking > 0 ||
chnGoodSignboard > 0 chnGoodSignboard > 0
) { ) {
const shortName = const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName;
chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName;
chainageData.push({ chainageData.push({
name: shortName, name: shortName,
defected_sign_board: chnDefectedSignboard, defected_sign_board: chnDefectedSignboard,
@@ -390,10 +377,7 @@ export default function DashboardPage() {
totalDamagedRoadMarking, totalDamagedRoadMarking,
totalGoodSignboard, totalGoodSignboard,
totalRoadDamage: totalRoadDamage:
totalDefectedSignboard + totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking,
totalPothole +
totalRoadCrack +
totalDamagedRoadMarking,
chainageData, chainageData,
}); });
}, [projectSummary, selectedPackageId, selectedChainageId]); }, [projectSummary, selectedPackageId, selectedChainageId]);
@@ -425,7 +409,9 @@ export default function DashboardPage() {
<Milestone className="h-4 w-4 text-primary" /> <Milestone className="h-4 w-4 text-primary" />
Filter Analysis Filter Analysis
</h3> </h3>
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or chainage</p> <p className="text-xs mt-1 text-muted-foreground">
Refine your view by project, package, or chainage
</p>
</div> </div>
<div> <div>
<FilterSelector <FilterSelector
@@ -446,7 +432,6 @@ export default function DashboardPage() {
</Reveal> </Reveal>
</div> </div>
{isLoading && !projectSummary ? ( {isLoading && !projectSummary ? (
<DashboardSkeleton /> <DashboardSkeleton />
) : ( ) : (
@@ -532,13 +517,12 @@ export default function DashboardPage() {
<Card className="flex-1 h-full"> <Card className="flex-1 h-full">
<CardHeader className="items-center pb-0"> <CardHeader className="items-center pb-0">
<CardTitle className="text-base font-bold">Severity by Chainage</CardTitle> <CardTitle className="text-base font-bold">Severity by Chainage</CardTitle>
<CardDescription className="text-xs">Detections grouped by road segment</CardDescription> <CardDescription className="text-xs">
Detections grouped by road segment
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="h-[300px]"> <CardContent className="h-[300px]">
<ChainageBarChart <ChainageBarChart data={stats.chainageData || []} isLoading={isLoading} />
data={stats.chainageData || []}
isLoading={isLoading}
/>
</CardContent> </CardContent>
<CardFooter className="flex-col gap-2 text-sm"> <CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground"> <div className="leading-none text-muted-foreground">
@@ -552,7 +536,7 @@ export default function DashboardPage() {
{/* Map - Full Width Below Charts */} {/* Map - Full Width Below Charts */}
<Reveal delay={0.4} direction="up"> <Reveal delay={0.4} direction="up">
<div> <div>
<DashboardMap <DashboardMap
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
selectedPackageId={selectedPackageId} selectedPackageId={selectedPackageId}
selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId' selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId'

View File

@@ -1,106 +1,95 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import { LayoutDashboard, Plus, Layers, Package, MapPin, Milestone } from 'lucide-react';
import { NavUser } from '@/components/nav-user';
import { ROUTES } from '@/utils/routes';
import { import {
LayoutDashboard, Sidebar,
Plus, SidebarContent,
Layers, SidebarFooter,
Package, SidebarHeader,
MapPin, SidebarRail,
Milestone, SidebarMenu,
} from "lucide-react"; SidebarMenuItem,
import { NavUser } from "@/components/nav-user"; SidebarMenuButton,
import { ROUTES } from "@/utils/routes"; SidebarGroup,
import { SidebarGroupLabel,
Sidebar, } from '@/components/ui/sidebar';
SidebarContent, import Link from 'next/link';
SidebarFooter, import { usePathname } from 'next/navigation';
SidebarHeader,
SidebarRail,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarGroup,
SidebarGroupLabel,
} from "@/components/ui/sidebar";
import Link from "next/link";
import { usePathname } from "next/navigation";
// This is the navigation data // This is the navigation data
const data = { const data = {
user: { user: {
name: "shadcn", name: 'Vision Admin',
email: "m@example.com", email: 'admin@visionroad.ai',
avatar: "/avatars/shadcn.jpg", avatar: '/avatars/profile.jpg',
},
navMain: [
{
title: 'Dashboard',
url: ROUTES.DASHBOARD,
icon: LayoutDashboard,
}, },
navMain: [ {
{ title: 'New Analysis',
title: "Dashboard", url: ROUTES.NEW_ANALYSIS,
url: ROUTES.DASHBOARD, icon: Plus,
icon: LayoutDashboard, },
}, {
{ title: 'Project',
title: "New Analysis", url: ROUTES.PROJECT,
url: ROUTES.NEW_ANALYSIS, icon: Layers,
icon: Plus, },
}, {
{ title: 'Package',
title: "Project", url: ROUTES.PACKAGE,
url: ROUTES.PROJECT, icon: Package,
icon: Layers, },
}, {
{ title: 'Chainage',
title: "Package", url: ROUTES.CHAINAGE,
url: ROUTES.PACKAGE, icon: Milestone,
icon: Package, },
}, ],
{
title: "Chainage",
url: ROUTES.CHAINAGE,
icon: Milestone,
},
],
}; };
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<Sidebar collapsible="icon" {...props}> <Sidebar collapsible="icon" {...props}>
<SidebarHeader> <SidebarHeader>
<div className="flex items-center gap-2 py-2"> <div className="flex items-center gap-2 py-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"> <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Package className="size-4" /> <Package className="size-4" />
</div> </div>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden"> <div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-semibold">Vision Road</span> <span className="truncate font-semibold">Vision Road</span>
<span className="truncate text-xs">Analytics Platform</span> <span className="truncate text-xs">Analytics Platform</span>
</div> </div>
</div> </div>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<SidebarGroup> <SidebarGroup>
<SidebarMenu> <SidebarMenu>
{data.navMain.map((item) => ( {data.navMain.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton <SidebarMenuButton asChild tooltip={item.title} isActive={pathname === item.url}>
asChild <Link href={item.url}>
tooltip={item.title} {item.icon && <item.icon />}
isActive={pathname === item.url} <span>{item.title}</span>
> </Link>
<Link href={item.url}> </SidebarMenuButton>
{item.icon && <item.icon />} </SidebarMenuItem>
<span>{item.title}</span> ))}
</Link> </SidebarMenu>
</SidebarMenuButton> </SidebarGroup>
</SidebarMenuItem> </SidebarContent>
))} <SidebarFooter>
</SidebarMenu> <NavUser user={data.user} />
</SidebarGroup> </SidebarFooter>
</SidebarContent> <SidebarRail />
<SidebarFooter> </Sidebar>
<NavUser user={data.user} /> );
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
} }

View File

@@ -1,77 +0,0 @@
"use client"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Project } from "@/types"
interface CompactProjectSelectorProps {
projects: Project[]
selectedProjectId: string | null
onProjectChange: (projectId: string) => void
selectedProject: Project | undefined
isLoading?: boolean
}
export function CompactProjectSelector({
projects,
selectedProjectId,
onProjectChange,
selectedProject,
isLoading = false
}: CompactProjectSelectorProps) {
return (
<div className="flex flex-wrap items-center gap-3 p-3 rounded-md border">
{/* Project Dropdown */}
<div className="flex flex-col min-w-[120px]">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Project</span>
<Select
value={selectedProjectId || ""}
onValueChange={onProjectChange}
disabled={isLoading || projects.length === 0}
>
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold bg-transparent text-left">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Corridor Badge */}
{selectedProject?.corridor_name && (
<>
<div className="h-8 w-px bg-border mx-2" />
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Corridor</span>
<span className="text-xs font-bold text-muted-foreground">
{selectedProject.corridor_name}
</span>
</div>
</>
)}
{/* State Badge */}
{selectedProject?.state && (
<>
<div className="h-8 w-px bg-border mx-2" />
<div className="flex flex-col">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">State</span>
<span className="text-xs font-bold text-muted-foreground">
{selectedProject.state}
</span>
</div>
</>
)}
</div>
)
}

View File

@@ -1,54 +0,0 @@
"use client"
import { Card, CardContent } from "@/components/ui/card"
import { LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
interface GradientStatsCardProps {
title: string
subtitle?: string
value: number | string
icon: LucideIcon
gradient?: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
isLoading?: boolean
}
export function GradientStatsCard({
title,
subtitle,
value,
icon: Icon,
isLoading = false
}: GradientStatsCardProps) {
return (
<Card className="overflow-hidden border shadow-sm">
<CardContent className="p-6 flex items-center justify-between gap-4">
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
{title}
</h3>
<div className="flex flex-col">
{isLoading ? (
<div className="h-10 w-24 bg-muted animate-pulse rounded-md mt-1" />
) : (
<>
<p className="text-3xl font-bold tracking-tight">
{value}
</p>
{subtitle && (
<p className="text-[10px] font-semibold text-muted-foreground mt-0.5">
{subtitle}
</p>
)}
</>
)}
</div>
</div>
<div className="p-3 rounded-lg bg-secondary shrink-0">
<Icon className="h-6 w-6 text-primary" />
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,73 +0,0 @@
"use client"
import { ChevronRight, type LucideIcon } from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon?: LucideIcon
isActive?: boolean
items?: {
title: string
url: string
}[]
}[]
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}

View File

@@ -1,89 +0,0 @@
"use client"
import {
Folder,
Forward,
MoreHorizontal,
Trash2,
type LucideIcon,
} from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function NavProjects({
projects,
}: {
projects: {
name: string
url: string
icon: LucideIcon
}[]
}) {
const { isMobile } = useSidebar()
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}

View File

@@ -1,89 +0,0 @@
"use client"
import * as React from "react"
import { ChevronsUpDown, Plus } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function TeamSwitcher({
teams,
}: {
teams: {
name: string
logo: React.ElementType
plan: string
}[]
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam.name}
</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -1,193 +0,0 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
role="list"
data-slot="item-group"
className={cn('group/item-group flex flex-col', className)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn('my-0', className)}
{...props}
/>
)
}
const itemVariants = cva(
'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a&]:hover:bg-accent/50 [a&]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
{
variants: {
variant: {
default: 'bg-transparent',
outline: 'border-border',
muted: 'bg-muted/50',
},
size: {
default: 'p-4 gap-4 ',
sm: 'py-3 px-4 gap-2.5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Item({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'div'> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'div'
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',
{
variants: {
variant: {
default: 'bg-transparent',
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',
},
},
defaultVariants: {
variant: 'default',
},
},
)
function ItemMedia({
className,
variant = 'default',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-content"
className={cn(
'flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none',
className,
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-title"
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium',
className,
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="item-description"
className={cn(
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className,
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-actions"
className={cn('flex items-center gap-2', className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-header"
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-footer"
className={cn(
'flex basis-full items-center justify-between gap-2',
className,
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}

View File

@@ -1,334 +0,0 @@
"use client"
import { useState, useRef } 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 { Progress } from "@/components/ui/progress"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Upload, Loader2, AlertCircle } from "lucide-react"
import type { DetectionData, DetectionType } from "@/types"
import { videoService } from "@/services/api"
const API_URL = process.env.NEXT_PUBLIC_API_URL
const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://")
const DETECTION_TYPES = [
{ value: "pothole-detection", label: "Pothole Detection" },
{ value: "sign-board-detection", label: "Signboard Detection" },
{ value: "pot-sign-detection", label: "Pothole & Signboard Detection" },
] as const
const DETECTION_METHODS = [
{ value: "yolo", label: "YOLO" },
{ value: "yolo_vl", label: "YOLO with VL" },
{ value: "sam3", label: "SAM 3" },
{ value: "yoloe", label: "YOLOE" },
{ value: "yoloe_trained_vl", label: "YOLOE Trained" },
] as const
type UploadSectionProps = {
onDetectionComplete: (data: DetectionData, videoId: string, file: File) => void
onDetectionTypeChange: (type: DetectionType) => void
}
export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: UploadSectionProps) {
const [file, setFile] = useState<File | null>(null)
const [jsonFile, setJsonFile] = useState<File | null>(null)
const [speed, setSpeed] = useState(30)
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
const [selectMethod, setSelectMethod] = useState("yolo_vl")
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const [statusMessage, setStatusMessage] = useState("")
const [error, setError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const jsonFileInputRef = useRef<HTMLInputElement>(null)
const wsRef = useRef<WebSocket | null>(null)
const handleDetectionTypeChange = (value: DetectionType) => {
setDetectionType(value)
onDetectionTypeChange(value)
}
const connectWebSocket = (videoId: string) => {
console.log("[Upload] Connecting WebSocket for video:", videoId)
const ws = new WebSocket(`${WS_URL}/ws/${videoId}`)
wsRef.current = ws
ws.onopen = () => {
console.log("[Upload] WebSocket connected")
setError(null)
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log("[Upload] WebSocket message:", data)
if (data.type === "progress" || data.progress !== undefined) {
const progressValue = data.progress || 0
setProgress(progressValue)
let message = data.message || "Processing..."
// Handle both pothole and signboard progress messages
if (data.unique_potholes !== undefined) {
message += ` | Unique: ${data.unique_potholes} | Total: ${data.total_detections || 0}`
} else if (data.unique_signboards !== undefined) {
message += ` | Unique: ${data.unique_signboards} | Total: ${data.total_detections || 0}`
}
setStatusMessage(message)
}
if (data.type === "complete" || data.status === "completed") {
setStatusMessage("Processing completed! Loading results...")
ws.close()
setTimeout(() => loadResults(videoId), 500)
}
if (data.type === "error") {
setError("Error: " + data.message)
setStatusMessage("")
setUploading(false)
ws.close()
}
}
ws.onerror = (error) => {
console.error("[Upload] WebSocket error:", error)
setStatusMessage("Connection error. Retrying...")
}
ws.onclose = () => {
console.log("[Upload] WebSocket closed")
}
}
const loadResults = async (videoId: string) => {
try {
console.log("[Upload] Loading results for:", videoId)
const detectionData: DetectionData = await videoService.getVideoResults(videoId);
console.log("[Upload] Results loaded:", detectionData)
setUploading(false)
setProgress(100)
setStatusMessage("✓ Complete!")
setError(null)
// Pass detection data, video_id, and the original file
if (file) {
onDetectionComplete(detectionData, videoId, file)
}
} catch (error) {
console.error("[Upload] Failed to load results:", error)
setError("Failed to load results. Please try again.")
setStatusMessage("")
setUploading(false)
}
}
const handleUpload = async () => {
if (!file) {
setError("Please select a video file")
return
}
const formData = new FormData()
formData.append("file", file)
formData.append("detection_type", detectionType)
formData.append("speed_kmh", speed.toString())
formData.append("detection_mode", selectMethod)
// Add JSON file if provided
if (jsonFile) {
formData.append("json_file", jsonFile)
}
setUploading(true)
setProgress(0)
setStatusMessage("Uploading...")
setError(null)
try {
const result = await videoService.uploadVideo(formData);
const videoId = result.video_id
console.log("[Upload] Video uploaded successfully, ID:", videoId)
console.log("[Upload] Response:", result)
setStatusMessage("Uploaded! Starting processing...")
setProgress(10)
// Connect WebSocket for progress updates
connectWebSocket(videoId)
} catch (error) {
console.error("[Upload] Upload error:", error)
let errorMessage = "Upload failed"
if (error instanceof TypeError && error.message === "Failed to fetch") {
errorMessage = "Cannot connect to server. Please check if backend is running."
} else if (error instanceof Error) {
errorMessage = error.message
}
setError(errorMessage)
setStatusMessage("")
setUploading(false)
setProgress(0)
}
}
return (
<Card>
<CardHeader>
<CardTitle>Upload Video</CardTitle>
<CardDescription>
Select video file, detection type, vehicle speed, and method for analysis
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="space-y-2">
<Label htmlFor="video-file">Video File</Label>
<div className="flex gap-2">
<Input
ref={fileInputRef}
id="video-file"
type="file"
accept="video/*"
onChange={(e) => {
setFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
className="flex-1"
/>
</div>
{file && (
<p className="text-sm text-muted-foreground">
Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="json-file">GPS JSON File</Label>
<div className="flex gap-2">
<Input
ref={jsonFileInputRef}
id="json-file"
type="file"
accept=".json,application/json"
onChange={(e) => {
setJsonFile(e.target.files?.[0] || null)
setError(null)
}}
disabled={uploading}
className="flex-1"
/>
</div>
{jsonFile && (
<p className="text-sm text-muted-foreground">
Selected: {jsonFile.name} ({(jsonFile.size / 1024).toFixed(2)} KB)
</p>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="detection-type">Detection Type</Label>
<Select
value={detectionType}
onValueChange={handleDetectionTypeChange}
disabled={uploading}
>
<SelectTrigger id="detection-type">
<SelectValue placeholder="Select detection type" />
</SelectTrigger>
<SelectContent>
{DETECTION_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
<span>{type.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="speed">Vehicle Speed (km/h)</Label>
<Input
id="speed"
type="number"
min={1}
max={200}
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
disabled={uploading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="select-method">Select Method</Label>
<Select
value={selectMethod}
onValueChange={setSelectMethod}
disabled={uploading}
>
<SelectTrigger id="select-method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{DETECTION_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
<span>{method.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{error && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
<AlertCircle className="h-5 w-5 mt-0.5 shrink-0" />
<p className="text-sm whitespace-pre-line">{error}</p>
</div>
)}
<Button
onClick={handleUpload}
disabled={!file || uploading}
className="w-full"
size="lg"
>
{uploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Upload & Process
</>
)}
</Button>
{uploading && (
<div className="space-y-3">
<Progress value={progress} className="h-3" />
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{statusMessage}</span>
<span className="font-semibold">{progress}%</span>
</div>
</div>
)}
</CardContent>
</Card>
)
}