ui changes
This commit is contained in:
@@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
|
|||||||
import { Loader2, CheckCircle2, MapPin, Navigation, Milestone } from "lucide-react"
|
import { Loader2, CheckCircle2, MapPin, Navigation, Milestone } from "lucide-react"
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
import { DataTable } from "@/components/data-table"
|
import { DataTable } from "@/components/data-table"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
import {
|
import {
|
||||||
fetchProjects,
|
fetchProjects,
|
||||||
fetchPackagesByProject,
|
fetchPackagesByProject,
|
||||||
@@ -261,7 +262,7 @@ export default function CreateLocationPage() {
|
|||||||
render: (location: Location) => {
|
render: (location: Location) => {
|
||||||
if (location.chainage_start_km !== null && location.chainage_end_km !== null) {
|
if (location.chainage_start_km !== null && location.chainage_end_km !== null) {
|
||||||
return (
|
return (
|
||||||
<span className="px-2 py-0.5 rounded-full bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 text-[10px] font-semibold border border-amber-100 dark:border-amber-800 whitespace-nowrap">
|
<span className="px-2 py-0.5 rounded-full bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 font-semibold border border-amber-100 dark:border-amber-800 whitespace-nowrap">
|
||||||
{location.chainage_start_km} - {location.chainage_end_km}
|
{location.chainage_start_km} - {location.chainage_end_km}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -273,7 +274,7 @@ export default function CreateLocationPage() {
|
|||||||
key: "start_gps",
|
key: "start_gps",
|
||||||
header: "Start GPS",
|
header: "Start GPS",
|
||||||
render: (location: Location) => (
|
render: (location: Location) => (
|
||||||
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-[10px] font-mono border border-blue-100 dark:border-blue-800 whitespace-nowrap">
|
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-100 dark:border-blue-800 whitespace-nowrap">
|
||||||
{location.start_lat.toFixed(4)}, {location.start_lng.toFixed(4)}
|
{location.start_lat.toFixed(4)}, {location.start_lng.toFixed(4)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -282,7 +283,7 @@ export default function CreateLocationPage() {
|
|||||||
key: "end_gps",
|
key: "end_gps",
|
||||||
header: "End GPS",
|
header: "End GPS",
|
||||||
render: (location: Location) => (
|
render: (location: Location) => (
|
||||||
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-[10px] font-mono border border-blue-100 dark:border-blue-800 whitespace-nowrap">
|
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-100 dark:border-blue-800 whitespace-nowrap">
|
||||||
{location.end_lat.toFixed(4)}, {location.end_lng.toFixed(4)}
|
{location.end_lat.toFixed(4)}, {location.end_lng.toFixed(4)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -290,25 +291,17 @@ export default function CreateLocationPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
<main className="ml-16 min-h-screen relative overflow-hidden">
|
<main className="ml-20 min-h-screen relative overflow-hidden">
|
||||||
<div className="mx-auto px-6 py-8 max-w-7xl relative z-10">
|
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
|
||||||
{/* Refined Header */}
|
{/* Refined Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-5">
|
<PageHeader
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
title="Location Management"
|
||||||
<MapPin className="h-8 w-8 text-white" />
|
description="Manage road segment locations"
|
||||||
</div>
|
icon={MapPin}
|
||||||
<div className="flex flex-col">
|
/>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
|
|
||||||
Location Management
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium italic">
|
|
||||||
Manage road segment locations
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Error Message */}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
|
|||||||
import { Loader2, CheckCircle2, Package, FolderKanban, Globe } from "lucide-react"
|
import { Loader2, CheckCircle2, Package, FolderKanban, Globe } from "lucide-react"
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
import { DataTable } from "@/components/data-table"
|
import { DataTable } from "@/components/data-table"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
import {
|
import {
|
||||||
fetchProjects,
|
fetchProjects,
|
||||||
fetchAllPackages,
|
fetchAllPackages,
|
||||||
@@ -178,7 +179,7 @@ export default function CreatePackagePage() {
|
|||||||
{project.state.split(',').map((item, idx) => (
|
{project.state.split(',').map((item, idx) => (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-[10px] font-semibold border border-blue-100 dark:border-blue-800 shadow-sm"
|
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-semibold border border-blue-100 dark:border-blue-800"
|
||||||
>
|
>
|
||||||
{item.trim()}
|
{item.trim()}
|
||||||
</span>
|
</span>
|
||||||
@@ -194,25 +195,17 @@ export default function CreatePackagePage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
<main className="ml-16 min-h-screen relative overflow-hidden">
|
<main className="ml-20 min-h-screen relative overflow-hidden">
|
||||||
<div className="mx-auto px-6 py-8 max-w-7xl relative z-10">
|
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
|
||||||
{/* Refined Header */}
|
{/* Refined Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-5">
|
<PageHeader
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
title="Package Management"
|
||||||
<Package className="h-8 w-8 text-white" />
|
description="Manage project packages"
|
||||||
</div>
|
icon={Package}
|
||||||
<div className="flex flex-col">
|
/>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
|
|
||||||
Package Management
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium italic">
|
|
||||||
Manage project packages
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Error Message */}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
|
|||||||
import { Loader2, CheckCircle2, FolderPlus, MapPin, Building2, Route, X } from "lucide-react"
|
import { Loader2, CheckCircle2, FolderPlus, MapPin, Building2, Route, X } from "lucide-react"
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
import { DataTable } from "@/components/data-table"
|
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 { createProject, fetchProjects, updateProject, deleteProject, type ProjectCreate, type Project, type ProjectUpdate } from "@/lib/api"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ export default function CreateProjectPage() {
|
|||||||
{project.state.split(',').map((item, idx) => (
|
{project.state.split(',').map((item, idx) => (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-[11px] font-semibold border border-blue-100 dark:border-blue-800 shadow-sm"
|
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-semibold border border-blue-100 dark:border-blue-800"
|
||||||
>
|
>
|
||||||
{item.trim()}
|
{item.trim()}
|
||||||
</span>
|
</span>
|
||||||
@@ -175,25 +176,17 @@ export default function CreateProjectPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
<main className="ml-16 min-h-screen relative overflow-hidden">
|
<main className="ml-20 min-h-screen relative overflow-hidden">
|
||||||
<div className="mx-auto px-6 py-8 max-w-7xl relative z-10">
|
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
|
||||||
{/* Refined Header */}
|
{/* Refined Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-5">
|
<PageHeader
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
title="Project Management"
|
||||||
<FolderPlus className="h-8 w-8 text-white" />
|
description="Manage road infrastructure projects"
|
||||||
</div>
|
icon={FolderPlus}
|
||||||
<div className="flex flex-col">
|
/>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
|
|
||||||
Project Management
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium italic">
|
|
||||||
Manage road infrastructure projects
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Error Message */}
|
||||||
|
|||||||
@@ -1,76 +1,83 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
TrendingUp,
|
Activity,
|
||||||
MapPin as MapPinIcon,
|
MapPin as MapPinIcon,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
RectangleHorizontal
|
Zap,
|
||||||
} from "lucide-react"
|
PencilLine,
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
CheckCircle2,
|
||||||
import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card"
|
} from "lucide-react";
|
||||||
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector"
|
import { SidebarNavigation } from "@/components/sidebar-navigation";
|
||||||
import { FilterSelector } from "@/components/dashboard/filter-selector"
|
import { GradientStatsCard } from "@/components/dashboard/gradient-stats-card";
|
||||||
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart"
|
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector";
|
||||||
import { LocationBarChart } from "@/components/dashboard/location-bar-chart"
|
import { FilterSelector } from "@/components/dashboard/filter-selector";
|
||||||
import { DashboardMap } from "@/components/dashboard/dashboard-map"
|
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 {
|
import {
|
||||||
fetchProjects,
|
Popover,
|
||||||
type Project
|
PopoverContent,
|
||||||
} from "@/lib/api"
|
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 {
|
interface ProjectSummary {
|
||||||
project: {
|
project: {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
corridor_name: string | null
|
corridor_name: string | null;
|
||||||
state: string | null
|
state: string | null;
|
||||||
}
|
};
|
||||||
packages: {
|
packages: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
package_id: string
|
package_id: string;
|
||||||
region: string | null
|
region: string | null;
|
||||||
locations: {
|
locations: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
location_id: string
|
location_id: string;
|
||||||
chainage: string | null
|
chainage: string | null;
|
||||||
detection_count: number
|
detection_count: number;
|
||||||
detections: Array<{
|
detections: Array<{
|
||||||
id: number
|
id: number;
|
||||||
type: string
|
type: string;
|
||||||
class: string
|
class: string;
|
||||||
confidence: number
|
confidence: number;
|
||||||
latitude: number
|
latitude: number;
|
||||||
longitude: number
|
longitude: number;
|
||||||
}>
|
}>;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DetectionStats {
|
interface DetectionStats {
|
||||||
totalDefectedSignboard: number
|
totalDefectedSignboard: number;
|
||||||
totalPothole: number
|
totalPothole: number;
|
||||||
totalRoadCrack: number
|
totalRoadCrack: number;
|
||||||
totalDamagedRoadMarking: number
|
totalDamagedRoadMarking: number;
|
||||||
totalGoodSignboard: number
|
totalGoodSignboard: number;
|
||||||
totalRoadDamage: number
|
totalRoadDamage: number;
|
||||||
locationData: Array<{
|
locationData: Array<{
|
||||||
name: string
|
name: string;
|
||||||
defected_sign_board: number
|
defected_sign_board: number;
|
||||||
pothole: number
|
pothole: number;
|
||||||
road_crack: number
|
road_crack: number;
|
||||||
damaged_road_marking: number
|
damaged_road_marking: number;
|
||||||
good_sign_board: number
|
good_sign_board: number;
|
||||||
total: number
|
total: number;
|
||||||
}>
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
||||||
@@ -82,51 +89,68 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
totalDamagedRoadMarking: 0,
|
totalDamagedRoadMarking: 0,
|
||||||
totalGoodSignboard: 0,
|
totalGoodSignboard: 0,
|
||||||
totalRoadDamage: 0,
|
totalRoadDamage: 0,
|
||||||
locationData: []
|
locationData: [],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalDefectedSignboard = 0
|
let totalDefectedSignboard = 0;
|
||||||
let totalPothole = 0
|
let totalPothole = 0;
|
||||||
let totalRoadCrack = 0
|
let totalRoadCrack = 0;
|
||||||
let totalDamagedRoadMarking = 0
|
let totalDamagedRoadMarking = 0;
|
||||||
let totalGoodSignboard = 0
|
let totalGoodSignboard = 0;
|
||||||
let totalRoadDamage = 0
|
let totalRoadDamage = 0;
|
||||||
const locationData: DetectionStats["locationData"] = []
|
const locationData: DetectionStats["locationData"] = [];
|
||||||
|
|
||||||
for (const pkg of Object.values(summary.packages || {})) {
|
for (const pkg of Object.values(summary.packages || {})) {
|
||||||
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
|
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
|
||||||
let locDefectedSignboard = 0
|
let locDefectedSignboard = 0;
|
||||||
let locPothole = 0
|
let locPothole = 0;
|
||||||
let locRoadCrack = 0
|
let locRoadCrack = 0;
|
||||||
let locDamagedRoadMarking = 0
|
let locDamagedRoadMarking = 0;
|
||||||
let locGoodSignboard = 0
|
let locGoodSignboard = 0;
|
||||||
|
|
||||||
for (const detection of loc.detections || []) {
|
for (const detection of loc.detections || []) {
|
||||||
const type = detection.type.toLowerCase()
|
const type = detection.type.toLowerCase();
|
||||||
if (type === "defected_sign_board") {
|
if (type === "defected_sign_board") {
|
||||||
locDefectedSignboard++
|
locDefectedSignboard++;
|
||||||
totalDefectedSignboard++
|
totalDefectedSignboard++;
|
||||||
} else if (type === "pothole") {
|
} else if (type === "pothole") {
|
||||||
locPothole++
|
locPothole++;
|
||||||
totalPothole++
|
totalPothole++;
|
||||||
} else if (type === "road_crack") {
|
} else if (type === "road_crack") {
|
||||||
locRoadCrack++
|
locRoadCrack++;
|
||||||
totalRoadCrack++
|
totalRoadCrack++;
|
||||||
} else if (type === "damaged_road_marking") {
|
} else if (type === "damaged_road_marking") {
|
||||||
locDamagedRoadMarking++
|
locDamagedRoadMarking++;
|
||||||
totalDamagedRoadMarking++
|
totalDamagedRoadMarking++;
|
||||||
} else if (type === "good_sign_board") {
|
} else if (type === "good_sign_board") {
|
||||||
locGoodSignboard++
|
locGoodSignboard++;
|
||||||
totalGoodSignboard++
|
totalGoodSignboard++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const locTotalDamage = locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking
|
const locTotalDamage =
|
||||||
totalRoadDamage += (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0) ? 1 : 0 // This logic might need refinement based on "total_road_damage" definition
|
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) {
|
if (
|
||||||
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
|
locDefectedSignboard > 0 ||
|
||||||
|
locPothole > 0 ||
|
||||||
|
locRoadCrack > 0 ||
|
||||||
|
locDamagedRoadMarking > 0 ||
|
||||||
|
locGoodSignboard > 0
|
||||||
|
) {
|
||||||
|
const shortName =
|
||||||
|
locName.length > 20 ? locName.substring(0, 20) + "..." : locName;
|
||||||
locationData.push({
|
locationData.push({
|
||||||
name: shortName,
|
name: shortName,
|
||||||
defected_sign_board: locDefectedSignboard,
|
defected_sign_board: locDefectedSignboard,
|
||||||
@@ -134,8 +158,13 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
road_crack: locRoadCrack,
|
road_crack: locRoadCrack,
|
||||||
damaged_road_marking: locDamagedRoadMarking,
|
damaged_road_marking: locDamagedRoadMarking,
|
||||||
good_sign_board: locGoodSignboard,
|
good_sign_board: locGoodSignboard,
|
||||||
total: locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking + locGoodSignboard
|
total:
|
||||||
})
|
locDefectedSignboard +
|
||||||
|
locPothole +
|
||||||
|
locRoadCrack +
|
||||||
|
locDamagedRoadMarking +
|
||||||
|
locGoodSignboard,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +172,11 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
// Recalculate totalRoadDamage based on backends unique count
|
// Recalculate totalRoadDamage based on backends unique count
|
||||||
// But since we are aggregating from locations, we just sum them up or use a simpler metric
|
// 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
|
// The user's JSON shows "total_road_damage": 9 which is sum of 2+6+0+1
|
||||||
totalRoadDamage = totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking
|
totalRoadDamage =
|
||||||
|
totalDefectedSignboard +
|
||||||
|
totalPothole +
|
||||||
|
totalRoadCrack +
|
||||||
|
totalDamagedRoadMarking;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalDefectedSignboard,
|
totalDefectedSignboard,
|
||||||
@@ -152,15 +185,19 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
totalDamagedRoadMarking,
|
totalDamagedRoadMarking,
|
||||||
totalGoodSignboard,
|
totalGoodSignboard,
|
||||||
totalRoadDamage,
|
totalRoadDamage,
|
||||||
locationData
|
locationData,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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>(null)
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
||||||
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(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,
|
||||||
@@ -168,101 +205,117 @@ export default function DashboardPage() {
|
|||||||
totalDamagedRoadMarking: 0,
|
totalDamagedRoadMarking: 0,
|
||||||
totalGoodSignboard: 0,
|
totalGoodSignboard: 0,
|
||||||
totalRoadDamage: 0,
|
totalRoadDamage: 0,
|
||||||
locationData: []
|
locationData: [],
|
||||||
})
|
});
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load projects on mount
|
// Load projects on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadProjects = async () => {
|
const loadProjects = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
setError(null)
|
setError(null);
|
||||||
const projectsData = await fetchProjects()
|
const projectsData = await fetchProjects();
|
||||||
setProjects(projectsData)
|
setProjects(projectsData);
|
||||||
|
|
||||||
if (projectsData.length > 0) {
|
if (projectsData.length > 0) {
|
||||||
setSelectedProjectId(projectsData[0].id)
|
setSelectedProjectId(projectsData[0].id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load projects:", err)
|
console.error("Failed to load projects:", err);
|
||||||
setError("Failed to load projects. Please check if the backend is running.")
|
setError(
|
||||||
|
"Failed to load projects. Please check if the backend is running.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setTimeout(() => {
|
||||||
}
|
setIsLoading(false);
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
loadProjects()
|
loadProjects();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const selectedProject = projects.find(p => p.id === selectedProjectId)
|
const selectedProject = projects.find((p) => p.id === selectedProjectId);
|
||||||
|
|
||||||
// Extract packages from project summary
|
// Extract packages from project summary
|
||||||
const packages = projectSummary
|
const packages = projectSummary
|
||||||
? Object.keys(projectSummary.packages || {}).map(pkgName => ({
|
? Object.keys(projectSummary.packages || {}).map((pkgName) => ({
|
||||||
id: pkgName,
|
id: pkgName,
|
||||||
name: pkgName
|
name: pkgName,
|
||||||
}))
|
}))
|
||||||
: []
|
: [];
|
||||||
|
|
||||||
// Extract locations from selected package
|
// Extract locations from selected package
|
||||||
const [selectedPackageId, setSelectedPackageId] = useState<string | null>(null)
|
const [selectedPackageId, setSelectedPackageId] = useState<string | null>(
|
||||||
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(null)
|
null,
|
||||||
|
);
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const locations = projectSummary && selectedPackageId && selectedPackageId !== "all"
|
const locations =
|
||||||
? Object.keys(projectSummary.packages[selectedPackageId]?.locations || {}).map(locName => ({
|
projectSummary && selectedPackageId && selectedPackageId !== "all"
|
||||||
|
? Object.keys(
|
||||||
|
projectSummary.packages[selectedPackageId]?.locations || {},
|
||||||
|
).map((locName) => ({
|
||||||
id: locName,
|
id: locName,
|
||||||
name: locName
|
name: locName,
|
||||||
}))
|
}))
|
||||||
: []
|
: [];
|
||||||
|
|
||||||
// Load project summary when project changes
|
// Load project summary when project changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProjectId) {
|
if (!selectedProjectId) {
|
||||||
setProjectSummary(null)
|
setProjectSummary(null);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadProjectSummary = async () => {
|
const loadProjectSummary = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
setError(null)
|
setError(null);
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/summary/projects/${selectedProjectId}`, {
|
const response = await fetch(
|
||||||
|
`${API_URL}/summary/projects/${selectedProjectId}`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"ngrok-skip-browser-warning": "true"
|
"ngrok-skip-browser-warning": "true",
|
||||||
}
|
},
|
||||||
})
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API Error: ${response.status}`)
|
throw new Error(`API Error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary: ProjectSummary = await response.json()
|
const summary: ProjectSummary = await response.json();
|
||||||
setProjectSummary(summary)
|
setProjectSummary(summary);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load project summary:", err)
|
console.error("Failed to load project summary:", err);
|
||||||
setError("Failed to load project summary.")
|
setError("Failed to load project summary.");
|
||||||
setProjectSummary(null)
|
setProjectSummary(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setTimeout(() => {
|
||||||
}
|
setIsLoading(false);
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
loadProjectSummary()
|
loadProjectSummary();
|
||||||
}, [selectedProjectId])
|
}, [selectedProjectId]);
|
||||||
|
|
||||||
// Reset package and location when project changes
|
// Reset package and location when project changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedPackageId(null)
|
setSelectedPackageId(null);
|
||||||
setSelectedLocationId(null)
|
setSelectedLocationId(null);
|
||||||
}, [selectedProjectId])
|
}, [selectedProjectId]);
|
||||||
|
|
||||||
// Reset location when package changes
|
// Reset location when package changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedLocationId(null)
|
setSelectedLocationId(null);
|
||||||
}, [selectedPackageId])
|
}, [selectedPackageId]);
|
||||||
|
|
||||||
// Filter stats based on selections
|
// Filter stats based on selections
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -274,58 +327,67 @@ export default function DashboardPage() {
|
|||||||
totalDamagedRoadMarking: 0,
|
totalDamagedRoadMarking: 0,
|
||||||
totalGoodSignboard: 0,
|
totalGoodSignboard: 0,
|
||||||
totalRoadDamage: 0,
|
totalRoadDamage: 0,
|
||||||
locationData: []
|
locationData: [],
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalDefectedSignboard = 0
|
let totalDefectedSignboard = 0;
|
||||||
let totalPothole = 0
|
let totalPothole = 0;
|
||||||
let totalRoadCrack = 0
|
let totalRoadCrack = 0;
|
||||||
let totalDamagedRoadMarking = 0
|
let totalDamagedRoadMarking = 0;
|
||||||
let totalGoodSignboard = 0
|
let totalGoodSignboard = 0;
|
||||||
const locationData: DetectionStats["locationData"] = []
|
const locationData: DetectionStats["locationData"] = [];
|
||||||
|
|
||||||
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
|
const packagesToProcess =
|
||||||
|
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 locationsToProcess = selectedLocationId && selectedLocationId !== "all"
|
const locationsToProcess =
|
||||||
|
selectedLocationId && selectedLocationId !== "all"
|
||||||
? { [selectedLocationId]: pkg.locations[selectedLocationId] }
|
? { [selectedLocationId]: pkg.locations[selectedLocationId] }
|
||||||
: pkg.locations || {}
|
: pkg.locations || {};
|
||||||
|
|
||||||
for (const [locName, loc] of Object.entries(locationsToProcess)) {
|
for (const [locName, loc] of Object.entries(locationsToProcess)) {
|
||||||
if (!loc) continue
|
if (!loc) continue;
|
||||||
|
|
||||||
let locDefectedSignboard = 0
|
let locDefectedSignboard = 0;
|
||||||
let locPothole = 0
|
let locPothole = 0;
|
||||||
let locRoadCrack = 0
|
let locRoadCrack = 0;
|
||||||
let locDamagedRoadMarking = 0
|
let locDamagedRoadMarking = 0;
|
||||||
let locGoodSignboard = 0
|
let locGoodSignboard = 0;
|
||||||
|
|
||||||
for (const detection of loc.detections || []) {
|
for (const detection of loc.detections || []) {
|
||||||
const type = detection.type.toLowerCase()
|
const type = detection.type.toLowerCase();
|
||||||
if (type === "defected_sign_board") {
|
if (type === "defected_sign_board") {
|
||||||
locDefectedSignboard++
|
locDefectedSignboard++;
|
||||||
totalDefectedSignboard++
|
totalDefectedSignboard++;
|
||||||
} else if (type === "pothole") {
|
} else if (type === "pothole") {
|
||||||
locPothole++
|
locPothole++;
|
||||||
totalPothole++
|
totalPothole++;
|
||||||
} else if (type === "road_crack") {
|
} else if (type === "road_crack") {
|
||||||
locRoadCrack++
|
locRoadCrack++;
|
||||||
totalRoadCrack++
|
totalRoadCrack++;
|
||||||
} else if (type === "damaged_road_marking") {
|
} else if (type === "damaged_road_marking") {
|
||||||
locDamagedRoadMarking++
|
locDamagedRoadMarking++;
|
||||||
totalDamagedRoadMarking++
|
totalDamagedRoadMarking++;
|
||||||
} else if (type === "good_sign_board") {
|
} else if (type === "good_sign_board") {
|
||||||
locGoodSignboard++
|
locGoodSignboard++;
|
||||||
totalGoodSignboard++
|
totalGoodSignboard++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0 || locGoodSignboard > 0) {
|
if (
|
||||||
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
|
locDefectedSignboard > 0 ||
|
||||||
|
locPothole > 0 ||
|
||||||
|
locRoadCrack > 0 ||
|
||||||
|
locDamagedRoadMarking > 0 ||
|
||||||
|
locGoodSignboard > 0
|
||||||
|
) {
|
||||||
|
const shortName =
|
||||||
|
locName.length > 20 ? locName.substring(0, 20) + "..." : locName;
|
||||||
locationData.push({
|
locationData.push({
|
||||||
name: shortName,
|
name: shortName,
|
||||||
defected_sign_board: locDefectedSignboard,
|
defected_sign_board: locDefectedSignboard,
|
||||||
@@ -333,8 +395,13 @@ export default function DashboardPage() {
|
|||||||
road_crack: locRoadCrack,
|
road_crack: locRoadCrack,
|
||||||
damaged_road_marking: locDamagedRoadMarking,
|
damaged_road_marking: locDamagedRoadMarking,
|
||||||
good_sign_board: locGoodSignboard,
|
good_sign_board: locGoodSignboard,
|
||||||
total: locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking + locGoodSignboard
|
total:
|
||||||
})
|
locDefectedSignboard +
|
||||||
|
locPothole +
|
||||||
|
locRoadCrack +
|
||||||
|
locDamagedRoadMarking +
|
||||||
|
locGoodSignboard,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,102 +412,65 @@ export default function DashboardPage() {
|
|||||||
totalRoadCrack,
|
totalRoadCrack,
|
||||||
totalDamagedRoadMarking,
|
totalDamagedRoadMarking,
|
||||||
totalGoodSignboard,
|
totalGoodSignboard,
|
||||||
totalRoadDamage: totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking,
|
totalRoadDamage:
|
||||||
locationData
|
totalDefectedSignboard +
|
||||||
})
|
totalPothole +
|
||||||
}, [projectSummary, selectedPackageId, selectedLocationId])
|
totalRoadCrack +
|
||||||
|
totalDamagedRoadMarking,
|
||||||
|
locationData,
|
||||||
|
});
|
||||||
|
}, [projectSummary, selectedPackageId, selectedLocationId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
{/* Sidebar Navigation */}
|
{/* Sidebar Navigation */}
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
|
|
||||||
{/* Main Content - offset by sidebar width */}
|
{/* Main Content - offset by sidebar width */}
|
||||||
<main className="ml-16 min-h-screen">
|
<main className="ml-20 min-h-screen">
|
||||||
<div className="p-4 px-8 max-w-full mx-auto">
|
<div className="p-4 px-8 max-w-340 mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex items-center gap-5">
|
<div className="mb-8 flex items-center justify-between">
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center relative overflow-hidden group">
|
<PageHeader
|
||||||
{/* Constant orbit animations removed */}
|
title="Dashboard"
|
||||||
<div className="absolute inset-0 bg-white/20 opacity-50"></div>
|
description="A Comprehensive Overview Of Your Road Infrastructure Analysis"
|
||||||
<div className="absolute inset-0 border-2 border-white/30 rounded-2xl opacity-30"></div>
|
>
|
||||||
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-white relative z-10">
|
<svg
|
||||||
<path d="M50 20L85 80H15L50 20Z" stroke="currentColor" strokeWidth="6" strokeLinejoin="round" />
|
viewBox="0 0 100 100"
|
||||||
<path d="M40 80L50 55L60 80" stroke="currentColor" strokeWidth="6" />
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-8 w-8 text-white relative z-10"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M50 20L85 80H15L50 20Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="6"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M40 80L50 55L60 80"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="6"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</PageHeader>
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent">
|
|
||||||
VisionRoad Analytics Dashboard
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
A Comprehensive Overview Of Your Road Infrastructure Analysis
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Display */}
|
<Popover>
|
||||||
{error && (
|
<PopoverTrigger asChild>
|
||||||
<div className="mb-4 flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-sm">
|
<Button variant="outline" className="bg-white/70 backdrop-blur-md shadow-none rounded-md hover:bg-white/50 border-none transition-all duration-300 gap-2 font-semibold">
|
||||||
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
<Filter className="h-4 w-4" />
|
||||||
<p>{error}</p>
|
<span>Filters</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[320px] p-0 border-none shadow-2xl rounded-md overflow-hidden" align="end">
|
||||||
|
<div className="bg-linear-to-b from-[#225999] to-[#56A5FF] p-4 text-white">
|
||||||
|
<h3 className="font-bold text-sm flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
Filter Analysis
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-blue-100 mt-1 opacity-80">Refine your view by project, package, or location</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="p-1 bg-white">
|
||||||
|
|
||||||
{/* Stats Cards - Top Row */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
||||||
<GradientStatsCard
|
|
||||||
title="Total Road Damage"
|
|
||||||
subtitle="Combined damage detections"
|
|
||||||
value={stats.totalRoadDamage}
|
|
||||||
icon={TrendingUp}
|
|
||||||
gradient="purple"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<GradientStatsCard
|
|
||||||
title="Potholes"
|
|
||||||
subtitle="Surface depressions"
|
|
||||||
value={stats.totalPothole}
|
|
||||||
icon={AlertTriangle}
|
|
||||||
gradient="green"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<GradientStatsCard
|
|
||||||
title="Defected Signboards"
|
|
||||||
subtitle="Damaged traffic signs"
|
|
||||||
value={stats.totalDefectedSignboard}
|
|
||||||
icon={RectangleHorizontal}
|
|
||||||
gradient="blue"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<GradientStatsCard
|
|
||||||
title="Road Cracks"
|
|
||||||
subtitle="Surface fissures"
|
|
||||||
value={stats.totalRoadCrack}
|
|
||||||
icon={AlertTriangle}
|
|
||||||
gradient="orange"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<GradientStatsCard
|
|
||||||
title="Damaged Markings"
|
|
||||||
subtitle="Worn road lines"
|
|
||||||
value={stats.totalDamagedRoadMarking}
|
|
||||||
icon={TrendingUp}
|
|
||||||
gradient="indigo"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<GradientStatsCard
|
|
||||||
title="Good Signboards"
|
|
||||||
subtitle="Informational markers"
|
|
||||||
value={stats.totalGoodSignboard}
|
|
||||||
icon={RectangleHorizontal}
|
|
||||||
gradient="emerald"
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Selector - Above main content */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<FilterSelector
|
<FilterSelector
|
||||||
projects={projects}
|
projects={projects}
|
||||||
selectedProjectId={selectedProjectId}
|
selectedProjectId={selectedProjectId}
|
||||||
@@ -454,17 +484,78 @@ export default function DashboardPage() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-sm">
|
||||||
|
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && !projectSummary ? (
|
||||||
|
<DashboardSkeleton />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Stats Cards - Top Row */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Total Road Damage"
|
||||||
|
subtitle="Combined damage detections"
|
||||||
|
value={stats.totalRoadDamage}
|
||||||
|
icon={Activity}
|
||||||
|
gradient="purple"
|
||||||
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Potholes"
|
||||||
|
subtitle="Surface depressions"
|
||||||
|
value={stats.totalPothole}
|
||||||
|
icon={AlertCircle}
|
||||||
|
gradient="green"
|
||||||
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Defected Signboards"
|
||||||
|
subtitle="Damaged traffic signs"
|
||||||
|
value={stats.totalDefectedSignboard}
|
||||||
|
icon={AlertTriangle}
|
||||||
|
gradient="blue"
|
||||||
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Road Cracks"
|
||||||
|
subtitle="Surface fissures"
|
||||||
|
value={stats.totalRoadCrack}
|
||||||
|
icon={Zap}
|
||||||
|
gradient="orange"
|
||||||
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Damaged Markings"
|
||||||
|
subtitle="Worn road lines"
|
||||||
|
value={stats.totalDamagedRoadMarking}
|
||||||
|
icon={PencilLine}
|
||||||
|
gradient="indigo"
|
||||||
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Good Signboards"
|
||||||
|
subtitle="Informational markers"
|
||||||
|
value={stats.totalGoodSignboard}
|
||||||
|
icon={CheckCircle2}
|
||||||
|
gradient="emerald"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Charts Row - Side by Side */}
|
{/* Charts Row - Side by Side */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||||
{/* Left Chart - Detection Distribution */}
|
{/* Left Chart - Detection Distribution */}
|
||||||
<Card className="rounded-xl overflow-hidden">
|
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden">
|
||||||
<CardHeader className="pb-2 border-b border-[var(--border)]">
|
<CardHeader className="pb-2 border-none">
|
||||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||||
<div className="p-1.5 rounded-lg bg-[#2563eb]/10 shadow-sm">
|
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
|
||||||
<BarChart3 className="h-4 w-4 text-[#2563eb]" />
|
<BarChart3 className="h-5 w-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[#2563eb]">
|
<span className="heading-blue-gradient text-xl">
|
||||||
Detection Distribution
|
Detection Distribution
|
||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
@@ -476,35 +567,31 @@ export default function DashboardPage() {
|
|||||||
roadCrack={stats.totalRoadCrack}
|
roadCrack={stats.totalRoadCrack}
|
||||||
damagedRoadMarking={stats.totalDamagedRoadMarking}
|
damagedRoadMarking={stats.totalDamagedRoadMarking}
|
||||||
goodSignboard={stats.totalGoodSignboard}
|
goodSignboard={stats.totalGoodSignboard}
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Right Chart - Location Bar Chart */}
|
{/* Right Chart - Location Bar Chart */}
|
||||||
<Card className="rounded-xl overflow-hidden">
|
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden">
|
||||||
<CardHeader className="pb-2 border-b border-[var(--border)]">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||||
<div className="p-1.5 rounded-lg bg-[#2563eb]/10 shadow-sm">
|
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
|
||||||
<MapPinIcon className="h-4 w-4 text-[#2563eb]" />
|
<MapPinIcon className="h-5 w-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[#2563eb]">
|
<span className="heading-blue-gradient text-xl">Detections by Location</span>
|
||||||
Detections by Location
|
|
||||||
</span>
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<LocationBarChart
|
<LocationBarChart
|
||||||
data={stats.locationData.map(loc => ({
|
data={stats.locationData.map((loc) => ({
|
||||||
name: loc.name,
|
name: loc.name,
|
||||||
defected_sign_board: loc.defected_sign_board,
|
defected_sign_board: loc.defected_sign_board,
|
||||||
pothole: loc.pothole,
|
pothole: loc.pothole,
|
||||||
road_crack: loc.road_crack,
|
road_crack: loc.road_crack,
|
||||||
damaged_road_marking: loc.damaged_road_marking,
|
damaged_road_marking: loc.damaged_road_marking,
|
||||||
good_sign_board: loc.good_sign_board,
|
good_sign_board: loc.good_sign_board,
|
||||||
total: loc.total
|
total: loc.total,
|
||||||
}))}
|
}))}
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -519,6 +606,8 @@ export default function DashboardPage() {
|
|||||||
projectSummary={projectSummary}
|
projectSummary={projectSummary}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
@@ -529,5 +618,5 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
@import 'tw-animate-css';
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@@ -9,11 +9,12 @@
|
|||||||
--primary-foreground: oklch(0.98 0.01 264);
|
--primary-foreground: oklch(0.98 0.01 264);
|
||||||
|
|
||||||
/* Background & Surfaces */
|
/* Background & Surfaces */
|
||||||
--background: #f8fafc;
|
--background: hsla(210, 40%, 98%, 0.2);
|
||||||
--foreground: oklch(0.15 0.02 280);
|
--foreground: oklch(0.15 0.02 280);
|
||||||
--card: oklch(1 0 0);
|
--dialog: white;
|
||||||
|
--card: white;
|
||||||
--card-foreground: oklch(0.15 0.02 280);
|
--card-foreground: oklch(0.15 0.02 280);
|
||||||
--popover: oklch(1 0 0);
|
--popover: white;
|
||||||
--popover-foreground: oklch(0.15 0.02 280);
|
--popover-foreground: oklch(0.15 0.02 280);
|
||||||
|
|
||||||
/* Secondary */
|
/* Secondary */
|
||||||
@@ -43,10 +44,10 @@
|
|||||||
|
|
||||||
/* Chart Colors - Vibrant Palette */
|
/* Chart Colors - Vibrant Palette */
|
||||||
--chart-1: oklch(0.55 0.24 264);
|
--chart-1: oklch(0.55 0.24 264);
|
||||||
--chart-2: oklch(0.65 0.20 200);
|
--chart-2: oklch(0.65 0.2 200);
|
||||||
--chart-3: oklch(0.60 0.18 160);
|
--chart-3: oklch(0.6 0.18 160);
|
||||||
--chart-4: oklch(0.70 0.20 85);
|
--chart-4: oklch(0.7 0.2 85);
|
||||||
--chart-5: oklch(0.60 0.22 320);
|
--chart-5: oklch(0.6 0.22 320);
|
||||||
|
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
@@ -63,23 +64,23 @@
|
|||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
/* Primary - Light Indigo */
|
/* 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);
|
--primary-foreground: oklch(0.15 0.02 264);
|
||||||
|
--dialog: white;
|
||||||
/* Background & Surfaces */
|
/* Background & Surfaces */
|
||||||
--background: oklch(0.12 0.02 280);
|
--background: hsla(280, 20%, 12%, 0.3);
|
||||||
--foreground: oklch(0.95 0.01 280);
|
--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);
|
--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);
|
--popover-foreground: oklch(0.95 0.01 280);
|
||||||
|
|
||||||
/* Secondary */
|
/* Secondary */
|
||||||
--secondary: oklch(0.22 0.03 264);
|
--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 */
|
||||||
--muted: oklch(0.20 0.02 280);
|
--muted: oklch(0.2 0.02 280);
|
||||||
--muted-foreground: oklch(0.65 0.02 280);
|
--muted-foreground: oklch(0.65 0.02 280);
|
||||||
|
|
||||||
/* Accent */
|
/* Accent */
|
||||||
@@ -88,19 +89,19 @@
|
|||||||
|
|
||||||
/* Destructive */
|
/* Destructive */
|
||||||
--destructive: oklch(0.45 0.18 25);
|
--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 */
|
/* Borders & Inputs */
|
||||||
--border: oklch(0.25 0.02 280);
|
--border: oklch(0.25 0.02 280);
|
||||||
--input: oklch(0.22 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 Colors */
|
||||||
--chart-1: oklch(0.65 0.22 264);
|
--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-3: oklch(0.65 0.16 160);
|
||||||
--chart-4: oklch(0.75 0.18 85);
|
--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 */
|
||||||
--sidebar: oklch(0.14 0.02 280);
|
--sidebar: oklch(0.14 0.02 280);
|
||||||
@@ -108,17 +109,18 @@
|
|||||||
--sidebar-primary: oklch(0.65 0.22 264);
|
--sidebar-primary: oklch(0.65 0.22 264);
|
||||||
--sidebar-primary-foreground: oklch(0.95 0.01 264);
|
--sidebar-primary-foreground: oklch(0.95 0.01 264);
|
||||||
--sidebar-accent: oklch(0.22 0.03 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-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 {
|
@theme inline {
|
||||||
--font-sans: 'Geist', 'Geist Fallback', system-ui, sans-serif;
|
--font-sans: "Geist", "Geist Fallback", system-ui, sans-serif;
|
||||||
--font-mono: 'Geist Mono', 'Geist Mono Fallback', monospace;
|
--font-mono: "Geist Mono", "Geist Mono Fallback", monospace;
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
|
--color-dialog: var(--dialog);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
@@ -161,7 +163,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
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 {
|
html {
|
||||||
scrollbar-gutter: stable;
|
/* scrollbar-gutter: stable; */
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +208,18 @@ body[data-scroll-locked] {
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.static-immediate {
|
.heading-blue-gradient {
|
||||||
transition: none !important;
|
@apply bg-linear-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent;
|
||||||
animation: none !important;
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
@@ -5,16 +5,16 @@ import { DashboardMap } from "@/components/dashboard/dashboard-map"
|
|||||||
|
|
||||||
export default function MapPage() {
|
export default function MapPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient relative overflow-hidden">
|
<div className="min-h-screen relative overflow-hidden ml-20">
|
||||||
{/* Navigation Menu */}
|
{/* Navigation Menu */}
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-8 max-w-7xl relative z-10">
|
<div className="container mx-auto px-4 py-8 max-w-340 relative z-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-gradient leading-tight">
|
<h1 className="text-3xl md:text-4xl font-bold heading-blue-gradient leading-tight">
|
||||||
Detection Map
|
Detection Map
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ProjectSelectionSection } from "@/components/project-selection-section"
|
|||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
import { type SessionContext, saveSession } from "@/lib/api"
|
import { type SessionContext, saveSession } from "@/lib/api"
|
||||||
import { TrendingUp } from "lucide-react"
|
import { TrendingUp } from "lucide-react"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
|
|
||||||
export default function NewAnalysisPage() {
|
export default function NewAnalysisPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -16,24 +17,21 @@ export default function NewAnalysisPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
{/* Sidebar Navigation */}
|
{/* Sidebar Navigation */}
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="ml-16 min-h-screen relative overflow-hidden">
|
<main className="ml-20 min-h-screen relative overflow-hidden">
|
||||||
|
|
||||||
<div className="container mx-auto px-6 py-10 max-w-7xl relative z-10">
|
<div className="container mx-auto px-6 py-10 max-w-340 relative z-10">
|
||||||
{/* Refined Left-Aligned Header */}
|
{/* Refined Left-Aligned Header */}
|
||||||
<div className="mb-8 flex items-center gap-5">
|
<div className="mb-8">
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
<PageHeader
|
||||||
<TrendingUp className="h-8 w-8 text-white" />
|
title="VisionRoad Detection System"
|
||||||
</div>
|
description="Select project details to begin your AI-powered road infrastructure analysis"
|
||||||
<div>
|
icon={TrendingUp}
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
|
/>
|
||||||
VisionRoad Detection System
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Project Selection Section */}
|
{/* Project Selection Section */}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Loader2, TrendingUp } from "lucide-react"
|
import { Loader2, TrendingUp } from "lucide-react"
|
||||||
import VideoPlayerSection from "@/components/video-player-section"
|
import VideoPlayerSection from "@/components/video-player-section"
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
import {
|
import {
|
||||||
type SessionContext,
|
type SessionContext,
|
||||||
loadSession,
|
loadSession,
|
||||||
@@ -91,7 +92,7 @@ export default function VideoResultsPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
||||||
<p className="text-gray-500">Loading detection results...</p>
|
<p className="text-gray-500">Loading detection results...</p>
|
||||||
@@ -102,7 +103,7 @@ export default function VideoResultsPage() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg text-center">
|
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg text-center">
|
||||||
<p className="text-red-500 font-medium">{error}</p>
|
<p className="text-red-500 font-medium">{error}</p>
|
||||||
<div className="flex gap-4 mt-4">
|
<div className="flex gap-4 mt-4">
|
||||||
@@ -115,22 +116,16 @@ export default function VideoResultsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
<main className="ml-16 min-h-screen">
|
<main className="ml-20 min-h-screen">
|
||||||
<div className="container mx-auto px-6 py-8 max-w-full">
|
<div className="container mx-auto px-6 py-8 max-w-full">
|
||||||
<div className="mb-8 flex items-center gap-5">
|
<div className="mb-8">
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
<PageHeader
|
||||||
<TrendingUp className="h-8 w-8 text-white" />
|
title={getTitle()}
|
||||||
</div>
|
description={`Video ID: ${videoId}`}
|
||||||
<div>
|
icon={TrendingUp}
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
|
/>
|
||||||
{getTitle()}
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium">
|
|
||||||
Video ID: {videoId}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{session && (
|
{session && (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Loader2, ArrowLeft, MapPin, Package, FolderKanban, RotateCcw, TrendingUp } from "lucide-react"
|
import { Loader2, ArrowLeft, MapPin, Package, FolderKanban, RotateCcw, TrendingUp } from "lucide-react"
|
||||||
import VideoPlayerSection from "@/components/video-player-section"
|
import VideoPlayerSection from "@/components/video-player-section"
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
import {
|
import {
|
||||||
type SessionContext,
|
type SessionContext,
|
||||||
loadSession,
|
loadSession,
|
||||||
@@ -73,7 +74,7 @@ export default function ResultsPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
||||||
<p className="text-gray-500">Loading detection results...</p>
|
<p className="text-gray-500">Loading detection results...</p>
|
||||||
@@ -84,7 +85,7 @@ export default function ResultsPage() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||||
<p className="text-red-500">{error}</p>
|
<p className="text-red-500">{error}</p>
|
||||||
<Button onClick={handleNewAnalysis} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white">Start New Analysis</Button>
|
<Button onClick={handleNewAnalysis} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white">Start New Analysis</Button>
|
||||||
@@ -94,26 +95,20 @@ export default function ResultsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
{/* Sidebar Navigation */}
|
{/* Sidebar Navigation */}
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="ml-16 min-h-screen">
|
<main className="ml-20 min-h-screen">
|
||||||
<div className="container mx-auto px-6 py-8 max-w-full">
|
<div className="container mx-auto px-6 py-8 max-w-full">
|
||||||
{/* Refined Header */}
|
{/* Refined Header */}
|
||||||
<div className="mb-8 flex items-center gap-5">
|
<div className="mb-8">
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
<PageHeader
|
||||||
<TrendingUp className="h-8 w-8 text-white" />
|
title={getTitle()}
|
||||||
</div>
|
description="View your AI-powered road analysis results"
|
||||||
<div>
|
icon={TrendingUp}
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
|
/>
|
||||||
{getTitle()}
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium">
|
|
||||||
View your AI-powered road analysis results
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Session Info Bar */}
|
{/* Session Info Bar */}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter, useParams } from "next/navigation"
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Loader2, TrendingUp } from "lucide-react"
|
import { Loader2, TrendingUp } from "lucide-react"
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
import {
|
import {
|
||||||
type SessionContext,
|
type SessionContext,
|
||||||
loadSession,
|
loadSession,
|
||||||
@@ -119,7 +120,7 @@ export default function VideoProcessingPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
||||||
</div>
|
</div>
|
||||||
@@ -128,19 +129,16 @@ export default function VideoProcessingPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
<main className="ml-16 min-h-screen relative overflow-hidden flex flex-col">
|
<main className="ml-20 min-h-screen relative overflow-hidden flex flex-col">
|
||||||
<div className="flex-1 container mx-auto px-6 py-6 max-w-7xl relative z-10 flex flex-col">
|
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 relative z-10 flex flex-col">
|
||||||
<div className="mb-6 flex items-center gap-5">
|
<div className="mb-6">
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
<PageHeader
|
||||||
<TrendingUp className="h-8 w-8 text-white" />
|
title="Processing Analysis"
|
||||||
</div>
|
description={`Real-time analysis progress for video ID: ${videoId}`}
|
||||||
<div>
|
icon={TrendingUp}
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
|
/>
|
||||||
Processing Analysis
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{session && (
|
{session && (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label"
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Loader2, TrendingUp } from "lucide-react"
|
import { Loader2, TrendingUp } from "lucide-react"
|
||||||
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
import { SidebarNavigation } from "@/components/sidebar-navigation"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
import {
|
import {
|
||||||
type SessionContext,
|
type SessionContext,
|
||||||
loadSession,
|
loadSession,
|
||||||
@@ -131,7 +132,7 @@ export default function UploadPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
<div className="p-8 rounded-2xl bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl shadow-lg">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
<Loader2 className="h-8 w-8 animate-spin text-indigo-500" />
|
||||||
</div>
|
</div>
|
||||||
@@ -140,24 +141,21 @@ export default function UploadPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-mesh-gradient text-gray-900 dark:text-gray-100">
|
<div className="min-h-screen text-gray-900 dark:text-gray-100">
|
||||||
{/* Sidebar Navigation */}
|
{/* Sidebar Navigation */}
|
||||||
<SidebarNavigation />
|
<SidebarNavigation />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="ml-16 min-h-screen relative overflow-hidden flex flex-col">
|
<main className="ml-20 min-h-screen relative overflow-hidden flex flex-col">
|
||||||
|
|
||||||
<div className="flex-1 container mx-auto px-6 py-6 max-w-7xl relative z-10 flex flex-col">
|
<div className="flex-1 container mx-auto px-6 py-6 max-w-340 relative z-10 flex flex-col">
|
||||||
{/* Refined Header */}
|
{/* Refined Header */}
|
||||||
<div className="mb-6 flex items-center gap-5">
|
<div className="mb-6">
|
||||||
<div className="p-3 rounded-2xl bg-gradient-to-br from-[#9bddeb] to-[#60a5fa] shadow-md flex items-center justify-center">
|
<PageHeader
|
||||||
<TrendingUp className="h-8 w-8 text-white" />
|
title={getTitle()}
|
||||||
</div>
|
description="Upload video file and fill in required details to start the road analysis"
|
||||||
<div>
|
icon={TrendingUp}
|
||||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-gray-900 via-indigo-800 to-indigo-600 dark:from-white dark:via-indigo-200 dark:to-indigo-400 bg-clip-text text-transparent leading-tight">
|
/>
|
||||||
{getTitle()}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compact Session Info Bar */}
|
{/* Compact Session Info Bar */}
|
||||||
|
|||||||
@@ -89,12 +89,12 @@ export function DashboardMap({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`overflow-hidden ${className}`}>
|
<Card className={`overflow-hidden pb-0 bg-white/60 rounded-b-none shadow-none border-none ${className}`}>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center shadow-lg border-2 border-[#1e40af]">
|
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
|
||||||
<MapPin className="h-5 w-5 text-[#2563eb]" />
|
<MapPin className="h-5 w-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base font-bold text-[#2563eb]">
|
<CardTitle className="text-base font-bold text-[#2563eb]">
|
||||||
@@ -132,7 +132,7 @@ export function DashboardMap({
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="h-[400px] w-full relative">
|
<div className="h-[500px] p-2 w-full relative">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||||
<Loader2 className="h-8 w-8 text-indigo-500" />
|
<Loader2 className="h-8 w-8 text-indigo-500" />
|
||||||
|
|||||||
59
components/dashboard/dashboard-skeleton.tsx
Normal file
59
components/dashboard/dashboard-skeleton.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"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-3 gap-4 mb-6">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<Card key={i} className="bg-white/60 backdrop-blur-lg border-none shadow-none overflow-hidden py-8 px-6">
|
||||||
|
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||||
|
<div className="flex flex-col gap-2 flex-1">
|
||||||
|
<Skeleton className="h-4 w-32" />
|
||||||
|
<Skeleton className="h-10 w-20" />
|
||||||
|
<Skeleton className="h-3 w-40" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="w-16 h-16 rounded-lg shrink-0" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Row Skeleton */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||||
|
{[...Array(2)].map((_, i) => (
|
||||||
|
<Card key={i} className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||||
|
<CardHeader className="pb-2 border-none">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Skeleton className="w-10 h-10 rounded-md" />
|
||||||
|
<Skeleton className="h-6 w-48" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-4 h-[350px] flex items-center justify-center">
|
||||||
|
<Skeleton className="h-[300px] w-full max-w-[300px] rounded-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map Skeleton */}
|
||||||
|
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Skeleton className="w-10 h-10 rounded-md" />
|
||||||
|
<Skeleton className="h-6 w-48" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-8 w-32 rounded-full" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-2">
|
||||||
|
<Skeleton className="h-[500px] w-full rounded-md" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ interface DetectionDonutChartProps {
|
|||||||
roadCrack: number
|
roadCrack: number
|
||||||
damagedRoadMarking: number
|
damagedRoadMarking: number
|
||||||
goodSignboard: number
|
goodSignboard: number
|
||||||
isLoading: boolean
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
@@ -26,7 +26,7 @@ export function DetectionDonutChart({
|
|||||||
roadCrack,
|
roadCrack,
|
||||||
damagedRoadMarking,
|
damagedRoadMarking,
|
||||||
goodSignboard,
|
goodSignboard,
|
||||||
isLoading
|
isLoading = false
|
||||||
}: DetectionDonutChartProps) {
|
}: DetectionDonutChartProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,23 +36,26 @@ export function FilterSelector({
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
}: FilterSelectorProps) {
|
}: FilterSelectorProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-3 p-3 rounded-xl bg-card border border-[var(--border)] shadow-sm">
|
<div className="flex flex-col gap-4 p-4">
|
||||||
{/* Project Dropdown */}
|
{/* Project Dropdown */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="space-y-2">
|
||||||
<div className="p-1.5 rounded-lg bg-blue-100 dark:bg-blue-900/50">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<FolderOpen className="h-4 w-4 text-indigo-500" />
|
<div className="p-1 rounded-md bg-blue-100 dark:bg-blue-900/50">
|
||||||
|
<FolderOpen className="h-3.5 w-3.5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Project</span>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={selectedProjectId || ""}
|
value={selectedProjectId || ""}
|
||||||
onValueChange={onProjectChange}
|
onValueChange={onProjectChange}
|
||||||
disabled={isLoading || projects.length === 0}
|
disabled={isLoading || projects.length === 0}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
|
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||||
<SelectValue placeholder="Select project" />
|
<SelectValue placeholder="Select project" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="rounded-xl border-slate-200">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<SelectItem key={project.id} value={project.id}>
|
<SelectItem key={project.id} value={project.id} className="rounded-lg">
|
||||||
{project.name}
|
{project.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -60,29 +63,27 @@ export function FilterSelector({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
{selectedProjectId && packages.length > 0 && (
|
|
||||||
<div className="h-5 w-px bg-gray-300 dark:bg-gray-600" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Package Dropdown */}
|
{/* Package Dropdown */}
|
||||||
{selectedProjectId && packages.length > 0 && (
|
{selectedProjectId && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="space-y-2">
|
||||||
<div className="p-1.5 rounded-lg bg-emerald-100 dark:bg-emerald-900/50">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Package className="h-4 w-4 text-emerald-500" />
|
<div className="p-1 rounded-md bg-emerald-100 dark:bg-emerald-900/50">
|
||||||
|
<Package className="h-3.5 w-3.5 text-emerald-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Package</span>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={selectedPackageId || "all"}
|
value={selectedPackageId || "all"}
|
||||||
onValueChange={onPackageChange}
|
onValueChange={onPackageChange}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
|
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||||
<SelectValue placeholder="All packages" />
|
<SelectValue placeholder="All packages" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="rounded-xl border-slate-200">
|
||||||
<SelectItem value="all">All Packages</SelectItem>
|
<SelectItem value="all" className="rounded-lg">All Packages</SelectItem>
|
||||||
{packages.map((pkg) => (
|
{packages.map((pkg) => (
|
||||||
<SelectItem key={pkg.id} value={pkg.id}>
|
<SelectItem key={pkg.id} value={pkg.id} className="rounded-lg">
|
||||||
{pkg.name}
|
{pkg.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -91,29 +92,27 @@ export function FilterSelector({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
{selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && (
|
|
||||||
<div className="h-5 w-px bg-gray-300 dark:bg-gray-600" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Location Dropdown */}
|
{/* Location Dropdown */}
|
||||||
{selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && (
|
{selectedPackageId && selectedPackageId !== "all" && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="space-y-2">
|
||||||
<div className="p-1.5 rounded-lg bg-purple-100 dark:bg-purple-900/50">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<MapPin className="h-4 w-4 text-purple-500" />
|
<div className="p-1 rounded-md bg-purple-100 dark:bg-purple-900/50">
|
||||||
|
<MapPin className="h-3.5 w-3.5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Location</span>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={selectedLocationId || "all"}
|
value={selectedLocationId || "all"}
|
||||||
onValueChange={onLocationChange}
|
onValueChange={onLocationChange}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
|
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||||
<SelectValue placeholder="All locations" />
|
<SelectValue placeholder="All locations" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="rounded-xl border-slate-200">
|
||||||
<SelectItem value="all">All Locations</SelectItem>
|
<SelectItem value="all" className="rounded-lg">All Locations</SelectItem>
|
||||||
{locations.map((loc) => (
|
{locations.map((loc) => (
|
||||||
<SelectItem key={loc.id} value={loc.id}>
|
<SelectItem key={loc.id} value={loc.id} className="rounded-lg">
|
||||||
{loc.name}
|
{loc.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -122,5 +121,5 @@ export function FilterSelector({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,43 +17,43 @@ const gradientStyles = {
|
|||||||
background: "bg-card",
|
background: "bg-card",
|
||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
|
||||||
},
|
},
|
||||||
coral: {
|
coral: {
|
||||||
background: "bg-card",
|
background: "bg-card",
|
||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
background: "bg-card",
|
background: "bg-card",
|
||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
|
||||||
},
|
},
|
||||||
purple: {
|
purple: {
|
||||||
background: "bg-card",
|
background: "bg-card",
|
||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
|
||||||
},
|
},
|
||||||
orange: {
|
orange: {
|
||||||
background: "bg-card",
|
background: "bg-card",
|
||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
|
||||||
},
|
},
|
||||||
indigo: {
|
indigo: {
|
||||||
background: "bg-card",
|
background: "bg-card",
|
||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
|
||||||
},
|
},
|
||||||
emerald: {
|
emerald: {
|
||||||
background: "bg-card",
|
background: "bg-card",
|
||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
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]
|
const styles = gradientStyles[gradient]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900 border-none shadow-xl shadow-blue-500/5 overflow-hidden py-8 px-6">
|
<Card className="bg-white/60 backdrop-blur-lg border-none shadow-none hover:shadow-none overflow-hidden py-8 px-6">
|
||||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<h3 className="text-sm font-bold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-widest">
|
<h3 className="text-sm font-bold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-widest">
|
||||||
@@ -79,11 +79,11 @@ export function GradientStatsCard({
|
|||||||
<div className="h-14 w-24 bg-gray-100 dark:bg-gray-800 rounded-xl mt-2" />
|
<div className="h-14 w-24 bg-gray-100 dark:bg-gray-800 rounded-xl mt-2" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-3xl md:text-4xl font-black tracking-tighter leading-tight text-[#1e3a8a]">
|
<p className="text-4xl font-bold heading-blue-gradient">
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mt-1 opacity-80 italic">
|
<p className="text-xs font-medium text-gray-500 mt-1">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -92,9 +92,12 @@ export function GradientStatsCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`
|
<div
|
||||||
w-20 h-20 rounded-3xl flex items-center justify-center flex-shrink-0
|
style={{
|
||||||
${styles.iconBg} shadow-[0_10px_30px_rgba(37,99,235,0.2)]
|
background: "linear-gradient(180deg, #225999 0%, #56A5FF 100%)",
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
w-16 h-16 self-start rounded-lg flex items-center justify-center
|
||||||
`}>
|
`}>
|
||||||
<Icon className="h-10 w-10 text-white" />
|
<Icon className="h-10 w-10 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface LocationData {
|
|||||||
|
|
||||||
interface LocationBarChartProps {
|
interface LocationBarChartProps {
|
||||||
data: LocationData[]
|
data: LocationData[]
|
||||||
isLoading: boolean
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
@@ -26,7 +26,7 @@ const COLORS = {
|
|||||||
good_sign_board: "#34d399" // Lighter Emerald
|
good_sign_board: "#34d399" // Lighter Emerald
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-[300px] flex items-center justify-center">
|
<div className="h-[300px] flex items-center justify-center">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Eye, AlertCircle } from "lucide-react"
|
import { Eye, AlertCircle } from "lucide-react"
|
||||||
import { type Video } from "@/lib/api"
|
import { type Video } from "@/lib/api"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
|
||||||
interface RecentAnalysesTableProps {
|
interface RecentAnalysesTableProps {
|
||||||
videos: Video[]
|
videos: Video[]
|
||||||
@@ -56,9 +57,19 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="p-6 space-y-3">
|
<div className="p-6 space-y-4">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
<div key={i} className="h-14 bg-primary/5 rounded-lg" />
|
<div key={i} className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Skeleton className="h-4 w-[60%]" />
|
||||||
|
<Skeleton className="h-3 w-[40%]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Skeleton className="h-6 w-16 rounded-full" />
|
||||||
|
<Skeleton className="h-6 w-16 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-8 w-16" />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : videos.length === 0 ? (
|
) : videos.length === 0 ? (
|
||||||
@@ -83,11 +94,11 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent
|
|||||||
{formatDate(video.created_at)}
|
{formatDate(video.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{getDetectionTypeBadge(video.detection_type)}
|
{getDetectionTypeBadge(video.detection_type)}
|
||||||
{getStatusBadge(video.status)}
|
{getStatusBadge(video.status)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right flex-shrink-0 w-20">
|
<div className="text-right shrink-0 w-20">
|
||||||
<p className="text-sm font-bold text-foreground">
|
<p className="text-sm font-bold text-foreground">
|
||||||
{video.detection_type === "pothole-detection"
|
{video.detection_type === "pothole-detection"
|
||||||
? video.unique_pothole ?? 0
|
? video.unique_pothole ?? 0
|
||||||
@@ -103,7 +114,7 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onViewResults(video.id, video.detection_type)}
|
onClick={() => onViewResults(video.id, video.detection_type)}
|
||||||
className="ml-4 flex-shrink-0"
|
className="ml-4 shrink-0"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4 mr-1" />
|
<Eye className="h-4 w-4 mr-1" />
|
||||||
View
|
View
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
|
||||||
interface Column<T> {
|
interface Column<T> {
|
||||||
key: string
|
key: string
|
||||||
@@ -42,51 +43,62 @@ export function DataTable<T extends Record<string, any>>({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Header with Title and Add Button */}
|
{/* Header with Title and Add Button */}
|
||||||
<div className="mb-4 flex justify-between items-center bg-card p-3 rounded-xl border border-[var(--border)] shadow-sm">
|
<div className="flex justify-between items-center p-3 ">
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<h2 className="text-lg font-bold tracking-tight text-blue-600">{title}</h2>
|
<h2 className="text-2xl font-bold tracking-tight heading-blue-gradient">{title}</h2>
|
||||||
<p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase font-medium">Total items: {data.length}</p>
|
<span className="badge-blue min-w-[28px] h-[28px] px-2">
|
||||||
|
{data.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={onAddNew}
|
onClick={onAddNew}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white shadow-lg shadow-blue-500/25 transition-all duration-300 hover:scale-105 active:scale-95 border-none h-8 px-4 text-xs font-semibold"
|
className="btn-blue-gradient rounded-full text-sm px-4 py-2.5 h-auto flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1.5 stroke-[3px]" />
|
<Plus className="h-4 w-4" />
|
||||||
{addButtonText}
|
{addButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="rounded-2xl border border-[var(--border)] bg-card shadow-2xl shadow-black/5 overflow-hidden">
|
<div className="rounded-lg overflow-hidden">
|
||||||
<div className="overflow-auto max-h-[400px]">
|
<div className="overflow-auto max-h-[450px]">
|
||||||
<table className="min-w-full divide-y divide-gray-200/50 dark:divide-gray-700/50 border-collapse">
|
<table className="min-w-full rounded-lg!">
|
||||||
<thead className="bg-[#f8fafc] dark:bg-gray-900/50 sticky top-0 z-10">
|
<thead className="bg-white/40 backdrop-blur-md rounded-lg! sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className="px-4 py-3 text-left text-[10px] font-black text-gray-500 dark:text-gray-400 uppercase tracking-[0.1em] border-b border-[var(--border)] bg-[#f8fafc] dark:bg-gray-900 shadow-[0_1px_0_0_rgba(0,0,0,0.05)]"
|
className="px-4 py-3 text-slate-500 text-left font-bold"
|
||||||
>
|
>
|
||||||
{col.header}
|
{col.header}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<th className="px-4 py-3 text-right text-[10px] font-black text-gray-500 dark:text-gray-400 uppercase tracking-[0.1em] border-b border-[var(--border)] bg-[#f8fafc] dark:bg-gray-900 shadow-[0_1px_0_0_rgba(0,0,0,0.05)]">
|
<th className="px-4 py-3 text-slate-500 text-left font-bold">
|
||||||
Actions
|
Actions
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100/30 dark:divide-gray-800/30">
|
<tbody className="divide-y divide-gray-300 backdrop-blur-lg bg-white/70">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<tr>
|
Array.from({ length: 5 }).map((_, idx) => (
|
||||||
<td colSpan={totalCols} className="px-4 py-12 text-center">
|
<tr key={idx} className="border-b border-gray-100 dark:border-gray-800">
|
||||||
<div className="flex flex-col items-center justify-center gap-3">
|
{columns.map((col) => (
|
||||||
<div className="h-8 w-8 border-3 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
|
<td key={col.key} className="px-4 py-4">
|
||||||
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">Loading records...</span>
|
<Skeleton className="h-4 w-full max-w-[120px]" />
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
{showActions && (
|
||||||
|
<td className="px-4 py-4 text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Skeleton className="h-8 w-8 rounded-sm" />
|
||||||
|
<Skeleton className="h-8 w-8 rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
))
|
||||||
) : data.length === 0 ? (
|
) : data.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={totalCols} className="px-4 py-12 text-center">
|
<td colSpan={totalCols} className="px-4 py-12 text-center">
|
||||||
@@ -121,7 +133,7 @@ export function DataTable<T extends Record<string, any>>({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => onEdit(item)}
|
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"
|
||||||
>
|
>
|
||||||
<Edit2 className="h-4 w-4" />
|
<Edit2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -138,7 +150,7 @@ export function DataTable<T extends Record<string, any>>({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => onDelete(item)}
|
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"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -114,12 +114,12 @@ export function NavigationMenu() {
|
|||||||
: "hover:bg-slate-50 border border-transparent hover:border-slate-100"
|
: "hover:bg-slate-50 border border-transparent hover:border-slate-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`p-2.5 rounded-lg transition-all duration-300 border shadow-sm ${isActive
|
<div className={`p-2.5 rounded-lg transition-all duration-300 ${isActive
|
||||||
? "bg-[#2563eb] text-white border-[#2563eb]"
|
? "bg-linear-to-b from-[#3895FF] to-[#86B8F1] text-white"
|
||||||
: "bg-[#f0fafd] text-slate-900 group-hover:bg-[#2563eb] group-hover:text-white group-hover:border-[#2563eb] border-slate-100"
|
: "bg-[#f0fafd] text-slate-900 group-hover:bg-linear-to-b group-hover:from-[#3895FF] group-hover:to-[#86B8F1] group-hover:text-white border border-slate-100 shadow-sm"
|
||||||
}`}>
|
}`}>
|
||||||
<Icon
|
<Icon
|
||||||
className={`h-5 w-5 ${isActive ? 'stroke-[2.5]' : 'stroke-[2]'}`}
|
className={`h-5 w-5 ${isActive ? 'stroke-[2.5]' : 'stroke-2'}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
30
components/page-header.tsx
Normal file
30
components/page-header.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="p-3 rounded-lg bg-blue-gradient flex items-center justify-center relative overflow-hidden">
|
||||||
|
|
||||||
|
{Icon && <Icon className="h-8 w-8 text-white relative z-10" />}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h1 className="text-3xl font-bold heading-blue-gradient">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useRouter, usePathname } from "next/navigation"
|
import { useRouter, usePathname } from "next/navigation"
|
||||||
import { LayoutDashboard, Plus, User, FolderPlus, Package, MapPin } from "lucide-react"
|
import { LayoutDashboard, Plus, User, FolderPlus, Package, MapPin } from "lucide-react"
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip"
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
title: string
|
title: string
|
||||||
@@ -19,19 +20,19 @@ const navItems: NavItem[] = [
|
|||||||
gradient: "from-blue-400 to-indigo-500"
|
gradient: "from-blue-400 to-indigo-500"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Create Project",
|
title: "Project",
|
||||||
href: "/create-project",
|
href: "/create-project",
|
||||||
icon: FolderPlus,
|
icon: FolderPlus,
|
||||||
gradient: "from-blue-400 to-blue-600"
|
gradient: "from-blue-400 to-blue-600"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Create Package",
|
title: "Package",
|
||||||
href: "/create-package",
|
href: "/create-package",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
gradient: "from-violet-400 to-purple-500"
|
gradient: "from-violet-400 to-purple-500"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Create Location",
|
title: "Location",
|
||||||
href: "/create-location",
|
href: "/create-location",
|
||||||
icon: MapPin,
|
icon: MapPin,
|
||||||
gradient: "from-amber-400 to-orange-500"
|
gradient: "from-amber-400 to-orange-500"
|
||||||
@@ -57,7 +58,7 @@ export function SidebarNavigation() {
|
|||||||
const isNewAnalysisActive = pathname === "/new-analysis"
|
const isNewAnalysisActive = pathname === "/new-analysis"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="fixed left-0 top-0 h-screen w-20 bg-white border-r border-slate-200 z-50 flex flex-col items-center py-8 shadow-sm">
|
<aside className="fixed left-0 top-0 h-screen w-20 bg-white/40 backdrop-blur-xl border-r border-slate-200/50 z-50 flex flex-col items-center py-8">
|
||||||
{/* Navigation Items */}
|
{/* Navigation Items */}
|
||||||
<nav className="flex-1 flex flex-col items-center gap-4">
|
<nav className="flex-1 flex flex-col items-center gap-4">
|
||||||
{/* Dashboard - first item */}
|
{/* Dashboard - first item */}
|
||||||
@@ -66,58 +67,52 @@ export function SidebarNavigation() {
|
|||||||
const Icon = item.icon
|
const Icon = item.icon
|
||||||
return (
|
return (
|
||||||
<div key={item.href} className="relative group">
|
<div key={item.href} className="relative group">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavigation(item)}
|
onClick={() => handleNavigation(item)}
|
||||||
className={`
|
className={`
|
||||||
relative w-12 h-12 rounded-xl flex items-center justify-center
|
relative w-12 h-12 rounded-md flex items-center justify-center
|
||||||
${isActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'}
|
${isActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
|
||||||
hover:bg-[#2563eb] hover:border-[#2563eb]
|
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
|
||||||
border
|
transition-all ease-in-out duration-200
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-900 group-hover:text-white'} stroke-[2.5]`}
|
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div className="
|
</TooltipTrigger>
|
||||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
<TooltipContent side="right" sideOffset={12}>
|
||||||
px-3 py-1.5 rounded-lg
|
<p>{item.title}</p>
|
||||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
</TooltipContent>
|
||||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
</Tooltip>
|
||||||
whitespace-nowrap shadow-lg pointer-events-none
|
|
||||||
">
|
|
||||||
{item.title}
|
|
||||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* New Analysis - Special eye-catchy button */}
|
{/* New Analysis - Special eye-catchy button */}
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push("/new-analysis")}
|
onClick={() => router.push("/new-analysis")}
|
||||||
className={`
|
className={`
|
||||||
relative w-12 h-12 rounded-xl flex items-center justify-center
|
relative w-12 h-12 rounded-md flex items-center justify-center
|
||||||
${isNewAnalysisActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'}
|
${isNewAnalysisActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
|
||||||
hover:bg-[#2563eb] hover:border-[#2563eb]
|
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
|
||||||
border
|
transition-all ease-in-out duration-200
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Plus
|
<Plus
|
||||||
className={`h-6 w-6 ${isNewAnalysisActive ? 'text-white' : 'text-slate-900 group-hover:text-white'} stroke-[2.5]`}
|
className={`h-6 w-6 ${isNewAnalysisActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div className="
|
</TooltipTrigger>
|
||||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
<TooltipContent side="right" sideOffset={12}>
|
||||||
px-3 py-1.5 rounded-lg
|
<p>New Analysis</p>
|
||||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
</TooltipContent>
|
||||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
</Tooltip>
|
||||||
whitespace-nowrap shadow-lg pointer-events-none
|
|
||||||
">
|
|
||||||
Start New Analysis
|
|
||||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
@@ -129,29 +124,26 @@ export function SidebarNavigation() {
|
|||||||
const Icon = item.icon
|
const Icon = item.icon
|
||||||
return (
|
return (
|
||||||
<div key={item.href} className="relative group">
|
<div key={item.href} className="relative group">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavigation(item)}
|
onClick={() => handleNavigation(item)}
|
||||||
className={`
|
className={`
|
||||||
relative w-12 h-12 rounded-xl flex items-center justify-center
|
relative w-12 h-12 rounded-md flex items-center justify-center
|
||||||
${isActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'}
|
${isActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
|
||||||
hover:bg-[#2563eb] hover:border-[#2563eb]
|
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
|
||||||
border
|
transition-all duration-200
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-900 group-hover:text-white'} stroke-[2.5]`}
|
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div className="
|
</TooltipTrigger>
|
||||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
<TooltipContent side="right" sideOffset={12}>
|
||||||
px-3 py-1.5 rounded-lg
|
<p>{item.title}</p>
|
||||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
</TooltipContent>
|
||||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
</Tooltip>
|
||||||
whitespace-nowrap shadow-lg pointer-events-none
|
|
||||||
">
|
|
||||||
{item.title}
|
|
||||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -160,23 +152,20 @@ export function SidebarNavigation() {
|
|||||||
{/* Bottom section */}
|
{/* Bottom section */}
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => { }}
|
onClick={() => { }}
|
||||||
className="w-12 h-12 rounded-full bg-[#f0fafd] flex items-center justify-center cursor-not-allowed opacity-80 border border-slate-100 hover:bg-[#2563eb] hover:border-[#2563eb]"
|
className="w-12 h-12 rounded-full flex items-center justify-center bg-linear-to-b from-[#225999] to-[#56A5FF] transition-all duration-200"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<User className="h-6 w-6 text-slate-900 group-hover:text-white" />
|
<User className="h-6 w-6 text-white" />
|
||||||
</button>
|
</button>
|
||||||
<div className="
|
</TooltipTrigger>
|
||||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
<TooltipContent side="right" sideOffset={12}>
|
||||||
px-3 py-1.5 rounded-lg
|
<p>Account</p>
|
||||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
</TooltipContent>
|
||||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
</Tooltip>
|
||||||
whitespace-nowrap shadow-lg pointer-events-none
|
|
||||||
">
|
|
||||||
Account (Coming Soon)
|
|
||||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm hover:shadow-md hover:border-[var(--card-hover-border)] hover:shadow-[var(--card-glow)]',
|
'bg-card text-card-foreground flex flex-col gap-6 rounded-md border-none py-6 shadow-none hover:shadow-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg',
|
'bg-dialog fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="skeleton"
|
data-slot="skeleton"
|
||||||
className={cn('bg-accent animate-pulse rounded-md', className)}
|
className={cn('bg-muted animate-pulse rounded-md', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function TooltipContent({
|
|||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-foreground text-background z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
'bg-foreground text-white z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -2511,7 +2511,6 @@
|
|||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2522,7 +2521,6 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -2624,7 +2622,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2887,8 +2884,7 @@
|
|||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
|
||||||
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==",
|
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
@@ -3018,8 +3014,7 @@
|
|||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.2",
|
||||||
@@ -3342,7 +3337,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
||||||
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
|
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.0.10",
|
"@next/env": "16.0.10",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
@@ -3468,7 +3462,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3506,7 +3499,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3537,7 +3529,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -3550,7 +3541,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
|
||||||
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
|
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -3850,8 +3840,7 @@
|
|||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
|||||||
BIN
public/background.png
Normal file
BIN
public/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user