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:
Chop
2025-06-19 20:57:50 +02:00
parent a8f52f6d28
commit aaa08a3504
8 changed files with 969 additions and 174 deletions

314
ROADMAP.md Normal file
View File

@@ -0,0 +1,314 @@
# App Development Roadmap
## Current Application Assessment
This is a solid Next.js-based project management system for construction/engineering projects with the following existing features:
### ✅ Currently Implemented
- **Project Management**: CRUD operations for projects with detailed information
- **Contract Management**: Contract creation, linking to projects, status tracking
- **Task Management**: Template-based and custom tasks with status tracking
- **Dashboard**: Statistics overview, recent projects, quick actions
- **Map Integration**: Leaflet maps with multiple layer support (OpenStreetMap, Polish Geoportal)
- **Database**: SQLite with better-sqlite3, well-structured schema
- **UI/UX**: Modern Tailwind CSS interface with responsive design
- **API Structure**: RESTful API endpoints for all entities
- **Docker Support**: Containerized development and deployment
- **Testing Setup**: Jest, Playwright, Testing Library configured
---
## Critical Missing Features for App
### 🔐 **1. Authentication & Authorization (HIGH PRIORITY)**
**Current State**: No authentication system
**Required**:
- User login/logout system
- Role-based access control (Admin, Project Manager, User, Read-only)
- Session management
- Password reset functionality
- User management interface
- API route protection
**Implementation Options**:
- NextAuth.js with database sessions
- Auth0 integration
- Custom JWT implementation
### 🔒 **2. Security & Data Protection (HIGH PRIORITY)**
**Current State**: No security measures
**Required**:
- Input validation and sanitization
- SQL injection protection (prepared statements are good start)
- XSS protection
- CSRF protection
- Rate limiting
- Environment variable security
- Data encryption for sensitive fields
- Audit logging
### 📊 **3. Advanced Reporting & Analytics (MEDIUM PRIORITY)**
**Current State**: Basic dashboard statistics
**Required**:
- Project timeline reports
- Budget tracking and financial reports
- Task completion analytics
- Project performance metrics
- Export to PDF/Excel
- Custom report builder
- Charts and graphs (Chart.js, D3.js)
### 💾 **4. Backup & Data Management (HIGH PRIORITY)**
**Current State**: Single SQLite file
**Required**:
- Automated database backups
- Data export/import functionality
- Database migration system
- Data archiving for old projects
- Recovery procedures
### 📱 **5. Mobile Responsiveness & PWA (MEDIUM PRIORITY)**
**Current State**: Basic responsive design
**Required**:
- Progressive Web App capabilities
- Offline functionality
- Mobile-optimized interface
- Push notifications
- App manifest and service workers
### 🔗 **6. API & Integration (MEDIUM PRIORITY)**
**Current State**: Internal REST API only
**Required**:
- External API integrations (accounting software, CRM)
- Webhook support
- API documentation (Swagger/OpenAPI)
- API versioning
- Third-party service integrations
### 📧 **7. Communication & Notifications (MEDIUM PRIORITY)**
**Current State**: No notification system
**Required**:
- Email notifications for deadlines, status changes
- In-app notifications
- SMS notifications (optional)
- Email templates
- Notification preferences per user
### 📋 **8. Enhanced Project Management (MEDIUM PRIORITY)**
**Current State**: Basic project tracking
**Required**:
- Gantt charts for project timelines
- Resource allocation and management
- Budget tracking per project
- Document attachment system
- Project templates
- Milestone tracking
- Dependencies between tasks
### 🔍 **9. Search & Filtering (LOW PRIORITY)**
**Current State**: Basic search implemented
**Required**:
- Advanced search with filters
- Full-text search
- Saved search queries
- Search autocomplete
- Global search across all entities
### ⚡ **10. Performance & Scalability (MEDIUM PRIORITY)**
**Current State**: Good for small-medium datasets
**Required**:
- Database optimization and indexing
- Caching layer (Redis)
- Image optimization
- Lazy loading
- Pagination for large datasets
- Background job processing
### 📝 **11. Documentation & Help System (LOW PRIORITY)**
**Current State**: README.md only
**Required**:
- User manual/documentation
- In-app help system
- API documentation
- Video tutorials
- FAQ section
### 🧪 **12. Testing & Quality Assurance (MEDIUM PRIORITY)**
**Current State**: Testing frameworks set up but no tests
**Required**:
- Unit tests for all components
- Integration tests for API endpoints
- E2E tests for critical user flows
- Performance testing
- Accessibility testing
- Code coverage reports
### 🚀 **13. DevOps & Deployment (MEDIUM PRIORITY)**
**Current State**: Docker setup exists
**Required**:
- CI/CD pipeline
- Production deployment strategy
- Environment management (dev, staging, prod)
- Monitoring and logging
- Error tracking (Sentry)
- Health checks
### 🎨 **14. UI/UX Improvements (LOW PRIORITY)**
**Current State**: Clean, functional interface
**Required**:
- Dark mode support
- Customizable themes
- Accessibility improvements (WCAG compliance)
- Keyboard navigation
- Better loading states
- Drag and drop functionality
---
## Implementation Priority Levels
### Phase 1: Security & Stability (Weeks 1-4)
1. Authentication system
2. Authorization and role management
3. Input validation and security
4. Backup system
5. Basic testing coverage
### Phase 2: Core Features (Weeks 5-8)
1. Advanced reporting
2. Mobile optimization
3. Notification system
4. Enhanced project management features
### Phase 3: Professional Features (Weeks 9-12)
1. API integrations
2. Performance optimization
3. Advanced UI features
4. Documentation
### Phase 4: Scale & Polish (Weeks 13-16)
1. DevOps improvements
2. Comprehensive testing
3. Advanced analytics
4. Third-party integrations
---
## Immediate Next Steps (Recommended Order)
1. **Set up Authentication**
- Install NextAuth.js or implement custom auth
- Create user management system
- Add login/logout functionality
2. **Implement Input Validation**
- Add Zod or Joi for schema validation
- Protect all API endpoints
- Add error handling
3. **Create Backup System**
- Implement database backup scripts
- Set up automated backups
- Create recovery procedures
4. **Add Basic Tests**
- Write unit tests for critical functions
- Add integration tests for API routes
- Set up test automation
5. **Implement Reporting**
- Add Chart.js for visualizations
- Create project timeline reports
- Add export functionality
---
## Technology Recommendations
### Authentication
- **NextAuth.js** - For easy authentication setup
- **Prisma** - For better database management (optional upgrade from better-sqlite3)
### Security
- **Zod** - Runtime type checking and validation
- **bcryptjs** - Password hashing
- **rate-limiter-flexible** - Rate limiting
### Reporting
- **Chart.js** or **Recharts** - Data visualization
- **jsPDF** - PDF generation
- **xlsx** - Excel export
### Notifications
- **Nodemailer** - Email sending
- **Socket.io** - Real-time notifications
### Testing
- **MSW** - API mocking for tests
- **Testing Library** - Component testing
- **Faker.js** - Test data generation
---
## Current Strengths
1. **Well-structured codebase** with clear separation of concerns
2. **Modern tech stack** (Next.js, React, Tailwind)
3. **Good database design** with proper relationships
4. **Responsive UI** with professional appearance
5. **Docker support** for easy deployment
6. **Map integration** with multiple layers
7. **Modular components** that are reusable
---
## Estimated Development Time
- **Minimum Viable Professional App**: 8-12 weeks
- **Full-featured Professional App**: 16-20 weeks
- **Enterprise-grade Application**: 24-30 weeks
This assessment is based on a single developer working full-time. Team development could reduce these timelines significantly.

