1st page implemented
This commit is contained in:
84
app/page.tsx
84
app/page.tsx
@@ -3,6 +3,10 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { UploadSection } from "@/components/upload-section"
|
import { UploadSection } from "@/components/upload-section"
|
||||||
import VideoPlayerSection from "@/components/video-player-section"
|
import VideoPlayerSection from "@/components/video-player-section"
|
||||||
|
import { ProjectSelectionSection } from "@/components/project-selection-section"
|
||||||
|
import { type SessionContext, emptySessionContext } from "@/lib/api"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ArrowLeft, MapPin, Package, FolderKanban } from "lucide-react"
|
||||||
|
|
||||||
export type DetectionType = "pothole-detection" | "sign-board-detection"
|
export type DetectionType = "pothole-detection" | "sign-board-detection"
|
||||||
|
|
||||||
@@ -67,11 +71,14 @@ export type DetectionData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DetectionPage() {
|
export default function DetectionPage() {
|
||||||
|
const [session, setSession] = useState<SessionContext>(emptySessionContext)
|
||||||
const [detectionData, setDetectionData] = useState<DetectionData | null>(null)
|
const [detectionData, setDetectionData] = useState<DetectionData | null>(null)
|
||||||
const [videoId, setVideoId] = useState<string | null>(null)
|
const [videoId, setVideoId] = useState<string | null>(null)
|
||||||
const [videoFile, setVideoFile] = useState<File | null>(null)
|
const [videoFile, setVideoFile] = useState<File | null>(null)
|
||||||
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
|
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
|
||||||
|
|
||||||
|
const isSessionComplete = session.projectId && session.packageId && session.locationId
|
||||||
|
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
return detectionType === "pothole-detection"
|
return detectionType === "pothole-detection"
|
||||||
? "Pothole Detection System"
|
? "Pothole Detection System"
|
||||||
@@ -84,28 +91,79 @@ export default function DetectionPage() {
|
|||||||
: "Upload a video to detect and identify signboards with AI-powered analysis"
|
: "Upload a video to detect and identify signboards with AI-powered analysis"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectionComplete = (newSession: SessionContext) => {
|
||||||
|
setSession(newSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBackToSelection = () => {
|
||||||
|
setSession(emptySessionContext)
|
||||||
|
setDetectionData(null)
|
||||||
|
setVideoId(null)
|
||||||
|
setVideoFile(null)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
|
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20">
|
||||||
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
|
<div className="mb-8 animate-in fade-in slide-in-from-top duration-700">
|
||||||
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
<h1 className="text-4xl font-bold mb-2 text-balance bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||||
{getTitle()}
|
{isSessionComplete ? getTitle() : "VisionRoad Detection System"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{getDescription()}</p>
|
<p className="text-muted-foreground">
|
||||||
|
{isSessionComplete ? getDescription() : "Select your project location to begin AI-powered road analysis"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload Section */}
|
{/* Session Info Bar */}
|
||||||
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
|
{isSessionComplete && (
|
||||||
<UploadSection
|
<div className="mb-6 animate-in fade-in slide-in-from-top duration-500">
|
||||||
onDetectionComplete={(data, vId, file) => {
|
<div className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border border-primary/20">
|
||||||
setDetectionData(data)
|
<div className="flex items-center gap-6 text-sm">
|
||||||
setVideoId(vId)
|
<div className="flex items-center gap-2">
|
||||||
setVideoFile(file)
|
<FolderKanban className="h-4 w-4 text-primary" />
|
||||||
}}
|
<span className="text-muted-foreground">Project:</span>
|
||||||
onDetectionTypeChange={setDetectionType}
|
<span className="font-medium">{session.projectName}</span>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<Package className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-muted-foreground">Package:</span>
|
||||||
|
<span className="font-medium">{session.packageName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPin className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-muted-foreground">Location:</span>
|
||||||
|
<span className="font-medium">{session.locationName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleBackToSelection}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Change Selection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Project Selection Section - Show when session is not complete */}
|
||||||
|
{!isSessionComplete && (
|
||||||
|
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
|
||||||
|
<ProjectSelectionSection onSelectionComplete={handleSelectionComplete} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upload Section - Show after session is complete */}
|
||||||
|
{isSessionComplete && (
|
||||||
|
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
|
||||||
|
<UploadSection
|
||||||
|
onDetectionComplete={(data, vId, file) => {
|
||||||
|
setDetectionData(data)
|
||||||
|
setVideoId(vId)
|
||||||
|
setVideoFile(file)
|
||||||
|
}}
|
||||||
|
onDetectionTypeChange={setDetectionType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Video Player Section */}
|
{/* Video Player Section */}
|
||||||
{detectionData && videoId && videoFile && (
|
{detectionData && videoId && videoFile && (
|
||||||
|
|||||||
312
components/project-selection-section.tsx
Normal file
312
components/project-selection-section.tsx
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { Loader2, MapPin, Package, FolderKanban, ArrowRight, AlertCircle } from "lucide-react"
|
||||||
|
import {
|
||||||
|
fetchProjects,
|
||||||
|
fetchPackagesByProject,
|
||||||
|
fetchLocationsByPackage,
|
||||||
|
type Project,
|
||||||
|
type Package as PackageType,
|
||||||
|
type Location,
|
||||||
|
type SessionContext
|
||||||
|
} from "@/lib/api"
|
||||||
|
|
||||||
|
type ProjectSelectionSectionProps = {
|
||||||
|
onSelectionComplete: (session: SessionContext) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectionSectionProps) {
|
||||||
|
// Data states
|
||||||
|
const [projects, setProjects] = useState<Project[]>([])
|
||||||
|
const [packages, setPackages] = useState<PackageType[]>([])
|
||||||
|
const [locations, setLocations] = useState<Location[]>([])
|
||||||
|
|
||||||
|
// Selection states
|
||||||
|
const [selectedProject, setSelectedProject] = useState<Project | null>(null)
|
||||||
|
const [selectedPackage, setSelectedPackage] = useState<PackageType | null>(null)
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null)
|
||||||
|
|
||||||
|
// Loading states
|
||||||
|
const [loadingProjects, setLoadingProjects] = useState(true)
|
||||||
|
const [loadingPackages, setLoadingPackages] = useState(false)
|
||||||
|
const [loadingLocations, setLoadingLocations] = useState(false)
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Load projects on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadProjects = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingProjects(true)
|
||||||
|
setError(null)
|
||||||
|
const data = await fetchProjects()
|
||||||
|
setProjects(data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load projects:", err)
|
||||||
|
setError("Failed to load projects. Please check if the backend is running.")
|
||||||
|
} finally {
|
||||||
|
setLoadingProjects(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadProjects()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load packages when project changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedProject) {
|
||||||
|
setPackages([])
|
||||||
|
setSelectedPackage(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPackages = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingPackages(true)
|
||||||
|
setError(null)
|
||||||
|
setSelectedPackage(null)
|
||||||
|
setSelectedLocation(null)
|
||||||
|
setLocations([])
|
||||||
|
const data = await fetchPackagesByProject(selectedProject.id)
|
||||||
|
setPackages(data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load packages:", err)
|
||||||
|
setError("Failed to load packages for the selected project.")
|
||||||
|
} finally {
|
||||||
|
setLoadingPackages(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadPackages()
|
||||||
|
}, [selectedProject])
|
||||||
|
|
||||||
|
// Load locations when package changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPackage) {
|
||||||
|
setLocations([])
|
||||||
|
setSelectedLocation(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadLocations = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingLocations(true)
|
||||||
|
setError(null)
|
||||||
|
setSelectedLocation(null)
|
||||||
|
const data = await fetchLocationsByPackage(selectedPackage.id)
|
||||||
|
setLocations(data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load locations:", err)
|
||||||
|
setError("Failed to load locations for the selected package.")
|
||||||
|
} finally {
|
||||||
|
setLoadingLocations(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadLocations()
|
||||||
|
}, [selectedPackage])
|
||||||
|
|
||||||
|
const handleProjectChange = (projectId: string) => {
|
||||||
|
const project = projects.find(p => p.id === projectId) || null
|
||||||
|
setSelectedProject(project)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePackageChange = (packageId: string) => {
|
||||||
|
const pkg = packages.find(p => p.id === packageId) || null
|
||||||
|
setSelectedPackage(pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLocationChange = (locationId: string) => {
|
||||||
|
const location = locations.find(l => l.id === locationId) || null
|
||||||
|
setSelectedLocation(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProceed = () => {
|
||||||
|
if (selectedProject && selectedPackage && selectedLocation) {
|
||||||
|
onSelectionComplete({
|
||||||
|
projectId: selectedProject.id,
|
||||||
|
projectName: selectedProject.name,
|
||||||
|
packageId: selectedPackage.id,
|
||||||
|
packageName: selectedPackage.name,
|
||||||
|
locationId: selectedLocation.id,
|
||||||
|
locationName: selectedLocation.segment_name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isComplete = selectedProject && selectedPackage && selectedLocation
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="transition-all hover:shadow-lg border-0 bg-gradient-to-br from-card via-card to-muted/30">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-lg bg-gradient-to-br from-primary/20 to-primary/5">
|
||||||
|
<FolderKanban className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl">Select Project Location</CardTitle>
|
||||||
|
<CardDescription className="mt-1">
|
||||||
|
Choose your project, package, and location to begin video analysis
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive animate-in fade-in slide-in-from-top duration-300">
|
||||||
|
<AlertCircle className="h-5 w-5 mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{/* Project Dropdown */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="project" className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<FolderKanban className="h-4 w-4 text-muted-foreground" />
|
||||||
|
Project
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedProject?.id || ""}
|
||||||
|
onValueChange={handleProjectChange}
|
||||||
|
disabled={loadingProjects}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="project" className="h-11">
|
||||||
|
{loadingProjects ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a project" />
|
||||||
|
)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<SelectItem key={project.id} value={project.id}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{project.name}</span>
|
||||||
|
{project.corridor_name && (
|
||||||
|
<span className="text-xs text-muted-foreground">{project.corridor_name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{selectedProject?.state && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
State: {selectedProject.state}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Package Dropdown */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="package" className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Package className="h-4 w-4 text-muted-foreground" />
|
||||||
|
Package
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedPackage?.id || ""}
|
||||||
|
onValueChange={handlePackageChange}
|
||||||
|
disabled={!selectedProject || loadingPackages}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="package" className="h-11">
|
||||||
|
{loadingPackages ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder={selectedProject ? "Select a package" : "Select project first"} />
|
||||||
|
)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{packages.map((pkg) => (
|
||||||
|
<SelectItem key={pkg.id} value={pkg.id}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{pkg.name}</span>
|
||||||
|
{pkg.region && (
|
||||||
|
<span className="text-xs text-muted-foreground">{pkg.region}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location Dropdown */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="location" className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||||
|
Location
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedLocation?.id || ""}
|
||||||
|
onValueChange={handleLocationChange}
|
||||||
|
disabled={!selectedPackage || loadingLocations}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="location" className="h-11">
|
||||||
|
{loadingLocations ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder={selectedPackage ? "Select a location" : "Select package first"} />
|
||||||
|
)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{locations.map((location) => (
|
||||||
|
<SelectItem key={location.id} value={location.id}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{location.segment_name}</span>
|
||||||
|
{location.chainage_start_km !== null && location.chainage_end_km !== null && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
KM {location.chainage_start_km} - {location.chainage_end_km}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected Summary */}
|
||||||
|
{isComplete && (
|
||||||
|
<div className="p-4 rounded-lg bg-primary/5 border border-primary/20 animate-in fade-in slide-in-from-bottom duration-300">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">Selected:</p>
|
||||||
|
<p className="font-medium">
|
||||||
|
{selectedProject?.name} → {selectedPackage?.name} → {selectedLocation?.segment_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Proceed Button */}
|
||||||
|
<Button
|
||||||
|
onClick={handleProceed}
|
||||||
|
disabled={!isComplete}
|
||||||
|
className="w-full h-12 text-base font-semibold transition-all"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{isComplete ? (
|
||||||
|
<>
|
||||||
|
Proceed to Upload
|
||||||
|
<ArrowRight className="ml-2 h-5 w-5" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Complete all selections to proceed"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
103
lib/api.ts
Normal file
103
lib/api.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* API Service Layer for VisionRoad Frontend
|
||||||
|
* Provides typed functions for interacting with the backend API
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1"
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
export interface Project {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
state: string | null
|
||||||
|
corridor_name: string | null
|
||||||
|
start_lat: number | null
|
||||||
|
start_lng: number | null
|
||||||
|
end_lat: number | null
|
||||||
|
end_lng: number | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Package {
|
||||||
|
id: string
|
||||||
|
project_id: string
|
||||||
|
name: string
|
||||||
|
region: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Location {
|
||||||
|
id: string
|
||||||
|
package_id: string
|
||||||
|
segment_name: string
|
||||||
|
chainage_start_km: number | null
|
||||||
|
chainage_end_km: number | null
|
||||||
|
start_lat: number
|
||||||
|
start_lng: number
|
||||||
|
end_lat: number
|
||||||
|
end_lng: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for API requests
|
||||||
|
async function apiRequest<T>(endpoint: string): Promise<T> {
|
||||||
|
const response = await fetch(`${API_URL}${endpoint}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"ngrok-skip-browser-warning": "true"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API Error: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all projects
|
||||||
|
*/
|
||||||
|
export async function fetchProjects(): Promise<Project[]> {
|
||||||
|
return apiRequest<Project[]>("/projects/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch packages filtered by project ID
|
||||||
|
*/
|
||||||
|
export async function fetchPackagesByProject(projectId: string): Promise<Package[]> {
|
||||||
|
return apiRequest<Package[]>(`/packages/?project_id=${projectId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch locations filtered by package ID
|
||||||
|
*/
|
||||||
|
export async function fetchLocationsByPackage(packageId: string): Promise<Location[]> {
|
||||||
|
return apiRequest<Location[]>(`/locations/?package_id=${packageId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session context type for storing user selections
|
||||||
|
*/
|
||||||
|
export interface SessionContext {
|
||||||
|
projectId: string | null
|
||||||
|
projectName: string | null
|
||||||
|
packageId: string | null
|
||||||
|
packageName: string | null
|
||||||
|
locationId: string | null
|
||||||
|
locationName: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emptySessionContext: SessionContext = {
|
||||||
|
projectId: null,
|
||||||
|
projectName: null,
|
||||||
|
packageId: null,
|
||||||
|
packageName: null,
|
||||||
|
locationId: null,
|
||||||
|
locationName: null
|
||||||
|
}
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -2480,6 +2480,7 @@
|
|||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2490,6 +2491,7 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -2591,6 +2593,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2853,7 +2856,8 @@
|
|||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
|
||||||
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==",
|
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
@@ -3300,6 +3304,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
||||||
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
|
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.0.10",
|
"@next/env": "16.0.10",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
@@ -3425,6 +3430,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3462,6 +3468,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3492,6 +3499,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -3504,6 +3512,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
|
||||||
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
|
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -3789,7 +3798,8 @@
|
|||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
|||||||
Reference in New Issue
Block a user