refactor: add animations
This commit is contained in:
@@ -30,6 +30,7 @@ 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;
|
||||
|
||||
@@ -215,10 +216,10 @@ export default function DashboardPage() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const projectsData = await fetchProjects();
|
||||
setProjects(projectsData);
|
||||
setProjects(projectsData.items);
|
||||
|
||||
if (projectsData.length > 0) {
|
||||
setSelectedProjectId(projectsData[0].id);
|
||||
if (projectsData.items.length > 0) {
|
||||
setSelectedProjectId(projectsData.items[0].id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load projects:", err);
|
||||
@@ -432,142 +433,161 @@ export default function DashboardPage() {
|
||||
icon={BarChart3}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="gap-2 font-semibold">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
<span>Filters</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[320px] p-0 overflow-hidden" align="end">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2 text-foreground">
|
||||
<Settings2 className="h-4 w-4 text-primary" />
|
||||
Filter Analysis
|
||||
</h3>
|
||||
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or location</p>
|
||||
</div>
|
||||
<div>
|
||||
<FilterSelector
|
||||
projects={projects}
|
||||
selectedProjectId={selectedProjectId}
|
||||
selectedPackageId={selectedPackageId}
|
||||
selectedLocationId={selectedLocationId}
|
||||
onProjectChange={setSelectedProjectId}
|
||||
onPackageChange={setSelectedPackageId}
|
||||
onLocationChange={setSelectedLocationId}
|
||||
packages={packages}
|
||||
locations={locations}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Reveal direction="down" delay={0.2}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="gap-2 font-semibold">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
<span>Filters</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[320px] p-0 overflow-hidden" align="end">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<h3 className="font-bold text-sm flex items-center gap-2 text-foreground">
|
||||
<Settings2 className="h-4 w-4 text-primary" />
|
||||
Filter Analysis
|
||||
</h3>
|
||||
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or location</p>
|
||||
</div>
|
||||
<div>
|
||||
<FilterSelector
|
||||
projects={projects}
|
||||
selectedProjectId={selectedProjectId}
|
||||
selectedPackageId={selectedPackageId}
|
||||
selectedLocationId={selectedLocationId}
|
||||
onProjectChange={setSelectedProjectId}
|
||||
onPackageChange={setSelectedPackageId}
|
||||
onLocationChange={setSelectedLocationId}
|
||||
packages={packages}
|
||||
locations={locations}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
|
||||
{isLoading && !projectSummary ? (
|
||||
<DashboardSkeleton />
|
||||
) : (
|
||||
<>
|
||||
{/* Stats Cards - Top Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<StatsCard
|
||||
title="Total Road Damage"
|
||||
subtitle="Combined damage detections"
|
||||
value={stats.totalRoadDamage}
|
||||
icon={Activity}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Potholes"
|
||||
subtitle="Surface depressions"
|
||||
value={stats.totalPothole}
|
||||
icon={AlertCircle}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Defected Signboards"
|
||||
subtitle="Damaged traffic signs"
|
||||
value={stats.totalDefectedSignboard}
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Road Cracks"
|
||||
subtitle="Surface fissures"
|
||||
value={stats.totalRoadCrack}
|
||||
icon={Zap}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Damaged Markings"
|
||||
subtitle="Worn road lines"
|
||||
value={stats.totalDamagedRoadMarking}
|
||||
icon={PencilLine}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Good Signboards"
|
||||
subtitle="Informational markers"
|
||||
value={stats.totalGoodSignboard}
|
||||
icon={CheckCircle2}
|
||||
/>
|
||||
</div>
|
||||
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<StaggerItem>
|
||||
<StatsCard
|
||||
title="Total Road Damage"
|
||||
subtitle="Combined damage detections"
|
||||
value={stats.totalRoadDamage}
|
||||
icon={Activity}
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<StatsCard
|
||||
title="Potholes"
|
||||
subtitle="Surface depressions"
|
||||
value={stats.totalPothole}
|
||||
icon={AlertCircle}
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<StatsCard
|
||||
title="Defected Signboards"
|
||||
subtitle="Damaged traffic signs"
|
||||
value={stats.totalDefectedSignboard}
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<StatsCard
|
||||
title="Road Cracks"
|
||||
subtitle="Surface fissures"
|
||||
value={stats.totalRoadCrack}
|
||||
icon={Zap}
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<StatsCard
|
||||
title="Damaged Markings"
|
||||
subtitle="Worn road lines"
|
||||
value={stats.totalDamagedRoadMarking}
|
||||
icon={PencilLine}
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<StatsCard
|
||||
title="Good Signboards"
|
||||
subtitle="Informational markers"
|
||||
value={stats.totalGoodSignboard}
|
||||
icon={CheckCircle2}
|
||||
/>
|
||||
</StaggerItem>
|
||||
</StaggerContainer>
|
||||
|
||||
{/* Charts Row - Side by Side */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
{/* Left Chart - Detection Distribution */}
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Detection Distribution</CardTitle>
|
||||
<CardDescription>Breakdown of all detected road conditions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<DetectionDonutChart
|
||||
defectedSignboard={stats.totalDefectedSignboard}
|
||||
pothole={stats.totalPothole}
|
||||
roadCrack={stats.totalRoadCrack}
|
||||
damagedRoadMarking={stats.totalDamagedRoadMarking}
|
||||
goodSignboard={stats.totalGoodSignboard}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Reveal delay={0.2} direction="left" className="flex flex-col">
|
||||
<Card className="flex-1 h-full">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Detection Distribution</CardTitle>
|
||||
<CardDescription>Breakdown of all detected road conditions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<DetectionDonutChart
|
||||
defectedSignboard={stats.totalDefectedSignboard}
|
||||
pothole={stats.totalPothole}
|
||||
roadCrack={stats.totalRoadCrack}
|
||||
damagedRoadMarking={stats.totalDamagedRoadMarking}
|
||||
goodSignboard={stats.totalGoodSignboard}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Reveal>
|
||||
|
||||
{/* Right Chart - Detections by Location */}
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Detections by Location</CardTitle>
|
||||
<CardDescription>Frequency of road issues across different map segments</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<LocationBarChart
|
||||
data={stats.locationData.map((loc) => ({
|
||||
name: loc.name,
|
||||
defected_sign_board: loc.defected_sign_board,
|
||||
pothole: loc.pothole,
|
||||
road_crack: loc.road_crack,
|
||||
damaged_road_marking: loc.damaged_road_marking,
|
||||
total: loc.total,
|
||||
}))}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Analysis based on latest processed sequence
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Reveal delay={0.3} direction="right" className="flex flex-col">
|
||||
<Card className="flex-1 h-full">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Detections by Location</CardTitle>
|
||||
<CardDescription>Frequency of road issues across different map segments</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<LocationBarChart
|
||||
data={stats.locationData.map((loc) => ({
|
||||
name: loc.name,
|
||||
defected_sign_board: loc.defected_sign_board,
|
||||
pothole: loc.pothole,
|
||||
road_crack: loc.road_crack,
|
||||
damaged_road_marking: loc.damaged_road_marking,
|
||||
total: loc.total,
|
||||
}))}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Analysis based on latest processed sequence
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Map - Full Width Below Charts */}
|
||||
<div>
|
||||
<DashboardMap
|
||||
selectedProjectId={selectedProjectId}
|
||||
selectedPackageId={selectedPackageId}
|
||||
selectedLocationId={selectedLocationId}
|
||||
projectSummary={projectSummary}
|
||||
/>
|
||||
</div>
|
||||
<Reveal delay={0.4} direction="up">
|
||||
<div>
|
||||
<DashboardMap
|
||||
selectedProjectId={selectedProjectId}
|
||||
selectedPackageId={selectedPackageId}
|
||||
selectedLocationId={selectedLocationId}
|
||||
projectSummary={projectSummary}
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||
|
||||
export default function LocationPage() {
|
||||
const [locations, setLocations] = useState<Location[]>([])
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [packages, setPackages] = useState<PackageType[]>([])
|
||||
const [allPackages, setAllPackages] = useState<PackageType[]>([])
|
||||
@@ -67,11 +68,15 @@ export default function LocationPage() {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const data = await fetchAllLocations({ skip: currentSkip, limit: currentLimit })
|
||||
setLocations(data)
|
||||
setLocations(data.items)
|
||||
setTotalItems(data.totalItems)
|
||||
} catch (err) {
|
||||
setError("Failed to load locations. Please check if the backend is running.")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
// Add a small delay for animation stability
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +84,7 @@ export default function LocationPage() {
|
||||
try {
|
||||
setLoadingProjects(true)
|
||||
const data = await fetchProjects({ skip: 0, limit: 1000 })
|
||||
setProjects(data)
|
||||
setProjects(data.items)
|
||||
} catch (err) {
|
||||
setError("Failed to load projects.")
|
||||
} finally {
|
||||
@@ -90,7 +95,7 @@ export default function LocationPage() {
|
||||
const loadAllPackages = async () => {
|
||||
try {
|
||||
const data = await fetchAllPackages({ skip: 0, limit: 1000 })
|
||||
setAllPackages(data)
|
||||
setAllPackages(data.items)
|
||||
} catch (err) {
|
||||
console.error("Failed to load all packages")
|
||||
}
|
||||
@@ -115,7 +120,7 @@ export default function LocationPage() {
|
||||
setLoadingPackages(true)
|
||||
if (!isEditing) setSelectedPackageId("")
|
||||
const data = await fetchPackagesByProject(selectedProjectId, { skip: 0, limit: 1000 })
|
||||
setPackages(data)
|
||||
setPackages(data.items)
|
||||
} catch (err) {
|
||||
setError("Failed to load packages for the selected project.")
|
||||
} finally {
|
||||
@@ -392,6 +397,7 @@ export default function LocationPage() {
|
||||
pagination={{
|
||||
skip,
|
||||
limit,
|
||||
totalItems,
|
||||
onPageChange: setSkip,
|
||||
onLimitChange: (newLimit) => {
|
||||
setLimit(newLimit);
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { useRouter } from "next/navigation"
|
||||
import dynamic from "next/dynamic"
|
||||
import { type SessionContext, saveSession } from "@/lib/api"
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { PowerCircle, TrendingUp } from "lucide-react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { PoweredBy } from "@/components/powered-by"
|
||||
import { ROUTES } from "@/utils/routes"
|
||||
import { Reveal } from "@/components/ui/reveal"
|
||||
|
||||
const ProjectSelectionSection = dynamic(
|
||||
() => import("@/components/project-selection-section").then(mod => mod.ProjectSelectionSection),
|
||||
@@ -39,13 +40,19 @@ export default function NewAnalysisPage() {
|
||||
</div>
|
||||
|
||||
{/* Project Selection Section */}
|
||||
<div>
|
||||
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
|
||||
</div>
|
||||
<Reveal delay={0.2} direction="up">
|
||||
<div>
|
||||
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<PoweredBy />
|
||||
|
||||
<Reveal delay={0.4}>
|
||||
<PoweredBy />
|
||||
</Reveal>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { PoweredBy } from "@/components/powered-by"
|
||||
|
||||
export default function PackagePage() {
|
||||
const [packages, setPackages] = useState<PackageType[]>([])
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
@@ -55,11 +56,15 @@ export default function PackagePage() {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const data = await fetchAllPackages({ skip: currentSkip, limit: currentLimit })
|
||||
setPackages(data)
|
||||
setPackages(data.items)
|
||||
setTotalItems(data.totalItems)
|
||||
} catch (err) {
|
||||
setError("Failed to load packages. Please check if the backend is running.")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
// Add a small delay for animation stability
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +72,7 @@ export default function PackagePage() {
|
||||
try {
|
||||
setLoadingProjects(true)
|
||||
const data = await fetchProjects({ skip: 0, limit: 1000 }) // Load all projects for selector
|
||||
setProjects(data)
|
||||
setProjects(data.items)
|
||||
} catch (err) {
|
||||
setError("Failed to load projects.")
|
||||
} finally {
|
||||
@@ -271,6 +276,7 @@ export default function PackagePage() {
|
||||
pagination={{
|
||||
skip,
|
||||
limit,
|
||||
totalItems,
|
||||
onPageChange: setSkip,
|
||||
onLimitChange: (newLimit) => {
|
||||
setLimit(newLimit);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Loader2, CheckCircle2, FolderPlus, MapPin, Building2, Route, X } from "lucide-react"
|
||||
import { Loader2, CheckCircle2, Layers, MapPin, Building2, Route, X } from "lucide-react"
|
||||
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"
|
||||
@@ -18,6 +18,7 @@ import { PoweredBy } from "@/components/powered-by"
|
||||
|
||||
export default function ProjectPage() {
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@@ -46,11 +47,15 @@ export default function ProjectPage() {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const data = await fetchProjects({ skip: currentSkip, limit: currentLimit })
|
||||
setProjects(data)
|
||||
setProjects(data.items)
|
||||
setTotalItems(data.totalItems)
|
||||
} catch (err) {
|
||||
setError("Failed to load projects. Please check if the backend is running.")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
// Add a small delay for animation stability
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,12 +238,12 @@ export default function ProjectPage() {
|
||||
<PageHeader
|
||||
title="Project Management"
|
||||
description="Manage road infrastructure projects"
|
||||
icon={FolderPlus}
|
||||
icon={Layers}
|
||||
actions={
|
||||
<Button
|
||||
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
|
||||
>
|
||||
<FolderPlus className="mr-2 h-5 w-5" />
|
||||
<Layers className="mr-2 h-5 w-5" />
|
||||
Add New Project
|
||||
</Button>
|
||||
}
|
||||
@@ -258,6 +263,7 @@ export default function ProjectPage() {
|
||||
pagination={{
|
||||
skip,
|
||||
limit,
|
||||
totalItems,
|
||||
onPageChange: setSkip,
|
||||
onLimitChange: (newLimit) => {
|
||||
setLimit(newLimit);
|
||||
@@ -280,7 +286,7 @@ export default function ProjectPage() {
|
||||
<DialogHeader className="gap-2">
|
||||
<DialogTitle className="flex items-center gap-3 text-xl">
|
||||
<div className="p-2 rounded-lg bg-primary text-primary-foreground shadow-sm">
|
||||
<FolderPlus className="h-5 w-5" />
|
||||
<Layers className="h-5 w-5" />
|
||||
</div>
|
||||
{isEditing ? 'Edit Project Details' : 'Create New Project'}
|
||||
</DialogTitle>
|
||||
@@ -431,7 +437,7 @@ export default function ProjectPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <FolderPlus className="h-4 w-4" />}
|
||||
{isEditing ? <CheckCircle2 className="h-4 w-4" /> : <Layers className="h-4 w-4" />}
|
||||
<span>{isEditing ? 'Update Project' : 'Create Project'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
20
src/app/(modules)/template.tsx
Normal file
20
src/app/(modules)/template.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import React from "react";
|
||||
|
||||
export default function Template({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.21, 0.47, 0.32, 0.98],
|
||||
}}
|
||||
className="flex-1 flex flex-col"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Plus,
|
||||
FolderPlus,
|
||||
Layers,
|
||||
Package,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
@@ -45,7 +45,7 @@ const data = {
|
||||
{
|
||||
title: "Project",
|
||||
url: ROUTES.PROJECT,
|
||||
icon: FolderPlus,
|
||||
icon: Layers,
|
||||
},
|
||||
{
|
||||
title: "Package",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
|
||||
interface StatsCardProps {
|
||||
title: string
|
||||
@@ -19,34 +20,41 @@ export function StatsCard({
|
||||
isLoading = false
|
||||
}: StatsCardProps) {
|
||||
return (
|
||||
<Card className="py-6 px-6 hover:scale-105 transition-all duration-300 ease-in-out">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="h-10 w-24 bg-muted rounded-md mt-2 animate-pulse" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-3xl font-bold">
|
||||
{value}
|
||||
</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{subtitle}
|
||||
<motion.div
|
||||
whileHover={{ y: -4, scale: 1.02 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
className="h-full"
|
||||
>
|
||||
<Card className="h-full py-6 px-4 transition-colors cursor-pointer">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="h-10 w-24 bg-muted rounded-md mt-2 animate-pulse" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-3xl font-bold tracking-tight">
|
||||
{value}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md bg-secondary text-secondary-foreground">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="p-3 rounded-xl bg-primary/10 text-primary shrink-0 group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { LucideIcon } from "lucide-react"
|
||||
import { Reveal } from "@/components/ui/reveal"
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string
|
||||
@@ -12,26 +13,29 @@ interface PageHeaderProps {
|
||||
|
||||
export function PageHeader({ title, description, icon: Icon, children, actions }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* <div className="p-3.5 rounded-xl bg-primary text-primary-foreground flex items-center justify-center shadow-lg shadow-primary/5 ring-1 ring-white/10">
|
||||
{Icon && <Icon className="h-7 w-7" />}
|
||||
{children}
|
||||
</div> */}
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl font-extrabold tracking-tight ">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm font-semibold tracking-wide opacity-80">
|
||||
{description}
|
||||
</p>
|
||||
<Reveal direction="down" className="w-full">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* <div className="p-3.5 rounded-xl bg-primary text-primary-foreground flex items-center justify-center shadow-lg shadow-primary/5 ring-1 ring-white/10">
|
||||
{Icon && <Icon className="h-7 w-7" />}
|
||||
{children}
|
||||
</div> */}
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl font-extrabold tracking-tight ">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm font-semibold tracking-wide opacity-80">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="flex items-center gap-4">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="flex items-center gap-4">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Reveal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
|
||||
setLoadingProjects(true)
|
||||
setError(null)
|
||||
const data = await fetchProjects()
|
||||
setProjects(data)
|
||||
setProjects(data.items)
|
||||
} catch (err) {
|
||||
console.error("Failed to load projects:", err)
|
||||
setError("Failed to load projects. Please check if the backend is running.")
|
||||
@@ -74,7 +74,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
|
||||
setSelectedLocation(null)
|
||||
setLocations([])
|
||||
const data = await fetchPackagesByProject(selectedProject.id)
|
||||
setPackages(data)
|
||||
setPackages(data.items)
|
||||
} catch (err) {
|
||||
console.error("Failed to load packages:", err)
|
||||
setError("Failed to load packages for the selected project.")
|
||||
@@ -99,7 +99,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
|
||||
setError(null)
|
||||
setSelectedLocation(null)
|
||||
const data = await fetchLocationsByPackage(selectedPackage.id)
|
||||
setLocations(data)
|
||||
setLocations(data.items)
|
||||
} catch (err) {
|
||||
console.error("Failed to load locations:", err)
|
||||
setError("Failed to load locations for the selected package.")
|
||||
|
||||
122
src/components/ui/reveal.tsx
Normal file
122
src/components/ui/reveal.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
import React from "react";
|
||||
|
||||
interface RevealProps extends HTMLMotionProps<"div"> {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
direction?: "up" | "down" | "left" | "right" | "none";
|
||||
duration?: number;
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
export function Reveal({
|
||||
children,
|
||||
delay = 0,
|
||||
direction = "up",
|
||||
duration = 0.5,
|
||||
distance = 20,
|
||||
className,
|
||||
...props
|
||||
}: RevealProps) {
|
||||
const offsets = {
|
||||
up: { y: distance },
|
||||
down: { y: -distance },
|
||||
left: { x: distance },
|
||||
right: { x: -distance },
|
||||
none: {},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
...(direction !== "none" ? offsets[direction] : {})
|
||||
}}
|
||||
whileInView={{ opacity: 1, x: 0, y: 0 }}
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
transition={{
|
||||
duration,
|
||||
delay,
|
||||
ease: [0.21, 0.47, 0.32, 0.98],
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StaggerContainerProps extends HTMLMotionProps<"div"> {
|
||||
children: React.ReactNode;
|
||||
staggerChildren?: number;
|
||||
delayChildren?: number;
|
||||
}
|
||||
|
||||
export function StaggerContainer({
|
||||
children,
|
||||
staggerChildren = 0.1,
|
||||
delayChildren = 0,
|
||||
className,
|
||||
...props
|
||||
}: StaggerContainerProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren,
|
||||
delayChildren,
|
||||
},
|
||||
},
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StaggerItem({
|
||||
children,
|
||||
direction = "up",
|
||||
distance = 20,
|
||||
className,
|
||||
...props
|
||||
}: RevealProps) {
|
||||
const offsets = {
|
||||
up: { y: distance },
|
||||
down: { y: -distance },
|
||||
left: { x: distance },
|
||||
right: { x: -distance },
|
||||
none: {},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
...(direction !== "none" ? offsets[direction] : {})
|
||||
},
|
||||
show: { opacity: 1, x: 0, y: 0 },
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: [0.21, 0.47, 0.32, 0.98],
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,11 @@ export interface Location {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[]
|
||||
totalItems: number
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
skip?: number;
|
||||
limit?: number;
|
||||
@@ -246,46 +251,46 @@ export async function deleteLocation(locationId: string): Promise<{ message: str
|
||||
/**
|
||||
* Fetch all projects
|
||||
*/
|
||||
export async function fetchProjects(params?: PaginationParams): Promise<Project[]> {
|
||||
export async function fetchProjects(params?: PaginationParams): Promise<PaginatedResponse<Project>> {
|
||||
const skip = params?.skip ?? 0
|
||||
const limit = params?.limit ?? 100
|
||||
return apiRequest<Project[]>(`/projects/?skip=${skip}&limit=${limit}`)
|
||||
return apiRequest<PaginatedResponse<Project>>(`/projects/?skip=${skip}&limit=${limit}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch packages filtered by project ID
|
||||
*/
|
||||
export async function fetchPackagesByProject(projectId: string, params?: PaginationParams): Promise<Package[]> {
|
||||
export async function fetchPackagesByProject(projectId: string, params?: PaginationParams): Promise<PaginatedResponse<Package>> {
|
||||
const skip = params?.skip ?? 0
|
||||
const limit = params?.limit ?? 100
|
||||
return apiRequest<Package[]>(`/packages/?project_id=${projectId}&skip=${skip}&limit=${limit}`)
|
||||
return apiRequest<PaginatedResponse<Package>>(`/packages/?project_id=${projectId}&skip=${skip}&limit=${limit}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch locations filtered by package ID
|
||||
*/
|
||||
export async function fetchLocationsByPackage(packageId: string, params?: PaginationParams): Promise<Location[]> {
|
||||
export async function fetchLocationsByPackage(packageId: string, params?: PaginationParams): Promise<PaginatedResponse<Location>> {
|
||||
const skip = params?.skip ?? 0
|
||||
const limit = params?.limit ?? 100
|
||||
return apiRequest<Location[]>(`/locations/?package_id=${packageId}&skip=${skip}&limit=${limit}`)
|
||||
return apiRequest<PaginatedResponse<Location>>(`/locations/?package_id=${packageId}&skip=${skip}&limit=${limit}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all packages (for dashboard)
|
||||
*/
|
||||
export async function fetchAllPackages(params?: PaginationParams): Promise<Package[]> {
|
||||
export async function fetchAllPackages(params?: PaginationParams): Promise<PaginatedResponse<Package>> {
|
||||
const skip = params?.skip ?? 0
|
||||
const limit = params?.limit ?? 100
|
||||
return apiRequest<Package[]>(`/packages/?skip=${skip}&limit=${limit}`)
|
||||
return apiRequest<PaginatedResponse<Package>>(`/packages/?skip=${skip}&limit=${limit}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all locations (for dashboard)
|
||||
*/
|
||||
export async function fetchAllLocations(params?: PaginationParams): Promise<Location[]> {
|
||||
export async function fetchAllLocations(params?: PaginationParams): Promise<PaginatedResponse<Location>> {
|
||||
const skip = params?.skip ?? 0
|
||||
const limit = params?.limit ?? 100
|
||||
return apiRequest<Location[]>(`/locations/?skip=${skip}&limit=${limit}`)
|
||||
return apiRequest<PaginatedResponse<Location>>(`/locations/?skip=${skip}&limit=${limit}`)
|
||||
}
|
||||
|
||||
// Video type for dashboard
|
||||
@@ -366,7 +371,7 @@ export interface Detection {
|
||||
export async function fetchAllDetections(): Promise<Detection[]> {
|
||||
try {
|
||||
// First get all projects
|
||||
const projects = await fetchProjects()
|
||||
const projects = (await fetchProjects()).items
|
||||
|
||||
// Then fetch detections for each project
|
||||
const allDetections: Detection[] = []
|
||||
|
||||
Reference in New Issue
Block a user