View File

@@ -1,21 +1,25 @@
import db from '../src/lib/db.js';
import db from "../src/lib/db.js";
// Create another test project with coordinates in a different location
const project = db.prepare(`
const project = db
.prepare(
`
INSERT INTO projects (
contract_id, project_name, project_number, address, city, coordinates,
project_type, project_status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
3, // Using the existing contract
'Test Project in Warsaw',
'2/TEST/2025',
'Warsaw Center, Poland',
'Warsaw',
'52.2297,21.0122', // Warsaw coordinates
'construction',
'in_progress_construction'
);
`
)
.run(
2, // Using the test contract we just created
"Test Project in Warsaw",
"2/TEST/2025",
"Warsaw Center, Poland",
"Warsaw",
"52.2297,21.0122", // Warsaw coordinates
"construction",
"in_progress_construction"
);
console.log('Additional test project created!');
console.log('Project ID:', project.lastInsertRowid);
console.log("Additional test project created!");
console.log("Project ID:", project.lastInsertRowid);

View File

@@ -0,0 +1,88 @@
import db from "../src/lib/db.js";
// Create projects with different statuses and locations around Poland
const testProjects = [
{
name: "Gdansk Port Project",
address: "Port of Gdansk, Gdansk",
city: "Gdansk",
coordinates: "54.3520,18.6466",
status: "registered",
type: "design",
},
{
name: "Wroclaw Shopping Center",
address: "Market Square, Wroclaw",
city: "Wroclaw",
coordinates: "51.1079,17.0385",
status: "in_progress_design",
type: "design+construction",
},
{
name: "Poznan Office Complex",
address: "Old Town, Poznan",
city: "Poznan",
coordinates: "52.4064,16.9252",
status: "in_progress_construction",
type: "construction",
},
{
name: "Lodz Residential Development",
address: "City Center, Lodz",
city: "Lodz",
coordinates: "51.7592,19.4600",
status: "fulfilled",
type: "design+construction",
},
{
name: "Katowice Industrial Park",
address: "Silesia Region, Katowice",
city: "Katowice",
coordinates: "50.2649,19.0238",
status: "in_progress_design",
type: "design",
},
{
name: "Lublin University Campus",
address: "University District, Lublin",
city: "Lublin",
coordinates: "51.2465,22.5684",
status: "fulfilled",
type: "construction",
},
];
let projectCounter = 3; // Starting from 3 since we already have projects 1 and 2
testProjects.forEach((project) => {
try {
const result = db
.prepare(
`
INSERT INTO projects (
contract_id, project_name, project_number, address, city, coordinates,
project_type, project_status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`
)
.run(
2, // Use the test contract
project.name,
`${projectCounter}/TEST/2025`,
project.address,
project.city,
project.coordinates,
project.type,
project.status
);
console.log(
`Created project: ${project.name} (ID: ${result.lastInsertRowid}) - Status: ${project.status}`
);
projectCounter++;
} catch (error) {
console.error(`Error creating project ${project.name}:`, error.message);
}
});
console.log("Diverse test projects created successfully!");

View File

@@ -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>
<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"

View File

@@ -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,8 +265,10 @@ 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="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">
@@ -235,6 +279,83 @@ export default function ProjectsMapPage() {
</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>
<div className="flex gap-2">
<Link href="/projects">
@@ -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">
<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>
)}
{/* 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 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">

View File

@@ -1,30 +1,58 @@
"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');
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',
iconRetinaUrl: "/leaflet/marker-icon-2x.png",
iconUrl: "/leaflet/marker-icon.png",
shadowUrl: "/leaflet/marker-shadow.png",
});
}
};
// 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'
defaultLayer = "OpenStreetMap",
}) {
useEffect(() => {
fixLeafletIcons();
@@ -36,7 +64,7 @@ export default function EnhancedLeafletMap({
<MapContainer
center={center}
zoom={zoom}
style={{ height: '100%', width: '100%' }}
style={{ height: "100%", width: "100%" }}
scrollWheelZoom={true}
>
{showLayerControl ? (
@@ -62,10 +90,15 @@ export default function EnhancedLeafletMap({
attribution='&copy; <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
key={index}
position={marker.position}
icon={
marker.color ? createColoredMarkerIcon(marker.color) : undefined
}
>
{marker.popup && <Popup>{marker.popup}</Popup>}
</Marker>
))}

View File

@@ -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'), {
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">
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,
projectStatus = "registered",
showLayerControl = true,
mapHeight = 'h-64',
defaultLayer = 'OpenStreetMap'
mapHeight = "h-64",
defaultLayer = "OpenStreetMap",
}) {
const [coords, setCoords] = useState(null);
// 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" },
};
useEffect(() => {
if (coordinates) {
// Parse coordinates string (e.g., "49.622958,20.629562")
const [lat, lng] = coordinates.split(',').map(coord => parseFloat(coord.trim()));
const [lat, lng] = coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
if (!isNaN(lat) && !isNaN(lng)) {
setCoords({ lat, lng });
}
}
}, [coordinates]);
if (!coords) {
return null;
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">
<h3 className="text-sm font-medium text-gray-700">Project Location</h3>
<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`}>
<div
className={`w-full ${mapHeight} rounded-lg overflow-hidden border border-gray-200`}
>
<DynamicMap
center={[coords.lat, coords.lng]}
zoom={15}
markers={[{
markers={[
{
position: [coords.lat, coords.lng],
popup: projectName || 'Project Location'
}]}
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>
);
}

41
test-mobile.html Normal file
View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Map Test - Mobile View</title>
<style>
body {
margin: 0;
padding: 20px;
}
.container {
max-width: 400px;
margin: 0 auto;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
}
iframe {
width: 100%;
height: 600px;
border: none;
}
.info {
padding: 10px;
background: #f5f5f5;
font-family: Arial, sans-serif;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<div class="info">
<strong>Mobile View Test (400px width)</strong><br />
Testing responsive behavior of the projects map
</div>
<iframe src="http://localhost:3000/projects/map"></iframe>
</div>
</body>
</html>