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:
@@ -365,7 +365,7 @@ export default async function ProjectViewPage({ params }) {
|
||||
</svg>
|
||||
Edit Project
|
||||
</Button>
|
||||
</Link>
|
||||
</Link>{" "}
|
||||
<Link href="/projects" className="block">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -388,6 +388,28 @@ export default async function ProjectViewPage({ params }) {
|
||||
Back to Projects
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/projects/map" className="block">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<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="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-1.447-.894L15 4m0 13V4m0 0L9 7"
|
||||
/>
|
||||
</svg>
|
||||
View All on Map
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -395,16 +417,38 @@ export default async function ProjectViewPage({ params }) {
|
||||
{/* Project Location Map */}
|
||||
{project.coordinates && (
|
||||
<div className="mb-8">
|
||||
{" "}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Project Location
|
||||
</h2>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Project Location
|
||||
</h2>
|
||||
<Link href="/projects/map">
|
||||
<Button variant="outline" size="sm">
|
||||
<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="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-1.447-.894L15 4m0 13V4m0 0L9 7"
|
||||
/>
|
||||
</svg>
|
||||
View on Full Map
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ProjectMap
|
||||
coordinates={project.coordinates}
|
||||
projectName={project.project_name}
|
||||
projectStatus={project.project_status}
|
||||
showLayerControl={true}
|
||||
mapHeight="h-80"
|
||||
defaultLayer="Polish Geoportal Orthophoto"
|
||||
|
||||
@@ -19,6 +19,59 @@ 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
|
||||
const [statusFilters, setStatusFilters] = useState({
|
||||
registered: true,
|
||||
in_progress_design: true,
|
||||
in_progress_construction: true,
|
||||
fulfilled: true,
|
||||
});
|
||||
|
||||
// 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],
|
||||
}));
|
||||
};
|
||||
// Hide navigation and ensure full-screen layout
|
||||
useEffect(() => {
|
||||
// Hide navigation bar for full-screen experience
|
||||
@@ -76,20 +129,24 @@ export default function ProjectsMapPage() {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Convert projects to map markers
|
||||
// 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">
|
||||
@@ -135,7 +192,6 @@ export default function ProjectsMapPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{project.wp && (
|
||||
<div>
|
||||
@@ -149,29 +205,15 @@ export default function ProjectsMapPage() {
|
||||
{project.plot}
|
||||
</div>
|
||||
)}
|
||||
</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"
|
||||
}`}
|
||||
className="inline-block px-2 py-1 rounded-full text-xs font-medium text-white"
|
||||
style={{ backgroundColor: statusInfo.color }}
|
||||
>
|
||||
{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}
|
||||
{statusInfo.shortLabel}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -223,15 +265,94 @@ export default function ProjectsMapPage() {
|
||||
}
|
||||
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 className="flex gap-3">
|
||||
<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>
|
||||
{/* Status Filter Panel */}
|
||||
<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>
|
||||
@@ -278,37 +399,79 @@ export default function ProjectsMapPage() {
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</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="flex items-center gap-2 mb-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="font-medium">
|
||||
{markers.length} projects shown
|
||||
{markers.length} of{" "}
|
||||
{projects.filter((p) => p.coordinates).length} projects shown
|
||||
</span>
|
||||
</div>
|
||||
{projects.length > markers.length && (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
{/* Status breakdown */}
|
||||
<div className="space-y-1 mb-2">
|
||||
{Object.entries(statusConfig).map(([status, config]) => {
|
||||
const totalCount = projects.filter(
|
||||
(p) => p.project_status === status && p.coordinates
|
||||
).length;
|
||||
const visibleCount = markers.filter((m) => {
|
||||
const project = projects.find(
|
||||
(p) =>
|
||||
p.coordinates &&
|
||||
p.coordinates
|
||||
.split(",")
|
||||
.map((c) => parseFloat(c.trim()))
|
||||
.toString() === m.position.toString()
|
||||
);
|
||||
return project && project.project_status === status;
|
||||
}).length;
|
||||
|
||||
if (totalCount === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={status} className="flex items-center gap-2 text-xs">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: config.color }}
|
||||
></div>
|
||||
<span
|
||||
className={
|
||||
statusFilters[status]
|
||||
? "text-gray-600"
|
||||
: "text-gray-400"
|
||||
}
|
||||
>
|
||||
{config.shortLabel}:{" "}
|
||||
{statusFilters[status] ? visibleCount : 0}/{totalCount}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{projects.length > projects.filter((p) => p.coordinates).length && (
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-gray-200">
|
||||
<div className="w-2 h-2 bg-amber-500 rounded-full"></div>
|
||||
<span className="text-amber-600">
|
||||
{projects.length - markers.length} missing coordinates
|
||||
<span className="text-amber-600 text-xs">
|
||||
{projects.length -
|
||||
projects.filter((p) => p.coordinates).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">
|
||||
|
||||
Reference in New Issue
Block a user