diff --git a/package-lock.json b/package-lock.json
index cca8f2b..a85b385 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"clsx": "^2.1.1",
"leaflet": "^1.9.4",
"lucide-react": "^0.548.0",
+ "motion": "^12.38.0",
"next": "16.0.10",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
@@ -3381,6 +3382,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.38.0",
+ "motion-utils": "^12.36.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -3726,6 +3754,47 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
+ "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.38.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.36.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.36.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
+ "license": "MIT"
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
diff --git a/package.json b/package.json
index 4501ac4..c5735a5 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"clsx": "^2.1.1",
"leaflet": "^1.9.4",
"lucide-react": "^0.548.0",
+ "motion": "^12.38.0",
"next": "16.0.10",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
diff --git a/src/app/(modules)/dashboard/page.tsx b/src/app/(modules)/dashboard/page.tsx
index 8971f56..2d683d6 100644
--- a/src/app/(modules)/dashboard/page.tsx
+++ b/src/app/(modules)/dashboard/page.tsx
@@ -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}
/>
-
-
-
-
-
-
-
-
- Filter Analysis
-
-
Refine your view by project, package, or location
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Filter Analysis
+
+
Refine your view by project, package, or location
+
+
+
+
+
+
+
+
{isLoading && !projectSummary ? (
) : (
<>
{/* Stats Cards - Top Row */}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{/* Charts Row - Side by Side */}
- {/* Left Chart - Detection Distribution */}
-
-
- Detection Distribution
- Breakdown of all detected road conditions
-
-
-
-
-
-
- Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
-
-
-
+
+
+
+ Detection Distribution
+ Breakdown of all detected road conditions
+
+
+
+
+
+
+ Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
+
+
+
+
- {/* Right Chart - Detections by Location */}
-
-
- Detections by Location
- Frequency of road issues across different map segments
-
-
- ({
- 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,
- }))}
- />
-
-
-
- Analysis based on latest processed sequence
-
-
-
+
+
+
+ Detections by Location
+ Frequency of road issues across different map segments
+
+
+ ({
+ 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,
+ }))}
+ />
+
+
+
+ Analysis based on latest processed sequence
+
+
+
+
{/* Map - Full Width Below Charts */}
-
-
-
+
+
+
+
+
>
)}
diff --git a/src/app/(modules)/location/page.tsx b/src/app/(modules)/location/page.tsx
index 3b0df0f..0b43bef 100644
--- a/src/app/(modules)/location/page.tsx
+++ b/src/app/(modules)/location/page.tsx
@@ -32,6 +32,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
export default function LocationPage() {
const [locations, setLocations] = useState([])
+ const [totalItems, setTotalItems] = useState(0)
const [projects, setProjects] = useState([])
const [packages, setPackages] = useState([])
const [allPackages, setAllPackages] = useState([])
@@ -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);
diff --git a/src/app/(modules)/new-analysis/page.tsx b/src/app/(modules)/new-analysis/page.tsx
index e8e850d..563d0ad 100644
--- a/src/app/(modules)/new-analysis/page.tsx
+++ b/src/app/(modules)/new-analysis/page.tsx
@@ -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() {
{/* Project Selection Section */}
-
+
+
+
-
+
+
+
+
)
}
+
diff --git a/src/app/(modules)/package/page.tsx b/src/app/(modules)/package/page.tsx
index 0730eec..aef22a0 100644
--- a/src/app/(modules)/package/page.tsx
+++ b/src/app/(modules)/package/page.tsx
@@ -29,6 +29,7 @@ import { PoweredBy } from "@/components/powered-by"
export default function PackagePage() {
const [packages, setPackages] = useState([])
+ const [totalItems, setTotalItems] = useState(0)
const [projects, setProjects] = useState([])
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);
diff --git a/src/app/(modules)/project/page.tsx b/src/app/(modules)/project/page.tsx
index 69a591a..f7178ad 100644
--- a/src/app/(modules)/project/page.tsx
+++ b/src/app/(modules)/project/page.tsx
@@ -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([])
+ 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() {
{ setIsEditing(false); setIsModalOpen(true); }}
>
-
+
Add New Project
}
@@ -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() {
-
+
{isEditing ? 'Edit Project Details' : 'Create New Project'}
@@ -431,7 +437,7 @@ export default function ProjectPage() {
) : (
- {isEditing ? : }
+ {isEditing ? : }
{isEditing ? 'Update Project' : 'Create Project'}
)}
diff --git a/src/app/(modules)/template.tsx b/src/app/(modules)/template.tsx
new file mode 100644
index 0000000..2564b80
--- /dev/null
+++ b/src/app/(modules)/template.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { motion } from "motion/react";
+import React from "react";
+
+export default function Template({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx
index 4844c1a..bf5632c 100644
--- a/src/components/app-sidebar.tsx
+++ b/src/components/app-sidebar.tsx
@@ -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",
diff --git a/src/components/dashboard/stats-card.tsx b/src/components/dashboard/stats-card.tsx
index 511b232..7fb99ec 100644
--- a/src/components/dashboard/stats-card.tsx
+++ b/src/components/dashboard/stats-card.tsx
@@ -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 (
-
-
-
-
- {title}
-
-
- {isLoading ? (
-
- ) : (
- <>
-
- {value}
-
- {subtitle && (
-
- {subtitle}
+
+
+
+
+
+ {title}
+
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+ {value}
- )}
- >
- )}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ >
+ )}
+
-
-
-
-
-
-
+
+
+
+
+
+
)
}
+
diff --git a/src/components/page-header.tsx b/src/components/page-header.tsx
index d9460c3..adcd8cc 100644
--- a/src/components/page-header.tsx
+++ b/src/components/page-header.tsx
@@ -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 (
-
-
- {/*
- {Icon && }
- {children}
-
*/}
-
-
- {title}
-
-
- {description}
-
+
+
+
+ {/*
+ {Icon && }
+ {children}
+
*/}
+
+
+ {title}
+
+
+ {description}
+
+
+ {actions && (
+
+ {actions}
+
+ )}
- {actions && (
-
- {actions}
-
- )}
-
+
)
}
+
diff --git a/src/components/project-selection-section.tsx b/src/components/project-selection-section.tsx
index c9a6800..d1a58e9 100644
--- a/src/components/project-selection-section.tsx
+++ b/src/components/project-selection-section.tsx
@@ -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.")
diff --git a/src/components/ui/reveal.tsx b/src/components/ui/reveal.tsx
new file mode 100644
index 0000000..d159dfa
--- /dev/null
+++ b/src/components/ui/reveal.tsx
@@ -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 (
+
+ {children}
+
+ );
+}
+
+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 (
+
+ {children}
+
+ );
+}
+
+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 (
+
+ {children}
+
+ );
+}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 2726c93..936ff13 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -43,6 +43,11 @@ export interface Location {
updated_at: string
}
+export interface PaginatedResponse
{
+ 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 {
+export async function fetchProjects(params?: PaginationParams): Promise> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
- return apiRequest(`/projects/?skip=${skip}&limit=${limit}`)
+ return apiRequest>(`/projects/?skip=${skip}&limit=${limit}`)
}
/**
* Fetch packages filtered by project ID
*/
-export async function fetchPackagesByProject(projectId: string, params?: PaginationParams): Promise {
+export async function fetchPackagesByProject(projectId: string, params?: PaginationParams): Promise> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
- return apiRequest(`/packages/?project_id=${projectId}&skip=${skip}&limit=${limit}`)
+ return apiRequest>(`/packages/?project_id=${projectId}&skip=${skip}&limit=${limit}`)
}
/**
* Fetch locations filtered by package ID
*/
-export async function fetchLocationsByPackage(packageId: string, params?: PaginationParams): Promise {
+export async function fetchLocationsByPackage(packageId: string, params?: PaginationParams): Promise> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
- return apiRequest(`/locations/?package_id=${packageId}&skip=${skip}&limit=${limit}`)
+ return apiRequest>(`/locations/?package_id=${packageId}&skip=${skip}&limit=${limit}`)
}
/**
* Fetch all packages (for dashboard)
*/
-export async function fetchAllPackages(params?: PaginationParams): Promise {
+export async function fetchAllPackages(params?: PaginationParams): Promise> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
- return apiRequest(`/packages/?skip=${skip}&limit=${limit}`)
+ return apiRequest>(`/packages/?skip=${skip}&limit=${limit}`)
}
/**
* Fetch all locations (for dashboard)
*/
-export async function fetchAllLocations(params?: PaginationParams): Promise {
+export async function fetchAllLocations(params?: PaginationParams): Promise> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
- return apiRequest(`/locations/?skip=${skip}&limit=${limit}`)
+ return apiRequest>(`/locations/?skip=${skip}&limit=${limit}`)
}
// Video type for dashboard
@@ -366,7 +371,7 @@ export interface Detection {
export async function fetchAllDetections(): Promise {
try {
// First get all projects
- const projects = await fetchProjects()
+ const projects = (await fetchProjects()).items
// Then fetch detections for each project
const allDetections: Detection[] = []