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[] = []