Files
panel/src/app/projects/map/page.js

925 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation";
import Button from "@/components/ui/Button";
import { mapLayers } from "@/components/ui/mapLayers";
// Dynamically import the map component to avoid SSR issues
const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), {
ssr: false,
loading: () => (
<div className="w-full h-96 bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
<span className="text-gray-500">Loading map...</span>
</div>
),
});
export default function ProjectsMapPage() {
const searchParams = useSearchParams();
const router = useRouter();
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland
const [mapZoom, setMapZoom] = useState(10); // Default zoom level
const [statusFilters, setStatusFilters] = useState({
registered: true,
in_progress_design: true,
in_progress_construction: true,
fulfilled: true,
}); const [activeBaseLayer, setActiveBaseLayer] = useState("Polish Geoportal Orthophoto");
const [activeOverlays, setActiveOverlays] = useState([]);
const [showLayerPanel, setShowLayerPanel] = useState(true);
const [currentTool, setCurrentTool] = useState("move"); // Current map tool
// Status configuration with colors and labels
const statusConfig = {
registered: {
color: "#6B7280",
label: "Registered",
shortLabel: "Zarejestr.",
},
in_progress_design: {
color: "#3B82F6",
label: "In Progress (Design)",
shortLabel: "W real. (P)",
},
in_progress_construction: {
color: "#F59E0B",
label: "In Progress (Construction)",
shortLabel: "W real. (R)",
},
fulfilled: {
color: "#10B981",
label: "Completed",
shortLabel: "Zakończony",
},
};
// Toggle all status filters
const toggleAllFilters = () => {
const allActive = Object.values(statusFilters).every((value) => value);
const newState = allActive
? Object.keys(statusFilters).reduce(
(acc, key) => ({ ...acc, [key]: false }),
{}
)
: Object.keys(statusFilters).reduce(
(acc, key) => ({ ...acc, [key]: true }),
{}
);
setStatusFilters(newState);
};
// Toggle status filter
const toggleStatusFilter = (status) => {
setStatusFilters((prev) => ({
...prev,
[status]: !prev[status],
}));
};
// Layer control functions
const handleBaseLayerChange = (layerName) => {
setActiveBaseLayer(layerName);
};
const toggleOverlay = (layerName) => {
setActiveOverlays((prev) => {
if (prev.includes(layerName)) {
return prev.filter((name) => name !== layerName);
} else {
return [...prev, layerName];
}
});
};
const toggleLayerPanel = () => {
setShowLayerPanel(!showLayerPanel);
};
// Update URL with current map state (debounced to avoid too many updates)
const updateURL = (center, zoom) => {
const params = new URLSearchParams();
params.set('lat', center[0].toFixed(6));
params.set('lng', center[1].toFixed(6));
params.set('zoom', zoom.toString());
// Use replace to avoid cluttering browser history
router.replace(`/projects/map?${params.toString()}`, { scroll: false });
};
// Handle map view changes with debouncing
const handleMapViewChange = (center, zoom) => {
setMapCenter(center);
setMapZoom(zoom);
// Debounce URL updates to avoid too many history entries
clearTimeout(window.mapUpdateTimeout);
window.mapUpdateTimeout = setTimeout(() => {
updateURL(center, zoom);
}, 500); // Wait 500ms after the last move to update URL
};
// Hide navigation and ensure full-screen layout
useEffect(() => {
// Check for URL parameters for coordinates and zoom
const lat = searchParams.get('lat');
const lng = searchParams.get('lng');
const zoom = searchParams.get('zoom');
if (lat && lng) {
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
if (!isNaN(latitude) && !isNaN(longitude)) {
setMapCenter([latitude, longitude]);
}
}
if (zoom) {
const zoomLevel = parseInt(zoom);
if (!isNaN(zoomLevel) && zoomLevel >= 1 && zoomLevel <= 20) {
setMapZoom(zoomLevel);
}
}
// Hide navigation bar for full-screen experience
const nav = document.querySelector("nav");
if (nav) {
nav.style.display = "none";
}
// Prevent scrolling on body
document.body.style.overflow = "hidden";
document.documentElement.style.overflow = "hidden";
// Cleanup when leaving page
return () => {
if (nav) {
nav.style.display = "";
}
document.body.style.overflow = "";
document.documentElement.style.overflow = "";
// Clear any pending URL updates
if (window.mapUpdateTimeout) {
clearTimeout(window.mapUpdateTimeout);
}
};
}, [searchParams]);
useEffect(() => {
fetch("/api/projects")
.then((res) => res.json())
.then((data) => {
setProjects(data);
// Only calculate center based on projects if no URL parameters are provided
const lat = searchParams.get('lat');
const lng = searchParams.get('lng');
if (!lat || !lng) {
// Calculate center based on projects with coordinates
const projectsWithCoords = data.filter((p) => p.coordinates);
if (projectsWithCoords.length > 0) {
const avgLat =
projectsWithCoords.reduce((sum, p) => {
const [lat] = p.coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
return sum + lat;
}, 0) / projectsWithCoords.length;
const avgLng =
projectsWithCoords.reduce((sum, p) => {
const [, lng] = p.coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
return sum + lng;
}, 0) / projectsWithCoords.length;
setMapCenter([avgLat, avgLng]);
}
}
setLoading(false);
})
.catch((error) => {
console.error("Error fetching projects:", error);
setLoading(false);
});
}, [searchParams]);
// Convert projects to map markers with filtering
const markers = projects
.filter((project) => project.coordinates)
.filter((project) => statusFilters[project.project_status] !== false)
.map((project) => {
const [lat, lng] = project.coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
if (isNaN(lat) || isNaN(lng)) {
return null;
}
const statusInfo =
statusConfig[project.project_status] || statusConfig.registered;
return {
position: [lat, lng],
color: statusInfo.color,
popup: (
<div className="min-w-72 max-w-80">
<div className="mb-3 pb-2 border-b border-gray-200">
<h3 className="font-semibold text-base mb-1 text-gray-900">
{project.project_name}
</h3>
{project.project_number && (
<div className="inline-block bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded-full">
{project.project_number}
</div>
)}
</div>
<div className="space-y-2 text-sm text-gray-600 mb-3">
{project.address && (
<div className="flex items-start gap-2">
<svg
className="w-4 h-4 mt-0.5 text-gray-400 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<div>
<span className="font-medium text-gray-700">
{project.address}
</span>
{project.city && (
<span className="text-gray-500">, {project.city}</span>
)}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-2">
{project.wp && (
<div>
<span className="font-medium text-gray-700">WP:</span>{" "}
{project.wp}
</div>
)}
{project.plot && (
<div>
<span className="font-medium text-gray-700">Plot:</span>{" "}
{project.plot}
</div>
)}
</div>
{project.project_status && (
<div className="flex items-center gap-2">
<span className="font-medium text-gray-700">Status:</span>
<span
className="inline-block px-2 py-1 rounded-full text-xs font-medium text-white"
style={{ backgroundColor: statusInfo.color }}
>
{statusInfo.shortLabel}
</span>
</div>
)}
</div>
<div className="pt-2 border-t border-gray-200">
<Link href={`/projects/${project.project_id}`}>
<Button variant="primary" size="sm" className="w-full">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
View Project Details
</Button>
</Link>
</div>
</div>
),
};
})
.filter((marker) => marker !== null);
if (loading) {
return (
<div className="fixed inset-0 bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-4 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin"></div>
<p className="text-gray-600 font-medium">Loading projects map...</p>
<p className="text-sm text-gray-500 mt-2">
Preparing your full-screen map experience
</p>
</div>
</div>
);
}
return (
<div
className="fixed inset-0 bg-gray-50 overflow-hidden"
>
{/* Floating Header - Left Side */}
<div className="absolute top-4 left-4 z-[1000]">
{/* Title Box */}
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold text-gray-900">
Projects Map
</h1>
<div className="text-sm text-gray-600">
{markers.length} of {projects.length} projects with coordinates
</div>
</div> </div>
</div>
{/* Zoom Controls - Below Title */}
<div className="absolute top-20 left-4 z-[1000]">
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 flex flex-col">
<button
className="px-3 py-2 hover:bg-gray-50 transition-colors duration-200 border-b border-gray-200 text-gray-700 font-medium text-lg"
onClick={() => {
// This will be handled by the map component
const event = new CustomEvent('mapZoomIn');
window.dispatchEvent(event);
}}
title="Zoom In"
>
+
</button>
<button
className="px-3 py-2 hover:bg-gray-50 transition-colors duration-200 text-gray-700 font-medium text-lg"
onClick={() => {
// This will be handled by the map component
const event = new CustomEvent('mapZoomOut');
window.dispatchEvent(event);
}}
title="Zoom Out"
>
</button> </div>
</div> {/* Tool Panel - Below Zoom Controls */}
<div className="absolute top-48 left-4 z-[1000]"> <div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 flex flex-col"> {/* Move Tool */}
<button
className={`p-3 transition-colors duration-200 border-b border-gray-200 ${
currentTool === "move"
? "bg-blue-100 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
onClick={() => setCurrentTool("move")}
title="Move Tool (Pan Map)"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 512 512"
>
<path d="M256 0c-25.3 0-47.2 14.7-57.6 36c-7-2.6-14.5-4-22.4-4c-35.3 0-64 28.7-64 64l0 165.5-2.7-2.7c-25-25-65.5-25-90.5 0s-25 65.5 0 90.5L106.5 437c48 48 113.1 75 181 75l8.5 0 8 0c1.5 0 3-.1 4.5-.4c91.7-6.2 165-79.4 171.1-171.1c.3-1.5 .4-3 .4-4.5l0-176c0-35.3-28.7-64-64-64c-5.5 0-10.9 .7-16 2l0-2c0-35.3-28.7-64-64-64c-7.9 0-15.4 1.4-22.4 4C303.2 14.7 281.3 0 256 0zM240 96.1l0-.1 0-32c0-8.8 7.2-16 16-16s16 7.2 16 16l0 31.9 0 .1 0 136c0 13.3 10.7 24 24 24s24-10.7 24-24l0-136c0 0 0 0 0-.1c0-8.8 7.2-16 16-16s16 7.2 16 16l0 55.9c0 0 0 .1 0 .1l0 80c0 13.3 10.7 24 24 24s24-10.7 24-24l0-71.9c0 0 0-.1 0-.1c0-8.8 7.2-16 16-16s16 7.2 16 16l0 172.9c-.1 .6-.1 1.3-.2 1.9c-3.4 69.7-59.3 125.6-129 129c-.6 0-1.3 .1-1.9 .2l-4.9 0-8.5 0c-55.2 0-108.1-21.9-147.1-60.9L52.7 315.3c-6.2-6.2-6.2-16.4 0-22.6s16.4-6.2 22.6 0L119 336.4c6.9 6.9 17.2 8.9 26.2 5.2s14.8-12.5 14.8-22.2L160 96c0-8.8 7.2-16 16-16c8.8 0 16 7.1 16 15.9L192 232c0 13.3 10.7 24 24 24s24-10.7 24-24l0-135.9z"/>
</svg>
</button>
{/* Select Tool */}
<button
className={`p-3 transition-colors duration-200 border-b border-gray-200 ${
currentTool === "select"
? "bg-blue-100 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
onClick={() => setCurrentTool("select")}
title="Select Tool"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
</button>
{/* Measure Tool */}
<button
className={`p-3 transition-colors duration-200 border-b border-gray-200 ${
currentTool === "measure"
? "bg-blue-100 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
onClick={() => setCurrentTool("measure")}
title="Measure Distance"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 21l10-10M7 21H3v-4l10-10 4 4M7 21l4-4M17 7l4-4M17 7l-4-4M17 7l-4 4"
/>
</svg>
</button>
{/* Draw Tool */}
<button
className={`p-3 transition-colors duration-200 border-b border-gray-200 ${
currentTool === "draw"
? "bg-blue-100 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
onClick={() => setCurrentTool("draw")}
title="Draw/Markup"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</button>
{/* Pin/Marker Tool */}
<button
className={`p-3 transition-colors duration-200 border-b border-gray-200 ${
currentTool === "pin"
? "bg-blue-100 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
onClick={() => setCurrentTool("pin")}
title="Add Pin/Marker"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
{/* Area Tool */}
<button
className={`p-3 transition-colors duration-200 ${
currentTool === "area"
? "bg-blue-100 text-blue-700"
: "text-gray-700 hover:bg-gray-50"
}`}
onClick={() => setCurrentTool("area")}
title="Measure Area"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 8V6a2 2 0 012-2h2M4 16v2a2 2 0 002 2h2m8-16h2a2 2 0 012 2v2m-4 12h2a2 2 0 002-2v-2"
/>
</svg>
</button>
</div>
</div>
{/* Layer Control Panel - Right Side */}
<div className="absolute top-4 right-4 z-[1000] flex flex-col gap-3">
{/* Action Buttons */}
<div className="flex gap-2">
<Link href="/projects">
<Button
variant="outline"
size="sm"
className="bg-white/95 backdrop-blur-sm border-gray-200 shadow-lg hover:bg-white"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 10h16M4 14h16M4 18h16"
/>
</svg>
List View
</Button>
</Link>
<Link href="/projects/new">
<Button variant="primary" size="sm" className="shadow-lg">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add Project
</Button>
</Link>
</div>
{/* Layer Control Panel */}
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 layer-panel-container">
{/* Layer Control Header */}
<div className="px-4 py-3 border-b border-gray-200">
<button
onClick={toggleLayerPanel}
className="flex items-center justify-between w-full text-left layer-toggle-button"
title="Toggle Layer Controls"
>
<div className="flex items-center gap-2">
<svg
className="w-4 h-4 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
<span className="text-sm font-medium text-gray-700">
Map Layers
</span>
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full">
{1 + activeOverlays.length} active
</span>
</div>
<svg
className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${
showLayerPanel ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div> {/* Layer Control Content */}
<div className={`transition-all duration-300 ease-in-out ${
showLayerPanel ? "max-h-[70vh] opacity-100 overflow-visible" : "max-h-0 opacity-0 overflow-hidden"
}`}>
<div className="p-4 min-w-80 max-w-96 max-h-[60vh] overflow-y-auto">
{/* Base Layers Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3 flex items-center gap-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Base Maps
</h3>
<div className="space-y-2">
{mapLayers.base.map((layer, index) => (
<label
key={index}
className="flex items-center gap-3 p-2 rounded hover:bg-gray-50 cursor-pointer transition-colors duration-200"
>
<input
type="radio"
name="baseLayer"
checked={activeBaseLayer === layer.name}
onChange={() => handleBaseLayerChange(layer.name)}
className="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 flex-1">
{layer.name}
</span>
</label>
))}
</div>
</div>
{/* Overlay Layers Section */}
{mapLayers.overlays && mapLayers.overlays.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-gray-900 mb-3 flex items-center gap-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"
/>
</svg>
Overlay Layers
</h3> <div className="space-y-2">
{mapLayers.overlays.map((layer, index) => (
<label
key={index}
className="flex items-center gap-3 p-2 rounded hover:bg-gray-50 cursor-pointer transition-colors duration-200"
>
<input
type="checkbox"
checked={activeOverlays.includes(layer.name)}
onChange={() => toggleOverlay(layer.name)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 flex-1">
{layer.name}
</span>
</label>
))}
</div>
</div>
)}
</div>
</div> </div>
</div>
{/* Status Filter Panel - Bottom Left */}
<div className="absolute bottom-4 left-4 z-[1000]">
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700 mr-2">
Filters:
</span>
{/* Toggle All Button */}
<button
onClick={toggleAllFilters}
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-100 hover:bg-gray-200 transition-colors duration-200 mr-2"
title="Toggle all filters"
>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<span className="text-gray-600">
{Object.values(statusFilters).every((v) => v)
? "Hide All"
: "Show All"}
</span>
</button>
{/* Individual Status Filters */}
{Object.entries(statusConfig).map(([status, config]) => {
const isActive = statusFilters[status];
const projectCount = projects.filter(
(p) => p.project_status === status && p.coordinates
).length;
return (
<button
key={status}
onClick={() => toggleStatusFilter(status)}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all duration-200 hover:bg-gray-100 ${
isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
}`}
title={`Toggle ${config.label} (${projectCount} projects)`}
>
<div
className={`w-3 h-3 rounded-full border-2 transition-all duration-200 ${
isActive ? "border-white shadow-sm" : "border-gray-300"
}`}
style={{
backgroundColor: isActive ? config.color : "#e5e7eb",
}}
></div>
<span
className={`transition-colors duration-200 ${
isActive ? "text-gray-700" : "text-gray-400"
}`}
>
{config.shortLabel}
</span>
<span
className={`ml-1 text-xs transition-colors duration-200 ${
isActive ? "text-gray-500" : "text-gray-300"
}`}
>
({projectCount})
</span>
</button>
);
})} </div>
</div>
</div> {/* Status Panel - Bottom Left */}
{markers.length > 0 && ( <div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700 mr-2">
Filters:
</span>
{/* Toggle All Button */}
<button
onClick={toggleAllFilters}
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-100 hover:bg-gray-200 transition-colors duration-200 mr-2"
title="Toggle all filters"
>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<span className="text-gray-600">
{Object.values(statusFilters).every((v) => v)
? "Hide All"
: "Show All"}
</span>
</button>
{/* Individual Status Filters */}
{Object.entries(statusConfig).map(([status, config]) => {
const isActive = statusFilters[status];
const projectCount = projects.filter(
(p) => p.project_status === status && p.coordinates
).length;
return (
<button
key={status}
onClick={() => toggleStatusFilter(status)}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all duration-200 hover:bg-gray-100 ${
isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
}`}
title={`Toggle ${config.label} (${projectCount} projects)`}
>
<div
className={`w-3 h-3 rounded-full border-2 transition-all duration-200 ${
isActive ? "border-white shadow-sm" : "border-gray-300"
}`}
style={{
backgroundColor: isActive ? config.color : "#e5e7eb",
}}
></div>
<span
className={`transition-colors duration-200 ${
isActive ? "text-gray-700" : "text-gray-400"
}`}
>
{config.shortLabel}
</span>
<span
className={`ml-1 text-xs transition-colors duration-200 ${
isActive ? "text-gray-500" : "text-gray-300"
}`}
>
({projectCount})
</span>
</button>
);
})}
</div>
</div>
)} {/* Full Screen Map */}
{markers.length === 0 ? (
<div className="h-full w-full flex items-center justify-center bg-gray-100">
<div className="text-center max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<div className="text-gray-400 mb-4">
<svg
className="w-16 h-16 mx-auto"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
clipRule="evenodd"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
No projects with coordinates
</h3>
<p className="text-gray-500 mb-6">
Projects need coordinates to appear on the map. Add coordinates
when creating or editing projects.
</p>
<div className="flex gap-3 justify-center">
<Link href="/projects">
<Button variant="outline">View All Projects</Button>
</Link>
<Link href="/projects/new">
<Button variant="primary">Add Project</Button>
</Link>
</div>
</div>
</div>
) : (
<div className="absolute inset-0">
<DynamicMap
center={mapCenter}
zoom={mapZoom}
markers={markers}
showLayerControl={false}
defaultLayer={activeBaseLayer}
activeOverlays={activeOverlays}
onViewChange={handleMapViewChange}
/>
</div>
)} </div>
);
}