feat: Add comprehensive roadmap for app development and prioritize features
- Created ROADMAP.md outlining current features, critical missing features, implementation phases, and technology recommendations. - Added immediate next steps for development focus. fix: Enhance test data creation scripts for diverse project scenarios - Implemented create-diverse-test-data.js to generate varied test projects with different statuses and locations. - Updated create-additional-test-data.js for better project creation consistency. refactor: Improve project view page with additional navigation and map features - Added link to view all projects on the map from the project view page. - Enhanced project location display with status indicators. feat: Implement project map page with status filtering - Added status filters for project markers on the map. - Integrated color-coded markers based on project status. fix: Update LeafletMap and ProjectMap components for better marker handling - Created colored marker icons for different project statuses. - Enhanced ProjectMap to display project status and coordinates more effectively. test: Add mobile view test HTML for responsive map testing - Created test-mobile.html to test the map's responsive behavior on mobile devices.
This commit is contained in:
@@ -1,74 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import { MapContainer, TileLayer, Marker, Popup, LayersControl } from 'react-leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { useEffect } from 'react';
|
||||
import { mapLayers } from './mapLayers';
|
||||
import {
|
||||
MapContainer,
|
||||
TileLayer,
|
||||
Marker,
|
||||
Popup,
|
||||
LayersControl,
|
||||
} from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { useEffect } from "react";
|
||||
import { mapLayers } from "./mapLayers";
|
||||
|
||||
// Fix for default markers in react-leaflet
|
||||
const fixLeafletIcons = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const L = require('leaflet');
|
||||
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: '/leaflet/marker-icon-2x.png',
|
||||
iconUrl: '/leaflet/marker-icon.png',
|
||||
shadowUrl: '/leaflet/marker-shadow.png',
|
||||
});
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
const L = require("leaflet");
|
||||
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: "/leaflet/marker-icon-2x.png",
|
||||
iconUrl: "/leaflet/marker-icon.png",
|
||||
shadowUrl: "/leaflet/marker-shadow.png",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function EnhancedLeafletMap({
|
||||
center,
|
||||
zoom = 13,
|
||||
markers = [],
|
||||
showLayerControl = true,
|
||||
defaultLayer = 'OpenStreetMap'
|
||||
// Create colored marker icons
|
||||
const createColoredMarkerIcon = (color) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const L = require("leaflet");
|
||||
|
||||
return new L.Icon({
|
||||
iconUrl: `data:image/svg+xml;base64,${btoa(`
|
||||
<svg width="25" height="41" viewBox="0 0 25 41" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 0C5.596 0 0 5.596 0 12.5c0 12.5 12.5 28.5 12.5 28.5S25 25 25 12.5C25 5.596 19.404 0 12.5 0z" fill="${color}" stroke="#ffffff" stroke-width="2"/>
|
||||
<circle cx="12.5" cy="12.5" r="6" fill="#ffffff"/>
|
||||
</svg>
|
||||
`)}`,
|
||||
shadowUrl: "/leaflet/marker-shadow.png",
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function EnhancedLeafletMap({
|
||||
center,
|
||||
zoom = 13,
|
||||
markers = [],
|
||||
showLayerControl = true,
|
||||
defaultLayer = "OpenStreetMap",
|
||||
}) {
|
||||
useEffect(() => {
|
||||
fixLeafletIcons();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fixLeafletIcons();
|
||||
}, []);
|
||||
|
||||
const { BaseLayer } = LayersControl;
|
||||
const { BaseLayer } = LayersControl;
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={zoom}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
scrollWheelZoom={true}
|
||||
>
|
||||
{showLayerControl ? (
|
||||
<LayersControl position="topright">
|
||||
{mapLayers.base.map((layer, index) => (
|
||||
<BaseLayer
|
||||
key={index}
|
||||
checked={layer.checked || layer.name === defaultLayer}
|
||||
name={layer.name}
|
||||
>
|
||||
<TileLayer
|
||||
attribution={layer.attribution}
|
||||
url={layer.url}
|
||||
maxZoom={layer.maxZoom}
|
||||
tileSize={layer.tileSize || 256}
|
||||
/>
|
||||
</BaseLayer>
|
||||
))}
|
||||
</LayersControl>
|
||||
) : (
|
||||
// Default layer when no layer control
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
)}
|
||||
|
||||
{markers.map((marker, index) => (
|
||||
<Marker key={index} position={marker.position}>
|
||||
{marker.popup && <Popup>{marker.popup}</Popup>}
|
||||
</Marker>
|
||||
))}
|
||||
</MapContainer>
|
||||
);
|
||||
return (
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={zoom}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
scrollWheelZoom={true}
|
||||
>
|
||||
{showLayerControl ? (
|
||||
<LayersControl position="topright">
|
||||
{mapLayers.base.map((layer, index) => (
|
||||
<BaseLayer
|
||||
key={index}
|
||||
checked={layer.checked || layer.name === defaultLayer}
|
||||
name={layer.name}
|
||||
>
|
||||
<TileLayer
|
||||
attribution={layer.attribution}
|
||||
url={layer.url}
|
||||
maxZoom={layer.maxZoom}
|
||||
tileSize={layer.tileSize || 256}
|
||||
/>
|
||||
</BaseLayer>
|
||||
))}
|
||||
</LayersControl>
|
||||
) : (
|
||||
// Default layer when no layer control
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
)}{" "}
|
||||
{markers.map((marker, index) => (
|
||||
<Marker
|
||||
key={index}
|
||||
position={marker.position}
|
||||
icon={
|
||||
marker.color ? createColoredMarkerIcon(marker.color) : undefined
|
||||
}
|
||||
>
|
||||
{marker.popup && <Popup>{marker.popup}</Popup>}
|
||||
</Marker>
|
||||
))}
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,65 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Dynamically import the map component to avoid SSR issues
|
||||
const DynamicMap = dynamic(() => import('./LeafletMap'), {
|
||||
ssr: false,
|
||||
loading: () => <div className="w-full h-64 bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
|
||||
<span className="text-gray-500">Loading map...</span>
|
||||
</div>
|
||||
const DynamicMap = dynamic(() => import("./LeafletMap"), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="w-full h-64 bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
|
||||
<span className="text-gray-500">Loading map...</span>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export default function ProjectMap({
|
||||
coordinates,
|
||||
projectName,
|
||||
showLayerControl = true,
|
||||
mapHeight = 'h-64',
|
||||
defaultLayer = 'OpenStreetMap'
|
||||
export default function ProjectMap({
|
||||
coordinates,
|
||||
projectName,
|
||||
projectStatus = "registered",
|
||||
showLayerControl = true,
|
||||
mapHeight = "h-64",
|
||||
defaultLayer = "OpenStreetMap",
|
||||
}) {
|
||||
const [coords, setCoords] = useState(null);
|
||||
const [coords, setCoords] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (coordinates) {
|
||||
// Parse coordinates string (e.g., "49.622958,20.629562")
|
||||
const [lat, lng] = coordinates.split(',').map(coord => parseFloat(coord.trim()));
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
setCoords({ lat, lng });
|
||||
}
|
||||
}
|
||||
}, [coordinates]);
|
||||
// Status configuration matching the main map
|
||||
const statusConfig = {
|
||||
registered: { color: "#6B7280", label: "Registered" },
|
||||
in_progress_design: { color: "#3B82F6", label: "In Progress (Design)" },
|
||||
in_progress_construction: {
|
||||
color: "#F59E0B",
|
||||
label: "In Progress (Construction)",
|
||||
},
|
||||
fulfilled: { color: "#10B981", label: "Completed" },
|
||||
};
|
||||
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (coordinates) {
|
||||
// Parse coordinates string (e.g., "49.622958,20.629562")
|
||||
const [lat, lng] = coordinates
|
||||
.split(",")
|
||||
.map((coord) => parseFloat(coord.trim()));
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-700">Project Location</h3>
|
||||
{showLayerControl && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Use the layer control (📚) to switch map views
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`w-full ${mapHeight} rounded-lg overflow-hidden border border-gray-200`}>
|
||||
<DynamicMap
|
||||
center={[coords.lat, coords.lng]}
|
||||
zoom={15}
|
||||
markers={[{
|
||||
position: [coords.lat, coords.lng],
|
||||
popup: projectName || 'Project Location'
|
||||
}]}
|
||||
showLayerControl={showLayerControl}
|
||||
defaultLayer={defaultLayer}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Coordinates: {coords.lat.toFixed(6)}, {coords.lng.toFixed(6)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
setCoords({ lat, lng });
|
||||
}
|
||||
}
|
||||
}, [coordinates]);
|
||||
if (!coords) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-700">
|
||||
Project Location
|
||||
</h3>
|
||||
<div className="text-xs text-gray-500">No coordinates available</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-full ${mapHeight} rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center`}
|
||||
>
|
||||
<div className="text-center text-gray-500">
|
||||
<svg
|
||||
className="w-8 h-8 mx-auto mb-2"
|
||||
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>
|
||||
<p className="text-sm">Map unavailable</p>
|
||||
<p className="text-xs">No coordinates provided</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statusInfo = statusConfig[projectStatus] || statusConfig.registered;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-gray-700">
|
||||
Project Location
|
||||
</h3>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-white shadow-sm"
|
||||
style={{ backgroundColor: statusInfo.color }}
|
||||
title={`Status: ${statusInfo.label}`}
|
||||
></div>
|
||||
</div>
|
||||
{showLayerControl && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Use the layer control (📚) to switch map views
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`w-full ${mapHeight} rounded-lg overflow-hidden border border-gray-200`}
|
||||
>
|
||||
<DynamicMap
|
||||
center={[coords.lat, coords.lng]}
|
||||
zoom={15}
|
||||
markers={[
|
||||
{
|
||||
position: [coords.lat, coords.lng],
|
||||
color: statusInfo.color,
|
||||
popup: (
|
||||
<div className="min-w-48">
|
||||
<div className="mb-2 pb-2 border-b border-gray-200">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">
|
||||
{projectName || "Project Location"}
|
||||
</h4>
|
||||
<span
|
||||
className="inline-block px-2 py-1 rounded-full text-xs font-medium text-white"
|
||||
style={{ backgroundColor: statusInfo.color }}
|
||||
>
|
||||
{statusInfo.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
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>
|
||||
<span className="font-mono text-xs">
|
||||
{coords.lat.toFixed(6)}, {coords.lng.toFixed(6)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
showLayerControl={showLayerControl}
|
||||
defaultLayer={defaultLayer}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-gray-500">
|
||||
Coordinates: {coords.lat.toFixed(6)}, {coords.lng.toFixed(6)}
|
||||
</p>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: statusInfo.color }}
|
||||
></div>
|
||||
<span>{statusInfo.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user