ui changes

This commit is contained in:
2026-03-10 15:03:50 +05:30
parent 01c1117a0b
commit dd13fa139e
28 changed files with 1022 additions and 849 deletions

View File

@@ -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 */}

View File

@@ -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 */}

View File

@@ -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 */}

View File

@@ -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>
) );
} }

View File

@@ -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;
} }

View File

@@ -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">

View File

@@ -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 */}

View File

@@ -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 && (

View File

@@ -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 */}

View File

@@ -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 && (

View File

@@ -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 */}

View File

@@ -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" />

View 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>
)
}

View File

@@ -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 (

View File

@@ -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>
) );
} }

View File

@@ -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>

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View 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>
)
}

View File

@@ -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>

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm 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}

View File

@@ -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}

View File

@@ -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}
/> />
) )

View File

@@ -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
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB