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:
314
ROADMAP.md
Normal file
314
ROADMAP.md
Normal 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.
|
||||||
@@ -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
|
// Create another test project with coordinates in a different location
|
||||||
const project = db.prepare(`
|
const project = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
INSERT INTO projects (
|
INSERT INTO projects (
|
||||||
contract_id, project_name, project_number, address, city, coordinates,
|
contract_id, project_name, project_number, address, city, coordinates,
|
||||||
project_type, project_status
|
project_type, project_status
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`
|
||||||
3, // Using the existing contract
|
)
|
||||||
'Test Project in Warsaw',
|
.run(
|
||||||
'2/TEST/2025',
|
2, // Using the test contract we just created
|
||||||
'Warsaw Center, Poland',
|
"Test Project in Warsaw",
|
||||||
'Warsaw',
|
"2/TEST/2025",
|
||||||
'52.2297,21.0122', // Warsaw coordinates
|
"Warsaw Center, Poland",
|
||||||
'construction',
|
"Warsaw",
|
||||||
'in_progress_construction'
|
"52.2297,21.0122", // Warsaw coordinates
|
||||||
);
|
"construction",
|
||||||
|
"in_progress_construction"
|
||||||
|
);
|
||||||
|
|
||||||
console.log('Additional test project created!');
|
console.log("Additional test project created!");
|
||||||
console.log('Project ID:', project.lastInsertRowid);
|
console.log("Project ID:", project.lastInsertRowid);
|
||||||
|
|||||||
88
scripts/create-diverse-test-data.js
Normal file
88
scripts/create-diverse-test-data.js
Normal 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!");
|
||||||
@@ -365,7 +365,7 @@ export default async function ProjectViewPage({ params }) {
|
|||||||
</svg>
|
</svg>
|
||||||
Edit Project
|
Edit Project
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>{" "}
|
||||||
<Link href="/projects" className="block">
|
<Link href="/projects" className="block">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -388,6 +388,28 @@ export default async function ProjectViewPage({ params }) {
|
|||||||
Back to Projects
|
Back to Projects
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -395,16 +417,38 @@ export default async function ProjectViewPage({ params }) {
|
|||||||
{/* Project Location Map */}
|
{/* Project Location Map */}
|
||||||
{project.coordinates && (
|
{project.coordinates && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
{" "}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<h2 className="text-xl font-semibold text-gray-900">
|
<div className="flex items-center justify-between">
|
||||||
Project Location
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
</h2>
|
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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ProjectMap
|
<ProjectMap
|
||||||
coordinates={project.coordinates}
|
coordinates={project.coordinates}
|
||||||
projectName={project.project_name}
|
projectName={project.project_name}
|
||||||
|
projectStatus={project.project_status}
|
||||||
showLayerControl={true}
|
showLayerControl={true}
|
||||||
mapHeight="h-80"
|
mapHeight="h-80"
|
||||||
defaultLayer="Polish Geoportal Orthophoto"
|
defaultLayer="Polish Geoportal Orthophoto"
|
||||||
|
|||||||
@@ -19,6 +19,59 @@ export default function ProjectsMapPage() {
|
|||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland
|
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
|
// Hide navigation and ensure full-screen layout
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Hide navigation bar for full-screen experience
|
// Hide navigation bar for full-screen experience
|
||||||
@@ -76,20 +129,24 @@ export default function ProjectsMapPage() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
// Convert projects to map markers with filtering
|
||||||
// Convert projects to map markers
|
|
||||||
const markers = projects
|
const markers = projects
|
||||||
.filter((project) => project.coordinates)
|
.filter((project) => project.coordinates)
|
||||||
|
.filter((project) => statusFilters[project.project_status] !== false)
|
||||||
.map((project) => {
|
.map((project) => {
|
||||||
const [lat, lng] = project.coordinates
|
const [lat, lng] = project.coordinates
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((coord) => parseFloat(coord.trim()));
|
.map((coord) => parseFloat(coord.trim()));
|
||||||
|
|
||||||
if (isNaN(lat) || isNaN(lng)) {
|
if (isNaN(lat) || isNaN(lng)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statusInfo =
|
||||||
|
statusConfig[project.project_status] || statusConfig.registered;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position: [lat, lng],
|
position: [lat, lng],
|
||||||
|
color: statusInfo.color,
|
||||||
popup: (
|
popup: (
|
||||||
<div className="min-w-72 max-w-80">
|
<div className="min-w-72 max-w-80">
|
||||||
<div className="mb-3 pb-2 border-b border-gray-200">
|
<div className="mb-3 pb-2 border-b border-gray-200">
|
||||||
@@ -135,7 +192,6 @@ export default function ProjectsMapPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{project.wp && (
|
{project.wp && (
|
||||||
<div>
|
<div>
|
||||||
@@ -149,29 +205,15 @@ export default function ProjectsMapPage() {
|
|||||||
{project.plot}
|
{project.plot}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>{" "}
|
||||||
|
|
||||||
{project.project_status && (
|
{project.project_status && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-gray-700">Status:</span>
|
<span className="font-medium text-gray-700">Status:</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
|
className="inline-block px-2 py-1 rounded-full text-xs font-medium text-white"
|
||||||
project.project_status === "fulfilled"
|
style={{ backgroundColor: statusInfo.color }}
|
||||||
? "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"
|
{statusInfo.shortLabel}
|
||||||
? "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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -223,15 +265,94 @@ export default function ProjectsMapPage() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-50 overflow-hidden">
|
<div className="fixed inset-0 bg-gray-50 overflow-hidden">
|
||||||
|
{" "}
|
||||||
{/* Floating Header with Controls */}
|
{/* Floating Header with Controls */}
|
||||||
<div className="absolute top-4 left-4 right-4 z-[1000] flex items-center justify-between">
|
<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 gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
|
||||||
<h1 className="text-lg font-semibold text-gray-900">
|
<div className="flex items-center gap-3">
|
||||||
Projects Map
|
<h1 className="text-lg font-semibold text-gray-900">
|
||||||
</h1>
|
Projects Map
|
||||||
<div className="text-sm text-gray-600">
|
</h1>
|
||||||
{markers.length} of {projects.length} projects with coordinates
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,37 +399,79 @@ export default function ProjectsMapPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>{" "}
|
||||||
|
|
||||||
{/* Stats Panel - Bottom Left */}
|
{/* Stats Panel - Bottom Left */}
|
||||||
{markers.length > 0 && (
|
{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="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="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>
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{markers.length} projects shown
|
{markers.length} of{" "}
|
||||||
|
{projects.filter((p) => p.coordinates).length} projects shown
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
<div className="w-2 h-2 bg-amber-500 rounded-full"></div>
|
||||||
<span className="text-amber-600">
|
<span className="text-amber-600 text-xs">
|
||||||
{projects.length - markers.length} missing coordinates
|
{projects.length -
|
||||||
|
projects.filter((p) => p.coordinates).length}{" "}
|
||||||
|
missing coordinates
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Help Panel - Bottom Right */}
|
{/* 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="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">
|
<div className="text-xs text-gray-500">
|
||||||
Click markers for details • Use 📚 to switch layers
|
Click markers for details • Use 📚 to switch layers
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Full Screen Map */}
|
{/* Full Screen Map */}
|
||||||
{markers.length === 0 ? (
|
{markers.length === 0 ? (
|
||||||
<div className="h-full w-full flex items-center justify-center bg-gray-100">
|
<div className="h-full w-full flex items-center justify-center bg-gray-100">
|
||||||
|
|||||||
@@ -1,74 +1,107 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { MapContainer, TileLayer, Marker, Popup, LayersControl } from 'react-leaflet';
|
import {
|
||||||
import 'leaflet/dist/leaflet.css';
|
MapContainer,
|
||||||
import { useEffect } from 'react';
|
TileLayer,
|
||||||
import { mapLayers } from './mapLayers';
|
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
|
// Fix for default markers in react-leaflet
|
||||||
const fixLeafletIcons = () => {
|
const fixLeafletIcons = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
const L = require('leaflet');
|
const L = require("leaflet");
|
||||||
|
|
||||||
delete L.Icon.Default.prototype._getIconUrl;
|
delete L.Icon.Default.prototype._getIconUrl;
|
||||||
L.Icon.Default.mergeOptions({
|
L.Icon.Default.mergeOptions({
|
||||||
iconRetinaUrl: '/leaflet/marker-icon-2x.png',
|
iconRetinaUrl: "/leaflet/marker-icon-2x.png",
|
||||||
iconUrl: '/leaflet/marker-icon.png',
|
iconUrl: "/leaflet/marker-icon.png",
|
||||||
shadowUrl: '/leaflet/marker-shadow.png',
|
shadowUrl: "/leaflet/marker-shadow.png",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EnhancedLeafletMap({
|
// Create colored marker icons
|
||||||
center,
|
const createColoredMarkerIcon = (color) => {
|
||||||
zoom = 13,
|
if (typeof window !== "undefined") {
|
||||||
markers = [],
|
const L = require("leaflet");
|
||||||
showLayerControl = true,
|
|
||||||
defaultLayer = 'OpenStreetMap'
|
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(() => {
|
useEffect(() => {
|
||||||
fixLeafletIcons();
|
fixLeafletIcons();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { BaseLayer } = LayersControl;
|
const { BaseLayer } = LayersControl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MapContainer
|
<MapContainer
|
||||||
center={center}
|
center={center}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
scrollWheelZoom={true}
|
scrollWheelZoom={true}
|
||||||
>
|
>
|
||||||
{showLayerControl ? (
|
{showLayerControl ? (
|
||||||
<LayersControl position="topright">
|
<LayersControl position="topright">
|
||||||
{mapLayers.base.map((layer, index) => (
|
{mapLayers.base.map((layer, index) => (
|
||||||
<BaseLayer
|
<BaseLayer
|
||||||
key={index}
|
key={index}
|
||||||
checked={layer.checked || layer.name === defaultLayer}
|
checked={layer.checked || layer.name === defaultLayer}
|
||||||
name={layer.name}
|
name={layer.name}
|
||||||
>
|
>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
attribution={layer.attribution}
|
attribution={layer.attribution}
|
||||||
url={layer.url}
|
url={layer.url}
|
||||||
maxZoom={layer.maxZoom}
|
maxZoom={layer.maxZoom}
|
||||||
tileSize={layer.tileSize || 256}
|
tileSize={layer.tileSize || 256}
|
||||||
/>
|
/>
|
||||||
</BaseLayer>
|
</BaseLayer>
|
||||||
))}
|
))}
|
||||||
</LayersControl>
|
</LayersControl>
|
||||||
) : (
|
) : (
|
||||||
// Default layer when no layer control
|
// Default layer when no layer control
|
||||||
<TileLayer
|
<TileLayer
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
)}
|
)}{" "}
|
||||||
|
{markers.map((marker, index) => (
|
||||||
{markers.map((marker, index) => (
|
<Marker
|
||||||
<Marker key={index} position={marker.position}>
|
key={index}
|
||||||
{marker.popup && <Popup>{marker.popup}</Popup>}
|
position={marker.position}
|
||||||
</Marker>
|
icon={
|
||||||
))}
|
marker.color ? createColoredMarkerIcon(marker.color) : undefined
|
||||||
</MapContainer>
|
}
|
||||||
);
|
>
|
||||||
|
{marker.popup && <Popup>{marker.popup}</Popup>}
|
||||||
|
</Marker>
|
||||||
|
))}
|
||||||
|
</MapContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,173 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
// Dynamically import the map component to avoid SSR issues
|
// Dynamically import the map component to avoid SSR issues
|
||||||
const DynamicMap = dynamic(() => import('./LeafletMap'), {
|
const DynamicMap = dynamic(() => import("./LeafletMap"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <div className="w-full h-64 bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
|
loading: () => (
|
||||||
<span className="text-gray-500">Loading map...</span>
|
<div className="w-full h-64 bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
|
||||||
</div>
|
<span className="text-gray-500">Loading map...</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function ProjectMap({
|
export default function ProjectMap({
|
||||||
coordinates,
|
coordinates,
|
||||||
projectName,
|
projectName,
|
||||||
showLayerControl = true,
|
projectStatus = "registered",
|
||||||
mapHeight = 'h-64',
|
showLayerControl = true,
|
||||||
defaultLayer = 'OpenStreetMap'
|
mapHeight = "h-64",
|
||||||
|
defaultLayer = "OpenStreetMap",
|
||||||
}) {
|
}) {
|
||||||
const [coords, setCoords] = useState(null);
|
const [coords, setCoords] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
// Status configuration matching the main map
|
||||||
if (coordinates) {
|
const statusConfig = {
|
||||||
// Parse coordinates string (e.g., "49.622958,20.629562")
|
registered: { color: "#6B7280", label: "Registered" },
|
||||||
const [lat, lng] = coordinates.split(',').map(coord => parseFloat(coord.trim()));
|
in_progress_design: { color: "#3B82F6", label: "In Progress (Design)" },
|
||||||
|
in_progress_construction: {
|
||||||
if (!isNaN(lat) && !isNaN(lng)) {
|
color: "#F59E0B",
|
||||||
setCoords({ lat, lng });
|
label: "In Progress (Construction)",
|
||||||
}
|
},
|
||||||
}
|
fulfilled: { color: "#10B981", label: "Completed" },
|
||||||
}, [coordinates]);
|
};
|
||||||
|
|
||||||
if (!coords) {
|
useEffect(() => {
|
||||||
return null;
|
if (coordinates) {
|
||||||
}
|
// Parse coordinates string (e.g., "49.622958,20.629562")
|
||||||
|
const [lat, lng] = coordinates
|
||||||
|
.split(",")
|
||||||
|
.map((coord) => parseFloat(coord.trim()));
|
||||||
|
|
||||||
return (
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
<div className="space-y-2">
|
setCoords({ lat, lng });
|
||||||
<div className="flex items-center justify-between">
|
}
|
||||||
<h3 className="text-sm font-medium text-gray-700">Project Location</h3>
|
}
|
||||||
{showLayerControl && (
|
}, [coordinates]);
|
||||||
<div className="text-xs text-gray-500">
|
if (!coords) {
|
||||||
Use the layer control (📚) to switch map views
|
return (
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
)}
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<h3 className="text-sm font-medium text-gray-700">
|
||||||
<div className={`w-full ${mapHeight} rounded-lg overflow-hidden border border-gray-200`}>
|
Project Location
|
||||||
<DynamicMap
|
</h3>
|
||||||
center={[coords.lat, coords.lng]}
|
<div className="text-xs text-gray-500">No coordinates available</div>
|
||||||
zoom={15}
|
</div>
|
||||||
markers={[{
|
<div
|
||||||
position: [coords.lat, coords.lng],
|
className={`w-full ${mapHeight} rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center`}
|
||||||
popup: projectName || 'Project Location'
|
>
|
||||||
}]}
|
<div className="text-center text-gray-500">
|
||||||
showLayerControl={showLayerControl}
|
<svg
|
||||||
defaultLayer={defaultLayer}
|
className="w-8 h-8 mx-auto mb-2"
|
||||||
/>
|
fill="currentColor"
|
||||||
</div>
|
viewBox="0 0 20 20"
|
||||||
<p className="text-xs text-gray-500">
|
>
|
||||||
Coordinates: {coords.lat.toFixed(6)}, {coords.lng.toFixed(6)}
|
<path
|
||||||
</p>
|
fillRule="evenodd"
|
||||||
</div>
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
41
test-mobile.html
Normal file
41
test-mobile.html
Normal 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>
|
||||||
Reference in New Issue
Block a user