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