feat: Implement full-screen map view and enhance project navigation
This commit is contained in:
@@ -58,3 +58,39 @@ body {
|
|||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.3s ease-out;
|
animation: fadeIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Full-screen map styles */
|
||||||
|
.map-fullscreen-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure map takes full container */
|
||||||
|
.leaflet-container {
|
||||||
|
height: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override any margin/padding that might cause scrollbars */
|
||||||
|
.map-page {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure floating panels are above map controls */
|
||||||
|
.map-floating-panel {
|
||||||
|
z-index: 1000 !important;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map controls positioning */
|
||||||
|
.leaflet-control-container .leaflet-top.leaflet-right {
|
||||||
|
top: 80px !important; /* Account for floating header */
|
||||||
|
}
|
||||||
|
|||||||
359
src/app/projects/map/page.js
Normal file
359
src/app/projects/map/page.js
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
|
||||||
|
// 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 [projects, setProjects] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland
|
||||||
|
// Hide navigation and ensure full-screen layout
|
||||||
|
useEffect(() => {
|
||||||
|
// 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 = "";
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/projects")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
setProjects(data);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Convert projects to map markers
|
||||||
|
const markers = projects
|
||||||
|
.filter((project) => project.coordinates)
|
||||||
|
.map((project) => {
|
||||||
|
const [lat, lng] = project.coordinates
|
||||||
|
.split(",")
|
||||||
|
.map((coord) => parseFloat(coord.trim()));
|
||||||
|
|
||||||
|
if (isNaN(lat) || isNaN(lng)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
position: [lat, lng],
|
||||||
|
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 ${
|
||||||
|
project.project_status === "fulfilled"
|
||||||
|
? "bg-green-100 text-green-800"
|
||||||
|
: project.project_status === "registered"
|
||||||
|
? "bg-gray-100 text-gray-800"
|
||||||
|
: "bg-blue-100 text-blue-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.project_status === "registered"
|
||||||
|
? "Zarejestr."
|
||||||
|
: project.project_status === "in_progress_design"
|
||||||
|
? "W real. (P)"
|
||||||
|
: project.project_status === "in_progress_construction"
|
||||||
|
? "W real. (R)"
|
||||||
|
: project.project_status === "fulfilled"
|
||||||
|
? "Zakończony"
|
||||||
|
: project.project_status}
|
||||||
|
</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 with Controls */}
|
||||||
|
<div className="absolute top-4 left-4 right-4 z-[1000] flex items-center justify-between">
|
||||||
|
<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 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Panel - Bottom Left */}
|
||||||
|
{markers.length > 0 && (
|
||||||
|
<div className="absolute bottom-4 left-4 z-[1000] bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200 max-w-xs">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
|
<span className="font-medium">
|
||||||
|
{markers.length} projects shown
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{projects.length > markers.length && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-amber-500 rounded-full"></div>
|
||||||
|
<span className="text-amber-600">
|
||||||
|
{projects.length - markers.length} missing coordinates
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help Panel - Bottom Right */}
|
||||||
|
<div className="absolute bottom-4 right-4 z-[1000] bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-3 py-2 border border-gray-200">
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Click markers for details • Use 📚 to switch layers
|
||||||
|
</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={10}
|
||||||
|
markers={markers}
|
||||||
|
showLayerControl={true}
|
||||||
|
defaultLayer="Polish Geoportal Orthophoto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -61,6 +61,25 @@ export default function ProjectListPage() {
|
|||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageHeader title="Projects" description="Manage and track your projects">
|
<PageHeader title="Projects" description="Manage and track your projects">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Link href="/projects/map">
|
||||||
|
<Button variant="outline" size="lg">
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 mr-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Map View
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
<Link href="/projects/new">
|
<Link href="/projects/new">
|
||||||
<Button variant="primary" size="lg">
|
<Button variant="primary" size="lg">
|
||||||
<svg
|
<svg
|
||||||
@@ -79,6 +98,7 @@ export default function ProjectListPage() {
|
|||||||
Add Project
|
Add Project
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<SearchBar
|
<SearchBar
|
||||||
|
|||||||
Reference in New Issue
Block a user