refactor: add animations

This commit is contained in:
2026-03-18 12:56:39 +05:30
parent c650da161e
commit f35389ab69
14 changed files with 480 additions and 206 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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