diff --git a/Readme.md b/Readme.md index 0264b06..ab1751e 100644 --- a/Readme.md +++ b/Readme.md @@ -163,7 +163,6 @@ if __name__ == "__main__": **`.env.local`** ```bash -NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 NEXT_PUBLIC_WS_URL=ws://localhost:8000/api/v1 ``` diff --git a/app/create-location/page.tsx b/app/create-location/page.tsx index 07f4d21..90e6a77 100644 --- a/app/create-location/page.tsx +++ b/app/create-location/page.tsx @@ -16,11 +16,15 @@ import { fetchAllLocations, fetchAllPackages, createLocation, + updateLocation, + deleteLocation, type Project, type Package as PackageType, type Location, - type LocationCreate + type LocationCreate, + type LocationUpdate } from "@/lib/api" +import { toast } from "sonner" export default function CreateLocationPage() { const [locations, setLocations] = useState([]) @@ -30,11 +34,14 @@ export default function CreateLocationPage() { const [isLoading, setIsLoading] = useState(true) const [isModalOpen, setIsModalOpen] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) - const [success, setSuccess] = useState(false) const [error, setError] = useState(null) const [loadingProjects, setLoadingProjects] = useState(false) const [loadingPackages, setLoadingPackages] = useState(false) + // Editing state + const [isEditing, setIsEditing] = useState(false) + const [currentLocation, setCurrentLocation] = useState(null) + // Form fields const [selectedProjectId, setSelectedProjectId] = useState("") const [selectedPackageId, setSelectedPackageId] = useState("") @@ -91,14 +98,14 @@ export default function CreateLocationPage() { useEffect(() => { if (!selectedProjectId) { setPackages([]) - setSelectedPackageId("") + if (!isEditing) setSelectedPackageId("") return } - const loadPackages = async () => { + const loadPackagesForProject = async () => { try { setLoadingPackages(true) - setSelectedPackageId("") + if (!isEditing) setSelectedPackageId("") const data = await fetchPackagesByProject(selectedProjectId) setPackages(data) } catch (err) { @@ -107,8 +114,8 @@ export default function CreateLocationPage() { setLoadingPackages(false) } } - loadPackages() - }, [selectedProjectId]) + loadPackagesForProject() + }, [selectedProjectId, isEditing]) const resetForm = () => { setSelectedProjectId("") @@ -121,11 +128,13 @@ export default function CreateLocationPage() { setEndLat("") setEndLng("") setError(null) + setIsEditing(false) + setCurrentLocation(null) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (!selectedPackageId) { + if (!selectedPackageId && !isEditing) { setError("Please select a project and package first") return } @@ -142,41 +151,86 @@ export default function CreateLocationPage() { setError(null) try { - const data: LocationCreate = { - package_id: selectedPackageId, - segment_name: segmentName.trim(), - chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : null, - chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : null, - start_lat: parseFloat(startLat), - start_lng: parseFloat(startLng), - end_lat: parseFloat(endLat), - end_lng: parseFloat(endLng), + if (isEditing && currentLocation) { + const data: LocationUpdate = { + segment_name: segmentName.trim(), + chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : null, + chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : null, + start_lat: parseFloat(startLat), + start_lng: parseFloat(startLng), + end_lat: parseFloat(endLat), + end_lng: parseFloat(endLng), + } + await updateLocation(currentLocation.id, data) + toast.success("Location updated successfully!") + } else { + const data: LocationCreate = { + package_id: selectedPackageId, + segment_name: segmentName.trim(), + chainage_start_km: chainageStartKm ? parseFloat(chainageStartKm) : null, + chainage_end_km: chainageEndKm ? parseFloat(chainageEndKm) : null, + start_lat: parseFloat(startLat), + start_lng: parseFloat(startLng), + end_lat: parseFloat(endLat), + end_lng: parseFloat(endLng), + } + await createLocation(data) + toast.success("Location created successfully!") } - await createLocation(data) - setSuccess(true) - // Refresh locations list await loadLocations() - setTimeout(() => { - resetForm() - setSuccess(false) - setIsModalOpen(false) - }, 2000) + // Close modal and reset form immediately + setIsModalOpen(false) + resetForm() } catch (err) { - setError(err instanceof Error ? err.message : "Failed to create location") + setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} location`) } finally { setIsSubmitting(false) } } + const handleEdit = (location: Location) => { + setIsEditing(true) + setCurrentLocation(location) + + // Find project for this package + const pkg = allPackages.find(p => p.id === location.package_id) + if (pkg) { + setSelectedProjectId(pkg.project_id) + setSelectedPackageId(location.package_id) + } + + setSegmentName(location.segment_name || "") + setChainageStartKm(location.chainage_start_km?.toString() || "") + setChainageEndKm(location.chainage_end_km?.toString() || "") + setStartLat(location.start_lat.toString()) + setStartLng(location.start_lng.toString()) + setEndLat(location.end_lat.toString()) + setEndLng(location.end_lng.toString()) + setIsModalOpen(true) + } + + const handleDelete = async (location: Location) => { + if (!confirm(`Are you sure you want to delete location "${location.segment_name}"?`)) return + + try { + setIsLoading(true) + await deleteLocation(location.id) + toast.success("Location deleted successfully!") + await loadLocations() + } catch (err) { + setError("Failed to delete location") + } finally { + setIsLoading(false) + } + } + const getPackageName = (packageId: string) => { return allPackages.find(p => p.id === packageId)?.name || packageId } - const selectedProject = projects.find(p => p.id === selectedProjectId) - const selectedPackage = packages.find(p => p.id === selectedPackageId) const isFormComplete = selectedPackageId && segmentName.trim() && startLat && startLng && endLat && endLng const columns = [ @@ -241,7 +295,7 @@ export default function CreateLocationPage() {
{/* Refined Header */} -
+
@@ -259,28 +313,30 @@ export default function CreateLocationPage() { {/* Error Message */} {error && !isModalOpen && ( -
+
!
-

{error}

+

{error}

)} {/* Data Table */} -
+
setIsModalOpen(true)} + onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }} + onEdit={handleEdit} + onDelete={handleDelete} addButtonText="Add New Location" isLoading={isLoading} />
{/* Footer */} -
+

