refactor: remove unused files
This commit is contained in:
BIN
public/avatars/profile.jpg
Normal file
BIN
public/avatars/profile.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -1,7 +1,14 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
} from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Activity,
|
Activity,
|
||||||
@@ -13,26 +20,21 @@ import {
|
|||||||
PencilLine,
|
PencilLine,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Settings2,
|
Settings2,
|
||||||
} from "lucide-react";
|
} from 'lucide-react';
|
||||||
import { StatsCard } from "@/components/dashboard/stats-card";
|
import { StatsCard } from '@/components/dashboard/stats-card';
|
||||||
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector";
|
import { FilterSelector } from '@/components/dashboard/filter-selector';
|
||||||
import { FilterSelector } from "@/components/dashboard/filter-selector";
|
import { DetectionDonutChart } from '@/components/dashboard/detection-donut-chart';
|
||||||
import { DetectionDonutChart } from "@/components/dashboard/detection-donut-chart";
|
import { ChainageBarChart } from '@/components/dashboard/chainage-bar-chart';
|
||||||
import { ChainageBarChart } from "@/components/dashboard/chainage-bar-chart"
|
import { DashboardMap } from '@/components/dashboard/dashboard-map';
|
||||||
import { DashboardMap } from "@/components/dashboard/dashboard-map";
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { projectService } from '@/services/api';
|
||||||
import { projectService } from "@/services/api";
|
import { Project } from '@/types';
|
||||||
import { Project } from "@/types";
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import {
|
import { Button } from '@/components/ui/button';
|
||||||
Popover,
|
import { DashboardSkeleton } from '@/components/dashboard/dashboard-skeleton';
|
||||||
PopoverContent,
|
import { PoweredBy } from '@/components/powered-by';
|
||||||
PopoverTrigger,
|
import { toast } from 'sonner';
|
||||||
} from "@/components/ui/popover";
|
import { Reveal, StaggerContainer, StaggerItem } from '@/components/ui/reveal';
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton"
|
|
||||||
import { PoweredBy } from "@/components/powered-by";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Reveal, StaggerContainer, StaggerItem } from "@/components/ui/reveal";
|
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
@@ -103,41 +105,40 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
let total_damaged_road_marking = 0;
|
let total_damaged_road_marking = 0;
|
||||||
let total_good_sign_board = 0;
|
let total_good_sign_board = 0;
|
||||||
let total_road_damage = 0;
|
let total_road_damage = 0;
|
||||||
const chainageData: DetectionStats["chainageData"] = [];
|
const chainageData: DetectionStats['chainageData'] = [];
|
||||||
|
|
||||||
for (const pkg of Object.values(summary.packages || {})) {
|
for (const pkg of Object.values(summary.packages || {})) {
|
||||||
for (const [chnName, chn] of Object.entries(pkg.chainages || {})) { // Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages'
|
for (const [chnName, chn] of Object.entries(pkg.chainages || {})) {
|
||||||
|
// Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages'
|
||||||
let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard'
|
let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard'
|
||||||
let chnPothole = 0; // Changed 'locPothole' to 'chnPothole'
|
let chnPothole = 0; // Changed 'locPothole' to 'chnPothole'
|
||||||
let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack'
|
let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack'
|
||||||
let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking'
|
let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking'
|
||||||
let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard'
|
let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard'
|
||||||
|
|
||||||
for (const detection of chn.detections || []) { // Changed 'loc.detections' to 'chn.detections'
|
for (const detection of chn.detections || []) {
|
||||||
|
// Changed 'loc.detections' to 'chn.detections'
|
||||||
const type = detection.type.toLowerCase();
|
const type = detection.type.toLowerCase();
|
||||||
if (type === "defected_sign_board") {
|
if (type === 'defected_sign_board') {
|
||||||
chnDefectedSignboard++;
|
chnDefectedSignboard++;
|
||||||
total_defected_sign_board++;
|
total_defected_sign_board++;
|
||||||
} else if (type === "pothole") {
|
} else if (type === 'pothole') {
|
||||||
chnPothole++;
|
chnPothole++;
|
||||||
total_pothole++;
|
total_pothole++;
|
||||||
} else if (type === "road_crack") {
|
} else if (type === 'road_crack') {
|
||||||
chnRoadCrack++;
|
chnRoadCrack++;
|
||||||
total_road_crack++;
|
total_road_crack++;
|
||||||
} else if (type === "damaged_road_marking") {
|
} else if (type === 'damaged_road_marking') {
|
||||||
chnDamagedRoadMarking++;
|
chnDamagedRoadMarking++;
|
||||||
total_damaged_road_marking++;
|
total_damaged_road_marking++;
|
||||||
} else if (type === "good_sign_board") {
|
} else if (type === 'good_sign_board') {
|
||||||
chnGoodSignboard++;
|
chnGoodSignboard++;
|
||||||
total_good_sign_board++;
|
total_good_sign_board++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage'
|
const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage'
|
||||||
chnDefectedSignboard +
|
chnDefectedSignboard + chnPothole + chnRoadCrack + chnDamagedRoadMarking;
|
||||||
chnPothole +
|
|
||||||
chnRoadCrack +
|
|
||||||
chnDamagedRoadMarking;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
chnDefectedSignboard > 0 ||
|
chnDefectedSignboard > 0 ||
|
||||||
@@ -146,9 +147,9 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
chnDamagedRoadMarking > 0 ||
|
chnDamagedRoadMarking > 0 ||
|
||||||
chnGoodSignboard > 0
|
chnGoodSignboard > 0
|
||||||
) {
|
) {
|
||||||
const shortName =
|
const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName;
|
||||||
chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName;
|
chainageData.push({
|
||||||
chainageData.push({ // Changed 'locationData.push' to 'chainageData.push'
|
// Changed 'locationData.push' to 'chainageData.push'
|
||||||
name: shortName,
|
name: shortName,
|
||||||
defected_sign_board: chnDefectedSignboard,
|
defected_sign_board: chnDefectedSignboard,
|
||||||
pothole: chnPothole,
|
pothole: chnPothole,
|
||||||
@@ -167,10 +168,7 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
total_road_damage =
|
total_road_damage =
|
||||||
total_defected_sign_board +
|
total_defected_sign_board + total_pothole + total_road_crack + total_damaged_road_marking;
|
||||||
total_pothole +
|
|
||||||
total_road_crack +
|
|
||||||
total_damaged_road_marking;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalDefectedSignboard: total_defected_sign_board,
|
totalDefectedSignboard: total_defected_sign_board,
|
||||||
@@ -186,12 +184,8 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
|||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||||
null,
|
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(null);
|
||||||
);
|
|
||||||
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [stats, setStats] = useState<DetectionStats>({
|
const [stats, setStats] = useState<DetectionStats>({
|
||||||
totalDefectedSignboard: 0,
|
totalDefectedSignboard: 0,
|
||||||
totalPothole: 0,
|
totalPothole: 0,
|
||||||
@@ -214,9 +208,9 @@ export default function DashboardPage() {
|
|||||||
setSelectedProjectId(projectsData.items[0].id);
|
setSelectedProjectId(projectsData.items[0].id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load projects:", err);
|
console.error('Failed to load projects:', err);
|
||||||
toast.error("Failed to load projects", {
|
toast.error('Failed to load projects', {
|
||||||
description: "Please check if the backend is running."
|
description: 'Please check if the backend is running.',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -239,18 +233,12 @@ export default function DashboardPage() {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Extract chainages from selected package
|
// Extract chainages from selected package
|
||||||
const [selectedPackageId, setSelectedPackageId] = useState<string | null>(
|
const [selectedPackageId, setSelectedPackageId] = useState<string | null>(null);
|
||||||
null,
|
const [selectedChainageId, setSelectedChainageId] = useState<string | null>(null); // Changed 'selectedLocationId' to 'selectedChainageId'
|
||||||
);
|
|
||||||
const [selectedChainageId, setSelectedChainageId] = useState<string | null>( // Changed 'selectedLocationId' to 'selectedChainageId'
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
const chainages =
|
const chainages =
|
||||||
projectSummary && selectedPackageId && selectedPackageId !== "all"
|
projectSummary && selectedPackageId && selectedPackageId !== 'all'
|
||||||
? Object.keys(
|
? Object.keys(projectSummary.packages[selectedPackageId]?.chainages || {}).map((chnName) => ({
|
||||||
projectSummary.packages[selectedPackageId]?.chainages || {},
|
|
||||||
).map((chnName) => ({
|
|
||||||
id: chnName,
|
id: chnName,
|
||||||
name: chnName,
|
name: chnName,
|
||||||
}))
|
}))
|
||||||
@@ -270,8 +258,8 @@ export default function DashboardPage() {
|
|||||||
const summary: ProjectSummary = await projectService.getProjectSummary(selectedProjectId);
|
const summary: ProjectSummary = await projectService.getProjectSummary(selectedProjectId);
|
||||||
setProjectSummary(summary);
|
setProjectSummary(summary);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load project summary:", err);
|
console.error('Failed to load project summary:', err);
|
||||||
toast.error("Failed to load project summary");
|
toast.error('Failed to load project summary');
|
||||||
setProjectSummary(null);
|
setProjectSummary(null);
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -314,16 +302,16 @@ export default function DashboardPage() {
|
|||||||
let totalRoadCrack = 0;
|
let totalRoadCrack = 0;
|
||||||
let totalDamagedRoadMarking = 0;
|
let totalDamagedRoadMarking = 0;
|
||||||
let totalGoodSignboard = 0;
|
let totalGoodSignboard = 0;
|
||||||
const chainageData: DetectionStats["chainageData"] = [];
|
const chainageData: DetectionStats['chainageData'] = [];
|
||||||
|
|
||||||
const packagesToProcess =
|
const packagesToProcess =
|
||||||
selectedPackageId && selectedPackageId !== "all"
|
selectedPackageId && selectedPackageId !== 'all'
|
||||||
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
|
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
|
||||||
: projectSummary.packages || {};
|
: projectSummary.packages || {};
|
||||||
|
|
||||||
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
|
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
|
||||||
const chainagesToProcess =
|
const chainagesToProcess =
|
||||||
selectedChainageId && selectedChainageId !== "all"
|
selectedChainageId && selectedChainageId !== 'all'
|
||||||
? { [selectedChainageId]: pkg.chainages[selectedChainageId] }
|
? { [selectedChainageId]: pkg.chainages[selectedChainageId] }
|
||||||
: pkg.chainages || {};
|
: pkg.chainages || {};
|
||||||
|
|
||||||
@@ -338,19 +326,19 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
for (const detection of chn.detections || []) {
|
for (const detection of chn.detections || []) {
|
||||||
const type = detection.type.toLowerCase();
|
const type = detection.type.toLowerCase();
|
||||||
if (type === "defected_sign_board") {
|
if (type === 'defected_sign_board') {
|
||||||
chnDefectedSignboard++;
|
chnDefectedSignboard++;
|
||||||
totalDefectedSignboard++;
|
totalDefectedSignboard++;
|
||||||
} else if (type === "pothole") {
|
} else if (type === 'pothole') {
|
||||||
chnPothole++;
|
chnPothole++;
|
||||||
totalPothole++;
|
totalPothole++;
|
||||||
} else if (type === "road_crack") {
|
} else if (type === 'road_crack') {
|
||||||
chnRoadCrack++;
|
chnRoadCrack++;
|
||||||
totalRoadCrack++;
|
totalRoadCrack++;
|
||||||
} else if (type === "damaged_road_marking") {
|
} else if (type === 'damaged_road_marking') {
|
||||||
chnDamagedRoadMarking++;
|
chnDamagedRoadMarking++;
|
||||||
totalDamagedRoadMarking++;
|
totalDamagedRoadMarking++;
|
||||||
} else if (type === "good_sign_board") {
|
} else if (type === 'good_sign_board') {
|
||||||
chnGoodSignboard++;
|
chnGoodSignboard++;
|
||||||
totalGoodSignboard++;
|
totalGoodSignboard++;
|
||||||
}
|
}
|
||||||
@@ -363,8 +351,7 @@ export default function DashboardPage() {
|
|||||||
chnDamagedRoadMarking > 0 ||
|
chnDamagedRoadMarking > 0 ||
|
||||||
chnGoodSignboard > 0
|
chnGoodSignboard > 0
|
||||||
) {
|
) {
|
||||||
const shortName =
|
const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName;
|
||||||
chnName.length > 20 ? chnName.substring(0, 20) + "..." : chnName;
|
|
||||||
chainageData.push({
|
chainageData.push({
|
||||||
name: shortName,
|
name: shortName,
|
||||||
defected_sign_board: chnDefectedSignboard,
|
defected_sign_board: chnDefectedSignboard,
|
||||||
@@ -390,10 +377,7 @@ export default function DashboardPage() {
|
|||||||
totalDamagedRoadMarking,
|
totalDamagedRoadMarking,
|
||||||
totalGoodSignboard,
|
totalGoodSignboard,
|
||||||
totalRoadDamage:
|
totalRoadDamage:
|
||||||
totalDefectedSignboard +
|
totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking,
|
||||||
totalPothole +
|
|
||||||
totalRoadCrack +
|
|
||||||
totalDamagedRoadMarking,
|
|
||||||
chainageData,
|
chainageData,
|
||||||
});
|
});
|
||||||
}, [projectSummary, selectedPackageId, selectedChainageId]);
|
}, [projectSummary, selectedPackageId, selectedChainageId]);
|
||||||
@@ -425,7 +409,9 @@ export default function DashboardPage() {
|
|||||||
<Milestone className="h-4 w-4 text-primary" />
|
<Milestone className="h-4 w-4 text-primary" />
|
||||||
Filter Analysis
|
Filter Analysis
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or chainage</p>
|
<p className="text-xs mt-1 text-muted-foreground">
|
||||||
|
Refine your view by project, package, or chainage
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FilterSelector
|
<FilterSelector
|
||||||
@@ -446,7 +432,6 @@ export default function DashboardPage() {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{isLoading && !projectSummary ? (
|
{isLoading && !projectSummary ? (
|
||||||
<DashboardSkeleton />
|
<DashboardSkeleton />
|
||||||
) : (
|
) : (
|
||||||
@@ -532,13 +517,12 @@ export default function DashboardPage() {
|
|||||||
<Card className="flex-1 h-full">
|
<Card className="flex-1 h-full">
|
||||||
<CardHeader className="items-center pb-0">
|
<CardHeader className="items-center pb-0">
|
||||||
<CardTitle className="text-base font-bold">Severity by Chainage</CardTitle>
|
<CardTitle className="text-base font-bold">Severity by Chainage</CardTitle>
|
||||||
<CardDescription className="text-xs">Detections grouped by road segment</CardDescription>
|
<CardDescription className="text-xs">
|
||||||
|
Detections grouped by road segment
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-[300px]">
|
<CardContent className="h-[300px]">
|
||||||
<ChainageBarChart
|
<ChainageBarChart data={stats.chainageData || []} isLoading={isLoading} />
|
||||||
data={stats.chainageData || []}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex-col gap-2 text-sm">
|
<CardFooter className="flex-col gap-2 text-sm">
|
||||||
<div className="leading-none text-muted-foreground">
|
<div className="leading-none text-muted-foreground">
|
||||||
@@ -552,7 +536,7 @@ export default function DashboardPage() {
|
|||||||
{/* Map - Full Width Below Charts */}
|
{/* Map - Full Width Below Charts */}
|
||||||
<Reveal delay={0.4} direction="up">
|
<Reveal delay={0.4} direction="up">
|
||||||
<div>
|
<div>
|
||||||
<DashboardMap
|
<DashboardMap
|
||||||
selectedProjectId={selectedProjectId}
|
selectedProjectId={selectedProjectId}
|
||||||
selectedPackageId={selectedPackageId}
|
selectedPackageId={selectedPackageId}
|
||||||
selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId'
|
selectedChainageId={selectedChainageId} // Changed 'selectedLocationId' to 'selectedChainageId'
|
||||||
|
|||||||
@@ -1,106 +1,95 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
|
import { LayoutDashboard, Plus, Layers, Package, MapPin, Milestone } from 'lucide-react';
|
||||||
|
import { NavUser } from '@/components/nav-user';
|
||||||
|
import { ROUTES } from '@/utils/routes';
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
Sidebar,
|
||||||
Plus,
|
SidebarContent,
|
||||||
Layers,
|
SidebarFooter,
|
||||||
Package,
|
SidebarHeader,
|
||||||
MapPin,
|
SidebarRail,
|
||||||
Milestone,
|
SidebarMenu,
|
||||||
} from "lucide-react";
|
SidebarMenuItem,
|
||||||
import { NavUser } from "@/components/nav-user";
|
SidebarMenuButton,
|
||||||
import { ROUTES } from "@/utils/routes";
|
SidebarGroup,
|
||||||
import {
|
SidebarGroupLabel,
|
||||||
Sidebar,
|
} from '@/components/ui/sidebar';
|
||||||
SidebarContent,
|
import Link from 'next/link';
|
||||||
SidebarFooter,
|
import { usePathname } from 'next/navigation';
|
||||||
SidebarHeader,
|
|
||||||
SidebarRail,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
|
|
||||||
// This is the navigation data
|
// This is the navigation data
|
||||||
const data = {
|
const data = {
|
||||||
user: {
|
user: {
|
||||||
name: "shadcn",
|
name: 'Vision Admin',
|
||||||
email: "m@example.com",
|
email: 'admin@visionroad.ai',
|
||||||
avatar: "/avatars/shadcn.jpg",
|
avatar: '/avatars/profile.jpg',
|
||||||
|
},
|
||||||
|
navMain: [
|
||||||
|
{
|
||||||
|
title: 'Dashboard',
|
||||||
|
url: ROUTES.DASHBOARD,
|
||||||
|
icon: LayoutDashboard,
|
||||||
},
|
},
|
||||||
navMain: [
|
{
|
||||||
{
|
title: 'New Analysis',
|
||||||
title: "Dashboard",
|
url: ROUTES.NEW_ANALYSIS,
|
||||||
url: ROUTES.DASHBOARD,
|
icon: Plus,
|
||||||
icon: LayoutDashboard,
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Project',
|
||||||
title: "New Analysis",
|
url: ROUTES.PROJECT,
|
||||||
url: ROUTES.NEW_ANALYSIS,
|
icon: Layers,
|
||||||
icon: Plus,
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Package',
|
||||||
title: "Project",
|
url: ROUTES.PACKAGE,
|
||||||
url: ROUTES.PROJECT,
|
icon: Package,
|
||||||
icon: Layers,
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Chainage',
|
||||||
title: "Package",
|
url: ROUTES.CHAINAGE,
|
||||||
url: ROUTES.PACKAGE,
|
icon: Milestone,
|
||||||
icon: Package,
|
},
|
||||||
},
|
],
|
||||||
{
|
|
||||||
title: "Chainage",
|
|
||||||
url: ROUTES.CHAINAGE,
|
|
||||||
icon: Milestone,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<div className="flex items-center gap-2 py-2">
|
<div className="flex items-center gap-2 py-2">
|
||||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||||
<Package className="size-4" />
|
<Package className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
||||||
<span className="truncate font-semibold">Vision Road</span>
|
<span className="truncate font-semibold">Vision Road</span>
|
||||||
<span className="truncate text-xs">Analytics Platform</span>
|
<span className="truncate text-xs">Analytics Platform</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{data.navMain.map((item) => (
|
{data.navMain.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton asChild tooltip={item.title} isActive={pathname === item.url}>
|
||||||
asChild
|
<Link href={item.url}>
|
||||||
tooltip={item.title}
|
{item.icon && <item.icon />}
|
||||||
isActive={pathname === item.url}
|
<span>{item.title}</span>
|
||||||
>
|
</Link>
|
||||||
<Link href={item.url}>
|
</SidebarMenuButton>
|
||||||
{item.icon && <item.icon />}
|
</SidebarMenuItem>
|
||||||
<span>{item.title}</span>
|
))}
|
||||||
</Link>
|
</SidebarMenu>
|
||||||
</SidebarMenuButton>
|
</SidebarGroup>
|
||||||
</SidebarMenuItem>
|
</SidebarContent>
|
||||||
))}
|
<SidebarFooter>
|
||||||
</SidebarMenu>
|
<NavUser user={data.user} />
|
||||||
</SidebarGroup>
|
</SidebarFooter>
|
||||||
</SidebarContent>
|
<SidebarRail />
|
||||||
<SidebarFooter>
|
</Sidebar>
|
||||||
<NavUser user={data.user} />
|
);
|
||||||
</SidebarFooter>
|
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select"
|
|
||||||
import { Project } from "@/types"
|
|
||||||
|
|
||||||
interface CompactProjectSelectorProps {
|
|
||||||
projects: Project[]
|
|
||||||
selectedProjectId: string | null
|
|
||||||
onProjectChange: (projectId: string) => void
|
|
||||||
selectedProject: Project | undefined
|
|
||||||
isLoading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CompactProjectSelector({
|
|
||||||
projects,
|
|
||||||
selectedProjectId,
|
|
||||||
onProjectChange,
|
|
||||||
selectedProject,
|
|
||||||
isLoading = false
|
|
||||||
}: CompactProjectSelectorProps) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap items-center gap-3 p-3 rounded-md border">
|
|
||||||
{/* Project Dropdown */}
|
|
||||||
<div className="flex flex-col min-w-[120px]">
|
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Project</span>
|
|
||||||
<Select
|
|
||||||
value={selectedProjectId || ""}
|
|
||||||
onValueChange={onProjectChange}
|
|
||||||
disabled={isLoading || projects.length === 0}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold bg-transparent text-left">
|
|
||||||
<SelectValue placeholder="Select project" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{projects.map((project) => (
|
|
||||||
<SelectItem key={project.id} value={project.id}>
|
|
||||||
{project.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Corridor Badge */}
|
|
||||||
{selectedProject?.corridor_name && (
|
|
||||||
<>
|
|
||||||
<div className="h-8 w-px bg-border mx-2" />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Corridor</span>
|
|
||||||
<span className="text-xs font-bold text-muted-foreground">
|
|
||||||
{selectedProject.corridor_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* State Badge */}
|
|
||||||
{selectedProject?.state && (
|
|
||||||
<>
|
|
||||||
<div className="h-8 w-px bg-border mx-2" />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">State</span>
|
|
||||||
<span className="text-xs font-bold text-muted-foreground">
|
|
||||||
{selectedProject.state}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
|
||||||
import { LucideIcon } from "lucide-react"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
interface GradientStatsCardProps {
|
|
||||||
title: string
|
|
||||||
subtitle?: string
|
|
||||||
value: number | string
|
|
||||||
icon: LucideIcon
|
|
||||||
gradient?: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
|
|
||||||
isLoading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GradientStatsCard({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
value,
|
|
||||||
icon: Icon,
|
|
||||||
isLoading = false
|
|
||||||
}: GradientStatsCardProps) {
|
|
||||||
return (
|
|
||||||
<Card className="overflow-hidden border shadow-sm">
|
|
||||||
<CardContent className="p-6 flex items-center justify-between gap-4">
|
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
|
||||||
<h3 className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="h-10 w-24 bg-muted animate-pulse rounded-md mt-1" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p className="text-3xl font-bold tracking-tight">
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
{subtitle && (
|
|
||||||
<p className="text-[10px] font-semibold text-muted-foreground mt-0.5">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 rounded-lg bg-secondary shrink-0">
|
|
||||||
<Icon className="h-6 w-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { ChevronRight, type LucideIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible"
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
} from "@/components/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavMain({
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
icon?: LucideIcon
|
|
||||||
isActive?: boolean
|
|
||||||
items?: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
}[]
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map((item) => (
|
|
||||||
<Collapsible
|
|
||||||
key={item.title}
|
|
||||||
asChild
|
|
||||||
defaultOpen={item.isActive}
|
|
||||||
className="group/collapsible"
|
|
||||||
>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
|
||||||
{item.icon && <item.icon />}
|
|
||||||
<span>{item.title}</span>
|
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<SidebarMenuSub>
|
|
||||||
{item.items?.map((subItem) => (
|
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
|
||||||
<SidebarMenuSubButton asChild>
|
|
||||||
<a href={subItem.url}>
|
|
||||||
<span>{subItem.title}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuSubButton>
|
|
||||||
</SidebarMenuSubItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenuSub>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</Collapsible>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Folder,
|
|
||||||
Forward,
|
|
||||||
MoreHorizontal,
|
|
||||||
Trash2,
|
|
||||||
type LucideIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu"
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from "@/components/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavProjects({
|
|
||||||
projects,
|
|
||||||
}: {
|
|
||||||
projects: {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
icon: LucideIcon
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
const { isMobile } = useSidebar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
|
||||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
|
||||||
<SidebarMenu>
|
|
||||||
{projects.map((item) => (
|
|
||||||
<SidebarMenuItem key={item.name}>
|
|
||||||
<SidebarMenuButton asChild>
|
|
||||||
<a href={item.url}>
|
|
||||||
<item.icon />
|
|
||||||
<span>{item.name}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<SidebarMenuAction showOnHover>
|
|
||||||
<MoreHorizontal />
|
|
||||||
<span className="sr-only">More</span>
|
|
||||||
</SidebarMenuAction>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-48 rounded-lg"
|
|
||||||
side={isMobile ? "bottom" : "right"}
|
|
||||||
align={isMobile ? "end" : "start"}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Folder className="text-muted-foreground" />
|
|
||||||
<span>View Project</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Forward className="text-muted-foreground" />
|
|
||||||
<span>Share Project</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Trash2 className="text-muted-foreground" />
|
|
||||||
<span>Delete Project</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
|
||||||
<MoreHorizontal className="text-sidebar-foreground/70" />
|
|
||||||
<span>More</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { ChevronsUpDown, Plus } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu"
|
|
||||||
import {
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from "@/components/ui/sidebar"
|
|
||||||
|
|
||||||
export function TeamSwitcher({
|
|
||||||
teams,
|
|
||||||
}: {
|
|
||||||
teams: {
|
|
||||||
name: string
|
|
||||||
logo: React.ElementType
|
|
||||||
plan: string
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
const { isMobile } = useSidebar()
|
|
||||||
const [activeTeam, setActiveTeam] = React.useState(teams[0])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<SidebarMenuButton
|
|
||||||
size="lg"
|
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
||||||
>
|
|
||||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
||||||
<activeTeam.logo className="size-4" />
|
|
||||||
</div>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-semibold">
|
|
||||||
{activeTeam.name}
|
|
||||||
</span>
|
|
||||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
|
||||||
</div>
|
|
||||||
<ChevronsUpDown className="ml-auto" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
||||||
align="start"
|
|
||||||
side={isMobile ? "bottom" : "right"}
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
|
||||||
Teams
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
{teams.map((team, index) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={team.name}
|
|
||||||
onClick={() => setActiveTeam(team)}
|
|
||||||
className="gap-2 p-2"
|
|
||||||
>
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
|
||||||
<team.logo className="size-4 shrink-0" />
|
|
||||||
</div>
|
|
||||||
{team.name}
|
|
||||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem className="gap-2 p-2">
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
|
||||||
<Plus className="size-4" />
|
|
||||||
</div>
|
|
||||||
<div className="font-medium text-muted-foreground">Add team</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
|
|
||||||
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="list"
|
|
||||||
data-slot="item-group"
|
|
||||||
className={cn('group/item-group flex flex-col', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
|
||||||
return (
|
|
||||||
<Separator
|
|
||||||
data-slot="item-separator"
|
|
||||||
orientation="horizontal"
|
|
||||||
className={cn('my-0', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemVariants = cva(
|
|
||||||
'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a&]:hover:bg-accent/50 [a&]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: 'bg-transparent',
|
|
||||||
outline: 'border-border',
|
|
||||||
muted: 'bg-muted/50',
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: 'p-4 gap-4 ',
|
|
||||||
sm: 'py-3 px-4 gap-2.5',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: 'default',
|
|
||||||
size: 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function Item({
|
|
||||||
className,
|
|
||||||
variant = 'default',
|
|
||||||
size = 'default',
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> &
|
|
||||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : 'div'
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="item"
|
|
||||||
data-variant={variant}
|
|
||||||
data-size={size}
|
|
||||||
className={cn(itemVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemMediaVariants = cva(
|
|
||||||
'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: 'bg-transparent',
|
|
||||||
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
image:
|
|
||||||
'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function ItemMedia({
|
|
||||||
className,
|
|
||||||
variant = 'default',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-media"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(itemMediaVariants({ variant, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-content"
|
|
||||||
className={cn(
|
|
||||||
'flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-title"
|
|
||||||
className={cn(
|
|
||||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="item-description"
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
|
|
||||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-actions"
|
|
||||||
className={cn('flex items-center gap-2', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-header"
|
|
||||||
className={cn(
|
|
||||||
'flex basis-full items-center justify-between gap-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-footer"
|
|
||||||
className={cn(
|
|
||||||
'flex basis-full items-center justify-between gap-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Item,
|
|
||||||
ItemMedia,
|
|
||||||
ItemContent,
|
|
||||||
ItemActions,
|
|
||||||
ItemGroup,
|
|
||||||
ItemSeparator,
|
|
||||||
ItemTitle,
|
|
||||||
ItemDescription,
|
|
||||||
ItemHeader,
|
|
||||||
ItemFooter,
|
|
||||||
}
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState, useRef } from "react"
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Progress } from "@/components/ui/progress"
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
||||||
import { Upload, Loader2, AlertCircle } from "lucide-react"
|
|
||||||
import type { DetectionData, DetectionType } from "@/types"
|
|
||||||
|
|
||||||
import { videoService } from "@/services/api"
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL
|
|
||||||
const WS_URL = API_URL?.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://")
|
|
||||||
|
|
||||||
const DETECTION_TYPES = [
|
|
||||||
{ value: "pothole-detection", label: "Pothole Detection" },
|
|
||||||
{ value: "sign-board-detection", label: "Signboard Detection" },
|
|
||||||
{ value: "pot-sign-detection", label: "Pothole & Signboard Detection" },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const DETECTION_METHODS = [
|
|
||||||
{ value: "yolo", label: "YOLO" },
|
|
||||||
{ value: "yolo_vl", label: "YOLO with VL" },
|
|
||||||
{ value: "sam3", label: "SAM 3" },
|
|
||||||
{ value: "yoloe", label: "YOLOE" },
|
|
||||||
{ value: "yoloe_trained_vl", label: "YOLOE Trained" },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
type UploadSectionProps = {
|
|
||||||
onDetectionComplete: (data: DetectionData, videoId: string, file: File) => void
|
|
||||||
onDetectionTypeChange: (type: DetectionType) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: UploadSectionProps) {
|
|
||||||
const [file, setFile] = useState<File | null>(null)
|
|
||||||
const [jsonFile, setJsonFile] = useState<File | null>(null)
|
|
||||||
const [speed, setSpeed] = useState(30)
|
|
||||||
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
|
|
||||||
const [selectMethod, setSelectMethod] = useState("yolo_vl")
|
|
||||||
const [uploading, setUploading] = useState(false)
|
|
||||||
const [progress, setProgress] = useState(0)
|
|
||||||
const [statusMessage, setStatusMessage] = useState("")
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const jsonFileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const wsRef = useRef<WebSocket | null>(null)
|
|
||||||
|
|
||||||
const handleDetectionTypeChange = (value: DetectionType) => {
|
|
||||||
setDetectionType(value)
|
|
||||||
onDetectionTypeChange(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectWebSocket = (videoId: string) => {
|
|
||||||
console.log("[Upload] Connecting WebSocket for video:", videoId)
|
|
||||||
|
|
||||||
const ws = new WebSocket(`${WS_URL}/ws/${videoId}`)
|
|
||||||
wsRef.current = ws
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log("[Upload] WebSocket connected")
|
|
||||||
setError(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data)
|
|
||||||
console.log("[Upload] WebSocket message:", data)
|
|
||||||
|
|
||||||
if (data.type === "progress" || data.progress !== undefined) {
|
|
||||||
const progressValue = data.progress || 0
|
|
||||||
setProgress(progressValue)
|
|
||||||
|
|
||||||
let message = data.message || "Processing..."
|
|
||||||
|
|
||||||
// Handle both pothole and signboard progress messages
|
|
||||||
if (data.unique_potholes !== undefined) {
|
|
||||||
message += ` | Unique: ${data.unique_potholes} | Total: ${data.total_detections || 0}`
|
|
||||||
} else if (data.unique_signboards !== undefined) {
|
|
||||||
message += ` | Unique: ${data.unique_signboards} | Total: ${data.total_detections || 0}`
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatusMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "complete" || data.status === "completed") {
|
|
||||||
setStatusMessage("Processing completed! Loading results...")
|
|
||||||
ws.close()
|
|
||||||
setTimeout(() => loadResults(videoId), 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "error") {
|
|
||||||
setError("Error: " + data.message)
|
|
||||||
setStatusMessage("")
|
|
||||||
setUploading(false)
|
|
||||||
ws.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error("[Upload] WebSocket error:", error)
|
|
||||||
setStatusMessage("Connection error. Retrying...")
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log("[Upload] WebSocket closed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadResults = async (videoId: string) => {
|
|
||||||
try {
|
|
||||||
console.log("[Upload] Loading results for:", videoId)
|
|
||||||
const detectionData: DetectionData = await videoService.getVideoResults(videoId);
|
|
||||||
console.log("[Upload] Results loaded:", detectionData)
|
|
||||||
|
|
||||||
setUploading(false)
|
|
||||||
setProgress(100)
|
|
||||||
setStatusMessage("✓ Complete!")
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
// Pass detection data, video_id, and the original file
|
|
||||||
if (file) {
|
|
||||||
onDetectionComplete(detectionData, videoId, file)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Upload] Failed to load results:", error)
|
|
||||||
setError("Failed to load results. Please try again.")
|
|
||||||
setStatusMessage("")
|
|
||||||
setUploading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpload = async () => {
|
|
||||||
if (!file) {
|
|
||||||
setError("Please select a video file")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append("file", file)
|
|
||||||
formData.append("detection_type", detectionType)
|
|
||||||
formData.append("speed_kmh", speed.toString())
|
|
||||||
formData.append("detection_mode", selectMethod)
|
|
||||||
|
|
||||||
// Add JSON file if provided
|
|
||||||
if (jsonFile) {
|
|
||||||
formData.append("json_file", jsonFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploading(true)
|
|
||||||
setProgress(0)
|
|
||||||
setStatusMessage("Uploading...")
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await videoService.uploadVideo(formData);
|
|
||||||
const videoId = result.video_id
|
|
||||||
|
|
||||||
console.log("[Upload] Video uploaded successfully, ID:", videoId)
|
|
||||||
console.log("[Upload] Response:", result)
|
|
||||||
setStatusMessage("Uploaded! Starting processing...")
|
|
||||||
setProgress(10)
|
|
||||||
|
|
||||||
// Connect WebSocket for progress updates
|
|
||||||
connectWebSocket(videoId)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Upload] Upload error:", error)
|
|
||||||
|
|
||||||
let errorMessage = "Upload failed"
|
|
||||||
|
|
||||||
if (error instanceof TypeError && error.message === "Failed to fetch") {
|
|
||||||
errorMessage = "Cannot connect to server. Please check if backend is running."
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
errorMessage = error.message
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(errorMessage)
|
|
||||||
setStatusMessage("")
|
|
||||||
setUploading(false)
|
|
||||||
setProgress(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Upload Video</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Select video file, detection type, vehicle speed, and method for analysis
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="video-file">Video File</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
ref={fileInputRef}
|
|
||||||
id="video-file"
|
|
||||||
type="file"
|
|
||||||
accept="video/*"
|
|
||||||
onChange={(e) => {
|
|
||||||
setFile(e.target.files?.[0] || null)
|
|
||||||
setError(null)
|
|
||||||
}}
|
|
||||||
disabled={uploading}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{file && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="json-file">GPS JSON File</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
ref={jsonFileInputRef}
|
|
||||||
id="json-file"
|
|
||||||
type="file"
|
|
||||||
accept=".json,application/json"
|
|
||||||
onChange={(e) => {
|
|
||||||
setJsonFile(e.target.files?.[0] || null)
|
|
||||||
setError(null)
|
|
||||||
}}
|
|
||||||
disabled={uploading}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{jsonFile && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Selected: {jsonFile.name} ({(jsonFile.size / 1024).toFixed(2)} KB)
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="detection-type">Detection Type</Label>
|
|
||||||
<Select
|
|
||||||
value={detectionType}
|
|
||||||
onValueChange={handleDetectionTypeChange}
|
|
||||||
disabled={uploading}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="detection-type">
|
|
||||||
<SelectValue placeholder="Select detection type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{DETECTION_TYPES.map((type) => (
|
|
||||||
<SelectItem key={type.value} value={type.value}>
|
|
||||||
<span>{type.label}</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="speed">Vehicle Speed (km/h)</Label>
|
|
||||||
<Input
|
|
||||||
id="speed"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={200}
|
|
||||||
value={speed}
|
|
||||||
onChange={(e) => setSpeed(Number(e.target.value))}
|
|
||||||
disabled={uploading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="select-method">Select Method</Label>
|
|
||||||
<Select
|
|
||||||
value={selectMethod}
|
|
||||||
onValueChange={setSelectMethod}
|
|
||||||
disabled={uploading}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="select-method">
|
|
||||||
<SelectValue placeholder="Select method" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{DETECTION_METHODS.map((method) => (
|
|
||||||
<SelectItem key={method.value} value={method.value}>
|
|
||||||
<span>{method.label}</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
|
|
||||||
<AlertCircle className="h-5 w-5 mt-0.5 shrink-0" />
|
|
||||||
<p className="text-sm whitespace-pre-line">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={!file || uploading}
|
|
||||||
className="w-full"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{uploading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Processing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Upload className="mr-2 h-4 w-4" />
|
|
||||||
Upload & Process
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{uploading && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Progress value={progress} className="h-3" />
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">{statusMessage}</span>
|
|
||||||
<span className="font-semibold">{progress}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user