diff --git a/app/create-location/page.tsx b/app/create-location/page.tsx index 90e6a77..d00bfd7 100644 --- a/app/create-location/page.tsx +++ b/app/create-location/page.tsx @@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { Loader2, CheckCircle2, MapPin, Navigation, Milestone } from "lucide-react" import { SidebarNavigation } from "@/components/sidebar-navigation" import { DataTable } from "@/components/data-table" +import { PageHeader } from "@/components/page-header" import { fetchProjects, fetchPackagesByProject, @@ -261,7 +262,7 @@ export default function CreateLocationPage() { render: (location: Location) => { if (location.chainage_start_km !== null && location.chainage_end_km !== null) { return ( - + {location.chainage_start_km} - {location.chainage_end_km} ) @@ -273,7 +274,7 @@ export default function CreateLocationPage() { key: "start_gps", header: "Start GPS", render: (location: Location) => ( - + {location.start_lat.toFixed(4)}, {location.start_lng.toFixed(4)} ) @@ -282,7 +283,7 @@ export default function CreateLocationPage() { key: "end_gps", header: "End GPS", render: (location: Location) => ( - + {location.end_lat.toFixed(4)}, {location.end_lng.toFixed(4)} ) @@ -290,25 +291,17 @@ export default function CreateLocationPage() { ] return ( -
+
-
-
+
+
{/* Refined Header */}
-
-
- -
-
-

- Location Management -

-

- Manage road segment locations -

-
-
+
{/* Error Message */} diff --git a/app/create-package/page.tsx b/app/create-package/page.tsx index d986674..928b31f 100644 --- a/app/create-package/page.tsx +++ b/app/create-package/page.tsx @@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { Loader2, CheckCircle2, Package, FolderKanban, Globe } from "lucide-react" import { SidebarNavigation } from "@/components/sidebar-navigation" import { DataTable } from "@/components/data-table" +import { PageHeader } from "@/components/page-header" import { fetchProjects, fetchAllPackages, @@ -178,7 +179,7 @@ export default function CreatePackagePage() { {project.state.split(',').map((item, idx) => ( {item.trim()} @@ -194,25 +195,17 @@ export default function CreatePackagePage() { ] return ( -
+
-
-
+
+
{/* Refined Header */}
-
-
- -
-
-

- Package Management -

-

- Manage project packages -

-
-
+
{/* Error Message */} diff --git a/app/create-project/page.tsx b/app/create-project/page.tsx index 07fc3b6..4ab5959 100644 --- a/app/create-project/page.tsx +++ b/app/create-project/page.tsx @@ -9,6 +9,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { Loader2, CheckCircle2, FolderPlus, MapPin, Building2, Route, X } from "lucide-react" import { SidebarNavigation } from "@/components/sidebar-navigation" import { DataTable } from "@/components/data-table" +import { PageHeader } from "@/components/page-header" import { createProject, fetchProjects, updateProject, deleteProject, type ProjectCreate, type Project, type ProjectUpdate } from "@/lib/api" import { toast } from "sonner" @@ -159,7 +160,7 @@ export default function CreateProjectPage() { {project.state.split(',').map((item, idx) => ( {item.trim()} @@ -175,25 +176,17 @@ export default function CreateProjectPage() { ] return ( -
+
-
-
+
+
{/* Refined Header */}
-
-
- -
-
-

- Project Management -

-

- Manage road infrastructure projects -

-
-
+
{/* Error Message */} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index e683bd1..8988f5f 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,533 +1,622 @@ -"use client" +"use client"; -import { useState, useEffect } from "react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { - AlertCircle, - TrendingUp, - MapPin as MapPinIcon, - BarChart3, - AlertTriangle, - RectangleHorizontal -} from "lucide-react" -import { SidebarNavigation } from "@/components/sidebar-navigation" -import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card" -import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector" -import { FilterSelector } from "@/components/dashboard/filter-selector" -import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart" -import { LocationBarChart } from "@/components/dashboard/location-bar-chart" -import { DashboardMap } from "@/components/dashboard/dashboard-map" + AlertCircle, + Activity, + MapPin as MapPinIcon, + BarChart3, + AlertTriangle, + Zap, + PencilLine, + CheckCircle2, +} from "lucide-react"; +import { SidebarNavigation } from "@/components/sidebar-navigation"; +import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card"; +import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"; +import { FilterSelector } from "@/components/dashboard/filter-selector"; +import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"; +import { LocationBarChart } from "@/components/dashboard/location-bar-chart"; +import { DashboardMap } from "@/components/dashboard/dashboard-map"; +import { PageHeader } from "@/components/page-header"; +import { fetchProjects, type Project } from "@/lib/api"; import { - fetchProjects, - type Project -} from "@/lib/api" + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Filter } from "lucide-react"; +import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton" - -const API_URL = process.env.NEXT_PUBLIC_API_URL +const API_URL = process.env.NEXT_PUBLIC_API_URL; interface ProjectSummary { - project: { - id: string - name: string - corridor_name: string | null - state: string | null - } - packages: { + project: { + id: string; + name: string; + corridor_name: string | null; + state: string | null; + }; + packages: { + [key: string]: { + package_id: string; + region: string | null; + locations: { [key: string]: { - package_id: string - region: string | null - locations: { - [key: string]: { - location_id: string - chainage: string | null - detection_count: number - detections: Array<{ - id: number - type: string - class: string - confidence: number - latitude: number - longitude: number - }> - } - } - } - } + location_id: string; + chainage: string | null; + detection_count: number; + detections: Array<{ + id: number; + type: string; + class: string; + confidence: number; + latitude: number; + longitude: number; + }>; + }; + }; + }; + }; } interface DetectionStats { - totalDefectedSignboard: number - totalPothole: number - totalRoadCrack: number - totalDamagedRoadMarking: number - totalGoodSignboard: number - totalRoadDamage: number - locationData: Array<{ - name: string - defected_sign_board: number - pothole: number - road_crack: number - damaged_road_marking: number - good_sign_board: number - total: number - }> + totalDefectedSignboard: number; + totalPothole: number; + totalRoadCrack: number; + totalDamagedRoadMarking: number; + totalGoodSignboard: number; + totalRoadDamage: number; + locationData: Array<{ + name: string; + defected_sign_board: number; + pothole: number; + road_crack: number; + damaged_road_marking: number; + good_sign_board: number; + total: number; + }>; } function calculateStats(summary: ProjectSummary | null): DetectionStats { - if (!summary) { - return { - totalDefectedSignboard: 0, - totalPothole: 0, - totalRoadCrack: 0, - totalDamagedRoadMarking: 0, - totalGoodSignboard: 0, - totalRoadDamage: 0, - locationData: [] - } - } - - let totalDefectedSignboard = 0 - let totalPothole = 0 - let totalRoadCrack = 0 - let totalDamagedRoadMarking = 0 - let totalGoodSignboard = 0 - let totalRoadDamage = 0 - const locationData: DetectionStats["locationData"] = [] - - for (const pkg of Object.values(summary.packages || {})) { - for (const [locName, loc] of Object.entries(pkg.locations || {})) { - let locDefectedSignboard = 0 - let locPothole = 0 - let locRoadCrack = 0 - let locDamagedRoadMarking = 0 - let locGoodSignboard = 0 - - for (const detection of loc.detections || []) { - const type = detection.type.toLowerCase() - if (type === "defected_sign_board") { - locDefectedSignboard++ - totalDefectedSignboard++ - } else if (type === "pothole") { - locPothole++ - totalPothole++ - } else if (type === "road_crack") { - locRoadCrack++ - totalRoadCrack++ - } else if (type === "damaged_road_marking") { - locDamagedRoadMarking++ - totalDamagedRoadMarking++ - } else if (type === "good_sign_board") { - locGoodSignboard++ - totalGoodSignboard++ - } - } - - const locTotalDamage = locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking - totalRoadDamage += (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0) ? 1 : 0 // This logic might need refinement based on "total_road_damage" definition - - if (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0 || locGoodSignboard > 0) { - const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName - locationData.push({ - name: shortName, - defected_sign_board: locDefectedSignboard, - pothole: locPothole, - road_crack: locRoadCrack, - damaged_road_marking: locDamagedRoadMarking, - good_sign_board: locGoodSignboard, - total: locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking + locGoodSignboard - }) - } - } - } - - // Recalculate totalRoadDamage based on backends unique count - // But since we are aggregating from locations, we just sum them up or use a simpler metric - // The user's JSON shows "total_road_damage": 9 which is sum of 2+6+0+1 - totalRoadDamage = totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking - + if (!summary) { return { - totalDefectedSignboard, - totalPothole, - totalRoadCrack, - totalDamagedRoadMarking, - totalGoodSignboard, - totalRoadDamage, - locationData + totalDefectedSignboard: 0, + totalPothole: 0, + totalRoadCrack: 0, + totalDamagedRoadMarking: 0, + totalGoodSignboard: 0, + totalRoadDamage: 0, + locationData: [], + }; + } + + let totalDefectedSignboard = 0; + let totalPothole = 0; + let totalRoadCrack = 0; + let totalDamagedRoadMarking = 0; + let totalGoodSignboard = 0; + let totalRoadDamage = 0; + const locationData: DetectionStats["locationData"] = []; + + for (const pkg of Object.values(summary.packages || {})) { + for (const [locName, loc] of Object.entries(pkg.locations || {})) { + let locDefectedSignboard = 0; + let locPothole = 0; + let locRoadCrack = 0; + let locDamagedRoadMarking = 0; + let locGoodSignboard = 0; + + for (const detection of loc.detections || []) { + const type = detection.type.toLowerCase(); + if (type === "defected_sign_board") { + locDefectedSignboard++; + totalDefectedSignboard++; + } else if (type === "pothole") { + locPothole++; + totalPothole++; + } else if (type === "road_crack") { + locRoadCrack++; + totalRoadCrack++; + } else if (type === "damaged_road_marking") { + locDamagedRoadMarking++; + totalDamagedRoadMarking++; + } else if (type === "good_sign_board") { + locGoodSignboard++; + totalGoodSignboard++; + } + } + + const locTotalDamage = + locDefectedSignboard + + locPothole + + locRoadCrack + + locDamagedRoadMarking; + totalRoadDamage += + locDefectedSignboard > 0 || + locPothole > 0 || + locRoadCrack > 0 || + locDamagedRoadMarking > 0 + ? 1 + : 0; // This logic might need refinement based on "total_road_damage" definition + + if ( + locDefectedSignboard > 0 || + locPothole > 0 || + locRoadCrack > 0 || + locDamagedRoadMarking > 0 || + locGoodSignboard > 0 + ) { + const shortName = + locName.length > 20 ? locName.substring(0, 20) + "..." : locName; + locationData.push({ + name: shortName, + defected_sign_board: locDefectedSignboard, + pothole: locPothole, + road_crack: locRoadCrack, + damaged_road_marking: locDamagedRoadMarking, + good_sign_board: locGoodSignboard, + total: + locDefectedSignboard + + locPothole + + locRoadCrack + + locDamagedRoadMarking + + locGoodSignboard, + }); + } } + } + + // Recalculate totalRoadDamage based on backends unique count + // But since we are aggregating from locations, we just sum them up or use a simpler metric + // The user's JSON shows "total_road_damage": 9 which is sum of 2+6+0+1 + totalRoadDamage = + totalDefectedSignboard + + totalPothole + + totalRoadCrack + + totalDamagedRoadMarking; + + return { + totalDefectedSignboard, + totalPothole, + totalRoadCrack, + totalDamagedRoadMarking, + totalGoodSignboard, + totalRoadDamage, + locationData, + }; } export default function DashboardPage() { - const [isLoading, setIsLoading] = useState(true) - const [projects, setProjects] = useState([]) - const [selectedProjectId, setSelectedProjectId] = useState(null) - const [projectSummary, setProjectSummary] = useState(null) - const [stats, setStats] = useState({ + const [isLoading, setIsLoading] = useState(true); + const [projects, setProjects] = useState([]); + const [selectedProjectId, setSelectedProjectId] = useState( + null, + ); + const [projectSummary, setProjectSummary] = useState( + null, + ); + const [stats, setStats] = useState({ + totalDefectedSignboard: 0, + totalPothole: 0, + totalRoadCrack: 0, + totalDamagedRoadMarking: 0, + totalGoodSignboard: 0, + totalRoadDamage: 0, + locationData: [], + }); + const [error, setError] = useState(null); + + // Load projects on mount + useEffect(() => { + const loadProjects = async () => { + try { + setIsLoading(true); + setError(null); + const projectsData = await fetchProjects(); + setProjects(projectsData); + + if (projectsData.length > 0) { + setSelectedProjectId(projectsData[0].id); + } + } catch (err) { + console.error("Failed to load projects:", err); + setError( + "Failed to load projects. Please check if the backend is running.", + ); + } finally { + setTimeout(() => { + setIsLoading(false); + }, 1500); + } + }; + + 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( + null, + ); + const [selectedLocationId, setSelectedLocationId] = useState( + 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 + useEffect(() => { + if (!selectedProjectId) { + setProjectSummary(null); + return; + } + + const loadProjectSummary = async () => { + try { + setIsLoading(true); + setError(null); + + const response = await fetch( + `${API_URL}/summary/projects/${selectedProjectId}`, + { + headers: { + "Content-Type": "application/json", + "ngrok-skip-browser-warning": "true", + }, + }, + ); + + if (!response.ok) { + throw new Error(`API Error: ${response.status}`); + } + + const summary: ProjectSummary = await response.json(); + setProjectSummary(summary); + } catch (err) { + console.error("Failed to load project summary:", err); + setError("Failed to load project summary."); + setProjectSummary(null); + } finally { + setTimeout(() => { + setIsLoading(false); + }, 1500); + } + }; + + loadProjectSummary(); + }, [selectedProjectId]); + + // Reset package and location when project changes + useEffect(() => { + setSelectedPackageId(null); + setSelectedLocationId(null); + }, [selectedProjectId]); + + // Reset location when package changes + useEffect(() => { + setSelectedLocationId(null); + }, [selectedPackageId]); + + // Filter stats based on selections + useEffect(() => { + if (!projectSummary) { + setStats({ totalDefectedSignboard: 0, totalPothole: 0, totalRoadCrack: 0, totalDamagedRoadMarking: 0, totalGoodSignboard: 0, totalRoadDamage: 0, - locationData: [] - }) - const [error, setError] = useState(null) + locationData: [], + }); + return; + } - // Load projects on mount - useEffect(() => { - const loadProjects = async () => { - try { - setIsLoading(true) - setError(null) - const projectsData = await fetchProjects() - setProjects(projectsData) + let totalDefectedSignboard = 0; + let totalPothole = 0; + let totalRoadCrack = 0; + let totalDamagedRoadMarking = 0; + let totalGoodSignboard = 0; + const locationData: DetectionStats["locationData"] = []; - if (projectsData.length > 0) { - setSelectedProjectId(projectsData[0].id) - } - } catch (err) { - console.error("Failed to load projects:", err) - setError("Failed to load projects. Please check if the backend is running.") - } finally { - setIsLoading(false) - } + 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)) { + if (!loc) continue; + + let locDefectedSignboard = 0; + let locPothole = 0; + let locRoadCrack = 0; + let locDamagedRoadMarking = 0; + let locGoodSignboard = 0; + + for (const detection of loc.detections || []) { + const type = detection.type.toLowerCase(); + if (type === "defected_sign_board") { + locDefectedSignboard++; + totalDefectedSignboard++; + } else if (type === "pothole") { + locPothole++; + totalPothole++; + } else if (type === "road_crack") { + locRoadCrack++; + totalRoadCrack++; + } else if (type === "damaged_road_marking") { + locDamagedRoadMarking++; + totalDamagedRoadMarking++; + } else if (type === "good_sign_board") { + locGoodSignboard++; + totalGoodSignboard++; + } } - 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(null) - const [selectedLocationId, setSelectedLocationId] = useState(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 - useEffect(() => { - if (!selectedProjectId) { - setProjectSummary(null) - return + if ( + locDefectedSignboard > 0 || + locPothole > 0 || + locRoadCrack > 0 || + locDamagedRoadMarking > 0 || + locGoodSignboard > 0 + ) { + const shortName = + locName.length > 20 ? locName.substring(0, 20) + "..." : locName; + locationData.push({ + name: shortName, + defected_sign_board: locDefectedSignboard, + pothole: locPothole, + road_crack: locRoadCrack, + damaged_road_marking: locDamagedRoadMarking, + good_sign_board: locGoodSignboard, + total: + locDefectedSignboard + + locPothole + + locRoadCrack + + locDamagedRoadMarking + + locGoodSignboard, + }); } + } + } - const loadProjectSummary = async () => { - try { - setIsLoading(true) - setError(null) + setStats({ + totalDefectedSignboard, + totalPothole, + totalRoadCrack, + totalDamagedRoadMarking, + totalGoodSignboard, + totalRoadDamage: + totalDefectedSignboard + + totalPothole + + totalRoadCrack + + totalDamagedRoadMarking, + locationData, + }); + }, [projectSummary, selectedPackageId, selectedLocationId]); - const response = await fetch(`${API_URL}/summary/projects/${selectedProjectId}`, { - headers: { - "Content-Type": "application/json", - "ngrok-skip-browser-warning": "true" - } - }) + return ( +
+ {/* Sidebar Navigation */} + - if (!response.ok) { - throw new Error(`API Error: ${response.status}`) - } + {/* Main Content - offset by sidebar width */} +
+
+ {/* Header */} +
+ + + + + + - const summary: ProjectSummary = await response.json() - setProjectSummary(summary) - } catch (err) { - console.error("Failed to load project summary:", err) - setError("Failed to load project summary.") - setProjectSummary(null) - } finally { - setIsLoading(false) - } - } - - loadProjectSummary() - }, [selectedProjectId]) - - // Reset package and location when project changes - useEffect(() => { - setSelectedPackageId(null) - setSelectedLocationId(null) - }, [selectedProjectId]) - - // Reset location when package changes - useEffect(() => { - setSelectedLocationId(null) - }, [selectedPackageId]) - - // Filter stats based on selections - useEffect(() => { - if (!projectSummary) { - setStats({ - totalDefectedSignboard: 0, - totalPothole: 0, - totalRoadCrack: 0, - totalDamagedRoadMarking: 0, - totalGoodSignboard: 0, - totalRoadDamage: 0, - locationData: [] - }) - return - } - - let totalDefectedSignboard = 0 - let totalPothole = 0 - let totalRoadCrack = 0 - let totalDamagedRoadMarking = 0 - let totalGoodSignboard = 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)) { - if (!loc) continue - - let locDefectedSignboard = 0 - let locPothole = 0 - let locRoadCrack = 0 - let locDamagedRoadMarking = 0 - let locGoodSignboard = 0 - - for (const detection of loc.detections || []) { - const type = detection.type.toLowerCase() - if (type === "defected_sign_board") { - locDefectedSignboard++ - totalDefectedSignboard++ - } else if (type === "pothole") { - locPothole++ - totalPothole++ - } else if (type === "road_crack") { - locRoadCrack++ - totalRoadCrack++ - } else if (type === "damaged_road_marking") { - locDamagedRoadMarking++ - totalDamagedRoadMarking++ - } else if (type === "good_sign_board") { - locGoodSignboard++ - totalGoodSignboard++ - } - } - - if (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0 || locGoodSignboard > 0) { - const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName - locationData.push({ - name: shortName, - defected_sign_board: locDefectedSignboard, - pothole: locPothole, - road_crack: locRoadCrack, - damaged_road_marking: locDamagedRoadMarking, - good_sign_board: locGoodSignboard, - total: locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking + locGoodSignboard - }) - } - } - } - - setStats({ - totalDefectedSignboard, - totalPothole, - totalRoadCrack, - totalDamagedRoadMarking, - totalGoodSignboard, - totalRoadDamage: totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking, - locationData - }) - }, [projectSummary, selectedPackageId, selectedLocationId]) - - return ( -
- {/* Sidebar Navigation */} - - - {/* Main Content - offset by sidebar width */} -
-
- {/* Header */} -
-
- {/* Constant orbit animations removed */} -
-
- - - - -
-
-

- VisionRoad Analytics Dashboard -

-

- A Comprehensive Overview Of Your Road Infrastructure Analysis -

-
-
- - {/* Error Display */} - {error && ( -
- -

{error}

-
- )} - - {/* Stats Cards - Top Row */} -
- - - - - - -
- - {/* Filter Selector - Above main content */} -
- -
- - {/* Charts Row - Side by Side */} -
- {/* Left Chart - Detection Distribution */} - - - -
- -
- - Detection Distribution - -
-
- - - -
- - {/* Right Chart - Location Bar Chart */} - - - -
- -
- - Detections by Location - -
-
- - ({ - name: loc.name, - defected_sign_board: loc.defected_sign_board, - pothole: loc.pothole, - road_crack: loc.road_crack, - damaged_road_marking: loc.damaged_road_marking, - good_sign_board: loc.good_sign_board, - total: loc.total - }))} - isLoading={isLoading} - /> - -
-
- - {/* Map - Full Width Below Charts */} -
- -
- - {/* Footer */} -
-

- Sentient Geeks Pvt. Ltd. -

-
+ + + + + +
+

+ + Filter Analysis +

+

Refine your view by project, package, or location

-
+
+ +
+ + +
+ + {/* Error Display */} + {error && ( +
+ +

{error}

+
+ )} + + {isLoading && !projectSummary ? ( + + ) : ( + <> + {/* Stats Cards - Top Row */} +
+ + + + + + +
+ + {/* Charts Row - Side by Side */} +
+ {/* Left Chart - Detection Distribution */} + + + +
+ +
+ + Detection Distribution + +
+
+ + + +
+ + {/* Right Chart - Location Bar Chart */} + + + +
+ +
+ Detections by Location +
+
+ + ({ + name: loc.name, + defected_sign_board: loc.defected_sign_board, + pothole: loc.pothole, + road_crack: loc.road_crack, + damaged_road_marking: loc.damaged_road_marking, + good_sign_board: loc.good_sign_board, + total: loc.total, + }))} + /> + +
+
+ + {/* Map - Full Width Below Charts */} +
+ +
+ + )} + + {/* Footer */} +
+

+ Sentient Geeks Pvt. Ltd. +

+
- ) +
+
+ ); } diff --git a/app/globals.css b/app/globals.css index 3029ea6..3b7fdf7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,5 @@ -@import 'tailwindcss'; -@import 'tw-animate-css'; +@import "tailwindcss"; +@import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); @@ -9,11 +9,12 @@ --primary-foreground: oklch(0.98 0.01 264); /* Background & Surfaces */ - --background: #f8fafc; + --background: hsla(210, 40%, 98%, 0.2); --foreground: oklch(0.15 0.02 280); - --card: oklch(1 0 0); + --dialog: white; + --card: white; --card-foreground: oklch(0.15 0.02 280); - --popover: oklch(1 0 0); + --popover: white; --popover-foreground: oklch(0.15 0.02 280); /* Secondary */ @@ -43,10 +44,10 @@ /* Chart Colors - Vibrant Palette */ --chart-1: oklch(0.55 0.24 264); - --chart-2: oklch(0.65 0.20 200); - --chart-3: oklch(0.60 0.18 160); - --chart-4: oklch(0.70 0.20 85); - --chart-5: oklch(0.60 0.22 320); + --chart-2: oklch(0.65 0.2 200); + --chart-3: oklch(0.6 0.18 160); + --chart-4: oklch(0.7 0.2 85); + --chart-5: oklch(0.6 0.22 320); --radius: 0.75rem; @@ -63,23 +64,23 @@ .dark { /* Primary - Light Indigo */ - --primary: oklch(0.70 0.20 264); + --primary: oklch(0.7 0.2 264); --primary-foreground: oklch(0.15 0.02 264); - + --dialog: white; /* Background & Surfaces */ - --background: oklch(0.12 0.02 280); + --background: hsla(280, 20%, 12%, 0.3); --foreground: oklch(0.95 0.01 280); - --card: oklch(0.16 0.02 280); + --card: hsla(280, 20%, 16%, 0.4); --card-foreground: oklch(0.95 0.01 280); - --popover: oklch(0.16 0.02 280); + --popover: hsla(280, 20%, 16%, 0.7); --popover-foreground: oklch(0.95 0.01 280); /* Secondary */ --secondary: oklch(0.22 0.03 264); - --secondary-foreground: oklch(0.90 0.02 264); + --secondary-foreground: oklch(0.9 0.02 264); /* Muted */ - --muted: oklch(0.20 0.02 280); + --muted: oklch(0.2 0.02 280); --muted-foreground: oklch(0.65 0.02 280); /* Accent */ @@ -88,19 +89,19 @@ /* Destructive */ --destructive: oklch(0.45 0.18 25); - --destructive-foreground: oklch(0.90 0.08 25); + --destructive-foreground: oklch(0.9 0.08 25); /* Borders & Inputs */ --border: oklch(0.25 0.02 280); --input: oklch(0.22 0.02 280); - --ring: oklch(0.70 0.20 264); + --ring: oklch(0.7 0.2 264); /* Chart Colors */ --chart-1: oklch(0.65 0.22 264); - --chart-2: oklch(0.70 0.18 200); + --chart-2: oklch(0.7 0.18 200); --chart-3: oklch(0.65 0.16 160); --chart-4: oklch(0.75 0.18 85); - --chart-5: oklch(0.65 0.20 320); + --chart-5: oklch(0.65 0.2 320); /* Sidebar */ --sidebar: oklch(0.14 0.02 280); @@ -108,17 +109,18 @@ --sidebar-primary: oklch(0.65 0.22 264); --sidebar-primary-foreground: oklch(0.95 0.01 264); --sidebar-accent: oklch(0.22 0.03 264); - --sidebar-accent-foreground: oklch(0.90 0.02 264); + --sidebar-accent-foreground: oklch(0.9 0.02 264); --sidebar-border: oklch(0.25 0.02 280); - --sidebar-ring: oklch(0.70 0.20 264); + --sidebar-ring: oklch(0.7 0.2 264); } @theme inline { - --font-sans: 'Geist', 'Geist Fallback', system-ui, sans-serif; - --font-mono: 'Geist Mono', 'Geist Mono Fallback', monospace; + --font-sans: "Geist", "Geist Fallback", system-ui, sans-serif; + --font-mono: "Geist Mono", "Geist Mono Fallback", monospace; --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); + --color-dialog: var(--dialog); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); @@ -161,7 +163,23 @@ } body { - @apply bg-background text-foreground; + @apply text-foreground; + background-image: url("/background.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + min-height: 100vh; + background-color: transparent !important; + } + + .bg-mesh-gradient { + background: transparent !important; + } + + [data-slot="card"] { + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); } } @@ -174,7 +192,7 @@ } html { - scrollbar-gutter: stable; + /* scrollbar-gutter: stable; */ scroll-behavior: smooth; } @@ -190,7 +208,18 @@ body[data-scroll-locked] { -ms-overflow-style: none; } -.static-immediate { - transition: none !important; - animation: none !important; -} \ No newline at end of file +.heading-blue-gradient { + @apply bg-linear-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent; +} + +.btn-blue-gradient { + @apply text-white bg-linear-to-r from-blue-400 to-blue-600 hover:bg-linear-to-bl focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium transition-all duration-200; +} + +.bg-blue-gradient { +background: linear-gradient(271.19deg, #86B8F1 1.02%, #3895FF 191.34%); +} + +.badge-blue { + @apply flex items-center justify-center bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400 text-sm font-bold rounded-full border border-blue-200/50; +} diff --git a/app/map/page.tsx b/app/map/page.tsx index 0e761a5..496012b 100644 --- a/app/map/page.tsx +++ b/app/map/page.tsx @@ -5,16 +5,16 @@ import { DashboardMap } from "@/components/dashboard/dashboard-map" export default function MapPage() { return ( -
+
{/* Navigation Menu */} -
+
{/* Header */}
-

+

Detection Map

diff --git a/app/new-analysis/page.tsx b/app/new-analysis/page.tsx index 6c7cd7a..d12af67 100644 --- a/app/new-analysis/page.tsx +++ b/app/new-analysis/page.tsx @@ -5,6 +5,7 @@ import { ProjectSelectionSection } from "@/components/project-selection-section" import { SidebarNavigation } from "@/components/sidebar-navigation" import { type SessionContext, saveSession } from "@/lib/api" import { TrendingUp } from "lucide-react" +import { PageHeader } from "@/components/page-header" export default function NewAnalysisPage() { const router = useRouter() @@ -16,24 +17,21 @@ export default function NewAnalysisPage() { } return ( -

+
{/* Sidebar Navigation */} {/* Main Content */} -
+
-
+
{/* Refined Left-Aligned Header */} -
-
- -
-
-

- VisionRoad Detection System -

-
+
+
{/* Project Selection Section */} diff --git a/app/results/[videoId]/page.tsx b/app/results/[videoId]/page.tsx index 9bd05c1..6a708d8 100644 --- a/app/results/[videoId]/page.tsx +++ b/app/results/[videoId]/page.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button" import { Loader2, TrendingUp } from "lucide-react" import VideoPlayerSection from "@/components/video-player-section" import { SidebarNavigation } from "@/components/sidebar-navigation" +import { PageHeader } from "@/components/page-header" import { type SessionContext, loadSession, @@ -91,7 +92,7 @@ export default function VideoResultsPage() { if (isLoading) { return ( -
+

Loading detection results...

@@ -102,7 +103,7 @@ export default function VideoResultsPage() { if (error) { return ( -
+

{error}

@@ -115,22 +116,16 @@ export default function VideoResultsPage() { } return ( -
+
-
+
-
-
- -
-
-

- {getTitle()} -

-

- Video ID: {videoId} -

-
+
+
{session && ( diff --git a/app/results/page.tsx b/app/results/page.tsx index ddf82b2..297daed 100644 --- a/app/results/page.tsx +++ b/app/results/page.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button" import { Loader2, ArrowLeft, MapPin, Package, FolderKanban, RotateCcw, TrendingUp } from "lucide-react" import VideoPlayerSection from "@/components/video-player-section" import { SidebarNavigation } from "@/components/sidebar-navigation" +import { PageHeader } from "@/components/page-header" import { type SessionContext, loadSession, @@ -73,7 +74,7 @@ export default function ResultsPage() { if (isLoading) { return ( -
+

Loading detection results...

@@ -84,7 +85,7 @@ export default function ResultsPage() { if (error) { return ( -
+

{error}

@@ -94,26 +95,20 @@ export default function ResultsPage() { } return ( -
+
{/* Sidebar Navigation */} {/* Main Content */} -
+
{/* Refined Header */} -
-
- -
-
-

- {getTitle()} -

-

- View your AI-powered road analysis results -

-
+
+
{/* Session Info Bar */} diff --git a/app/upload/[videoId]/page.tsx b/app/upload/[videoId]/page.tsx index 785f03c..4511aba 100644 --- a/app/upload/[videoId]/page.tsx +++ b/app/upload/[videoId]/page.tsx @@ -5,6 +5,7 @@ import { useRouter, useParams } from "next/navigation" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Loader2, TrendingUp } from "lucide-react" import { SidebarNavigation } from "@/components/sidebar-navigation" +import { PageHeader } from "@/components/page-header" import { type SessionContext, loadSession, @@ -119,7 +120,7 @@ export default function VideoProcessingPage() { if (isLoading) { return ( -
+
@@ -128,19 +129,16 @@ export default function VideoProcessingPage() { } return ( -
+
-
-
-
-
- -
-
-

- Processing Analysis -

-
+
+
+
+
{session && ( diff --git a/app/upload/page.tsx b/app/upload/page.tsx index 327ab90..07732ed 100644 --- a/app/upload/page.tsx +++ b/app/upload/page.tsx @@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Loader2, TrendingUp } from "lucide-react" import { SidebarNavigation } from "@/components/sidebar-navigation" +import { PageHeader } from "@/components/page-header" import { type SessionContext, loadSession, @@ -131,7 +132,7 @@ export default function UploadPage() { if (isLoading) { return ( -
+
@@ -140,24 +141,21 @@ export default function UploadPage() { } return ( -
+
{/* Sidebar Navigation */} {/* Main Content */} -
+
-
+
{/* Refined Header */} -
-
- -
-
-

- {getTitle()} -

-
+
+
{/* Compact Session Info Bar */} diff --git a/components/dashboard/dashboard-map.tsx b/components/dashboard/dashboard-map.tsx index 7e4478f..db99aae 100644 --- a/components/dashboard/dashboard-map.tsx +++ b/components/dashboard/dashboard-map.tsx @@ -89,12 +89,12 @@ export function DashboardMap({ } return ( - +
-
- +
+
@@ -132,7 +132,7 @@ export function DashboardMap({
-
+
{isLoading ? (
diff --git a/components/dashboard/dashboard-skeleton.tsx b/components/dashboard/dashboard-skeleton.tsx new file mode 100644 index 0000000..50b48f5 --- /dev/null +++ b/components/dashboard/dashboard-skeleton.tsx @@ -0,0 +1,59 @@ +"use client" + +import { Skeleton } from "@/components/ui/skeleton" +import { Card, CardContent, CardHeader } from "@/components/ui/card" + +export function DashboardSkeleton() { + return ( +
+ {/* Stats Cards Skeleton */} +
+ {[...Array(6)].map((_, i) => ( + + +
+ + + +
+ +
+
+ ))} +
+ + {/* Charts Row Skeleton */} +
+ {[...Array(2)].map((_, i) => ( + + +
+ + +
+
+ + + +
+ ))} +
+ + {/* Map Skeleton */} + + +
+
+ + +
+ +
+
+ + + +
+
+ ) +} diff --git a/components/dashboard/detection-donut-chart.tsx b/components/dashboard/detection-donut-chart.tsx index 53b4c83..c3816f4 100644 --- a/components/dashboard/detection-donut-chart.tsx +++ b/components/dashboard/detection-donut-chart.tsx @@ -9,7 +9,7 @@ interface DetectionDonutChartProps { roadCrack: number damagedRoadMarking: number goodSignboard: number - isLoading: boolean + isLoading?: boolean } const COLORS = { @@ -26,7 +26,7 @@ export function DetectionDonutChart({ roadCrack, damagedRoadMarking, goodSignboard, - isLoading + isLoading = false }: DetectionDonutChartProps) { if (isLoading) { return ( diff --git a/components/dashboard/filter-selector.tsx b/components/dashboard/filter-selector.tsx index 2425483..a393018 100644 --- a/components/dashboard/filter-selector.tsx +++ b/components/dashboard/filter-selector.tsx @@ -36,23 +36,26 @@ export function FilterSelector({ isLoading = false }: FilterSelectorProps) { return ( -
+
{/* Project Dropdown */} -
-
- +
+
+
+ +
+ Project
- {/* Divider */} - {selectedProjectId && packages.length > 0 && ( -
- )} - {/* Package Dropdown */} - {selectedProjectId && packages.length > 0 && ( -
-
- + {selectedProjectId && ( +
+
+
+ +
+ Package
- + - - All Locations + + All Locations {locations.map((loc) => ( - + {loc.name} ))} @@ -122,5 +121,5 @@ export function FilterSelector({
)}
- ) + ); } diff --git a/components/dashboard/gradient-stats-card.tsx b/components/dashboard/gradient-stats-card.tsx index 290bff4..5b12662 100644 --- a/components/dashboard/gradient-stats-card.tsx +++ b/components/dashboard/gradient-stats-card.tsx @@ -17,43 +17,43 @@ const gradientStyles = { background: "bg-card", border: "border-l-4 border-l-[var(--border)]", iconBg: "bg-gradient-to-br from-blue-400 to-blue-600", - iconShadow: "shadow-lg shadow-blue-500/20" + }, coral: { background: "bg-card", border: "border-l-4 border-l-[var(--border)]", iconBg: "bg-gradient-to-br from-blue-400 to-blue-600", - iconShadow: "shadow-lg shadow-blue-500/20" + }, blue: { background: "bg-card", border: "border-l-4 border-l-[var(--border)]", iconBg: "bg-gradient-to-br from-blue-400 to-blue-600", - iconShadow: "shadow-lg shadow-blue-500/20" + }, purple: { background: "bg-card", border: "border-l-4 border-l-[var(--border)]", iconBg: "bg-gradient-to-br from-blue-400 to-blue-600", - iconShadow: "shadow-lg shadow-blue-500/20" + }, orange: { background: "bg-card", border: "border-l-4 border-l-[var(--border)]", iconBg: "bg-gradient-to-br from-blue-400 to-blue-600", - iconShadow: "shadow-lg shadow-blue-500/20" + }, indigo: { background: "bg-card", border: "border-l-4 border-l-[var(--border)]", iconBg: "bg-gradient-to-br from-blue-400 to-blue-600", - iconShadow: "shadow-lg shadow-blue-500/20" + }, emerald: { background: "bg-card", border: "border-l-4 border-l-[var(--border)]", iconBg: "bg-gradient-to-br from-blue-400 to-blue-600", - iconShadow: "shadow-lg shadow-blue-500/20" + } } @@ -68,7 +68,7 @@ export function GradientStatsCard({ const styles = gradientStyles[gradient] return ( - +

@@ -79,11 +79,11 @@ export function GradientStatsCard({
) : ( <> -

+

{value}

{subtitle && ( -

+

{subtitle}

)} @@ -92,9 +92,12 @@ export function GradientStatsCard({

-
diff --git a/components/dashboard/location-bar-chart.tsx b/components/dashboard/location-bar-chart.tsx index 9641832..123ecc7 100644 --- a/components/dashboard/location-bar-chart.tsx +++ b/components/dashboard/location-bar-chart.tsx @@ -15,7 +15,7 @@ interface LocationData { interface LocationBarChartProps { data: LocationData[] - isLoading: boolean + isLoading?: boolean } const COLORS = { @@ -26,7 +26,7 @@ const COLORS = { good_sign_board: "#34d399" // Lighter Emerald } -export function LocationBarChart({ data, isLoading }: LocationBarChartProps) { +export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) { if (isLoading) { return (
diff --git a/components/dashboard/recent-analyses-table.tsx b/components/dashboard/recent-analyses-table.tsx index 00b6417..965247e 100644 --- a/components/dashboard/recent-analyses-table.tsx +++ b/components/dashboard/recent-analyses-table.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Eye, AlertCircle } from "lucide-react" import { type Video } from "@/lib/api" +import { Skeleton } from "@/components/ui/skeleton" interface RecentAnalysesTableProps { videos: Video[] @@ -56,9 +57,19 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent {isLoading ? ( -
- {[1, 2, 3, 4].map((i) => ( -
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
+ + +
+
+ + +
+ +
))}
) : videos.length === 0 ? ( @@ -83,11 +94,11 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent {formatDate(video.created_at)}

-
+
{getDetectionTypeBadge(video.detection_type)} {getStatusBadge(video.status)}
-
+

{video.detection_type === "pothole-detection" ? video.unique_pothole ?? 0 @@ -103,7 +114,7 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent variant="ghost" size="sm" onClick={() => onViewResults(video.id, video.detection_type)} - className="ml-4 flex-shrink-0" + className="ml-4 shrink-0" > View diff --git a/components/data-table.tsx b/components/data-table.tsx index 9eb99c9..4a48344 100644 --- a/components/data-table.tsx +++ b/components/data-table.tsx @@ -8,6 +8,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" +import { Skeleton } from "@/components/ui/skeleton" interface Column { key: string @@ -42,51 +43,62 @@ export function DataTable>({ return (

{/* Header with Title and Add Button */} -
-
-

{title}

-

Total items: {data.length}

+
+
+

{title}

+ + {data.length} +
{/* Table */} -
-
- - +
+
+
+ {columns.map((col) => ( ))} {showActions && ( - )} - + {isLoading ? ( - - - + Array.from({ length: 5 }).map((_, idx) => ( + + {columns.map((col) => ( + + ))} + {showActions && ( + + )} + + )) ) : data.length === 0 ? (
{col.header} + Actions
-
-
- Loading records... -
-
+ + +
+ + +
+
@@ -121,7 +133,7 @@ export function DataTable>({ variant="ghost" size="icon" onClick={() => onEdit(item)} - className="h-8 w-8 rounded-full bg-amber-100 text-amber-600 hover:bg-amber-600 hover:text-white dark:bg-amber-900/40 dark:text-amber-400 dark:hover:bg-amber-700" + className="h-8 w-8 rounded-sm bg-white text-blue-500 border-slate-200! border hover:bg-blue-500 hover:text-white" > @@ -138,7 +150,7 @@ export function DataTable>({ variant="ghost" size="icon" onClick={() => onDelete(item)} - className="h-8 w-8 rounded-full bg-rose-100 text-rose-600 hover:bg-rose-600 hover:text-white dark:bg-rose-900/40 dark:text-rose-400 dark:hover:bg-rose-700" + className="h-8 w-8 rounded-sm border border-slate-200! bg-white text-red-400 hover:bg-red-400 hover:text-white" > diff --git a/components/navigation-menu.tsx b/components/navigation-menu.tsx index 32049f5..a36a1bf 100644 --- a/components/navigation-menu.tsx +++ b/components/navigation-menu.tsx @@ -114,12 +114,12 @@ export function NavigationMenu() { : "hover:bg-slate-50 border border-transparent hover:border-slate-100" }`} > -
diff --git a/components/page-header.tsx b/components/page-header.tsx new file mode 100644 index 0000000..4aa0986 --- /dev/null +++ b/components/page-header.tsx @@ -0,0 +1,30 @@ +"use client" + +import { LucideIcon } from "lucide-react" + +interface PageHeaderProps { + title: string + description: string + icon?: LucideIcon + children?: React.ReactNode +} + +export function PageHeader({ title, description, icon: Icon, children }: PageHeaderProps) { + return ( +
+
+ + {Icon && } + {children} +
+
+

+ {title} +

+

+ {description} +

+
+
+ ) +} diff --git a/components/sidebar-navigation.tsx b/components/sidebar-navigation.tsx index 779670e..481b2b8 100644 --- a/components/sidebar-navigation.tsx +++ b/components/sidebar-navigation.tsx @@ -2,6 +2,7 @@ import { useRouter, usePathname } from "next/navigation" import { LayoutDashboard, Plus, User, FolderPlus, Package, MapPin } from "lucide-react" +import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip" interface NavItem { title: string @@ -19,19 +20,19 @@ const navItems: NavItem[] = [ gradient: "from-blue-400 to-indigo-500" }, { - title: "Create Project", + title: "Project", href: "/create-project", icon: FolderPlus, gradient: "from-blue-400 to-blue-600" }, { - title: "Create Package", + title: "Package", href: "/create-package", icon: Package, gradient: "from-violet-400 to-purple-500" }, { - title: "Create Location", + title: "Location", href: "/create-location", icon: MapPin, gradient: "from-amber-400 to-orange-500" @@ -57,7 +58,7 @@ export function SidebarNavigation() { const isNewAnalysisActive = pathname === "/new-analysis" return ( -