Sentient Geeks Pvt. Ltd.

@@ -289,7 +345,7 @@ export default function CreateLocationPage() {
{/* Modal Dialog */} - + { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}> e.preventDefault()} @@ -297,98 +353,97 @@ export default function CreateLocationPage() { - Create New Location + {isEditing ? 'Edit Location' : 'Create New Location'} - Select project & package, then fill in the location details + {isEditing ? 'Update disclosure details for your road infrastructure segment' : 'Select project & package, then fill in the location details'} - {/* Success Message in Modal */} - {success && ( -
- -

Location created successfully!

-
- )} {/* Error Message in Modal */} {error && ( -
+
!
-

{error}

+

{error}

)}
{/* Step 1: Select Project */} -
-
-
- 1 + {!isEditing && ( +
+
+
+ 1 +
+

Select Project

-

Select Project

+
- -
+ )} {/* Step 2: Select Package */} -
-
-
- 2 + {!isEditing && ( +
+
+
+ 2 +
+

Select Package

-

Select Package

+
- -
+ )} {/* Step 3: Location Details */} -
-
-
- 3 +
+ {!isEditing && ( +
+
+ 3 +
+

Location Information

-

Location Information

-
+ )} {/* Segment Name & Chainage Row */}
@@ -535,12 +590,12 @@ export default function CreateLocationPage() { {isSubmitting ? ( - Creating... + {isEditing ? 'Updating...' : 'Creating...'} ) : ( - - Create Location + {isEditing ? : } + {isEditing ? 'Update Location' : 'Create Location'} )} diff --git a/app/create-package/page.tsx b/app/create-package/page.tsx index 1109e74..d986674 100644 --- a/app/create-package/page.tsx +++ b/app/create-package/page.tsx @@ -14,10 +14,14 @@ import { fetchProjects, fetchAllPackages, createPackage, + updatePackage, + deletePackage, type Project, type Package as PackageType, - type PackageCreate + type PackageCreate, + type PackageUpdate } from "@/lib/api" +import { toast } from "sonner" export default function CreatePackagePage() { const [packages, setPackages] = useState([]) @@ -25,10 +29,13 @@ export default function CreatePackagePage() { const [isLoading, setIsLoading] = useState(true) const [isModalOpen, setIsModalOpen] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) - const [success, setSuccess] = useState(false) const [error, setError] = useState(null) const [loadingProjects, setLoadingProjects] = useState(false) + // Editing state + const [isEditing, setIsEditing] = useState(false) + const [currentPackage, setCurrentPackage] = useState(null) + // Form fields const [selectedProjectId, setSelectedProjectId] = useState("") const [name, setName] = useState("") @@ -70,6 +77,8 @@ export default function CreatePackagePage() { setName("") setRegion("") setError(null) + setIsEditing(false) + setCurrentPackage(null) } const handleSubmit = async (e: React.FormEvent) => { @@ -87,36 +96,64 @@ export default function CreatePackagePage() { setError(null) try { - const data: PackageCreate = { - project_id: selectedProjectId, - name: name.trim(), - region: region.trim() || null, + if (isEditing && currentPackage) { + const data: PackageUpdate = { + name: name.trim(), + region: region.trim() || null, + } + await updatePackage(currentPackage.id, data) + toast.success("Package updated successfully!") + } else { + const data: PackageCreate = { + project_id: selectedProjectId, + name: name.trim(), + region: region.trim() || null, + } + await createPackage(data) + toast.success("Package created successfully!") } - await createPackage(data) - setSuccess(true) - // Refresh packages list await loadPackages() - setTimeout(() => { - resetForm() - setSuccess(false) - setIsModalOpen(false) - }, 2000) + // Close modal and reset form immediately + setIsModalOpen(false) + resetForm() } catch (err) { - setError(err instanceof Error ? err.message : "Failed to create package") + setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} package`) } finally { setIsSubmitting(false) } } + const handleEdit = (pkg: PackageType) => { + setIsEditing(true) + setCurrentPackage(pkg) + setSelectedProjectId(pkg.project_id) + setName(pkg.name || "") + setRegion(pkg.region || "") + setIsModalOpen(true) + } + + const handleDelete = async (pkg: PackageType) => { + if (!confirm(`Are you sure you want to delete package "${pkg.name}"?`)) return + + try { + setIsLoading(true) + await deletePackage(pkg.id) + toast.success("Package deleted successfully!") + await loadPackages() + } catch (err) { + setError("Failed to delete package") + } finally { + setIsLoading(false) + } + } + const getProjectName = (projectId: string) => { return projects.find(p => p.id === projectId)?.name || projectId } - const selectedProject = projects.find(p => p.id === selectedProjectId) - const columns = [ { key: "name", @@ -162,7 +199,7 @@ export default function CreatePackagePage() {
{/* Refined Header */} -
+
@@ -180,28 +217,30 @@ export default function CreatePackagePage() { {/* Error Message */} {error && !isModalOpen && ( -
+
!
-

{error}

+

{error}

)} {/* Data Table */} -
+
setIsModalOpen(true)} + onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }} + onEdit={handleEdit} + onDelete={handleDelete} addButtonText="Add New Package" isLoading={isLoading} />
{/* Footer */} -
+

Sentient Geeks Pvt. Ltd.

@@ -210,7 +249,7 @@ export default function CreatePackagePage() {
{/* Modal Dialog */} - + { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}> e.preventDefault()} @@ -218,69 +257,67 @@ export default function CreatePackagePage() { - Create New Package + {isEditing ? 'Edit Package' : 'Create New Package'} - Select a project and fill in the package details + {isEditing ? 'Update disclosure details for your road infrastructure package' : 'Select a project and fill in the package details'} - {/* Success Message in Modal */} - {success && ( -
- -

Package created successfully!

-
- )} {/* Error Message in Modal */} {error && ( -
+
!
-

{error}

+

{error}

)} {/* Step 1: Select Project */} -
-
-
- 1 + {!isEditing && ( +
+
+
+ 1 +
+

Select Project

-

Select Project

+
- -
+ )} + {/* Step 2: Package Info */} -
-
-
- 2 +
+ {!isEditing && ( +
+
+ 2 +
+

Package Information

-

Package Information

-
+ )}
@@ -307,7 +344,7 @@ export default function CreatePackagePage() { value={region} onChange={(e) => setRegion(e.target.value)} placeholder="Region" - className="h-10 text-sm" + className="h-10 text-sm focus-visible:ring-blue-500" />
@@ -335,12 +372,12 @@ export default function CreatePackagePage() { {isSubmitting ? ( - Creating... + {isEditing ? 'Updating...' : 'Creating...'} ) : ( - - Create Package + {isEditing ? : } + {isEditing ? 'Update Package' : 'Create Package'} )} diff --git a/app/create-project/page.tsx b/app/create-project/page.tsx index 994deb9..07fc3b6 100644 --- a/app/create-project/page.tsx +++ b/app/create-project/page.tsx @@ -9,16 +9,20 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { Loader2, CheckCircle2, FolderPlus, MapPin, Building2, Route, X } from "lucide-react" import { SidebarNavigation } from "@/components/sidebar-navigation" import { DataTable } from "@/components/data-table" -import { createProject, fetchProjects, type ProjectCreate, type Project } from "@/lib/api" +import { createProject, fetchProjects, updateProject, deleteProject, type ProjectCreate, type Project, type ProjectUpdate } from "@/lib/api" +import { toast } from "sonner" export default function CreateProjectPage() { const [projects, setProjects] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isModalOpen, setIsModalOpen] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) - const [success, setSuccess] = useState(false) const [error, setError] = useState(null) + // Editing state + const [isEditing, setIsEditing] = useState(false) + const [currentProject, setCurrentProject] = useState(null) + // Form fields const [name, setName] = useState("") const [state, setState] = useState("") @@ -55,6 +59,8 @@ export default function CreateProjectPage() { setEndLat("") setEndLng("") setError(null) + setIsEditing(false) + setCurrentProject(null) } const handleSubmit = async (e: React.FormEvent) => { @@ -68,34 +74,73 @@ export default function CreateProjectPage() { setError(null) try { - const data: ProjectCreate = { - name: name.trim(), - state: state.trim() || null, - corridor_name: corridorName.trim() || null, - start_lat: startLat ? parseFloat(startLat) : null, - start_lng: startLng ? parseFloat(startLng) : null, - end_lat: endLat ? parseFloat(endLat) : null, - end_lng: endLng ? parseFloat(endLng) : null, + if (isEditing && currentProject) { + const data: ProjectUpdate = { + name: name.trim(), + state: state.trim() || null, + corridor_name: corridorName.trim() || null, + start_lat: startLat ? parseFloat(startLat) : null, + start_lng: startLng ? parseFloat(startLng) : null, + end_lat: endLat ? parseFloat(endLat) : null, + end_lng: endLng ? parseFloat(endLng) : null, + } + await updateProject(currentProject.id, data) + toast.success("Project updated successfully!") + } else { + const data: ProjectCreate = { + name: name.trim(), + state: state.trim() || null, + corridor_name: corridorName.trim() || null, + start_lat: startLat ? parseFloat(startLat) : null, + start_lng: startLng ? parseFloat(startLng) : null, + end_lat: endLat ? parseFloat(endLat) : null, + end_lng: endLng ? parseFloat(endLng) : null, + } + await createProject(data) + toast.success("Project created successfully!") } - await createProject(data) - setSuccess(true) - // Refresh projects list await loadProjects() - setTimeout(() => { - resetForm() - setSuccess(false) - setIsModalOpen(false) - }, 2000) + // Close modal and reset form immediately + setIsModalOpen(false) + resetForm() } catch (err) { - setError(err instanceof Error ? err.message : "Failed to create project") + setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} project`) } finally { setIsSubmitting(false) } } + const handleEdit = (project: Project) => { + setIsEditing(true) + setCurrentProject(project) + setName(project.name || "") + setState(project.state || "") + setCorridorName(project.corridor_name || "") + setStartLat(project.start_lat?.toString() || "") + setStartLng(project.start_lng?.toString() || "") + setEndLat(project.end_lat?.toString() || "") + setEndLng(project.end_lng?.toString() || "") + setIsModalOpen(true) + } + + const handleDelete = async (project: Project) => { + if (!confirm(`Are you sure you want to delete project "${project.name}"?`)) return + + try { + setIsLoading(true) + await deleteProject(project.id) + toast.success("Project deleted successfully!") + await loadProjects() + } catch (err) { + setError("Failed to delete project") + } finally { + setIsLoading(false) + } + } + const columns = [ { key: "name", @@ -135,7 +180,7 @@ export default function CreateProjectPage() {
{/* Refined Header */} -
+
@@ -153,28 +198,30 @@ export default function CreateProjectPage() { {/* Error Message */} {error && !isModalOpen && ( -
+
!
-

{error}

+

{error}

)} {/* Data Table */} -
+
setIsModalOpen(true)} + onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }} + onEdit={handleEdit} + onDelete={handleDelete} addButtonText="Add New Project" isLoading={isLoading} />
{/* Footer */} -
+

Sentient Geeks Pvt. Ltd.

@@ -191,28 +238,21 @@ export default function CreateProjectPage() { - Create New Project + {isEditing ? 'Edit Project' : 'Create New Project'} - Fill in the details for your new road infrastructure project + {isEditing ? 'Update disclosure details for your road infrastructure project' : 'Fill in the details for your new road infrastructure project'} - {/* Success Message in Modal */} - {success && ( -
- -

Project created successfully!

-
- )} {/* Error Message in Modal */} {error && ( -
+
!
-

{error}

+

{error}

)} @@ -347,17 +387,17 @@ export default function CreateProjectPage() { diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 418d9eb..177417a 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -22,7 +22,7 @@ import { type Project } from "@/lib/api" -const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1" +const API_URL = "http://127.0.0.1:8000/api/v1" interface ProjectSummary { project: { @@ -236,6 +236,8 @@ export default function DashboardPage() { : pkg.locations || {} for (const [locName, loc] of Object.entries(locationsToProcess)) { + if (!loc) continue + let locPotholes = 0 let locSignboards = 0 @@ -278,12 +280,12 @@ export default function DashboardPage() {
{/* Header */} -
+
- {/* Constant orbit animations */} -
-
- + {/* Constant orbit animations removed */} +
+
+ @@ -300,14 +302,14 @@ export default function DashboardPage() { {/* Error Display */} {error && ( -
+

{error}

)} {/* Stats Cards - Top Row */} -
+
{/* Filter Selector - Above main content */} -
+
{/* Charts Row - Side by Side */} -
+
{/* Left Chart - Detection Distribution */} @@ -395,7 +397,7 @@ export default function DashboardPage() {
{/* Map - Full Width Below Charts */} -
+
{/* Footer */} -
+

Sentient Geeks Pvt. Ltd.

diff --git a/app/globals.css b/app/globals.css index e7a18c7..48c9550 100644 --- a/app/globals.css +++ b/app/globals.css @@ -154,6 +154,7 @@ --color-sidebar-ring: var(--sidebar-ring); } +/* Base styles */ @layer base { * { @apply border-border outline-ring/50; @@ -164,213 +165,31 @@ } } -/* Premium Background with Animated Mesh Gradient */ -@layer utilities { - .bg-mesh-gradient { - background: #f8fafc; - } - - .dark .bg-mesh-gradient { - background: hsla(210, 30%, 15%, 1); - } - - /* Glassmorphism Card */ - .glass-card { - background: oklch(1 0 0 / 0.7); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border: 1px solid oklch(1 0 0 / 0.2); - box-shadow: - 0 4px 6px -1px oklch(0 0 0 / 0.05), - 0 10px 15px -3px oklch(0 0 0 / 0.08), - 0 20px 25px -5px oklch(0 0 0 / 0.05), - inset 0 1px 0 oklch(1 0 0 / 0.5); - } - - .dark .glass-card { - background: oklch(0.18 0.02 280 / 0.7); - border: 1px solid oklch(1 0 0 / 0.08); - box-shadow: - 0 4px 6px -1px oklch(0 0 0 / 0.15), - 0 10px 15px -3px oklch(0 0 0 / 0.20), - 0 20px 25px -5px oklch(0 0 0 / 0.15), - inset 0 1px 0 oklch(1 0 0 / 0.05); - } - - /* Gradient Text */ - .text-gradient { - background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - } - - .dark .text-gradient { - background: linear-gradient(135deg, oklch(0.75 0.20 264), oklch(0.70 0.18 200)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - } - - /* Gradient Button */ - .btn-gradient { - background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280)); - box-shadow: - 0 4px 14px 0 oklch(0.55 0.24 264 / 0.35), - inset 0 1px 0 oklch(1 0 0 / 0.15); - transition: all 0.3s ease; - } - - .btn-gradient:hover { - box-shadow: - 0 6px 20px 0 oklch(0.55 0.24 264 / 0.45), - inset 0 1px 0 oklch(1 0 0 / 0.2); - transform: translateY(-1px); - } - - .btn-gradient:active { - transform: translateY(0); - box-shadow: - 0 2px 8px 0 oklch(0.55 0.24 264 / 0.30), - inset 0 1px 0 oklch(1 0 0 / 0.15); - } - - /* Card Glow Border */ - .card-glow { - position: relative; - } - - .card-glow::before { - content: ''; - position: absolute; - inset: -1px; - background: linear-gradient(135deg, oklch(0.55 0.24 264 / 0.3), oklch(0.60 0.20 200 / 0.3)); - border-radius: inherit; - z-index: -1; - opacity: 0; - transition: opacity 0.3s ease; - } - - .card-glow:hover::before { - opacity: 1; - } - - /* Progress Bar Gradient */ - .progress-gradient { - background: linear-gradient(90deg, oklch(0.55 0.24 264), oklch(0.60 0.20 200), oklch(0.65 0.18 160)); - background-size: 200% 100%; - animation: progress-shimmer 2s ease infinite; - } - - @keyframes progress-shimmer { - 0% { - background-position: 200% 0; - } - - 100% { - background-position: -200% 0; - } - } - - /* Subtle Float Animation */ - .float-subtle { - animation: float-subtle 6s ease-in-out infinite; - } - - @keyframes float-subtle { - - 0%, - 100% { - transform: translateY(0); - } - - 50% { - transform: translateY(-5px); - } - } - - /* Input Focus Glow */ - .input-glow:focus-within { - box-shadow: 0 0 0 3px oklch(0.55 0.24 264 / 0.15); - } - - /* Step Indicator */ - .step-active { - background: linear-gradient(135deg, oklch(0.55 0.24 264), oklch(0.50 0.22 280)); - color: white; - box-shadow: 0 2px 8px oklch(0.55 0.24 264 / 0.3); - } - - .step-completed { - background: oklch(0.65 0.18 160); - color: white; - } - - .step-pending { - background: oklch(0.90 0.02 280); - color: oklch(0.50 0.02 280); - } - - .dark .step-pending { - background: oklch(0.25 0.02 280); - color: oklch(0.60 0.02 280); - } +/* + Definitive Layout Stability Reset + Prevents Radix UI / Shadcn from "sliding" content when scroll-locking occurs +*/ +:root { + --removed-body-scroll-bar-size: 0px; } -@layer utilities { - @keyframes logo-float { - - 0%, - 100% { - transform: translateY(0) scale(1); - } - - 50% { - transform: translateY(-3px) scale(1.02); - } - } - - @keyframes logo-spin-slow { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } - } - - @keyframes logo-spin-reverse-slow { - from { - transform: rotate(360deg); - } - - to { - transform: rotate(0deg); - } - } - - .animate-logo-float { - animation: logo-float 3s ease-in-out infinite; - } - - .animate-logo-spin-slow { - animation: logo-spin-slow 12s linear infinite; - } - - .animate-logo-spin-reverse-slow { - animation: logo-spin-reverse-slow 8s linear infinite; - } - -} - -/* Prevent layout shift when Dialog locks body scroll */ html { - overflow-y: scroll; - /* always show scrollbar to prevent width jump */ + scrollbar-gutter: stable; } body[data-scroll-locked] { - overflow: hidden; - padding-right: var(--removed-body-scroll-bar-size, 0px) !important; + padding-right: 0 !important; + margin-right: 0 !important; + overflow: hidden !important; +} + +/* Fix for fixed position elements like sidebars/headers */ +[data-radix-scroll-area-viewport] { + scrollbar-width: none; + -ms-overflow-style: none; +} + +.static-immediate { + transition: none !important; + animation: none !important; } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index de17178..04b372b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -31,6 +31,9 @@ export const metadata: Metadata = { }, } +import { Toaster } from "@/components/ui/sonner" +import { TooltipProvider } from "@/components/ui/tooltip" + export default function RootLayout({ children, }: Readonly<{ @@ -39,7 +42,10 @@ export default function RootLayout({ return ( - {children} + + {children} + + diff --git a/app/map/page.tsx b/app/map/page.tsx index 568ad7d..0e761a5 100644 --- a/app/map/page.tsx +++ b/app/map/page.tsx @@ -11,7 +11,7 @@ export default function MapPage() {
{/* Header */} -
+

@@ -25,12 +25,12 @@ export default function MapPage() {

{/* Map */} -
+
{/* Legend */} -
+
Pothole @@ -42,7 +42,7 @@ export default function MapPage() {
{/* Footer */} -
+

Sentient Geeks Pvt. Ltd.

diff --git a/app/new-analysis/page.tsx b/app/new-analysis/page.tsx index 7e880d6..6c7cd7a 100644 --- a/app/new-analysis/page.tsx +++ b/app/new-analysis/page.tsx @@ -25,7 +25,7 @@ export default function NewAnalysisPage() {
{/* Refined Left-Aligned Header */} -
+
@@ -37,12 +37,12 @@ export default function NewAnalysisPage() {
{/* Project Selection Section */} -
+
{/* Footer */} -
+

Sentient Geeks Pvt. Ltd.

diff --git a/app/results/page.tsx b/app/results/page.tsx index 2dcaf25..00bb375 100644 --- a/app/results/page.tsx +++ b/app/results/page.tsx @@ -16,7 +16,7 @@ import { import { getVideoFile, clearVideoFile } from "@/lib/video-storage" import { DetectionData, DetectionType } from "@/lib/types" -const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1" +const API_URL = "http://127.0.0.1:8000/api/v1" export default function ResultsPage() { const router = useRouter() @@ -126,7 +126,7 @@ export default function ResultsPage() {
{/* Refined Header */} -
+
@@ -142,7 +142,7 @@ export default function ResultsPage() { {/* Session Info Bar */} {session && ( -
+
@@ -170,7 +170,7 @@ export default function ResultsPage() { {/* Video Player Section */} {detectionData && videoId && ( -
+
{/* Refined Header */} -
+
@@ -189,7 +189,7 @@ export default function UploadPage() { {/* Compact Session Info Bar */} {session && ( -
+
@@ -226,7 +226,7 @@ export default function UploadPage() { )} {/* Upload Card */} - + @@ -322,7 +322,7 @@ export default function UploadPage() { {/* Error Display */} {error && ( -
+
!
@@ -352,7 +352,7 @@ export default function UploadPage() { {/* Progress Section */} {uploading && ( -
+
Processing Progress @@ -374,7 +374,7 @@ export default function UploadPage() { {/* Footer */} -
+

Sentient Geeks Pvt. Ltd.

diff --git a/components/dashboard/dashboard-map-content.tsx b/components/dashboard/dashboard-map-content.tsx index 788d131..bb071b2 100644 --- a/components/dashboard/dashboard-map-content.tsx +++ b/components/dashboard/dashboard-map-content.tsx @@ -73,6 +73,7 @@ export default function DashboardMapContent({ detections }: DashboardMapContentP zoom={13} className="h-full w-full" scrollWheelZoom={true} + zoomAnimation={false} > (
- +
) } @@ -61,6 +61,7 @@ export function DashboardMap({ : (pkg as any).locations || {} for (const [locName, loc] of Object.entries(locationsToProcess)) { + if (!loc) continue const locationDetections = (loc as any).detections || [] filteredDetections.push(...locationDetections) } @@ -90,7 +91,7 @@ export function DashboardMap({
-
+
@@ -120,7 +121,7 @@ export function DashboardMap({
{isLoading ? (
- +
) : error ? (
diff --git a/components/dashboard/detection-chart.tsx b/components/dashboard/detection-chart.tsx index bf97433..05c0f85 100644 --- a/components/dashboard/detection-chart.tsx +++ b/components/dashboard/detection-chart.tsx @@ -21,7 +21,7 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha {isLoading ? (
-
+
) : total === 0 ? (
@@ -52,7 +52,7 @@ export function DetectionChart({ potholes, signboards, isLoading }: DetectionCha strokeWidth="16" strokeDasharray={`${potholePercent * 2.51} 251`} strokeLinecap="round" - className="transition-all duration-700" + className="" /> {/* Signboards arc */} diff --git a/components/dashboard/detection-donut-chart.tsx b/components/dashboard/detection-donut-chart.tsx index 92f6f6a..aad9038 100644 --- a/components/dashboard/detection-donut-chart.tsx +++ b/components/dashboard/detection-donut-chart.tsx @@ -18,7 +18,7 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti if (isLoading) { return (
- +
) } @@ -52,13 +52,14 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti paddingAngle={5} dataKey="value" strokeWidth={0} + isAnimationActive={false} > {data.map((entry, index) => ( ))} diff --git a/components/dashboard/gradient-stats-card.tsx b/components/dashboard/gradient-stats-card.tsx index df444f5..23518eb 100644 --- a/components/dashboard/gradient-stats-card.tsx +++ b/components/dashboard/gradient-stats-card.tsx @@ -58,7 +58,7 @@ export function GradientStatsCard({
{isLoading ? ( -
+
) : ( <>

diff --git a/components/dashboard/location-bar-chart.tsx b/components/dashboard/location-bar-chart.tsx index b3280a3..08c94fb 100644 --- a/components/dashboard/location-bar-chart.tsx +++ b/components/dashboard/location-bar-chart.tsx @@ -24,7 +24,7 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) { if (isLoading) { return (

- +
) } @@ -123,12 +123,14 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) { name="Potholes" fill="url(#barGradientPothole)" radius={[4, 4, 0, 0]} + isAnimationActive={false} /> diff --git a/components/dashboard/recent-analyses-table.tsx b/components/dashboard/recent-analyses-table.tsx index ba23925..28a9dd0 100644 --- a/components/dashboard/recent-analyses-table.tsx +++ b/components/dashboard/recent-analyses-table.tsx @@ -58,7 +58,7 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent {isLoading ? (
{[1, 2, 3, 4].map((i) => ( -
+
))}
) : videos.length === 0 ? ( @@ -72,7 +72,7 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent {videos.slice(0, 8).map((video) => (
diff --git a/components/dashboard/stats-card.tsx b/components/dashboard/stats-card.tsx index 31728a0..c525bdb 100644 --- a/components/dashboard/stats-card.tsx +++ b/components/dashboard/stats-card.tsx @@ -13,7 +13,7 @@ interface StatsCardProps { export function StatsCard({ title, value, icon: Icon, gradient, isLoading }: StatsCardProps) { return ( - +
@@ -21,14 +21,14 @@ export function StatsCard({ title, value, icon: Icon, gradient, isLoading }: Sta {title}

{isLoading ? ( -
+
) : (

{value}

)}
-
+
diff --git a/components/data-table.tsx b/components/data-table.tsx index 8093433..9eb99c9 100644 --- a/components/data-table.tsx +++ b/components/data-table.tsx @@ -1,7 +1,13 @@ "use client" -import { Plus } from "lucide-react" +import { Plus, Edit2, Trash2 } from "lucide-react" import { Button } from "@/components/ui/button" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" interface Column { key: string @@ -16,6 +22,8 @@ interface DataTableProps { onAddNew: () => void addButtonText: string isLoading?: boolean + onEdit?: (item: T) => void + onDelete?: (item: T) => void } export function DataTable>({ @@ -24,8 +32,13 @@ export function DataTable>({ columns, onAddNew, addButtonText, - isLoading = false + isLoading = false, + onEdit, + onDelete }: DataTableProps) { + const showActions = !!onEdit || !!onDelete; + const totalCols = columns.length + (showActions ? 1 : 0); + return (
{/* Header with Title and Add Button */} @@ -57,12 +70,17 @@ export function DataTable>({ {col.header} ))} + {showActions && ( + + Actions + + )} {isLoading ? ( - +
Loading records... @@ -71,7 +89,7 @@ export function DataTable>({ ) : data.length === 0 ? ( - +
@@ -92,6 +110,48 @@ export function DataTable>({ {col.render ? col.render(item) : (item[col.key as keyof T] !== null && item[col.key as keyof T] !== undefined ? String(item[col.key as keyof T]) : "—")} ))} + {showActions && ( + +
+ + {onEdit && ( + + + + + +

Edit

+
+
+ )} + {onDelete && ( + + + + + +

Delete

+
+
+ )} +
+
+ + )} )) )} diff --git a/components/project-selection-section.tsx b/components/project-selection-section.tsx index 99f4449..14d75f8 100644 --- a/components/project-selection-section.tsx +++ b/components/project-selection-section.tsx @@ -167,20 +167,20 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
{step}
- {labels[index]}
{index < 2 && ( -
)}
@@ -193,7 +193,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio {/* Error Display */} {error && ( -
+
!
@@ -213,7 +213,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio onValueChange={handleProjectChange} disabled={loadingProjects} > - + {loadingProjects ? (
@@ -245,7 +245,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio onValueChange={handlePackageChange} disabled={!selectedProject || loadingPackages} > - + {loadingPackages ? (
@@ -277,7 +277,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio onValueChange={handleLocationChange} disabled={!selectedPackage || loadingLocations} > - + {loadingLocations ? (
@@ -301,7 +301,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio {/* Selected Summary */} {isComplete && ( -
+

Path: {selectedProject?.name} @@ -317,7 +317,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio

{item.title} @@ -102,13 +100,12 @@ export function SidebarNavigation() { className={` relative w-12 h-12 rounded-xl flex items-center justify-center ${isNewAnalysisActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'} - transition-all duration-300 ease-out - hover:scale-110 hover:shadow-lg hover:bg-[#2563eb] hover:border-[#2563eb] - active:scale-95 border + hover:bg-[#2563eb] hover:border-[#2563eb] + border `} >
Start New Analysis @@ -138,13 +134,12 @@ export function SidebarNavigation() { className={` relative w-12 h-12 rounded-xl flex items-center justify-center ${isActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'} - transition-all duration-300 ease-out - hover:scale-110 hover:shadow-lg hover:bg-[#2563eb] hover:border-[#2563eb] - active:scale-95 border + hover:bg-[#2563eb] hover:border-[#2563eb] + border `} >
{item.title} @@ -168,17 +162,16 @@ export function SidebarNavigation() {
Account (Coming Soon) diff --git a/components/summary-section.tsx b/components/summary-section.tsx index 4a191b1..c75698d 100644 --- a/components/summary-section.tsx +++ b/components/summary-section.tsx @@ -67,10 +67,9 @@ export function SummarySection({ data }: SummarySectionProps) { return (
-
+
{stat.value}
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx index e538a33..474cb3e 100644 --- a/components/ui/accordion.tsx +++ b/components/ui/accordion.tsx @@ -35,13 +35,13 @@ function AccordionTrigger({ svg]:rotate-180', + 'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180', className, )} {...props} > {children} - + ) @@ -55,7 +55,7 @@ function AccordionContent({ return (
{children}
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx index 9704452..389ccc6 100644 --- a/components/ui/alert-dialog.tsx +++ b/components/ui/alert-dialog.tsx @@ -36,7 +36,7 @@ function AlertDialogOverlay({ ) diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 3166550..5e8cb16 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { diff --git a/components/ui/card.tsx b/components/ui/card.tsx index 5c564bc..0dcb2c1 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
Close diff --git a/components/ui/drawer.tsx b/components/ui/drawer.tsx index 307bdce..180377a 100644 --- a/components/ui/drawer.tsx +++ b/components/ui/drawer.tsx @@ -37,7 +37,7 @@ function DrawerOverlay({ -
+
)}
diff --git a/components/ui/menubar.tsx b/components/ui/menubar.tsx index 791360c..d692e15 100644 --- a/components/ui/menubar.tsx +++ b/components/ui/menubar.tsx @@ -79,7 +79,7 @@ function MenubarContent({ alignOffset={alignOffset} sideOffset={sideOffset} className={cn( - 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md', + 'bg-popover text-popover-foreground z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md', className, )} {...props} @@ -248,7 +248,7 @@ function MenubarSubContent({ {children}{' '}