🚀 Complete Laca City Website with VPS Deployment
- Added complete Next.js frontend with responsive design - Added NestJS backend with PostgreSQL and Redis - Added comprehensive VPS deployment script (vps-deploy.sh) - Added deployment guide and documentation - Added all assets and static files - Configured SSL, Nginx, PM2, and monitoring - Ready for production deployment on any VPS
510
DEPLOYMENT.md
@@ -1,510 +0,0 @@
|
||||
# 🚀 Deployment Guide
|
||||
|
||||
This guide covers different deployment strategies for the Smart Parking Finder application.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Development Deployment](#development-deployment)
|
||||
2. [Production Deployment](#production-deployment)
|
||||
3. [Cloud Deployment Options](#cloud-deployment-options)
|
||||
4. [Environment Configuration](#environment-configuration)
|
||||
5. [Monitoring & Logging](#monitoring--logging)
|
||||
6. [Backup & Recovery](#backup--recovery)
|
||||
7. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## 🛠️ Development Deployment
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone and setup
|
||||
git clone <repository-url>
|
||||
cd smart-parking-finder
|
||||
./setup.sh
|
||||
|
||||
# 2. Start development environment
|
||||
docker-compose up -d
|
||||
|
||||
# 3. Start development servers
|
||||
cd frontend && npm run dev &
|
||||
cd backend && npm run start:dev &
|
||||
```
|
||||
|
||||
### Development Services
|
||||
|
||||
- **Frontend**: http://localhost:3000
|
||||
- **Backend API**: http://localhost:3001
|
||||
- **PostgreSQL**: localhost:5432
|
||||
- **Redis**: localhost:6379
|
||||
- **Valhalla**: http://localhost:8002
|
||||
- **pgAdmin**: http://localhost:5050 (with `--profile tools`)
|
||||
|
||||
## 🏭 Production Deployment
|
||||
|
||||
### Docker Compose Production
|
||||
|
||||
```bash
|
||||
# 1. Create production environment
|
||||
cp docker-compose.yml docker-compose.prod.yml
|
||||
|
||||
# 2. Update production configuration
|
||||
# Edit docker-compose.prod.yml with production settings
|
||||
|
||||
# 3. Deploy
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Production Docker Compose
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.prod
|
||||
ports:
|
||||
- "80:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- NEXT_PUBLIC_API_URL=https://api.yourparking.com
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.prod
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- REDIS_URL=${REDIS_URL}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "443:443"
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./nginx/ssl:/etc/nginx/ssl
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: postgis/postgis:15-3.3
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
valhalla:
|
||||
build: ./valhalla
|
||||
volumes:
|
||||
- valhalla_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
valhalla_data:
|
||||
```
|
||||
|
||||
## ☁️ Cloud Deployment Options
|
||||
|
||||
### 1. DigitalOcean Droplet
|
||||
|
||||
**Recommended for small to medium deployments**
|
||||
|
||||
```bash
|
||||
# 1. Create Droplet (4GB RAM minimum for Valhalla)
|
||||
doctl compute droplet create parking-app \
|
||||
--size s-2vcpu-4gb \
|
||||
--image ubuntu-22-04-x64 \
|
||||
--region sgp1
|
||||
|
||||
# 2. Install Docker
|
||||
ssh root@your-droplet-ip
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
|
||||
# 3. Deploy application
|
||||
git clone <repository-url>
|
||||
cd smart-parking-finder
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 2. AWS EC2 + RDS
|
||||
|
||||
**Recommended for scalable production**
|
||||
|
||||
```bash
|
||||
# 1. Launch EC2 instance (t3.medium minimum)
|
||||
# 2. Setup RDS PostgreSQL with PostGIS
|
||||
# 3. Setup ElastiCache Redis
|
||||
# 4. Deploy application containers
|
||||
|
||||
# User data script for EC2:
|
||||
#!/bin/bash
|
||||
yum update -y
|
||||
yum install -y docker git
|
||||
systemctl start docker
|
||||
systemctl enable docker
|
||||
usermod -a -G docker ec2-user
|
||||
|
||||
# Clone and deploy
|
||||
git clone <repository-url> /opt/parking-app
|
||||
cd /opt/parking-app
|
||||
docker-compose -f docker-compose.aws.yml up -d
|
||||
```
|
||||
|
||||
### 3. Google Cloud Platform
|
||||
|
||||
**Using Cloud Run and Cloud SQL**
|
||||
|
||||
```bash
|
||||
# 1. Build and push images
|
||||
gcloud builds submit --tag gcr.io/PROJECT_ID/parking-frontend ./frontend
|
||||
gcloud builds submit --tag gcr.io/PROJECT_ID/parking-backend ./backend
|
||||
|
||||
# 2. Deploy to Cloud Run
|
||||
gcloud run deploy parking-frontend \
|
||||
--image gcr.io/PROJECT_ID/parking-frontend \
|
||||
--platform managed \
|
||||
--region asia-southeast1
|
||||
|
||||
gcloud run deploy parking-backend \
|
||||
--image gcr.io/PROJECT_ID/parking-backend \
|
||||
--platform managed \
|
||||
--region asia-southeast1
|
||||
```
|
||||
|
||||
### 4. Kubernetes Deployment
|
||||
|
||||
**For large-scale deployments**
|
||||
|
||||
```yaml
|
||||
# k8s/namespace.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: parking-finder
|
||||
|
||||
---
|
||||
# k8s/configmap.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: app-config
|
||||
namespace: parking-finder
|
||||
data:
|
||||
DATABASE_HOST: "postgres-service"
|
||||
REDIS_HOST: "redis-service"
|
||||
VALHALLA_URL: "http://valhalla-service:8002"
|
||||
|
||||
---
|
||||
# k8s/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontend-deployment
|
||||
namespace: parking-finder
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: your-registry/parking-frontend:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: app-config
|
||||
```
|
||||
|
||||
## 🔧 Environment Configuration
|
||||
|
||||
### Production Environment Variables
|
||||
|
||||
```bash
|
||||
# .env.production
|
||||
NODE_ENV=production
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:pass@db-host:5432/parking_db
|
||||
POSTGRES_SSL=true
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis-host:6379
|
||||
REDIS_SSL=true
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-super-secure-jwt-secret-256-bit
|
||||
JWT_EXPIRATION=1h
|
||||
CORS_ORIGIN=https://yourparking.com
|
||||
|
||||
# APIs
|
||||
VALHALLA_URL=http://valhalla:8002
|
||||
MAP_TILES_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
|
||||
# Monitoring
|
||||
SENTRY_DSN=your-sentry-dsn
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Performance
|
||||
REDIS_CACHE_TTL=3600
|
||||
DB_POOL_SIZE=10
|
||||
API_RATE_LIMIT=100
|
||||
```
|
||||
|
||||
### SSL Configuration
|
||||
|
||||
```nginx
|
||||
# nginx/nginx.conf
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name yourparking.com;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
proxy_pass http://frontend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Backend API
|
||||
location /api {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# WebSocket support
|
||||
location /ws {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Monitoring & Logging
|
||||
|
||||
### Docker Logging
|
||||
|
||||
```yaml
|
||||
# docker-compose.monitoring.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter
|
||||
ports:
|
||||
- "9100:9100"
|
||||
|
||||
volumes:
|
||||
grafana_data:
|
||||
```
|
||||
|
||||
### Application Monitoring
|
||||
|
||||
```typescript
|
||||
// backend/src/monitoring/metrics.ts
|
||||
import { createPrometheusMetrics } from '@prometheus/client';
|
||||
|
||||
export const metrics = {
|
||||
httpRequests: new Counter({
|
||||
name: 'http_requests_total',
|
||||
help: 'Total HTTP requests',
|
||||
labelNames: ['method', 'route', 'status']
|
||||
}),
|
||||
|
||||
routeCalculationTime: new Histogram({
|
||||
name: 'route_calculation_duration_seconds',
|
||||
help: 'Route calculation duration',
|
||||
buckets: [0.1, 0.5, 1, 2, 5]
|
||||
}),
|
||||
|
||||
databaseQueries: new Counter({
|
||||
name: 'database_queries_total',
|
||||
help: 'Total database queries',
|
||||
labelNames: ['operation', 'table']
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
## 💾 Backup & Recovery
|
||||
|
||||
### Database Backup
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
|
||||
# Variables
|
||||
BACKUP_DIR="/opt/backups"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
DB_NAME="parking_db"
|
||||
|
||||
# Create backup
|
||||
docker-compose exec postgres pg_dump \
|
||||
-U parking_user \
|
||||
-h localhost \
|
||||
-d $DB_NAME \
|
||||
--clean \
|
||||
--if-exists \
|
||||
--create \
|
||||
> "$BACKUP_DIR/db_backup_$DATE.sql"
|
||||
|
||||
# Compress backup
|
||||
gzip "$BACKUP_DIR/db_backup_$DATE.sql"
|
||||
|
||||
# Keep only last 7 days
|
||||
find $BACKUP_DIR -name "db_backup_*.sql.gz" -mtime +7 -delete
|
||||
|
||||
echo "Backup completed: db_backup_$DATE.sql.gz"
|
||||
```
|
||||
|
||||
### Automated Backup with Cron
|
||||
|
||||
```bash
|
||||
# Add to crontab: crontab -e
|
||||
# Daily backup at 2 AM
|
||||
0 2 * * * /opt/parking-app/scripts/backup.sh >> /var/log/backup.log 2>&1
|
||||
|
||||
# Weekly full system backup
|
||||
0 3 * * 0 /opt/parking-app/scripts/full-backup.sh >> /var/log/backup.log 2>&1
|
||||
```
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Valhalla not starting**
|
||||
```bash
|
||||
# Check OSM data
|
||||
ls -la valhalla/custom_files/
|
||||
|
||||
# Check logs
|
||||
docker-compose logs valhalla
|
||||
|
||||
# Verify memory allocation
|
||||
docker stats valhalla
|
||||
```
|
||||
|
||||
2. **Database connection issues**
|
||||
```bash
|
||||
# Test connection
|
||||
docker-compose exec postgres psql -U parking_user -d parking_db
|
||||
|
||||
# Check network
|
||||
docker network ls
|
||||
docker network inspect parking-finder_parking-network
|
||||
```
|
||||
|
||||
3. **High memory usage**
|
||||
```bash
|
||||
# Monitor services
|
||||
docker stats
|
||||
|
||||
# Optimize Valhalla cache
|
||||
# Edit valhalla.json: reduce max_cache_size
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# health-check.sh
|
||||
|
||||
echo "=== Health Check ==="
|
||||
|
||||
# Frontend
|
||||
curl -f http://localhost:3000 || echo "❌ Frontend down"
|
||||
|
||||
# Backend
|
||||
curl -f http://localhost:3001/health || echo "❌ Backend down"
|
||||
|
||||
# Database
|
||||
docker-compose exec postgres pg_isready -U parking_user || echo "❌ Database down"
|
||||
|
||||
# Redis
|
||||
docker-compose exec redis redis-cli ping || echo "❌ Redis down"
|
||||
|
||||
# Valhalla
|
||||
curl -f http://localhost:8002/status || echo "❌ Valhalla down"
|
||||
|
||||
echo "=== Check complete ==="
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
```yaml
|
||||
# docker-compose.optimized.yml
|
||||
services:
|
||||
backend:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: '0.5'
|
||||
reservations:
|
||||
memory: 512M
|
||||
cpus: '0.25'
|
||||
|
||||
valhalla:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
cpus: '2'
|
||||
reservations:
|
||||
memory: 2G
|
||||
cpus: '1'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For additional support, refer to the [main README](../README.md) or contact the development team.
|
||||
750
DEVELOPMENT.md
@@ -1,750 +0,0 @@
|
||||
# 🛠️ Development Guide
|
||||
|
||||
This guide covers the development workflow, coding standards, and best practices for the Smart Parking Finder application.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Development Setup](#development-setup)
|
||||
2. [Project Structure](#project-structure)
|
||||
3. [Development Workflow](#development-workflow)
|
||||
4. [Coding Standards](#coding-standards)
|
||||
5. [Testing Strategy](#testing-strategy)
|
||||
6. [Debugging](#debugging)
|
||||
7. [Performance Guidelines](#performance-guidelines)
|
||||
8. [Contributing](#contributing)
|
||||
|
||||
## 🚀 Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+ and npm
|
||||
- Docker and Docker Compose
|
||||
- Git
|
||||
- VS Code (recommended)
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone <repository-url>
|
||||
cd smart-parking-finder
|
||||
|
||||
# 2. Run automated setup
|
||||
./setup.sh
|
||||
|
||||
# 3. Start development environment
|
||||
docker-compose up -d
|
||||
|
||||
# 4. Install dependencies
|
||||
cd frontend && npm install
|
||||
cd ../backend && npm install
|
||||
|
||||
# 5. Start development servers
|
||||
npm run dev:all # Starts both frontend and backend
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
NODE_ENV=development
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://parking_user:parking_pass@localhost:5432/parking_db
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Valhalla
|
||||
VALHALLA_URL=http://localhost:8002
|
||||
|
||||
# Development
|
||||
DEBUG=true
|
||||
LOG_LEVEL=debug
|
||||
HOT_RELOAD=true
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
smart-parking-finder/
|
||||
├── frontend/ # Next.js frontend application
|
||||
│ ├── src/
|
||||
│ │ ├── app/ # App router pages
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── services/ # API service layers
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── public/ # Static assets
|
||||
│ └── tests/ # Frontend tests
|
||||
├── backend/ # NestJS backend application
|
||||
│ ├── src/
|
||||
│ │ ├── modules/ # Feature modules
|
||||
│ │ ├── common/ # Shared utilities
|
||||
│ │ ├── config/ # Configuration files
|
||||
│ │ └── database/ # Database related files
|
||||
│ └── tests/ # Backend tests
|
||||
├── valhalla/ # Routing engine setup
|
||||
├── scripts/ # Development scripts
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── app/ # Next.js 14 App Router
|
||||
│ ├── (dashboard)/ # Route groups
|
||||
│ ├── api/ # API routes
|
||||
│ ├── globals.css # Global styles
|
||||
│ ├── layout.tsx # Root layout
|
||||
│ └── page.tsx # Home page
|
||||
├── components/ # UI Components
|
||||
│ ├── ui/ # Base UI components
|
||||
│ ├── forms/ # Form components
|
||||
│ ├── map/ # Map-related components
|
||||
│ └── parking/ # Parking-specific components
|
||||
├── hooks/ # Custom hooks
|
||||
│ ├── useGeolocation.ts
|
||||
│ ├── useParking.ts
|
||||
│ └── useRouting.ts
|
||||
├── services/ # API services
|
||||
│ ├── api.ts # Base API client
|
||||
│ ├── parkingService.ts
|
||||
│ └── routingService.ts
|
||||
└── types/ # TypeScript definitions
|
||||
├── parking.ts
|
||||
├── routing.ts
|
||||
└── user.ts
|
||||
```
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
```
|
||||
backend/src/
|
||||
├── modules/ # Feature modules
|
||||
│ ├── auth/ # Authentication
|
||||
│ ├── parking/ # Parking management
|
||||
│ ├── routing/ # Route calculation
|
||||
│ └── users/ # User management
|
||||
├── common/ # Shared code
|
||||
│ ├── decorators/ # Custom decorators
|
||||
│ ├── filters/ # Exception filters
|
||||
│ ├── guards/ # Auth guards
|
||||
│ └── pipes/ # Validation pipes
|
||||
├── config/ # Configuration
|
||||
│ ├── database.config.ts
|
||||
│ ├── redis.config.ts
|
||||
│ └── app.config.ts
|
||||
└── database/ # Database files
|
||||
├── migrations/ # Database migrations
|
||||
├── seeds/ # Seed data
|
||||
└── entities/ # TypeORM entities
|
||||
```
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
### Git Workflow
|
||||
|
||||
```bash
|
||||
# 1. Create feature branch
|
||||
git checkout -b feature/parking-search-improvements
|
||||
|
||||
# 2. Make changes with atomic commits
|
||||
git add .
|
||||
git commit -m "feat(parking): add distance-based search filtering"
|
||||
|
||||
# 3. Push and create PR
|
||||
git push origin feature/parking-search-improvements
|
||||
```
|
||||
|
||||
### Commit Message Convention
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
Types:
|
||||
- feat: New feature
|
||||
- fix: Bug fix
|
||||
- docs: Documentation changes
|
||||
- style: Code style changes
|
||||
- refactor: Code refactoring
|
||||
- test: Adding tests
|
||||
- chore: Maintenance tasks
|
||||
|
||||
Examples:
|
||||
feat(map): add real-time parking availability indicators
|
||||
fix(routing): resolve incorrect distance calculations
|
||||
docs(api): update parking endpoint documentation
|
||||
```
|
||||
|
||||
### Development Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev:backend": "cd backend && npm run start:dev",
|
||||
"dev:all": "concurrently \"npm run dev\" \"npm run dev:backend\"",
|
||||
"build": "next build",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"type-check": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Coding Standards
|
||||
|
||||
### TypeScript Configuration
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "es6"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/services/*": ["./src/services/*"],
|
||||
"@/types/*": ["./src/types/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESLint Configuration
|
||||
|
||||
```json
|
||||
// .eslintrc.json
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "warn",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Code Style Guidelines
|
||||
|
||||
#### Frontend Components
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Functional component with proper typing
|
||||
interface ParkingListProps {
|
||||
parkingLots: ParkingLot[];
|
||||
onSelect: (lot: ParkingLot) => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
parkingLots,
|
||||
onSelect,
|
||||
loading = false
|
||||
}) => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const handleSelect = useCallback((lot: ParkingLot) => {
|
||||
setSelectedId(lot.id);
|
||||
onSelect(lot);
|
||||
}, [onSelect]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="parking-list">
|
||||
{parkingLots.map((lot) => (
|
||||
<ParkingCard
|
||||
key={lot.id}
|
||||
lot={lot}
|
||||
isSelected={selectedId === lot.id}
|
||||
onClick={() => handleSelect(lot)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Backend Services
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Service with proper error handling and typing
|
||||
@Injectable()
|
||||
export class ParkingService {
|
||||
constructor(
|
||||
@InjectRepository(ParkingLot)
|
||||
private readonly parkingRepository: Repository<ParkingLot>,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async findNearbyParking(
|
||||
dto: FindNearbyParkingDto
|
||||
): Promise<ParkingLot[]> {
|
||||
try {
|
||||
const cacheKey = `nearby:${dto.latitude}:${dto.longitude}:${dto.radius}`;
|
||||
|
||||
// Check cache first
|
||||
const cached = await this.cacheService.get<ParkingLot[]>(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Query database with spatial index
|
||||
const lots = await this.parkingRepository
|
||||
.createQueryBuilder('lot')
|
||||
.where(
|
||||
'ST_DWithin(lot.location::geography, ST_Point(:lng, :lat)::geography, :radius)',
|
||||
{
|
||||
lng: dto.longitude,
|
||||
lat: dto.latitude,
|
||||
radius: dto.radius
|
||||
}
|
||||
)
|
||||
.andWhere('lot.isActive = :isActive', { isActive: true })
|
||||
.orderBy(
|
||||
'ST_Distance(lot.location::geography, ST_Point(:lng, :lat)::geography)',
|
||||
'ASC'
|
||||
)
|
||||
.limit(dto.limit || 20)
|
||||
.getMany();
|
||||
|
||||
// Cache results
|
||||
await this.cacheService.set(cacheKey, lots, 300); // 5 minutes
|
||||
|
||||
return lots;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to find nearby parking', error);
|
||||
throw new InternalServerErrorException('Failed to find nearby parking');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # Unit tests
|
||||
├── integration/ # Integration tests
|
||||
├── e2e/ # End-to-end tests
|
||||
└── fixtures/ # Test data
|
||||
```
|
||||
|
||||
### Frontend Testing
|
||||
|
||||
```typescript
|
||||
// components/ParkingList.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ParkingList } from './ParkingList';
|
||||
import { mockParkingLots } from '../../../tests/fixtures/parking';
|
||||
|
||||
describe('ParkingList', () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnSelect.mockClear();
|
||||
});
|
||||
|
||||
it('renders parking lots correctly', () => {
|
||||
render(
|
||||
<ParkingList
|
||||
parkingLots={mockParkingLots}
|
||||
onSelect={mockOnSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Central Mall Parking')).toBeInTheDocument();
|
||||
expect(screen.getByText('$5/hour')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onSelect when parking lot is clicked', () => {
|
||||
render(
|
||||
<ParkingList
|
||||
parkingLots={mockParkingLots}
|
||||
onSelect={mockOnSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Central Mall Parking'));
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(mockParkingLots[0]);
|
||||
});
|
||||
|
||||
it('shows loading spinner when loading', () => {
|
||||
render(
|
||||
<ParkingList
|
||||
parkingLots={[]}
|
||||
onSelect={mockOnSelect}
|
||||
loading={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Backend Testing
|
||||
|
||||
```typescript
|
||||
// parking/parking.service.spec.ts
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ParkingService } from './parking.service';
|
||||
import { ParkingLot } from './entities/parking-lot.entity';
|
||||
import { mockRepository } from '../../tests/mocks/repository.mock';
|
||||
|
||||
describe('ParkingService', () => {
|
||||
let service: ParkingService;
|
||||
let repository: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ParkingService,
|
||||
{
|
||||
provide: getRepositoryToken(ParkingLot),
|
||||
useValue: mockRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ParkingService>(ParkingService);
|
||||
repository = module.get(getRepositoryToken(ParkingLot));
|
||||
});
|
||||
|
||||
describe('findNearbyParking', () => {
|
||||
it('should return nearby parking lots', async () => {
|
||||
const mockLots = [/* mock data */];
|
||||
repository.createQueryBuilder().getMany.mockResolvedValue(mockLots);
|
||||
|
||||
const result = await service.findNearbyParking({
|
||||
latitude: 1.3521,
|
||||
longitude: 103.8198,
|
||||
radius: 1000,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockLots);
|
||||
expect(repository.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
```typescript
|
||||
// parking/parking.controller.integration.spec.ts
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../app.module';
|
||||
|
||||
describe('ParkingController (Integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/parking/nearby (POST)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/parking/nearby')
|
||||
.send({
|
||||
latitude: 1.3521,
|
||||
longitude: 103.8198,
|
||||
radius: 1000,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body).toHaveProperty('data');
|
||||
expect(Array.isArray(res.body.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🐛 Debugging
|
||||
|
||||
### Frontend Debugging
|
||||
|
||||
```typescript
|
||||
// Development debugging utilities
|
||||
export const debugLog = (message: string, data?: any): void => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[DEBUG] ${message}`, data);
|
||||
}
|
||||
};
|
||||
|
||||
// React Developer Tools
|
||||
// Component debugging
|
||||
export const ParkingDebugger: React.FC = () => {
|
||||
const [parkingLots, setParkingLots] = useLocalStorage('debug:parking', []);
|
||||
|
||||
useEffect(() => {
|
||||
// Log component updates
|
||||
debugLog('ParkingDebugger mounted');
|
||||
|
||||
return () => {
|
||||
debugLog('ParkingDebugger unmounted');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="debug-panel">
|
||||
<h3>Parking Debug Info</h3>
|
||||
<pre>{JSON.stringify(parkingLots, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Backend Debugging
|
||||
|
||||
```typescript
|
||||
// Logger configuration
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class DebugService {
|
||||
private readonly logger = new Logger(DebugService.name);
|
||||
|
||||
logRequest(req: Request, res: Response, next: NextFunction): void {
|
||||
const { method, originalUrl, body, query } = req;
|
||||
|
||||
this.logger.debug(`${method} ${originalUrl}`, {
|
||||
body,
|
||||
query,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
logDatabaseQuery(query: string, parameters?: any[]): void {
|
||||
this.logger.debug('Database Query', {
|
||||
query,
|
||||
parameters,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VS Code Debug Configuration
|
||||
|
||||
```json
|
||||
// .vscode/launch.json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Frontend",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/frontend",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"]
|
||||
},
|
||||
{
|
||||
"name": "Debug Backend",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/backend",
|
||||
"program": "${workspaceFolder}/backend/dist/main.js",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"restart": true,
|
||||
"protocol": "inspector"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## ⚡ Performance Guidelines
|
||||
|
||||
### Frontend Performance
|
||||
|
||||
```typescript
|
||||
// Use React.memo for expensive components
|
||||
export const ParkingMap = React.memo<ParkingMapProps>(({
|
||||
parkingLots,
|
||||
onMarkerClick
|
||||
}) => {
|
||||
// Component implementation
|
||||
});
|
||||
|
||||
// Optimize re-renders with useMemo
|
||||
const filteredLots = useMemo(() => {
|
||||
return parkingLots.filter(lot =>
|
||||
lot.availableSpaces > 0 &&
|
||||
lot.distance <= maxDistance
|
||||
);
|
||||
}, [parkingLots, maxDistance]);
|
||||
|
||||
// Virtual scrolling for large lists
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
|
||||
const VirtualizedParkingList: React.FC = ({ items }) => (
|
||||
<List
|
||||
height={600}
|
||||
itemCount={items.length}
|
||||
itemSize={120}
|
||||
itemData={items}
|
||||
>
|
||||
{ParkingRow}
|
||||
</List>
|
||||
);
|
||||
```
|
||||
|
||||
### Backend Performance
|
||||
|
||||
```typescript
|
||||
// Database query optimization
|
||||
@Injectable()
|
||||
export class OptimizedParkingService {
|
||||
// Use spatial indexes
|
||||
async findNearbyOptimized(dto: FindNearbyParkingDto): Promise<ParkingLot[]> {
|
||||
return this.parkingRepository.query(`
|
||||
SELECT *
|
||||
FROM parking_lots
|
||||
WHERE ST_DWithin(
|
||||
location::geography,
|
||||
ST_Point($1, $2)::geography,
|
||||
$3
|
||||
)
|
||||
AND available_spaces > 0
|
||||
ORDER BY location <-> ST_Point($1, $2)
|
||||
LIMIT $4
|
||||
`, [dto.longitude, dto.latitude, dto.radius, dto.limit]);
|
||||
}
|
||||
|
||||
// Implement caching
|
||||
@Cacheable('parking:nearby', 300) // 5 minutes
|
||||
async findNearbyCached(dto: FindNearbyParkingDto): Promise<ParkingLot[]> {
|
||||
return this.findNearbyOptimized(dto);
|
||||
}
|
||||
}
|
||||
|
||||
// Connection pooling
|
||||
export const databaseConfig: TypeOrmModuleOptions = {
|
||||
type: 'postgres',
|
||||
url: process.env.DATABASE_URL,
|
||||
extra: {
|
||||
max: 20, // maximum number of connections
|
||||
connectionTimeoutMillis: 2000,
|
||||
idleTimeoutMillis: 30000,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. **Fork and Clone**
|
||||
```bash
|
||||
git clone https://github.com/your-username/smart-parking-finder.git
|
||||
cd smart-parking-finder
|
||||
git remote add upstream https://github.com/original/smart-parking-finder.git
|
||||
```
|
||||
|
||||
2. **Create Feature Branch**
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
3. **Development**
|
||||
- Follow coding standards
|
||||
- Write tests for new features
|
||||
- Update documentation
|
||||
|
||||
4. **Submit PR**
|
||||
- Ensure all tests pass
|
||||
- Update CHANGELOG.md
|
||||
- Provide clear description
|
||||
|
||||
### Code Review Guidelines
|
||||
|
||||
- **Code Quality**: Follows TypeScript best practices
|
||||
- **Testing**: Adequate test coverage (>80%)
|
||||
- **Performance**: No performance regressions
|
||||
- **Documentation**: Updated documentation
|
||||
- **Security**: No security vulnerabilities
|
||||
|
||||
### Issue Templates
|
||||
|
||||
```markdown
|
||||
## Bug Report
|
||||
|
||||
**Describe the bug**
|
||||
A clear description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
What you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots.
|
||||
|
||||
**Environment:**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more information, see the [README](../README.md) and [Technical Specification](../TECHNICAL_SPECIFICATION.md).
|
||||
@@ -1,189 +0,0 @@
|
||||
# 🚀 Git Upload Guide for Smart Parking Finder
|
||||
|
||||
## 📋 Instructions to Upload to Gitea Repository
|
||||
|
||||
### 1. Navigate to Project Directory
|
||||
```bash
|
||||
cd /Users/phongworking/Desktop/Working/Laca_city/Website_Demo_App
|
||||
```
|
||||
|
||||
### 2. Initialize Git Repository (if not already done)
|
||||
```bash
|
||||
# Check if Git is already initialized
|
||||
git status
|
||||
|
||||
# If not initialized, run:
|
||||
git init
|
||||
```
|
||||
|
||||
### 3. Configure Git User (if not set globally)
|
||||
```bash
|
||||
git config user.name "Phong Pham"
|
||||
git config user.email "your-email@example.com"
|
||||
```
|
||||
|
||||
### 4. Add All Files to Git
|
||||
```bash
|
||||
# Add all files to staging
|
||||
git add .
|
||||
|
||||
# Check what will be committed
|
||||
git status
|
||||
```
|
||||
|
||||
### 5. Create Initial Commit
|
||||
```bash
|
||||
git commit -m "🚀 Initial commit: Smart Parking Finder
|
||||
|
||||
✨ Features:
|
||||
- Next.js 14 frontend with React 18 & TypeScript
|
||||
- NestJS backend with PostgreSQL & Redis
|
||||
- Interactive OpenStreetMap with React Leaflet
|
||||
- Real-time parking search and reservations
|
||||
- Docker development environment
|
||||
- Comprehensive documentation
|
||||
|
||||
📁 Project Structure:
|
||||
- /frontend - Next.js application
|
||||
- /backend - NestJS API server
|
||||
- /scripts - Deployment and development scripts
|
||||
- /Documents - Complete documentation
|
||||
- /valhalla - Routing engine configuration
|
||||
|
||||
🛠️ Quick Start:
|
||||
- ./launch.sh - Interactive launcher
|
||||
- ./scripts/setup.sh - Initial setup
|
||||
- ./scripts/frontend-only.sh - Quick demo
|
||||
- ./scripts/full-dev.sh - Full development
|
||||
- ./scripts/docker-dev.sh - Docker environment
|
||||
|
||||
📚 Documentation:
|
||||
- Complete system architecture
|
||||
- API schemas and database design
|
||||
- Deployment guides and troubleshooting
|
||||
- Performance optimization reports"
|
||||
```
|
||||
|
||||
### 6. Add Remote Repository
|
||||
```bash
|
||||
# Add your Gitea repository as remote origin
|
||||
git remote add origin https://gitea.phongprojects.id.vn/phongpham/Laca-City.git
|
||||
|
||||
# Verify remote is added
|
||||
git remote -v
|
||||
```
|
||||
|
||||
### 7. Set Main Branch and Push
|
||||
```bash
|
||||
# Set main branch
|
||||
git branch -M main
|
||||
|
||||
# Push to remote repository
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### 8. Verify Upload
|
||||
```bash
|
||||
# Check if push was successful
|
||||
git status
|
||||
|
||||
# View commit history
|
||||
git log --oneline -5
|
||||
|
||||
# Check remote tracking
|
||||
git branch -vv
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### If Authentication is Required:
|
||||
```bash
|
||||
# Option 1: Use personal access token
|
||||
git remote set-url origin https://your-username:your-token@gitea.phongprojects.id.vn/phongpham/Laca-City.git
|
||||
|
||||
# Option 2: Use SSH (if SSH key is configured)
|
||||
git remote set-url origin git@gitea.phongprojects.id.vn:phongpham/Laca-City.git
|
||||
```
|
||||
|
||||
### If Repository Already Exists:
|
||||
```bash
|
||||
# Force push (use with caution)
|
||||
git push -u origin main --force
|
||||
|
||||
# Or pull first then push
|
||||
git pull origin main --allow-unrelated-histories
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### If Large Files Need to be Excluded:
|
||||
The `.gitignore` file has been created to exclude:
|
||||
- `node_modules/`
|
||||
- `.next/`
|
||||
- `dist/`
|
||||
- `.env` files
|
||||
- Database files
|
||||
- Log files
|
||||
- Cache files
|
||||
|
||||
## 📊 Repository Structure After Upload
|
||||
|
||||
Your Gitea repository will contain:
|
||||
```
|
||||
Laca-City/
|
||||
├── 📁 Documents/ # Complete documentation
|
||||
├── 📁 scripts/ # Deployment scripts
|
||||
├── 📁 frontend/ # Next.js application
|
||||
├── 📁 backend/ # NestJS API
|
||||
├── 📁 valhalla/ # Routing engine
|
||||
├── 📁 assets/ # Static assets
|
||||
├── 🚀 launch.sh # Quick launcher
|
||||
├── 🐳 docker-compose.yml # Docker configuration
|
||||
├── 📋 .gitignore # Git ignore rules
|
||||
└── 🧹 REORGANIZATION_GUIDE.md
|
||||
```
|
||||
|
||||
## 🎯 Next Steps After Upload
|
||||
|
||||
1. **Clone on other machines:**
|
||||
```bash
|
||||
git clone https://gitea.phongprojects.id.vn/phongpham/Laca-City.git
|
||||
cd Laca-City
|
||||
./scripts/setup.sh
|
||||
```
|
||||
|
||||
2. **Development workflow:**
|
||||
```bash
|
||||
# Make changes
|
||||
git add .
|
||||
git commit -m "Description of changes"
|
||||
git push
|
||||
```
|
||||
|
||||
3. **Branching strategy:**
|
||||
```bash
|
||||
# Create feature branch
|
||||
git checkout -b feature/new-feature
|
||||
|
||||
# After development
|
||||
git push -u origin feature/new-feature
|
||||
# Create pull request in Gitea
|
||||
```
|
||||
|
||||
## 🔒 Security Notes
|
||||
|
||||
- ✅ `.gitignore` excludes sensitive files (`.env`, `node_modules`)
|
||||
- ✅ No database credentials in repository
|
||||
- ✅ No API keys or secrets committed
|
||||
- ⚠️ Remember to set environment variables in production
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you encounter issues:
|
||||
1. Check network connectivity to gitea.phongprojects.id.vn
|
||||
2. Verify repository permissions in Gitea web interface
|
||||
3. Ensure Git credentials are correct
|
||||
4. Check if repository size limits are exceeded
|
||||
|
||||
---
|
||||
|
||||
*Run these commands in your terminal to upload the complete Smart Parking Finder project to your Gitea repository.*
|
||||
@@ -1,104 +0,0 @@
|
||||
# 📦 MAPVIEW VERSION HISTORY
|
||||
|
||||
## 🎯 MapView v2.0 - Global Deployment Ready
|
||||
**Ngày:** 20/07/2025
|
||||
**File:** `MapView-v2.0.tsx`
|
||||
|
||||
### ✨ **TÍNH NĂNG CHÍNH:**
|
||||
|
||||
#### 🗺️ **Auto-Zoom Intelligence**
|
||||
- **Smart Bounds Fitting:** Tự động zoom để hiển thị vừa GPS và parking đã chọn
|
||||
- **Adaptive Padding:** 50px padding cho visual balance tối ưu
|
||||
- **Max Zoom Control:** Giới hạn zoom level 16 để tránh quá gần
|
||||
- **Dynamic Centering:** Center trên user location khi không chọn parking
|
||||
|
||||
#### 🎨 **Enhanced Visual Design**
|
||||
- **3D GPS Marker:** Multi-layer pulsing với gradient effects
|
||||
- **Advanced Parking Icons:** Status-based colors với availability indicators
|
||||
- **Enhanced Selection Effects:** Highlighted states với animation
|
||||
- **Dimming System:** Non-selected parkings được làm mờ khi có selection
|
||||
|
||||
#### 🛣️ **Professional Route Display**
|
||||
- **Multi-layer Route:** 6 layers với glow, shadow, main, animated dash
|
||||
- **Real-time Calculation:** OpenRouteService API integration
|
||||
- **Visual Route Info:** Distance & duration display trong popup
|
||||
- **Animated Flow:** CSS animations cho movement effect
|
||||
|
||||
#### 📱 **Production Optimizations**
|
||||
- **SSR Safe:** Dynamic imports cho Leaflet components
|
||||
- **Performance:** Optimized re-renders và memory management
|
||||
- **Error Handling:** Robust route calculation với fallback
|
||||
- **Global Ready:** Deployed và tested trên Vercel
|
||||
|
||||
### 🔧 **TECHNICAL SPECS:**
|
||||
|
||||
```typescript
|
||||
// Core Features
|
||||
- Auto-zoom với fitBounds()
|
||||
- Enhanced marker systems
|
||||
- Route calculation API
|
||||
- Status-based styling
|
||||
- Animation frameworks
|
||||
|
||||
// Performance
|
||||
- Dynamic imports
|
||||
- Optimized effects
|
||||
- Memory management
|
||||
- Error boundaries
|
||||
```
|
||||
|
||||
### 🌍 **DEPLOYMENT STATUS:**
|
||||
- ✅ **Production Build:** Successful
|
||||
- ✅ **Vercel Deploy:** https://whatever-ctk2auuxr-phong12hexdockworks-projects.vercel.app
|
||||
- ✅ **Global Access:** Worldwide availability
|
||||
- ✅ **HTTPS Ready:** Secure connections
|
||||
- ✅ **CDN Optimized:** Fast loading globally
|
||||
|
||||
### 🎯 **USE CASES:**
|
||||
1. **Smart Parking Discovery:** Auto-zoom to show user + nearby parking
|
||||
2. **Route Planning:** Visual route với distance/time info
|
||||
3. **Status Monitoring:** Real-time parking availability
|
||||
4. **Global Access:** Use from anywhere in the world
|
||||
|
||||
### 📊 **PERFORMANCE METRICS:**
|
||||
- **Bundle Size:** 22.8 kB optimized
|
||||
- **First Load:** 110 kB total
|
||||
- **Build Time:** ~1 minute
|
||||
- **Global Latency:** <200ms via CDN
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ **PREVIOUS VERSIONS:**
|
||||
|
||||
### MapView v1.x
|
||||
- Basic Leaflet integration
|
||||
- Simple markers
|
||||
- Local development only
|
||||
- No auto-zoom features
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **DEPLOYMENT COMMANDS:**
|
||||
|
||||
```bash
|
||||
# Local development
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Global deployment
|
||||
./deploy-vercel.sh
|
||||
|
||||
# Alternative global access
|
||||
./start-global.sh # ngrok tunnel
|
||||
```
|
||||
|
||||
## 📝 **NOTES:**
|
||||
- Version 2.0 marks the first globally accessible release
|
||||
- All major build errors resolved for production
|
||||
- Auto-zoom feature is the key differentiator
|
||||
- Route calculation adds professional UX
|
||||
- Enhanced visuals provide premium feel
|
||||
|
||||
**Status:** ✅ PRODUCTION READY - GLOBALLY ACCESSIBLE
|
||||
@@ -1,145 +0,0 @@
|
||||
# 🧹 Project Cleanup & Organization Completed
|
||||
|
||||
## ✅ New Project Structure
|
||||
|
||||
```
|
||||
Website_Demo_App/
|
||||
├── 📁 Documents/ # All documentation files
|
||||
│ ├── README.md # Main project documentation
|
||||
│ ├── LOCAL_DEPLOYMENT_GUIDE.md
|
||||
│ ├── SYSTEM_ARCHITECTURE.md
|
||||
│ ├── API_SCHEMA.md
|
||||
│ ├── DATABASE_DESIGN.md
|
||||
│ ├── DEVELOPMENT.md
|
||||
│ ├── DEPLOYMENT.md
|
||||
│ ├── TECHNICAL_SPECIFICATION.md
|
||||
│ ├── PERFORMANCE_OPTIMIZATION_REPORT.md
|
||||
│ └── MAPVIEW_VERSIONS.md
|
||||
│
|
||||
├── 📁 scripts/ # All deployment scripts
|
||||
│ ├── README.md # Scripts documentation
|
||||
│ ├── start.sh # Interactive menu with all options
|
||||
│ ├── frontend-only.sh # Quick frontend demo
|
||||
│ ├── full-dev.sh # Full development environment
|
||||
│ ├── docker-dev.sh # Docker development
|
||||
│ └── setup.sh # Initial project setup
|
||||
│
|
||||
├── 📁 frontend/ # Next.js application
|
||||
├── 📁 backend/ # NestJS application
|
||||
├── 📁 valhalla/ # Routing engine
|
||||
├── 📁 assets/ # Static assets
|
||||
├── 🚀 launch.sh # Quick launcher script
|
||||
└── 🐳 docker-compose.yml # Docker configuration
|
||||
```
|
||||
|
||||
## 🎯 How to Use the New Structure
|
||||
|
||||
### 1. Quick Start Options
|
||||
|
||||
```bash
|
||||
# Interactive launcher (recommended for new users)
|
||||
./launch.sh
|
||||
|
||||
# Direct script access
|
||||
./scripts/start.sh # Interactive menu
|
||||
./scripts/frontend-only.sh # Quick demo
|
||||
./scripts/full-dev.sh # Full development
|
||||
./scripts/docker-dev.sh # Docker environment
|
||||
./scripts/setup.sh # Initial setup
|
||||
```
|
||||
|
||||
### 2. First Time Setup
|
||||
|
||||
```bash
|
||||
# 1. Initial setup
|
||||
./scripts/setup.sh
|
||||
|
||||
# 2. Start development
|
||||
./launch.sh # Choose option 2 for quick demo
|
||||
```
|
||||
|
||||
### 3. Daily Development
|
||||
|
||||
```bash
|
||||
# Most common: Quick frontend demo
|
||||
./scripts/frontend-only.sh
|
||||
|
||||
# Full stack development
|
||||
./scripts/full-dev.sh
|
||||
|
||||
# Complete environment with database
|
||||
./scripts/docker-dev.sh
|
||||
```
|
||||
|
||||
## 📋 Script Organization Benefits
|
||||
|
||||
### ✅ Cleaner Root Directory
|
||||
- Only essential files in root
|
||||
- All scripts organized in `/scripts/` folder
|
||||
- All documentation in `/Documents/` folder
|
||||
|
||||
### ✅ Better Script Names
|
||||
- `frontend-only.sh` (instead of `start-frontend-only.sh`)
|
||||
- `full-dev.sh` (instead of `start-local.sh`)
|
||||
- `docker-dev.sh` (instead of `start-dev.sh`)
|
||||
- Clear, concise naming convention
|
||||
|
||||
### ✅ Enhanced Functionality
|
||||
- Interactive launcher (`launch.sh`) in root for quick access
|
||||
- Comprehensive menu system in `scripts/start.sh`
|
||||
- Better error handling and colored output
|
||||
- Auto-dependency installation
|
||||
|
||||
### ✅ Improved Documentation
|
||||
- Centralized documentation in `/Documents/`
|
||||
- Scripts documentation in `/scripts/README.md`
|
||||
- Clear usage examples and troubleshooting
|
||||
|
||||
## 🔄 Migration Guide
|
||||
|
||||
If you were using old scripts, here's the mapping:
|
||||
|
||||
| Old Command | New Command | Notes |
|
||||
|-------------|-------------|-------|
|
||||
| `./start.sh` | `./scripts/start.sh` | Enhanced interactive menu |
|
||||
| `./start-frontend-only.sh` | `./scripts/frontend-only.sh` | Renamed for clarity |
|
||||
| `./start-dev.sh` | `./scripts/docker-dev.sh` | Docker environment |
|
||||
| `./start-local.sh` | `./scripts/full-dev.sh` | Full development |
|
||||
| `./setup.sh` | `./scripts/setup.sh` | Moved to scripts folder |
|
||||
|
||||
## 🚀 Quick Commands Reference
|
||||
|
||||
```bash
|
||||
# Setup (first time only)
|
||||
./scripts/setup.sh
|
||||
|
||||
# Quick demo
|
||||
./scripts/frontend-only.sh
|
||||
|
||||
# Full development
|
||||
./scripts/full-dev.sh
|
||||
|
||||
# Docker environment
|
||||
./scripts/docker-dev.sh
|
||||
|
||||
# Interactive menu
|
||||
./scripts/start.sh
|
||||
|
||||
# Quick launcher
|
||||
./launch.sh
|
||||
```
|
||||
|
||||
## 📚 Documentation Access
|
||||
|
||||
All documentation is now organized in the `Documents/` folder:
|
||||
|
||||
- **Main docs**: `Documents/README.md`
|
||||
- **Deployment**: `Documents/LOCAL_DEPLOYMENT_GUIDE.md`
|
||||
- **Architecture**: `Documents/SYSTEM_ARCHITECTURE.md`
|
||||
- **API**: `Documents/API_SCHEMA.md`
|
||||
- **Database**: `Documents/DATABASE_DESIGN.md`
|
||||
- **Scripts**: `scripts/README.md`
|
||||
|
||||
---
|
||||
|
||||
*Project reorganization completed for better maintainability and cleaner structure.*
|
||||
@@ -1,420 +0,0 @@
|
||||
# 🚗 Smart Parking Finder - Technical Specification
|
||||
|
||||
## 📋 Project Overview
|
||||
|
||||
A responsive web application that helps users find and navigate to the nearest available parking lots using OpenStreetMap and Valhalla Routing Engine with real-time availability and turn-by-turn navigation.
|
||||
|
||||
## 🎯 Core Features
|
||||
|
||||
### 🔍 Location & Discovery
|
||||
- GPS-based user location detection
|
||||
- Interactive map with nearby parking lots
|
||||
- Real-time availability display
|
||||
- Distance and direction calculation
|
||||
- Smart parking suggestions
|
||||
|
||||
### 🗺️ Navigation & Routing
|
||||
- Valhalla-powered route generation
|
||||
- Turn-by-turn directions
|
||||
- Visual route display on map
|
||||
- Estimated arrival time
|
||||
- Alternative route options
|
||||
|
||||
### 📊 Parking Information
|
||||
- Name, address, and contact details
|
||||
- Real-time available slots
|
||||
- Pricing per hour
|
||||
- Operating hours
|
||||
- Amenities and features
|
||||
|
||||
## 🏗️ System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Frontend │ │ Backend API │ │ Database │
|
||||
│ (Next.js) │◄──►│ (NestJS) │◄──►│ PostgreSQL + │
|
||||
│ │ │ │ │ PostGIS │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Valhalla Engine │
|
||||
│ (Docker) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 🔧 Technology Stack
|
||||
|
||||
### Frontend
|
||||
- **Framework**: Next.js 14 with TypeScript
|
||||
- **Map Library**: React Leaflet + OpenStreetMap
|
||||
- **UI Framework**: Tailwind CSS with custom branding
|
||||
- **State Management**: React Query + Zustand
|
||||
- **HTTP Client**: Axios with interceptors
|
||||
- **PWA Support**: Next.js PWA plugin
|
||||
|
||||
### Backend
|
||||
- **Framework**: NestJS with TypeScript
|
||||
- **Database ORM**: TypeORM with PostGIS
|
||||
- **Caching**: Redis for route caching
|
||||
- **API Documentation**: Swagger/OpenAPI
|
||||
- **Authentication**: JWT + Passport.js
|
||||
- **Rate Limiting**: Express rate limiter
|
||||
|
||||
### Infrastructure
|
||||
- **Routing Engine**: Valhalla (Docker)
|
||||
- **Database**: PostgreSQL 15 + PostGIS 3.3
|
||||
- **Deployment**: Docker Compose
|
||||
- **Monitoring**: Prometheus + Grafana
|
||||
- **CDN**: CloudFlare for static assets
|
||||
|
||||
## 📊 Database Schema
|
||||
|
||||
```sql
|
||||
-- Parking lots table
|
||||
CREATE TABLE parking_lots (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
location GEOGRAPHY(POINT, 4326) NOT NULL,
|
||||
lat DOUBLE PRECISION NOT NULL,
|
||||
lng DOUBLE PRECISION NOT NULL,
|
||||
hourly_rate DECIMAL(10,2),
|
||||
open_time TIME,
|
||||
close_time TIME,
|
||||
available_slots INTEGER DEFAULT 0,
|
||||
total_slots INTEGER NOT NULL,
|
||||
amenities JSONB DEFAULT '{}',
|
||||
contact_info JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Spatial index for location queries
|
||||
CREATE INDEX idx_parking_lots_location ON parking_lots USING GIST (location);
|
||||
|
||||
-- Users table (for favorites, history)
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE,
|
||||
name VARCHAR(255),
|
||||
preferences JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Parking history
|
||||
CREATE TABLE parking_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
parking_lot_id INTEGER REFERENCES parking_lots(id),
|
||||
visit_date TIMESTAMP DEFAULT NOW(),
|
||||
duration_minutes INTEGER,
|
||||
rating INTEGER CHECK (rating >= 1 AND rating <= 5)
|
||||
);
|
||||
|
||||
-- Real-time parking updates
|
||||
CREATE TABLE parking_updates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
parking_lot_id INTEGER REFERENCES parking_lots(id),
|
||||
available_slots INTEGER NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT NOW(),
|
||||
source VARCHAR(50) DEFAULT 'sensor'
|
||||
);
|
||||
```
|
||||
|
||||
## 🚀 API Endpoints
|
||||
|
||||
### Parking Discovery
|
||||
```typescript
|
||||
// GET /api/parking/nearby
|
||||
interface NearbyParkingRequest {
|
||||
lat: number;
|
||||
lng: number;
|
||||
radius?: number; // meters, default 4000
|
||||
maxResults?: number; // default 20
|
||||
priceRange?: [number, number];
|
||||
amenities?: string[];
|
||||
}
|
||||
|
||||
interface NearbyParkingResponse {
|
||||
parkingLots: ParkingLot[];
|
||||
userLocation: { lat: number; lng: number };
|
||||
searchRadius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Route Planning
|
||||
```typescript
|
||||
// POST /api/routing/calculate
|
||||
interface RouteRequest {
|
||||
origin: { lat: number; lng: number };
|
||||
destination: { lat: number; lng: number };
|
||||
costing: 'auto' | 'bicycle' | 'pedestrian';
|
||||
alternatives?: number;
|
||||
}
|
||||
|
||||
interface RouteResponse {
|
||||
routes: Route[];
|
||||
summary: {
|
||||
distance: number; // km
|
||||
time: number; // minutes
|
||||
cost: number; // estimated fuel cost
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time Updates
|
||||
```typescript
|
||||
// WebSocket: /ws/parking-updates
|
||||
interface ParkingUpdate {
|
||||
parkingLotId: number;
|
||||
availableSlots: number;
|
||||
timestamp: string;
|
||||
confidence: number; // 0-1
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Brand Integration
|
||||
|
||||
Based on the existing assets in `/assets/`:
|
||||
- **Logo**: Use Logo.png for header branding
|
||||
- **Logo with Slogan**: Use Logo_and_sologan.png for splash screen
|
||||
- **Location Icons**: Integrate Location.png and mini_location.png for map markers
|
||||
|
||||
### Color Palette
|
||||
```css
|
||||
:root {
|
||||
--primary: #E85A4F; /* LACA Red */
|
||||
--secondary: #D73502; /* Darker Red */
|
||||
--accent: #8B2635; /* Deep Red */
|
||||
--success: #22C55E; /* Green for available */
|
||||
--warning: #F59E0B; /* Amber for limited */
|
||||
--danger: #EF4444; /* Red for unavailable */
|
||||
--neutral: #6B7280; /* Gray */
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 UI/UX Design
|
||||
|
||||
### Layout Structure
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Header [Logo] [Search] [Profile] │
|
||||
├─────────────────┬───────────────────────┤
|
||||
│ Sidebar │ Map View │
|
||||
│ - Filters │ - User location │
|
||||
│ - Parking List │ - Parking markers │
|
||||
│ - Selected Info │ - Route overlay │
|
||||
│ - Directions │ - Controls │
|
||||
└─────────────────┴───────────────────────┘
|
||||
```
|
||||
|
||||
### Responsive Breakpoints
|
||||
- **Mobile**: < 768px (full-screen map with drawer)
|
||||
- **Tablet**: 768px - 1024px (split view)
|
||||
- **Desktop**: > 1024px (sidebar + map)
|
||||
|
||||
## 🐳 Docker Configuration
|
||||
|
||||
### Valhalla Setup
|
||||
```dockerfile
|
||||
# Dockerfile.valhalla
|
||||
FROM ghcr.io/gis-ops/docker-valhalla/valhalla:latest
|
||||
|
||||
# Copy OSM data
|
||||
COPY ./osm-data/*.pbf /custom_files/
|
||||
|
||||
# Configuration
|
||||
COPY valhalla.json /valhalla.json
|
||||
|
||||
EXPOSE 8002
|
||||
|
||||
CMD ["valhalla_service", "/valhalla.json"]
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NEXT_PUBLIC_API_URL=http://backend:3001
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://user:pass@postgres:5432/parking_db
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- VALHALLA_URL=http://valhalla:8002
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- valhalla
|
||||
|
||||
postgres:
|
||||
image: postgis/postgis:15-3.3
|
||||
environment:
|
||||
- POSTGRES_DB=parking_db
|
||||
- POSTGRES_USER=user
|
||||
- POSTGRES_PASSWORD=pass
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
valhalla:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.valhalla
|
||||
ports:
|
||||
- "8002:8002"
|
||||
volumes:
|
||||
- ./valhalla-data:/valhalla-data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### Frontend Security
|
||||
- Content Security Policy (CSP)
|
||||
- HTTPS enforcement
|
||||
- API key protection
|
||||
- Input sanitization
|
||||
|
||||
### Backend Security
|
||||
- Rate limiting per IP
|
||||
- JWT token validation
|
||||
- SQL injection prevention
|
||||
- CORS configuration
|
||||
|
||||
### Infrastructure Security
|
||||
- Database encryption at rest
|
||||
- SSL/TLS certificates
|
||||
- Network segmentation
|
||||
- Regular security updates
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### Frontend Optimization
|
||||
- Code splitting by routes
|
||||
- Image optimization with Next.js
|
||||
- Service worker for caching
|
||||
- Lazy loading for map components
|
||||
|
||||
### Backend Optimization
|
||||
- Database query optimization
|
||||
- Redis caching for frequent requests
|
||||
- Connection pooling
|
||||
- Response compression
|
||||
|
||||
### Database Optimization
|
||||
- Spatial indexes for geo queries
|
||||
- Query result caching
|
||||
- Read replicas for scaling
|
||||
- Partitioning for large datasets
|
||||
|
||||
## 🚀 Deployment Strategy
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Local development setup
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
npm run dev:frontend
|
||||
npm run dev:backend
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# Production deployment
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
1. **Build**: Docker images for each service
|
||||
2. **Test**: Unit tests, integration tests, E2E tests
|
||||
3. **Deploy**: Blue-green deployment strategy
|
||||
4. **Monitor**: Health checks and performance metrics
|
||||
|
||||
## 📊 Monitoring & Analytics
|
||||
|
||||
### Application Metrics
|
||||
- Response times
|
||||
- Error rates
|
||||
- User engagement
|
||||
- Route calculation performance
|
||||
|
||||
### Business Metrics
|
||||
- Popular parking locations
|
||||
- Peak usage times
|
||||
- User retention
|
||||
- Revenue per parking lot
|
||||
|
||||
## 🔄 Future Enhancements
|
||||
|
||||
### Phase 2 Features
|
||||
- Parking reservations
|
||||
- Payment integration
|
||||
- User reviews and ratings
|
||||
- Push notifications for parking alerts
|
||||
|
||||
### Phase 3 Features
|
||||
- AI-powered parking predictions
|
||||
- Electric vehicle charging stations
|
||||
- Multi-language support
|
||||
- Offline mode with cached data
|
||||
|
||||
## 📋 Implementation Timeline
|
||||
|
||||
### Week 1-2: Foundation
|
||||
- Project setup and infrastructure
|
||||
- Database schema and migrations
|
||||
- Basic API endpoints
|
||||
|
||||
### Week 3-4: Core Features
|
||||
- Map integration with Leaflet
|
||||
- Parking lot display and search
|
||||
- User location detection
|
||||
|
||||
### Week 5-6: Navigation
|
||||
- Valhalla integration
|
||||
- Route calculation and display
|
||||
- Turn-by-turn directions
|
||||
|
||||
### Week 7-8: Polish
|
||||
- UI/UX improvements
|
||||
- Performance optimization
|
||||
- Testing and bug fixes
|
||||
|
||||
### Week 9-10: Deployment
|
||||
- Production setup
|
||||
- CI/CD pipeline
|
||||
- Monitoring and analytics
|
||||
|
||||
## 🏁 Success Metrics
|
||||
|
||||
### Technical KPIs
|
||||
- Page load time < 2 seconds
|
||||
- Route calculation < 3 seconds
|
||||
- 99.9% uptime
|
||||
- Zero security vulnerabilities
|
||||
|
||||
### User Experience KPIs
|
||||
- User retention > 60%
|
||||
- Average session time > 5 minutes
|
||||
- Route accuracy > 95%
|
||||
- User satisfaction score > 4.5/5
|
||||
|
||||
This comprehensive specification provides a solid foundation for building a world-class parking finder application with modern web technologies and best practices.
|
||||
251
VPS-DEPLOYMENT-GUIDE.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# 🚀 Laca City Website - VPS Deployment Guide
|
||||
|
||||
This guide will help you deploy your Laca City website to a VPS with a custom domain using a single script.
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
### VPS Requirements
|
||||
- **OS**: Ubuntu 20.04+ or Debian 11+
|
||||
- **RAM**: Minimum 2GB (4GB recommended)
|
||||
- **Storage**: Minimum 20GB SSD
|
||||
- **CPU**: 2+ cores recommended
|
||||
- **Root/Sudo access**: Required
|
||||
|
||||
### Domain Requirements
|
||||
- Domain name pointed to your VPS IP address
|
||||
- DNS A record: `yourdomain.com` → `your-vps-ip`
|
||||
- DNS A record: `www.yourdomain.com` → `your-vps-ip`
|
||||
|
||||
### Before You Start
|
||||
- [ ] VPS is running Ubuntu/Debian
|
||||
- [ ] You have SSH access to your VPS
|
||||
- [ ] Domain DNS is properly configured
|
||||
- [ ] You have a valid email address for SSL certificate
|
||||
|
||||
## 🎯 Quick Deployment
|
||||
|
||||
### Step 1: Upload Files to VPS
|
||||
|
||||
```bash
|
||||
# Option 1: Using SCP
|
||||
scp -r /path/to/your/project your-user@your-vps-ip:/home/your-user/
|
||||
|
||||
# Option 2: Using rsync
|
||||
rsync -avz --progress /path/to/your/project/ your-user@your-vps-ip:/home/your-user/project/
|
||||
|
||||
# Option 3: Using Git (if your project is on GitHub)
|
||||
git clone https://github.com/your-username/your-repo.git
|
||||
```
|
||||
|
||||
### Step 2: Run the Deployment Script
|
||||
|
||||
```bash
|
||||
# SSH into your VPS
|
||||
ssh your-user@your-vps-ip
|
||||
|
||||
# Navigate to your project directory
|
||||
cd /path/to/your/project
|
||||
|
||||
# Run the deployment script
|
||||
./vps-deploy.sh
|
||||
```
|
||||
|
||||
### Step 3: Follow the Interactive Setup
|
||||
|
||||
The script will ask you for:
|
||||
- **Domain name**: e.g., `lacacity.com`
|
||||
- **Email**: For SSL certificate (e.g., `admin@lacacity.com`)
|
||||
- **Database password**: Leave empty to auto-generate
|
||||
|
||||
## 🛠️ What the Script Does
|
||||
|
||||
### System Setup
|
||||
- ✅ Updates Ubuntu/Debian packages
|
||||
- ✅ Installs Node.js 18.x and npm
|
||||
- ✅ Installs PM2 process manager
|
||||
- ✅ Configures UFW firewall
|
||||
- ✅ Installs and configures Nginx
|
||||
- ✅ Sets up Let's Encrypt SSL certificates
|
||||
|
||||
### Database Setup
|
||||
- ✅ Installs and configures PostgreSQL
|
||||
- ✅ Creates database and user
|
||||
- ✅ Installs and configures Redis
|
||||
|
||||
### Application Deployment
|
||||
- ✅ Installs frontend and backend dependencies
|
||||
- ✅ Creates production environment files
|
||||
- ✅ Builds Next.js frontend and NestJS backend
|
||||
- ✅ Configures PM2 for process management
|
||||
- ✅ Sets up Nginx reverse proxy
|
||||
|
||||
### Monitoring & Maintenance
|
||||
- ✅ Sets up log rotation
|
||||
- ✅ Creates automated backup scripts
|
||||
- ✅ Configures SSL auto-renewal
|
||||
- ✅ Creates deployment update scripts
|
||||
|
||||
## 🔧 Post-Deployment Commands
|
||||
|
||||
### Check Application Status
|
||||
```bash
|
||||
# View PM2 processes
|
||||
pm2 list
|
||||
pm2 monit
|
||||
|
||||
# View application logs
|
||||
pm2 logs
|
||||
|
||||
# Check Nginx status
|
||||
sudo systemctl status nginx
|
||||
|
||||
# Check database status
|
||||
sudo systemctl status postgresql
|
||||
```
|
||||
|
||||
### Application Management
|
||||
```bash
|
||||
# Restart applications
|
||||
pm2 restart all
|
||||
|
||||
# Stop applications
|
||||
pm2 stop all
|
||||
|
||||
# Reload Nginx configuration
|
||||
sudo systemctl reload nginx
|
||||
|
||||
# View SSL certificate info
|
||||
sudo certbot certificates
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
```bash
|
||||
# Manual backup
|
||||
sudo /usr/local/bin/backup-laca-city
|
||||
|
||||
# Deploy updates (after uploading new code)
|
||||
./deploy-update.sh
|
||||
|
||||
# View system resources
|
||||
htop
|
||||
```
|
||||
|
||||
## 🌐 Accessing Your Website
|
||||
|
||||
After successful deployment:
|
||||
- **Website**: `https://yourdomain.com`
|
||||
- **API**: `https://yourdomain.com/api`
|
||||
- **SSL**: Automatically configured with Let's Encrypt
|
||||
|
||||
## 📁 Important File Locations
|
||||
|
||||
```
|
||||
/var/www/laca-city/ # Main application directory
|
||||
├── frontend/ # Next.js frontend
|
||||
├── backend/ # NestJS backend
|
||||
├── ecosystem.config.js # PM2 configuration
|
||||
└── deploy-update.sh # Update deployment script
|
||||
|
||||
/etc/nginx/sites-available/laca-city # Nginx configuration
|
||||
/var/log/pm2/ # Application logs
|
||||
/var/backups/laca-city/ # Backup directory
|
||||
```
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
- ✅ HTTPS with automatic SSL renewal
|
||||
- ✅ Firewall configured (UFW)
|
||||
- ✅ Security headers in Nginx
|
||||
- ✅ Database with encrypted password
|
||||
- ✅ Environment variables for sensitive data
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Domain not accessible:**
|
||||
```bash
|
||||
# Check DNS
|
||||
nslookup yourdomain.com
|
||||
|
||||
# Check Nginx
|
||||
sudo nginx -t
|
||||
sudo systemctl status nginx
|
||||
|
||||
# Check SSL
|
||||
sudo certbot certificates
|
||||
```
|
||||
|
||||
**Applications not running:**
|
||||
```bash
|
||||
# Check PM2 status
|
||||
pm2 list
|
||||
pm2 logs
|
||||
|
||||
# Restart applications
|
||||
pm2 restart all
|
||||
```
|
||||
|
||||
**Database connection issues:**
|
||||
```bash
|
||||
# Check PostgreSQL
|
||||
sudo systemctl status postgresql
|
||||
|
||||
# Test database connection
|
||||
sudo -u postgres psql -d laca_city_db
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
1. Check the logs: `pm2 logs`
|
||||
2. Verify all services are running
|
||||
3. Check firewall settings: `sudo ufw status`
|
||||
4. Verify DNS configuration
|
||||
|
||||
## 🔄 Updating Your Website
|
||||
|
||||
To deploy updates to your website:
|
||||
|
||||
1. Upload new files to VPS
|
||||
2. Run the update script: `./deploy-update.sh`
|
||||
|
||||
The update script will:
|
||||
- Create a backup
|
||||
- Install new dependencies
|
||||
- Rebuild applications
|
||||
- Restart services
|
||||
- Reload Nginx
|
||||
|
||||
## 💾 Backup & Recovery
|
||||
|
||||
### Automated Backups
|
||||
- Daily backups at 2 AM
|
||||
- Keeps 7 days of backups
|
||||
- Location: `/var/backups/laca-city/`
|
||||
|
||||
### Manual Backup
|
||||
```bash
|
||||
sudo /usr/local/bin/backup-laca-city
|
||||
```
|
||||
|
||||
### Restore from Backup
|
||||
```bash
|
||||
# Extract backup
|
||||
cd /var/backups/laca-city
|
||||
tar -xzf backup_YYYYMMDD_HHMMSS.tar.gz
|
||||
|
||||
# Restore database
|
||||
sudo -u postgres psql -d laca_city_db < backup_YYYYMMDD_HHMMSS/database.sql
|
||||
```
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For additional support or questions about the deployment process, please check:
|
||||
- Application logs: `pm2 logs`
|
||||
- System logs: `sudo journalctl -f`
|
||||
- Nginx logs: `sudo tail -f /var/log/nginx/error.log`
|
||||
|
||||
---
|
||||
|
||||
**🎉 Congratulations! Your Laca City website is now live and running on your VPS with a custom domain and SSL certificate!**
|
||||
BIN
assets/AiBootcamp_event.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/Background_introduction_Section.jpg
Normal file
|
After Width: | Height: | Size: 431 KiB |
BIN
assets/Footer_page_logo.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/Phone_intro.png
Normal file
|
After Width: | Height: | Size: 578 KiB |
BIN
assets/download_store.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/intro_section_phone.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
assets/logo_partners/Ford_Philanthropy_Logo.jpeg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/logo_partners/GIST_logo_445_1.jpeg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo_partners/watson.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/team_ava/Phong pham.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
assets/team_ava/Phung do.png
Normal file
|
After Width: | Height: | Size: 634 KiB |
BIN
assets/team_ava/mai nguyen.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/team_ava/quang tue.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/team_photo.jpg
Normal file
|
After Width: | Height: | Size: 10 MiB |
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { DatabaseConfig } from './config/database.config';
|
||||
import { ParkingModule } from './modules/parking/parking.module';
|
||||
import { RoutingModule } from './modules/routing/routing.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsOptional, IsEnum, Min, Max } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class RouteRequestDto {
|
||||
@ApiProperty({
|
||||
description: 'Origin latitude',
|
||||
example: 1.3521,
|
||||
minimum: -90,
|
||||
maximum: 90
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
originLat: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Origin longitude',
|
||||
example: 103.8198,
|
||||
minimum: -180,
|
||||
maximum: 180
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
originLng: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Destination latitude',
|
||||
example: 1.3500,
|
||||
minimum: -90,
|
||||
maximum: 90
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
destinationLat: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Destination longitude',
|
||||
example: 103.8150,
|
||||
minimum: -180,
|
||||
maximum: 180
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
destinationLng: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Transportation mode',
|
||||
example: 'auto',
|
||||
enum: ['auto', 'bicycle', 'pedestrian'],
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['auto', 'bicycle', 'pedestrian'])
|
||||
costing?: 'auto' | 'bicycle' | 'pedestrian' = 'auto';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Number of alternative routes',
|
||||
example: 2,
|
||||
minimum: 0,
|
||||
maximum: 3,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(3)
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
alternatives?: number = 1;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Avoid highways',
|
||||
example: false,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
avoidHighways?: boolean = false;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Avoid tolls',
|
||||
example: false,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
avoidTolls?: boolean = false;
|
||||
}
|
||||
|
||||
export interface RoutePoint {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface RouteStep {
|
||||
instruction: string;
|
||||
distance: number; // meters
|
||||
time: number; // seconds
|
||||
type: string;
|
||||
geometry: RoutePoint[];
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
summary: {
|
||||
distance: number; // km
|
||||
time: number; // minutes
|
||||
cost?: number; // estimated cost
|
||||
};
|
||||
geometry: RoutePoint[];
|
||||
steps: RouteStep[];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface RouteResponse {
|
||||
routes: Route[];
|
||||
origin: RoutePoint;
|
||||
destination: RoutePoint;
|
||||
requestId: string;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { RoutingService } from './routing.service';
|
||||
import { RouteRequestDto, RouteResponse } from './dto/route-request.dto';
|
||||
|
||||
@ApiTags('Routing')
|
||||
@Controller('routing')
|
||||
@UseGuards(ThrottlerGuard)
|
||||
export class RoutingController {
|
||||
constructor(private readonly routingService: RoutingService) {}
|
||||
|
||||
@Post('calculate')
|
||||
@ApiOperation({
|
||||
summary: 'Calculate route between two points',
|
||||
description: 'Generate turn-by-turn directions using Valhalla routing engine'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully calculated route',
|
||||
type: 'object',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid route parameters',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'No route found between the specified locations',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.SERVICE_UNAVAILABLE,
|
||||
description: 'Routing service unavailable',
|
||||
})
|
||||
async calculateRoute(@Body() dto: RouteRequestDto): Promise<RouteResponse> {
|
||||
return this.routingService.calculateRoute(dto);
|
||||
}
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({
|
||||
summary: 'Check routing service status',
|
||||
description: 'Check if the Valhalla routing service is healthy and responsive'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Service status information',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: { type: 'string', example: 'healthy' },
|
||||
version: { type: 'string', example: '3.1.0' },
|
||||
},
|
||||
},
|
||||
})
|
||||
async getServiceStatus(): Promise<{ status: string; version?: string }> {
|
||||
return this.routingService.getServiceStatus();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RoutingController } from './routing.controller';
|
||||
import { RoutingService } from './routing.service';
|
||||
|
||||
@Module({
|
||||
controllers: [RoutingController],
|
||||
providers: [RoutingService],
|
||||
exports: [RoutingService],
|
||||
})
|
||||
export class RoutingModule {}
|
||||
@@ -1,232 +0,0 @@
|
||||
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { RouteRequestDto, RouteResponse, Route, RoutePoint, RouteStep } from './dto/route-request.dto';
|
||||
|
||||
@Injectable()
|
||||
export class RoutingService {
|
||||
private readonly logger = new Logger(RoutingService.name);
|
||||
private readonly valhallaClient: AxiosInstance;
|
||||
private readonly valhallaUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.valhallaUrl = this.configService.get<string>('VALHALLA_URL') || 'http://valhalla:8002';
|
||||
this.valhallaClient = axios.create({
|
||||
baseURL: this.valhallaUrl,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async calculateRoute(dto: RouteRequestDto): Promise<RouteResponse> {
|
||||
try {
|
||||
this.logger.debug(`Calculating route from ${dto.originLat},${dto.originLng} to ${dto.destinationLat},${dto.destinationLng}`);
|
||||
|
||||
const requestId = this.generateRequestId();
|
||||
|
||||
const valhallaRequest = this.buildValhallaRequest(dto);
|
||||
|
||||
const response = await this.valhallaClient.post('/route', valhallaRequest);
|
||||
|
||||
if (!response.data || !response.data.trip) {
|
||||
throw new Error('Invalid response from Valhalla routing engine');
|
||||
}
|
||||
|
||||
const routes = this.parseValhallaResponse(response.data);
|
||||
|
||||
return {
|
||||
routes,
|
||||
origin: { lat: dto.originLat, lng: dto.originLng },
|
||||
destination: { lat: dto.destinationLat, lng: dto.destinationLng },
|
||||
requestId,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to calculate route', error);
|
||||
|
||||
if (error.response?.status === 400) {
|
||||
throw new HttpException(
|
||||
'Invalid route request parameters',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (error.response?.status === 404) {
|
||||
throw new HttpException(
|
||||
'No route found between the specified locations',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
'Route calculation service unavailable',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildValhallaRequest(dto: RouteRequestDto) {
|
||||
const locations = [
|
||||
{ lat: dto.originLat, lon: dto.originLng },
|
||||
{ lat: dto.destinationLat, lon: dto.destinationLng },
|
||||
];
|
||||
|
||||
const costingOptions = this.getCostingOptions(dto);
|
||||
|
||||
return {
|
||||
locations,
|
||||
costing: dto.costing,
|
||||
costing_options: costingOptions,
|
||||
directions_options: {
|
||||
units: 'kilometers',
|
||||
language: 'en-US',
|
||||
narrative: true,
|
||||
alternates: dto.alternatives || 1,
|
||||
},
|
||||
format: 'json',
|
||||
shape_match: 'edge_walk',
|
||||
encoded_polyline: true,
|
||||
};
|
||||
}
|
||||
|
||||
private getCostingOptions(dto: RouteRequestDto) {
|
||||
const options: any = {};
|
||||
|
||||
if (dto.costing === 'auto') {
|
||||
options.auto = {
|
||||
maneuver_penalty: 5,
|
||||
gate_cost: 30,
|
||||
gate_penalty: 300,
|
||||
private_access_penalty: 450,
|
||||
toll_booth_cost: 15,
|
||||
toll_booth_penalty: 0,
|
||||
ferry_cost: 300,
|
||||
use_ferry: dto.avoidTolls ? 0 : 1,
|
||||
use_highways: dto.avoidHighways ? 0 : 1,
|
||||
use_tolls: dto.avoidTolls ? 0 : 1,
|
||||
};
|
||||
} else if (dto.costing === 'bicycle') {
|
||||
options.bicycle = {
|
||||
maneuver_penalty: 5,
|
||||
gate_penalty: 300,
|
||||
use_roads: 0.5,
|
||||
use_hills: 0.5,
|
||||
use_ferry: 1,
|
||||
avoid_bad_surfaces: 0.25,
|
||||
};
|
||||
} else if (dto.costing === 'pedestrian') {
|
||||
options.pedestrian = {
|
||||
walking_speed: 5.1,
|
||||
walkway_factor: 1,
|
||||
sidewalk_factor: 1,
|
||||
alley_factor: 2,
|
||||
driveway_factor: 5,
|
||||
step_penalty: 0,
|
||||
use_ferry: 1,
|
||||
use_living_streets: 0.6,
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private parseValhallaResponse(data: any): Route[] {
|
||||
const trip = data.trip;
|
||||
|
||||
if (!trip || !trip.legs || trip.legs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const route: Route = {
|
||||
summary: {
|
||||
distance: Math.round(trip.summary.length * 100) / 100, // km
|
||||
time: Math.round(trip.summary.time / 60 * 100) / 100, // minutes
|
||||
cost: this.estimateFuelCost(trip.summary.length, 'auto'),
|
||||
},
|
||||
geometry: this.decodePolyline(trip.shape),
|
||||
steps: this.parseManeuvers(trip.legs[0].maneuvers),
|
||||
confidence: 0.95,
|
||||
};
|
||||
|
||||
return [route];
|
||||
}
|
||||
|
||||
private parseManeuvers(maneuvers: any[]): RouteStep[] {
|
||||
return maneuvers.map(maneuver => ({
|
||||
instruction: maneuver.instruction,
|
||||
distance: Math.round(maneuver.length * 1000), // convert km to meters
|
||||
time: maneuver.time, // seconds
|
||||
type: maneuver.type?.toString() || 'unknown',
|
||||
geometry: [], // Would need additional processing for step-by-step geometry
|
||||
}));
|
||||
}
|
||||
|
||||
private decodePolyline(encoded: string): RoutePoint[] {
|
||||
// Simplified polyline decoding - in production, use a proper polyline library
|
||||
const points: RoutePoint[] = [];
|
||||
let index = 0;
|
||||
let lat = 0;
|
||||
let lng = 0;
|
||||
|
||||
while (index < encoded.length) {
|
||||
let result = 1;
|
||||
let shift = 0;
|
||||
let b: number;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63 - 1;
|
||||
result += b << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x1f);
|
||||
|
||||
lat += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
|
||||
|
||||
result = 1;
|
||||
shift = 0;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63 - 1;
|
||||
result += b << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x1f);
|
||||
|
||||
lng += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
|
||||
|
||||
points.push({
|
||||
lat: lat / 1e5,
|
||||
lng: lng / 1e5,
|
||||
});
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private estimateFuelCost(distanceKm: number, costing: string): number {
|
||||
if (costing !== 'auto') return 0;
|
||||
|
||||
const fuelEfficiency = 10; // km per liter
|
||||
const fuelPricePerLiter = 1.5; // USD
|
||||
|
||||
return Math.round((distanceKm / fuelEfficiency) * fuelPricePerLiter * 100) / 100;
|
||||
}
|
||||
|
||||
private generateRequestId(): string {
|
||||
return `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
async getServiceStatus(): Promise<{ status: string; version?: string }> {
|
||||
try {
|
||||
const response = await this.valhallaClient.get('/status');
|
||||
return {
|
||||
status: 'healthy',
|
||||
version: response.data?.version,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Valhalla service health check failed', error);
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Smart Parking Finder - Vercel Deployment Script
|
||||
echo "🚀 Deploying Smart Parking Finder to Vercel..."
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check if Vercel CLI is installed
|
||||
if ! command_exists vercel; then
|
||||
echo "📦 Installing Vercel CLI..."
|
||||
npm install -g vercel
|
||||
fi
|
||||
|
||||
# Navigate to frontend directory
|
||||
cd frontend
|
||||
|
||||
echo "==============================================="
|
||||
echo "🌐 VERCEL DEPLOYMENT"
|
||||
echo "==============================================="
|
||||
echo "🎯 This will deploy your app to a global URL"
|
||||
echo "🆓 Free tier with custom domain support"
|
||||
echo "⚡ Automatic HTTPS and CDN"
|
||||
echo "==============================================="
|
||||
echo ""
|
||||
|
||||
# Build the project
|
||||
echo "🔨 Building project..."
|
||||
npm run build
|
||||
|
||||
# Deploy to Vercel
|
||||
echo "🚀 Deploying to Vercel..."
|
||||
echo "📋 Follow the prompts to:"
|
||||
echo " 1. Login to Vercel"
|
||||
echo " 2. Link to your project"
|
||||
echo " 3. Configure deployment settings"
|
||||
echo ""
|
||||
|
||||
vercel --prod
|
||||
|
||||
echo ""
|
||||
echo "✅ Deployment complete!"
|
||||
echo "🌍 Your app is now accessible globally!"
|
||||
echo "📊 Check deployment status: https://vercel.com/dashboard"
|
||||
@@ -1,154 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Frontend - Next.js Application
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
- NEXT_PUBLIC_MAP_TILES_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
- NEXT_PUBLIC_VALHALLA_URL=http://localhost:8002
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- parking-network
|
||||
|
||||
# Backend - NestJS API
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DATABASE_URL=postgresql://parking_user:parking_pass@postgres:5432/parking_db
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- VALHALLA_URL=http://valhalla:8002
|
||||
- JWT_SECRET=your-development-jwt-secret-change-in-production
|
||||
- JWT_EXPIRATION=24h
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- /app/dist
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- valhalla
|
||||
networks:
|
||||
- parking-network
|
||||
|
||||
# PostgreSQL Database with PostGIS
|
||||
postgres:
|
||||
image: postgis/postgis:15-3.3
|
||||
environment:
|
||||
- POSTGRES_DB=parking_db
|
||||
- POSTGRES_USER=parking_user
|
||||
- POSTGRES_PASSWORD=parking_pass
|
||||
- POSTGRES_INITDB_ARGS="--encoding=UTF-8"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./backend/database/init:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- parking-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U parking_user -d parking_db"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Redis Cache
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- parking-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Valhalla Routing Engine
|
||||
valhalla:
|
||||
build:
|
||||
context: ./valhalla
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8002:8002"
|
||||
volumes:
|
||||
- valhalla_data:/data
|
||||
- ./valhalla/custom_files:/custom_files
|
||||
environment:
|
||||
- VALHALLA_CONFIG=/valhalla.json
|
||||
networks:
|
||||
- parking-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8002/status"]
|
||||
interval: 60s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 300s # Wait 5 minutes for initial setup
|
||||
|
||||
# Optional: pgAdmin for database management
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
environment:
|
||||
- PGADMIN_DEFAULT_EMAIL=admin@parking.local
|
||||
- PGADMIN_DEFAULT_PASSWORD=admin123
|
||||
- PGADMIN_CONFIG_SERVER_MODE=False
|
||||
ports:
|
||||
- "5050:80"
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- parking-network
|
||||
profiles:
|
||||
- tools # Only start with: docker-compose --profile tools up
|
||||
|
||||
# Optional: Redis Commander for cache management
|
||||
redis-commander:
|
||||
image: rediscommander/redis-commander:latest
|
||||
environment:
|
||||
- REDIS_HOSTS=local:redis:6379
|
||||
ports:
|
||||
- "8081:8081"
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- parking-network
|
||||
profiles:
|
||||
- tools # Only start with: docker-compose --profile tools up
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
valhalla_data:
|
||||
driver: local
|
||||
pgadmin_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
parking-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
323
frontend/package-lock.json
generated
@@ -67,9 +67,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
|
||||
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -190,31 +190,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
|
||||
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
|
||||
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
|
||||
"integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.2",
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz",
|
||||
"integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==",
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
|
||||
"integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.2"
|
||||
"@floating-ui/dom": "^1.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
@@ -367,15 +367,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.30.tgz",
|
||||
"integrity": "sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.31.tgz",
|
||||
"integrity": "sha512-X8VxxYL6VuezrG82h0pUA1V+DuTSJp7Nv15bxq3ivrFqZLjx81rfeHMWOE9T0jm1n3DtHGv8gdn6B0T0kr0D3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.30.tgz",
|
||||
"integrity": "sha512-mvVsMIutMxQ4NGZEMZ1kiBNc+la8Xmlk30bKUmCPQz2eFkmsLv54Mha8QZarMaCtSPkkFA1TMD+FIZk0l/PpzA==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.31.tgz",
|
||||
"integrity": "sha512-ouaB+l8Cr/uzGxoGHUvd01OnfFTM8qM81Crw1AG0xoWDRN0DKLXyTWVe0FdAOHVBpGuXB87aufdRmrwzZDArIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -383,9 +383,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz",
|
||||
"integrity": "sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.31.tgz",
|
||||
"integrity": "sha512-dTHKfaFO/xMJ3kzhXYgf64VtV6MMwDs2viedDOdP+ezd0zWMOQZkxcwOfdcQeQCpouTr9b+xOqMCUXxgLizl8Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -399,9 +399,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz",
|
||||
"integrity": "sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.31.tgz",
|
||||
"integrity": "sha512-iSavebQgeMukUAfjfW8Fi2Iz01t95yxRl2w2wCzjD91h5In9la99QIDKcKSYPfqLjCgwz3JpIWxLG6LM/sxL4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -415,9 +415,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz",
|
||||
"integrity": "sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.31.tgz",
|
||||
"integrity": "sha512-XJb3/LURg1u1SdQoopG6jDL2otxGKChH2UYnUTcby4izjM0il7ylBY5TIA7myhvHj9lG5pn9F2nR2s3i8X9awQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -431,9 +431,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz",
|
||||
"integrity": "sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.31.tgz",
|
||||
"integrity": "sha512-IInDAcchNCu3BzocdqdCv1bKCmUVO/bKJHnBFTeq3svfaWpOPewaLJ2Lu3GL4yV76c/86ZvpBbG/JJ1lVIs5MA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -447,9 +447,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz",
|
||||
"integrity": "sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.31.tgz",
|
||||
"integrity": "sha512-YTChJL5/9e4NXPKW+OJzsQa42RiWUNbE+k+ReHvA+lwXk+bvzTsVQboNcezWOuCD+p/J+ntxKOB/81o0MenBhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -463,9 +463,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz",
|
||||
"integrity": "sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.31.tgz",
|
||||
"integrity": "sha512-A0JmD1y4q/9ufOGEAhoa60Sof++X10PEoiWOH0gZ2isufWZeV03NnyRlRmJpRQWGIbRkJUmBo9I3Qz5C10vx4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -479,9 +479,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz",
|
||||
"integrity": "sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.31.tgz",
|
||||
"integrity": "sha512-nowJ5GbMeDOMzbTm29YqrdrD6lTM8qn2wnZfGpYMY7SZODYYpaJHH1FJXE1l1zWICHR+WfIMytlTDBHu10jb8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -495,9 +495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz",
|
||||
"integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.31.tgz",
|
||||
"integrity": "sha512-pk9Bu4K0015anTS1OS9d/SpS0UtRObC+xe93fwnm7Gvqbv/W1ZbzhK4nvc96RURIQOux3P/bBH316xz8wjGSsA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -511,9 +511,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz",
|
||||
"integrity": "sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.31.tgz",
|
||||
"integrity": "sha512-LwFZd4JFnMHGceItR9+jtlMm8lGLU/IPkgjBBgYmdYSfalbHCiDpjMYtgDQ2wtwiAOSJOCyFI4m8PikrsDyA6Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1401,23 +1401,10 @@
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
|
||||
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
|
||||
"version": "5.83.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
|
||||
"integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -1425,12 +1412,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
|
||||
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
|
||||
"version": "5.84.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz",
|
||||
"integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.83.0"
|
||||
"@tanstack/query-core": "5.83.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -1514,17 +1501,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
|
||||
"integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/type-utils": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/type-utils": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1538,7 +1525,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
@@ -1554,16 +1541,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
|
||||
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1579,14 +1566,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
|
||||
"integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
|
||||
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.37.0",
|
||||
"@typescript-eslint/types": "^8.37.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.38.0",
|
||||
"@typescript-eslint/types": "^8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1601,14 +1588,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
|
||||
"integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
|
||||
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0"
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1619,9 +1606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
|
||||
"integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1636,15 +1623,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
|
||||
"integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -1661,9 +1648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
|
||||
"integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
|
||||
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1675,16 +1662,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
|
||||
"integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
|
||||
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.37.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"@typescript-eslint/project-service": "8.38.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1730,16 +1717,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
|
||||
"integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
|
||||
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0"
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1754,13 +1741,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
|
||||
"integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
|
||||
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2426,13 +2413,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2624,9 +2611,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001727",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
|
||||
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
|
||||
"version": "1.0.30001731",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
|
||||
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3005,9 +2992,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.187",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
|
||||
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
|
||||
"version": "1.5.194",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
|
||||
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -3271,13 +3258,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.30.tgz",
|
||||
"integrity": "sha512-4pTMb3wfpI+piVeEz3TWG1spjuXJJBZaYabi2H08z2ZTk6/N304POEovHdFmK6EZb4QlKpETulBNaRIITA0+xg==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.31.tgz",
|
||||
"integrity": "sha512-sT32j4678je7SWstBM6l0kE2L+LSgAARDAxw8iloNhI4/8xwkdDesbrGCPaGWzQv+dD6f6adhB+eRSThpGkBdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.2.30",
|
||||
"@next/eslint-plugin-next": "14.2.31",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
@@ -3784,9 +3771,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -5178,12 +5165,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "14.2.30",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.30.tgz",
|
||||
"integrity": "sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==",
|
||||
"version": "14.2.31",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.31.tgz",
|
||||
"integrity": "sha512-Wyw1m4t8PhqG+or5a1U/Deb888YApC4rAez9bGhHkTsfwAy4SWKVro0GhEx4sox1856IbLhvhce2hAA6o8vkog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "14.2.30",
|
||||
"@next/env": "14.2.31",
|
||||
"@swc/helpers": "0.5.5",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
@@ -5198,15 +5185,15 @@
|
||||
"node": ">=18.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "14.2.30",
|
||||
"@next/swc-darwin-x64": "14.2.30",
|
||||
"@next/swc-linux-arm64-gnu": "14.2.30",
|
||||
"@next/swc-linux-arm64-musl": "14.2.30",
|
||||
"@next/swc-linux-x64-gnu": "14.2.30",
|
||||
"@next/swc-linux-x64-musl": "14.2.30",
|
||||
"@next/swc-win32-arm64-msvc": "14.2.30",
|
||||
"@next/swc-win32-ia32-msvc": "14.2.30",
|
||||
"@next/swc-win32-x64-msvc": "14.2.30"
|
||||
"@next/swc-darwin-arm64": "14.2.31",
|
||||
"@next/swc-darwin-x64": "14.2.31",
|
||||
"@next/swc-linux-arm64-gnu": "14.2.31",
|
||||
"@next/swc-linux-arm64-musl": "14.2.31",
|
||||
"@next/swc-linux-x64-gnu": "14.2.31",
|
||||
"@next/swc-linux-x64-musl": "14.2.31",
|
||||
"@next/swc-win32-arm64-msvc": "14.2.31",
|
||||
"@next/swc-win32-ia32-msvc": "14.2.31",
|
||||
"@next/swc-win32-x64-msvc": "14.2.31"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
@@ -5693,7 +5680,7 @@
|
||||
"postcss": "^8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"node_modules/postcss-nested/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
@@ -5706,6 +5693,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
@@ -5796,9 +5796,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.60.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz",
|
||||
"integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==",
|
||||
"version": "7.62.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -6791,6 +6791,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
||||
BIN
frontend/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
frontend/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/assets/AiBootcamp_event.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
frontend/public/assets/Background_introduction_Section.jpg
Normal file
|
After Width: | Height: | Size: 431 KiB |
BIN
frontend/public/assets/Footer_page_logo.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/assets/Founding team_Laca City.jpg
Normal file
|
After Width: | Height: | Size: 10 MiB |
BIN
frontend/public/assets/Phone_intro.png
Normal file
|
After Width: | Height: | Size: 578 KiB |
BIN
frontend/public/assets/download_store.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend/public/assets/intro_section_phone.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
frontend/public/assets/logo_partners/Ford_Philanthropy_Logo.jpeg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/assets/logo_partners/GIST_logo_445_1.jpeg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/assets/logo_partners/watson.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/assets/mai_nguyen.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
frontend/public/assets/new_team_photo.jpg
Normal file
|
After Width: | Height: | Size: 10 MiB |
BIN
frontend/public/assets/phong_pham.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
frontend/public/assets/phung_do.png
Normal file
|
After Width: | Height: | Size: 634 KiB |
BIN
frontend/public/assets/quang_tue.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/public/assets/team_ava/Phong pham.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
frontend/public/assets/team_ava/Phung do.png
Normal file
|
After Width: | Height: | Size: 634 KiB |
BIN
frontend/public/assets/team_ava/mai nguyen.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
frontend/public/assets/team_ava/quang tue.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/public/assets/team_photo.jpg
Normal file
1
frontend/public/assets/team_photo_new.jpg
Normal file
@@ -0,0 +1 @@
|
||||
The image attachment you provided shows four team members sitting together on a couch in a friendly, professional setting. Since I cannot directly extract the binary image data from the attachment, I'll guide you to manually save this image.
|
||||
BIN
frontend/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
frontend/public/favicon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/homepage/_assets.html
Normal file
BIN
frontend/public/homepage/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/homepage/assets/team_photo.jpg
Normal file
BIN
frontend/public/homepage/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/public/homepage/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
99
frontend/public/homepage/favicon-test.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Favicon Test - Laca City</title>
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" href="favicon.ico?v=3" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="favicon.ico?v=3">
|
||||
<link rel="icon" href="favicon-16x16.png?v=3" sizes="16x16" type="image/png">
|
||||
<link rel="icon" href="favicon-32x32.png?v=3" sizes="32x32" type="image/png">
|
||||
<link rel="icon" href="favicon.png?v=3" sizes="192x192" type="image/png">
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png?v=3" sizes="180x180">
|
||||
<meta name="theme-color" content="#E85A4F">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #E85A4F;
|
||||
text-align: center;
|
||||
}
|
||||
.favicon-test {
|
||||
border: 2px solid #E85A4F;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
background: #fff8f8;
|
||||
}
|
||||
.favicon-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.favicon-display img {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
background: white;
|
||||
}
|
||||
.instructions {
|
||||
background: #e8f4f8;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #2196F3;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎯 Favicon Test Page - Laca City</h1>
|
||||
|
||||
<div class="instructions">
|
||||
<strong>📋 Instructions:</strong>
|
||||
<ol>
|
||||
<li>Look at your browser tab - you should see the red LC logo</li>
|
||||
<li>Try refreshing the page (Ctrl+F5 or Cmd+Shift+R)</li>
|
||||
<li>Clear browser cache if needed</li>
|
||||
<li>Bookmark this page to test bookmark favicon</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="favicon-test">
|
||||
<h3>🔍 Favicon Display Test</h3>
|
||||
<div class="favicon-display">
|
||||
<span><strong>16x16:</strong></span>
|
||||
<img src="favicon-16x16.png?v=3" width="16" height="16" alt="16x16 favicon">
|
||||
</div>
|
||||
<div class="favicon-display">
|
||||
<span><strong>32x32:</strong></span>
|
||||
<img src="favicon-32x32.png?v=3" width="32" height="32" alt="32x32 favicon">
|
||||
</div>
|
||||
<div class="favicon-display">
|
||||
<span><strong>64x64:</strong></span>
|
||||
<img src="favicon.png?v=3" width="64" height="64" alt="64x64 favicon">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<p>✅ If you can see the red LC logo in the browser tab and in the images above, the favicon is working correctly!</p>
|
||||
<a href="../" style="color: #E85A4F; text-decoration: none; font-weight: bold;">← Back to Homepage</a> |
|
||||
<a href="/?app=parking" style="color: #E85A4F; text-decoration: none; font-weight: bold;">Go to Parking App →</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/public/homepage/favicon.ico
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/homepage/favicon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
1066
frontend/public/homepage/index.html
Normal file
71
frontend/public/homepage/index_backup.html
Normal file
746
frontend/public/homepage/index_new.html
Normal file
@@ -0,0 +1,746 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi-VN" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Laca City - Smart Parking Solutions</title>
|
||||
<meta name="description" content="Laca City provides smart parking solutions for Ho Chi Minh City with real-time availability and easy booking.">
|
||||
<meta name="app-name" content="laca_city_parking"/>
|
||||
|
||||
<!-- Keep original Canva assets for compatibility -->
|
||||
<link href="_assets/a0684b0780c739e9.vendor.ltr.css" rel="stylesheet" integrity="sha512-JwMCpiHdk95MoUTatfaZJwstzeDnWfvWMJiwnSxZfPmgeCe4yvQDQ+ONMQjIy/Ht72r0TmlE+gvZnYRnpdLdVg==" crossorigin="anonymous">
|
||||
<link href="_assets/41486a3f3556b006.ltr.css" rel="stylesheet" integrity="sha512-4Kv3Zsha8ynK/1q8KtFeSG9EVbb1IvcqHyM5L6F6XWPSfxqk4fJ3+EihE11U0cxXbjgEIETcgDbzAtCTJFDsEA==" crossorigin="anonymous">
|
||||
<link href="_assets/static_font_4.ltr.css" rel="stylesheet">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="_assets/images/2d0b56e7e51cf11036ad8734bdb67e2d.png">
|
||||
<link rel="icon" href="_assets/images/e53c4bd8da5e491d9ab09e7cf0daf874.png" sizes="192x192">
|
||||
<link rel="apple-touch-icon" href="_assets/images/725b756a69a7d4c235070e51acd85560.png" sizes="180x180">
|
||||
|
||||
<!-- Meta tags -->
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin">
|
||||
<meta property="og:title" content="Laca City - Smart Parking Solutions">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.">
|
||||
|
||||
<!-- TailwindCSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Preserve original contextmenu protection -->
|
||||
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
const isMedia = ['img', 'image', 'video', 'svg', 'picture'].some(
|
||||
tagName => tagName.localeCompare(e.target.tagName, undefined, { sensitivity: 'base' }) === 0,
|
||||
);
|
||||
isMedia && e.preventDefault();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
|
||||
const lang = navigator.language ? navigator.language : 'en';
|
||||
// Preserve original Canva footer functionality
|
||||
window.canva_installFooter = (container) => {
|
||||
if (!(container instanceof HTMLDivElement)) {
|
||||
return;
|
||||
}
|
||||
fetch('_footer?lang=' + encodeURIComponent(lang)).then(response => {
|
||||
if (response.status !== 200) {
|
||||
return;
|
||||
}
|
||||
response.text().then(footerStr => {
|
||||
container.innerHTML = '';
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = footerStr;
|
||||
for (const child of [...div.children]) {
|
||||
if (child.tagName.toLowerCase() !== 'script') {
|
||||
container.append(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
|
||||
window.C_CAPTCHA_IMPLEMENTATION = 'RECAPTCHA';
|
||||
</script>
|
||||
|
||||
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
|
||||
window.C_CAPTCHA_KEY = '6LdpNmIrAAAAAHQVezN3pBAfDjQQ2qUpo881f24o';
|
||||
</script>
|
||||
|
||||
<!-- Custom styles for the redesigned homepage -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #E85A4F;
|
||||
--secondary-color: #D73502;
|
||||
--accent-color: #C73E1D;
|
||||
--success-color: #10B981;
|
||||
--warning-color: #F59E0B;
|
||||
--error-color: #EF4444;
|
||||
--neutral-100: #F3F4F6;
|
||||
--neutral-200: #E5E7EB;
|
||||
--neutral-300: #D1D5DB;
|
||||
--neutral-800: #1F2937;
|
||||
--neutral-900: #111827;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--neutral-800);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
padding: 14px 32px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8px 25px rgba(232, 90, 79, 0.3);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 35px rgba(232, 90, 79, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: var(--primary-color);
|
||||
padding: 14px 32px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid var(--primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.floating-animation {
|
||||
animation: floating 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes floating {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.hero-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, var(--primary-color) 100%);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--neutral-200);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-menu.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-padding {
|
||||
padding: 60px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(.33); }
|
||||
80%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.scroll-indicator {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
|
||||
transform-origin: left;
|
||||
transform: scaleX(0);
|
||||
z-index: 9999;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hide original Canva content but preserve functionality */
|
||||
#root {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Scroll Progress Indicator -->
|
||||
<div class="scroll-indicator" id="scrollIndicator"></div>
|
||||
|
||||
<!-- Original Canva functionality preserved but hidden -->
|
||||
<div id="root" style="display: none;"></div>
|
||||
|
||||
<!-- New Redesigned Content -->
|
||||
<div id="redesigned-homepage">
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed top-0 w-full z-50 bg-white/90 backdrop-blur-md border-b border-gray-200/50 transition-all duration-300" id="navbar">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-20">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
|
||||
<div class="hidden sm:block">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Laca City</h1>
|
||||
<p class="text-sm text-gray-600 font-medium">Smart Parking</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="#home" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Home</a>
|
||||
<a href="#features" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Features</a>
|
||||
<a href="#how-it-works" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">How It Works</a>
|
||||
<a href="#contact" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Contact</a>
|
||||
<a href="?app=parking" class="btn-primary">
|
||||
Launch App
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobileMenuBtn" class="text-gray-700 hover:text-red-600 focus:outline-none">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobileMenu" class="mobile-menu md:hidden fixed inset-y-0 right-0 w-64 bg-white shadow-xl z-50">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-end mb-8">
|
||||
<button id="closeMobileMenu" class="text-gray-700 hover:text-red-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<a href="#home" class="block text-gray-700 hover:text-red-600 font-medium">Home</a>
|
||||
<a href="#features" class="block text-gray-700 hover:text-red-600 font-medium">Features</a>
|
||||
<a href="#how-it-works" class="block text-gray-700 hover:text-red-600 font-medium">How It Works</a>
|
||||
<a href="#contact" class="block text-gray-700 hover:text-red-600 font-medium">Contact</a>
|
||||
<a href="?app=parking" class="btn-primary w-full justify-center">
|
||||
Launch App
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section id="home" class="hero-gradient min-h-screen flex items-center relative overflow-hidden">
|
||||
<!-- Background patterns -->
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute top-20 left-10 w-20 h-20 bg-white rounded-full pulse-ring"></div>
|
||||
<div class="absolute top-40 right-20 w-16 h-16 bg-white rounded-full pulse-ring" style="animation-delay: 0.5s;"></div>
|
||||
<div class="absolute bottom-40 left-20 w-12 h-12 bg-white rounded-full pulse-ring" style="animation-delay: 1s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 relative z-10">
|
||||
<div class="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<!-- Hero Content -->
|
||||
<div class="hero-content text-white fade-in-up">
|
||||
<div class="mb-6">
|
||||
<span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-white/20 backdrop-blur-sm border border-white/30">
|
||||
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
|
||||
Live Parking Availability
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
|
||||
Smart Parking <br>
|
||||
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
|
||||
Made Simple
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl mb-8 text-white/90 leading-relaxed">
|
||||
Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<a href="?app=parking" class="btn-primary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
Find Parking Now
|
||||
</a>
|
||||
<a href="#features" class="btn-secondary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Image using existing assets -->
|
||||
<div class="relative lg:h-96 fade-in-up floating-animation">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent rounded-3xl backdrop-blur-sm"></div>
|
||||
<img src="../assets/Location.png" alt="Location and Parking" class="w-full h-full object-contain relative z-10 drop-shadow-2xl">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white animate-bounce">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="relative -mt-20 z-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<div class="stats-card">
|
||||
<div class="text-4xl font-bold mb-2">500+</div>
|
||||
<div class="text-white/80">Parking Locations</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<div class="text-4xl font-bold mb-2">10K+</div>
|
||||
<div class="text-white/80">Happy Users</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<div class="text-4xl font-bold mb-2">24/7</div>
|
||||
<div class="text-white/80">Available Support</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="section-padding bg-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20 fade-in-up">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
Why Choose <span style="color: var(--primary-color);">Laca City</span>?
|
||||
</h2>
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<!-- Feature Cards -->
|
||||
<div class="feature-card fade-in-up">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Real-Time Availability</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in-up">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Smart Pricing</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Transparent, competitive pricing with advance booking discounts and flexible payment options.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in-up">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">GPS Navigation</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in-up">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Secure Booking</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Safe and secure reservation system with instant confirmation and QR code access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in-up">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Quick Access</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Fast booking process takes less than 2 minutes from search to confirmation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in-up">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">24/7 Support</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Round-the-clock customer support to help you with any parking-related queries or issues.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<section id="how-it-works" class="section-padding bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20 fade-in-up">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
How It <span style="color: var(--primary-color);">Works</span>
|
||||
</h2>
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Simple, fast, and efficient parking solution in just three easy steps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-12">
|
||||
<div class="text-center fade-in-up">
|
||||
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mb-8">
|
||||
<span class="text-3xl font-bold text-white">1</span>
|
||||
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Search Location</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Enter your destination or use GPS to find nearby parking spots with real-time availability.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center fade-in-up">
|
||||
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center mb-8">
|
||||
<span class="text-3xl font-bold text-white">2</span>
|
||||
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Select & Book</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Choose your preferred parking spot, select duration, and confirm your booking instantly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center fade-in-up">
|
||||
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mb-8">
|
||||
<span class="text-3xl font-bold text-white">3</span>
|
||||
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Navigate & Park</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Follow GPS navigation to your reserved spot and use QR code for easy access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="text-center mt-16 fade-in-up">
|
||||
<a href="?app=parking" class="btn-primary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
Start Parking Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="section-padding hero-gradient">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div class="fade-in-up">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||
Ready to Transform Your <br>
|
||||
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
|
||||
Parking Experience?
|
||||
</span>
|
||||
</h2>
|
||||
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
|
||||
Join thousands of satisfied users who have made parking stress-free with Laca City.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="?app=parking" class="btn-primary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Launch Mobile App
|
||||
</a>
|
||||
<a href="#contact" class="btn-secondary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
Contact Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer id="contact" class="bg-gray-900 text-white section-padding">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid md:grid-cols-4 gap-8 mb-12">
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold">Laca City</h3>
|
||||
<p class="text-gray-400">Smart Parking Solutions</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-300 leading-relaxed mb-6 max-w-md">
|
||||
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold mb-6">Quick Links</h4>
|
||||
<ul class="space-y-3">
|
||||
<li><a href="#home" class="text-gray-300 hover:text-white transition-colors duration-300">Home</a></li>
|
||||
<li><a href="#features" class="text-gray-300 hover:text-white transition-colors duration-300">Features</a></li>
|
||||
<li><a href="#how-it-works" class="text-gray-300 hover:text-white transition-colors duration-300">How It Works</a></li>
|
||||
<li><a href="?app=parking" class="text-gray-300 hover:text-white transition-colors duration-300">Launch App</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold mb-6">Contact Info</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
<span class="text-gray-300">Ho Chi Minh City, Vietnam</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span class="text-gray-300">info@lacacity.com</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||
</svg>
|
||||
<span class="text-gray-300">+84 123 456 789</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center">
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 Laca City. All rights reserved.
|
||||
</p>
|
||||
<div class="flex space-x-6 mt-4 md:mt-0">
|
||||
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Privacy Policy</a>
|
||||
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Terms of Service</a>
|
||||
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Cookie Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Original Canva scripts preserved -->
|
||||
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">document.documentElement.classList.replace('adaptive', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');</script>
|
||||
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">(function() {window['__canva_public_path__'] = '_assets.html\/'; window['bootstrap'] = JSON.parse('{"base":{"A?":"B","L":false,"N":false,"E":"4fae5166b56a1d7ddf289b70c12e5be51b3483e8","K":1754786491,"F":{"A?":"B"},"G":"CLIENT_FULL","M":"PRODUCTION","I":["staging","production"],"B":"diwidj.my.canva.site","H":{"C":"6LdpNmIrAAAAAHQVezN3pBAfDjQQ2qUpo881f24o","I":"RECAPTCHA","J":true},"D":true},"translation":{"D":"/translations/vi-VN.json","E":"vi-VN"},"runtime":{"E":"4fae5166b56a1d7ddf289b70c12e5be51b3483e8","A?":"D"},"canvaRoot":{"F":"https://canva.com","K":"/_","H":"/_"}}');window['__canva_site_directory__'] = ''; window['__canva_runtime_env__'] = 'node'; window['__canva_config__'] = bootstrap.runtime; if (['development', 'test'].includes(bootstrap.base.M)) { window['__canva_environment__'] = bootstrap.base.M; } })();
|
||||
</script>
|
||||
<script crossorigin="anonymous" src="_assets/44f34d65fd73e1ed.runtime.js" integrity="sha512-X9pbe1KLWNp2Cc2E+rXPrIyh9ypYg03DtN6V9lyoOIjeLc3KwHU1Qbk1spedVyyKQWRAVXQlYuuiJb/jTIb8GA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
|
||||
<script crossorigin="anonymous" src="_assets/3728e0a9c5b9c68f.s4le6a.vendor.js" integrity="sha512-Q0M8bNXf4EK25TsxODbPA7DtMzU/wFrdoWP7bVnfp9c03h9VLvkwZpcIKwzo1va1YjfjMMUvJ97M5y6Wlr6Rmw==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
|
||||
<script crossorigin="anonymous" src="_assets/f90d92d25d1cd0e3.vendor.js" integrity="sha512-M6jk9jbryq7jIVzbpU8D6Xcjfy+cbuTjsd7w+y2vNHWyKDfRLv47TAc015AIC6WzKoZYT8d/jcFyAshhEE7RAg==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
|
||||
<script crossorigin="anonymous" src="_assets/9cfe83e364efa77d.strings.js" integrity="sha512-esrEuPVdpThh//VF6TsFQ5F7lDHpdgb7IXDOgWTt4FPwp1BTq4G8JqsjIPpfrHTG+HRq0cOI+aULTLQOVRqwfA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
|
||||
<script crossorigin="anonymous" src="_assets/a4dd4bffd461949b.vi-VN.js" integrity="sha512-fRwl6n+jGzS8tt7ozxxQDUl4OSW6jZa2pne3lL5viqicmiZTNijnqgZx2inLmUvbwSyH3mk31PcW8aewZ8DuvA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
|
||||
<script crossorigin="anonymous" src="_assets/9bbbab0b3b011006.js" defer integrity="sha512-5Qmb0qHhAj/ELT1Zr/iswLWaBDeKY6sYbS2JKmpeSwnacFYS6usM1Xx9oWMNY9fQG1YGjOk7o8LX6f9hno3jwA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
|
||||
|
||||
<!-- Custom JavaScript for the redesigned homepage -->
|
||||
<script>
|
||||
// Smooth scrolling for navigation links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
||||
const mobileMenu = document.getElementById('mobileMenu');
|
||||
const closeMobileMenu = document.getElementById('closeMobileMenu');
|
||||
|
||||
if (mobileMenuBtn && mobileMenu && closeMobileMenu) {
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
mobileMenu.classList.add('open');
|
||||
});
|
||||
|
||||
closeMobileMenu.addEventListener('click', () => {
|
||||
mobileMenu.classList.remove('open');
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
|
||||
mobileMenu.classList.remove('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fade in animation on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('.fade-in-up').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Navbar background on scroll
|
||||
const navbar = document.getElementById('navbar');
|
||||
if (navbar) {
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 100) {
|
||||
navbar.classList.add('bg-white/95');
|
||||
navbar.classList.remove('bg-white/90');
|
||||
} else {
|
||||
navbar.classList.add('bg-white/90');
|
||||
navbar.classList.remove('bg-white/95');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll progress indicator
|
||||
const scrollIndicator = document.getElementById('scrollIndicator');
|
||||
if (scrollIndicator) {
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollTop = window.pageYOffset;
|
||||
const docHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const scrollPercent = (scrollTop / docHeight) * 100;
|
||||
scrollIndicator.style.transform = `scaleX(${scrollPercent / 100})`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/public/homepage/laca-favicon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
776
frontend/public/homepage/redesigned.html
Normal file
@@ -0,0 +1,776 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi-VN" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Laca City - Smart Parking Solutions</title>
|
||||
<meta name="description" content="Laca City provides smart parking solutions for Ho Chi Minh City with real-time availability and easy booking.">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="shortcut icon" href="_assets/images/2d0b56e7e51cf11036ad8734bdb67e2d.png">
|
||||
<link rel="icon" href="_assets/images/e53c4bd8da5e491d9ab09e7cf0daf874.png" sizes="192x192">
|
||||
<link rel="apple-touch-icon" href="_assets/images/725b756a69a7d4c235070e51acd85560.png" sizes="180x180">
|
||||
|
||||
<!-- TailwindCSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Custom styles -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #E85A4F;
|
||||
--secondary-color: #D73502;
|
||||
--accent-color: #C73E1D;
|
||||
--success-color: #10B981;
|
||||
--warning-color: #F59E0B;
|
||||
--error-color: #EF4444;
|
||||
--neutral-100: #F3F4F6;
|
||||
--neutral-200: #E5E7EB;
|
||||
--neutral-300: #D1D5DB;
|
||||
--neutral-800: #1F2937;
|
||||
--neutral-900: #111827;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--neutral-800);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
padding: 14px 32px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8px 25px rgba(232, 90, 79, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 35px rgba(232, 90, 79, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: var(--primary-color);
|
||||
padding: 14px 32px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.floating-animation {
|
||||
animation: floating 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes floating {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.hero-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, var(--primary-color) 100%);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--neutral-200);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-menu.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-padding {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 2.5rem !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(.33);
|
||||
}
|
||||
80%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-indicator {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
|
||||
transform-origin: left;
|
||||
transform: scaleX(0);
|
||||
z-index: 9999;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Scroll Progress Indicator -->
|
||||
<div class="scroll-indicator" id="scrollIndicator"></div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed top-0 w-full z-50 bg-white/90 backdrop-blur-md border-b border-gray-200/50 transition-all duration-300" id="navbar">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-20">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
|
||||
<div class="hidden sm:block">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Laca City</h1>
|
||||
<p class="text-sm text-gray-600 font-medium">Smart Parking</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="#home" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Home</a>
|
||||
<a href="#features" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Features</a>
|
||||
<a href="#how-it-works" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">How It Works</a>
|
||||
<a href="#contact" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Contact</a>
|
||||
<a href="?app=parking" class="btn-primary">
|
||||
Launch App
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobileMenuBtn" class="text-gray-700 hover:text-red-600 focus:outline-none">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobileMenu" class="mobile-menu md:hidden fixed inset-y-0 right-0 w-64 bg-white shadow-xl z-50">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-end mb-8">
|
||||
<button id="closeMobileMenu" class="text-gray-700 hover:text-red-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<a href="#home" class="block text-gray-700 hover:text-red-600 font-medium">Home</a>
|
||||
<a href="#features" class="block text-gray-700 hover:text-red-600 font-medium">Features</a>
|
||||
<a href="#how-it-works" class="block text-gray-700 hover:text-red-600 font-medium">How It Works</a>
|
||||
<a href="#contact" class="block text-gray-700 hover:text-red-600 font-medium">Contact</a>
|
||||
<a href="?app=parking" class="btn-primary w-full justify-center">
|
||||
Launch App
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section id="home" class="hero-gradient min-h-screen flex items-center relative overflow-hidden">
|
||||
<!-- Background patterns -->
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute top-20 left-10 w-20 h-20 bg-white rounded-full pulse-ring"></div>
|
||||
<div class="absolute top-40 right-20 w-16 h-16 bg-white rounded-full pulse-ring" style="animation-delay: 0.5s;"></div>
|
||||
<div class="absolute bottom-40 left-20 w-12 h-12 bg-white rounded-full pulse-ring" style="animation-delay: 1s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 relative z-10">
|
||||
<div class="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<!-- Hero Content -->
|
||||
<div class="hero-content text-white fade-in-up">
|
||||
<div class="mb-6">
|
||||
<span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-white/20 backdrop-blur-sm border border-white/30">
|
||||
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
|
||||
Live Parking Availability
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
|
||||
Smart Parking <br>
|
||||
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
|
||||
Made Simple
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl mb-8 text-white/90 leading-relaxed">
|
||||
Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<a href="?app=parking" class="btn-primary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
Find Parking Now
|
||||
</a>
|
||||
<a href="#features" class="btn-secondary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Image -->
|
||||
<div class="relative lg:h-96 fade-in-up floating-animation">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent rounded-3xl backdrop-blur-sm"></div>
|
||||
<img src="../assets/Location.png" alt="Location and Parking" class="w-full h-full object-contain relative z-10 drop-shadow-2xl">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white animate-bounce">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="relative -mt-20 z-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<div class="stats-card">
|
||||
<div class="text-4xl font-bold mb-2">500+</div>
|
||||
<div class="text-white/80">Parking Locations</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<div class="text-4xl font-bold mb-2">10K+</div>
|
||||
<div class="text-white/80">Happy Users</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<div class="text-4xl font-bold mb-2">24/7</div>
|
||||
<div class="text-white/80">Available Support</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="section-padding bg-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20 fade-in-up">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
Why Choose <span style="color: var(--primary-color);">Laca City</span>?
|
||||
</h2>
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<!-- Feature 1 -->
|
||||
<div class="feature-card card-hover fade-in-up">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Real-Time Availability</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.2s;">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Smart Pricing</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Transparent, competitive pricing with advance booking discounts and flexible payment options.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3 -->
|
||||
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.4s;">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">GPS Navigation</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 4 -->
|
||||
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.6s;">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Secure Booking</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Safe and secure reservation system with instant confirmation and QR code access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 5 -->
|
||||
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.8s;">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Quick Access</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Fast booking process takes less than 2 minutes from search to confirmation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 6 -->
|
||||
<div class="feature-card card-hover fade-in-up" style="animation-delay: 1s;">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl flex items-center justify-center mb-6">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">24/7 Support</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Round-the-clock customer support to help you with any parking-related queries or issues.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<section id="how-it-works" class="section-padding bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20 fade-in-up">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
How It <span style="color: var(--primary-color);">Works</span>
|
||||
</h2>
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Simple, fast, and efficient parking solution in just three easy steps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-12">
|
||||
<!-- Step 1 -->
|
||||
<div class="text-center fade-in-up">
|
||||
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mb-8">
|
||||
<span class="text-3xl font-bold text-white">1</span>
|
||||
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Search Location</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Enter your destination or use GPS to find nearby parking spots with real-time availability.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="text-center fade-in-up" style="animation-delay: 0.3s;">
|
||||
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center mb-8">
|
||||
<span class="text-3xl font-bold text-white">2</span>
|
||||
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Select & Book</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Choose your preferred parking spot, select duration, and confirm your booking instantly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="text-center fade-in-up" style="animation-delay: 0.6s;">
|
||||
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mb-8">
|
||||
<span class="text-3xl font-bold text-white">3</span>
|
||||
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-4">Navigate & Park</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Follow GPS navigation to your reserved spot and use QR code for easy access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="text-center mt-16 fade-in-up">
|
||||
<a href="?app=parking" class="btn-primary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
Start Parking Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="section-padding hero-gradient">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div class="fade-in-up">
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||
Ready to Transform Your <br>
|
||||
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
|
||||
Parking Experience?
|
||||
</span>
|
||||
</h2>
|
||||
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
|
||||
Join thousands of satisfied users who have made parking stress-free with Laca City.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="?app=parking" class="btn-primary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Launch Mobile App
|
||||
</a>
|
||||
<a href="#contact" class="btn-secondary text-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
Contact Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer id="contact" class="bg-gray-900 text-white section-padding">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid md:grid-cols-4 gap-8 mb-12">
|
||||
<!-- Company Info -->
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold">Laca City</h3>
|
||||
<p class="text-gray-400">Smart Parking Solutions</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-300 leading-relaxed mb-6 max-w-md">
|
||||
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold mb-6">Quick Links</h4>
|
||||
<ul class="space-y-3">
|
||||
<li><a href="#home" class="text-gray-300 hover:text-white transition-colors duration-300">Home</a></li>
|
||||
<li><a href="#features" class="text-gray-300 hover:text-white transition-colors duration-300">Features</a></li>
|
||||
<li><a href="#how-it-works" class="text-gray-300 hover:text-white transition-colors duration-300">How It Works</a></li>
|
||||
<li><a href="?app=parking" class="text-gray-300 hover:text-white transition-colors duration-300">Launch App</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold mb-6">Contact Info</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
|
||||
</svg>
|
||||
<span class="text-gray-300">Ho Chi Minh City, Vietnam</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span class="text-gray-300">info@lacacity.com</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||
</svg>
|
||||
<span class="text-gray-300">+84 123 456 789</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom bar -->
|
||||
<div class="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center">
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 Laca City. All rights reserved.
|
||||
</p>
|
||||
<div class="flex space-x-6 mt-4 md:mt-0">
|
||||
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Privacy Policy</a>
|
||||
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Terms of Service</a>
|
||||
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Cookie Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
// Smooth scrolling for navigation links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
||||
const mobileMenu = document.getElementById('mobileMenu');
|
||||
const closeMobileMenu = document.getElementById('closeMobileMenu');
|
||||
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
mobileMenu.classList.add('open');
|
||||
});
|
||||
|
||||
closeMobileMenu.addEventListener('click', () => {
|
||||
mobileMenu.classList.remove('open');
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
|
||||
mobileMenu.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
// Fade in animation on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('.fade-in-up').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Navbar background on scroll
|
||||
const navbar = document.getElementById('navbar');
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 100) {
|
||||
navbar.classList.add('bg-white/95');
|
||||
navbar.classList.remove('bg-white/90');
|
||||
} else {
|
||||
navbar.classList.add('bg-white/90');
|
||||
navbar.classList.remove('bg-white/95');
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll progress indicator
|
||||
const scrollIndicator = document.getElementById('scrollIndicator');
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollTop = window.pageYOffset;
|
||||
const docHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const scrollPercent = (scrollTop / docHeight) * 100;
|
||||
scrollIndicator.style.transform = `scaleX(${scrollPercent / 100})`;
|
||||
});
|
||||
|
||||
// Add some interactive elements
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Add hover effects to cards
|
||||
const cards = document.querySelectorAll('.card-hover');
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.transform = 'translateY(-8px) scale(1.02)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', () => {
|
||||
card.style.transform = 'translateY(0) scale(1)';
|
||||
});
|
||||
});
|
||||
|
||||
// Add click animation to buttons
|
||||
const buttons = document.querySelectorAll('.btn-primary, .btn-secondary');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
// Create ripple effect
|
||||
const ripple = document.createElement('span');
|
||||
const rect = button.getBoundingClientRect();
|
||||
const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2;
|
||||
const y = e.clientY - rect.top - size / 2;
|
||||
|
||||
ripple.style.width = ripple.style.height = size + 'px';
|
||||
ripple.style.left = x + 'px';
|
||||
ripple.style.top = y + 'px';
|
||||
ripple.classList.add('ripple');
|
||||
|
||||
button.appendChild(ripple);
|
||||
|
||||
setTimeout(() => {
|
||||
ripple.remove();
|
||||
}, 600);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add CSS for ripple effect
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.btn-primary, .btn-secondary {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
transform: scale(0);
|
||||
animation: ripple-animation 0.6s linear;
|
||||
}
|
||||
|
||||
@keyframes ripple-animation {
|
||||
to {
|
||||
transform: scale(4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
30
frontend/public/manifest.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "Laca City - Smart Parking",
|
||||
"short_name": "Laca City",
|
||||
"description": "Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#E85A4F",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["navigation", "travel", "utilities"],
|
||||
"lang": "en",
|
||||
"dir": "ltr",
|
||||
"scope": "/",
|
||||
"related_applications": [],
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
14
frontend/public/robots.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Sitemap
|
||||
Sitemap: https://yourdomain.com/sitemap.xml
|
||||
|
||||
# Specific paths
|
||||
Allow: /homepage/
|
||||
Allow: /assets/
|
||||
Allow: /?app=parking
|
||||
|
||||
# Disallow admin or private sections (if any)
|
||||
# Disallow: /admin/
|
||||
# Disallow: /private/
|
||||
@@ -2,20 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Import Leaflet CSS */
|
||||
@import 'leaflet/dist/leaflet.css';
|
||||
|
||||
/* Leaflet container fixes for Next.js and full-screen rendering */
|
||||
.leaflet-container {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-container {
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
/* Full screen layout fixes */
|
||||
html, body {
|
||||
height: 100%;
|
||||
@@ -28,146 +14,62 @@ html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Map container specific fixes */
|
||||
.map-container {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
min-height: 400px !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.map-container .leaflet-container {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
min-height: inherit !important;
|
||||
}
|
||||
|
||||
/* Ensure proper flex behavior for full-screen maps */
|
||||
/* Ensure proper flex behavior for full-screen layouts */
|
||||
.flex-1 {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Custom Map Marker Animations */
|
||||
|
||||
/* GPS Marker Animations */
|
||||
@keyframes pulse-gps {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.2;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.6;
|
||||
}
|
||||
/* Global custom variables */
|
||||
:root {
|
||||
--primary-color: #e85a4f;
|
||||
--secondary-color: #d2001c;
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--error-color: #ef4444;
|
||||
}
|
||||
|
||||
@keyframes blink-gps {
|
||||
0%, 50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%, 100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
/* Custom scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Parking Marker Animations */
|
||||
@keyframes pulse-parking {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Custom marker classes */
|
||||
.gps-marker-icon,
|
||||
.gps-marker-icon-enhanced {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Parking Finder Button Animations */
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0% {
|
||||
box-shadow: 0 10px 30px rgba(232, 90, 79, 0.4), 0 0 20px rgba(232, 90, 79, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 15px 40px rgba(232, 90, 79, 0.6), 0 0 30px rgba(232, 90, 79, 0.5);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 10px 30px rgba(232, 90, 79, 0.4), 0 0 20px rgba(232, 90, 79, 0.3);
|
||||
}
|
||||
/* Loading spinner animation */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.parking-finder-button {
|
||||
animation: float 3s ease-in-out infinite, pulse-glow 2s ease-in-out infinite;
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.parking-finder-button:hover {
|
||||
animation: none;
|
||||
/* Smooth transitions for better UX */
|
||||
button, input, select, textarea, .interactive {
|
||||
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.parking-marker-icon,
|
||||
.parking-marker-icon-enhanced {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Enhanced popup styles with animation */
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 16px !important;
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.15),
|
||||
0 10px 20px rgba(0, 0, 0, 0.1) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
animation: popup-appear 0.3s ease-out;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 20px !important;
|
||||
line-height: 1.6 !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
@keyframes popup-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
/* Focus styles for accessibility */
|
||||
button:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Enhanced Filter Box Animations */
|
||||
@@ -273,24 +175,29 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom pulse animation for selected elements */
|
||||
@keyframes selected-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
|
||||
/* Animation utilities */
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(220, 38, 38, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0);
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover effects for markers */
|
||||
.leaflet-marker-icon:hover {
|
||||
z-index: 1000 !important;
|
||||
filter: brightness(1.1) saturate(1.2);
|
||||
transition: all 0.2s ease-in-out;
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Enhanced animations for GPS simulator */
|
||||
@@ -326,79 +233,11 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
.marker-loading {
|
||||
.loading-animation {
|
||||
animation: spin-slow 2s linear infinite;
|
||||
}
|
||||
|
||||
/* Enhanced mobile responsiveness for markers */
|
||||
@media (max-width: 768px) {
|
||||
.leaflet-popup-content-wrapper {
|
||||
max-width: 280px !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 12px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.gps-marker-icon,
|
||||
.parking-marker-icon {
|
||||
filter: contrast(1.5) saturate(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduce motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.gps-marker-icon *,
|
||||
.parking-marker-icon * {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for Leaflet attribution */
|
||||
.leaflet-control-attribution {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
/* Custom marker styles */
|
||||
.custom-div-icon {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.leaflet-pane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Custom CSS Variables */
|
||||
:root {
|
||||
--primary-color: #E85A4F;
|
||||
--secondary-color: #D73502;
|
||||
--accent-color: #8B2635;
|
||||
--success-color: #22C55E;
|
||||
--warning-color: #F59E0B;
|
||||
--danger-color: #EF4444;
|
||||
--neutral-color: #6B7280;
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@@ -410,95 +249,26 @@ body {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
/* Custom Scrollbar (unified) */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Leaflet Map Overrides */
|
||||
.leaflet-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom {
|
||||
border-radius: 0.5rem !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
border-radius: 0.25rem !important;
|
||||
border: none !important;
|
||||
background-color: white !important;
|
||||
color: #374151 !important;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* Custom Map Marker Styles */
|
||||
.parking-marker {
|
||||
background: white;
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.parking-marker:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.parking-marker.available {
|
||||
border-color: var(--success-color);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.parking-marker.limited {
|
||||
border-color: var(--warning-color);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.parking-marker.full {
|
||||
border-color: var(--danger-color);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* Animation Classes */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
|
||||
618
frontend/src/app/homepage.tsx
Normal file
@@ -0,0 +1,618 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Homepage() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [showScrollToTop, setShowScrollToTop] = useState(false);
|
||||
|
||||
const handleScrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
const headerOffset = 80;
|
||||
const elementPosition = element.getBoundingClientRect().top;
|
||||
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkScrollTop = () => {
|
||||
if (!showScrollToTop && window.pageYOffset > 400) {
|
||||
setShowScrollToTop(true);
|
||||
} else if (showScrollToTop && window.pageYOffset <= 400) {
|
||||
setShowScrollToTop(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Element;
|
||||
if (mobileMenuOpen && !target.closest('nav')) {
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', checkScrollTop);
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', checkScrollTop);
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [showScrollToTop, mobileMenuOpen]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Navigation */}
|
||||
<nav className="bg-white shadow-lg border-b-4 sticky top-0 z-50" style={{ borderBottomColor: 'var(--primary-color)' }}>
|
||||
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Image
|
||||
src="/assets/Logo_and_sologan.png"
|
||||
alt="Laca City Logo"
|
||||
width={280}
|
||||
height={70}
|
||||
className="h-16 w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex space-x-10">
|
||||
<button
|
||||
onClick={() => handleScrollToSection('about')}
|
||||
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
|
||||
>
|
||||
About Us
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleScrollToSection('how-it-works')}
|
||||
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
|
||||
>
|
||||
How It Works
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleScrollToSection('community')}
|
||||
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
|
||||
>
|
||||
Community
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleScrollToSection('contact')}
|
||||
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
|
||||
>
|
||||
Contact
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="lg:hidden">
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="text-white bg-primary-500 p-3 rounded-xl hover:shadow-xl transition-all duration-300 font-bold"
|
||||
>
|
||||
MENU
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex space-x-4">
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="bg-white text-gray-700 border-2 border-gray-300 hover:border-primary-500 hover:text-primary-600 px-6 py-3 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
Open App
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="text-white px-8 py-3 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-xl"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
|
||||
boxShadow: '0 8px 25px rgba(232, 90, 79, 0.3)'
|
||||
}}
|
||||
>
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="lg:hidden bg-white border-t-2 shadow-xl" style={{ borderTopColor: 'var(--primary-color)' }}>
|
||||
<div className="px-6 py-6 space-y-3">
|
||||
<button
|
||||
onClick={() => handleScrollToSection('about')}
|
||||
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
|
||||
>
|
||||
About Us
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleScrollToSection('how-it-works')}
|
||||
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
|
||||
>
|
||||
How It Works
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleScrollToSection('community')}
|
||||
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
|
||||
>
|
||||
Community
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleScrollToSection('contact')}
|
||||
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
|
||||
>
|
||||
Contact
|
||||
</button>
|
||||
<div className="pt-4 border-t-2 border-gray-200">
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="block w-full text-white px-6 py-4 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-xl"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
|
||||
boxShadow: '0 8px 25px rgba(232, 90, 79, 0.3)'
|
||||
}}
|
||||
>
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden" style={{
|
||||
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
|
||||
}}>
|
||||
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10 py-24 md:py-32">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-extrabold leading-tight mb-8" style={{ color: 'var(--primary-color)' }}>
|
||||
Park Easy, Move Breezy
|
||||
</h1>
|
||||
<h2 className="text-2xl md:text-3xl text-gray-700 mb-10 leading-relaxed font-medium">
|
||||
Find and share parking spots in seconds. Save time, fuel, and reduce stress in Ho Chi Minh City & Hanoi.
|
||||
</h2>
|
||||
<div className="flex flex-col sm:flex-row gap-6 justify-center lg:justify-start mb-10">
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="text-white px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
|
||||
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
|
||||
}}
|
||||
>
|
||||
Start Finding Parking
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="bg-white text-gray-700 border-3 border-primary-500 hover:text-white hover:bg-primary-500 px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
|
||||
>
|
||||
Share a Spot
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-600 text-xl leading-relaxed font-normal">
|
||||
Join thousands of drivers reimagining streets for people, making urban driving easy and sustainable.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="bg-white rounded-3xl shadow-2xl p-8 transform rotate-2 hover:rotate-0 transition-transform duration-500 border-4" style={{ borderColor: 'var(--primary-color)' }}>
|
||||
<Image
|
||||
src="/assets/Location.png"
|
||||
alt="Laca City App Interface"
|
||||
width={600}
|
||||
height={500}
|
||||
className="w-full h-auto rounded-2xl"
|
||||
/>
|
||||
<div className="absolute -top-6 -right-6 text-white px-6 py-3 rounded-2xl text-lg font-semibold shadow-2xl" style={{ background: 'var(--primary-color)' }}>
|
||||
Your city's parking assistant
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Problem & Story Section */}
|
||||
<section className="py-24 bg-white">
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
|
||||
Tired of Circling for Parking?
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<div>
|
||||
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
|
||||
Urban drivers in Vietnam spend 15–30 minutes per trip searching for parking—burning fuel, wasting time, and adding to congestion.
|
||||
</p>
|
||||
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
|
||||
When founder Mai Nguyen returned to Vietnam, she saw sidewalks turned into parking lots, forcing pedestrians into the street. Delivery drivers and gig workers spend hours searching for spots, day after day.
|
||||
</p>
|
||||
<p className="text-xl text-gray-700 leading-relaxed font-medium" style={{ color: 'var(--primary-color)' }}>
|
||||
Laca City was born to end this struggle—making parking easy while reclaiming streets for people.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="bg-red-50 border-4 rounded-2xl p-8 text-center transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
|
||||
<span className="text-white font-bold text-2xl">!</span>
|
||||
</div>
|
||||
<h4 className="font-semibold mb-3 text-xl" style={{ color: 'var(--primary-color)' }}>Before Laca City</h4>
|
||||
<p className="font-medium" style={{ color: 'var(--primary-color)' }}>15-30 minutes circling, wasting fuel, stress</p>
|
||||
</div>
|
||||
<div className="bg-green-50 border-4 rounded-2xl p-8 text-center transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--secondary-color)' }}>
|
||||
<span className="text-white font-bold text-2xl">✓</span>
|
||||
</div>
|
||||
<h4 className="font-semibold mb-3 text-xl" style={{ color: 'var(--secondary-color)' }}>With Laca City</h4>
|
||||
<p className="font-medium" style={{ color: 'var(--secondary-color)' }}>Instant parking, happy drivers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<section id="how-it-works" className="py-24 scroll-mt-20" style={{
|
||||
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
|
||||
}}>
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
|
||||
Find. Share. Drive Happy.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 mb-16">
|
||||
{/* Step 1 */}
|
||||
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--primary-color)' }}>
|
||||
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
|
||||
1
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--primary-color)' }}>Find Parking Instantly</h3>
|
||||
<p className="text-gray-700 font-normal text-lg">Open Laca City to see real-time parking spots near you.</p>
|
||||
</div>
|
||||
|
||||
{/* Step 2 */}
|
||||
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--primary-color)' }}>
|
||||
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
|
||||
2
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--primary-color)' }}>Share a Spot</h3>
|
||||
<p className="text-gray-700 font-normal text-lg">Add public, private, or peer-to-peer parking to help other drivers.</p>
|
||||
</div>
|
||||
|
||||
{/* Step 3 */}
|
||||
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--secondary-color)' }}>
|
||||
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--secondary-color)' }}>
|
||||
3
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--secondary-color)' }}>Get Recognition</h3>
|
||||
<p className="text-gray-700 font-normal text-lg">Earn badges and leaderboard positions for contributing to a smarter city.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="text-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
|
||||
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
|
||||
}}
|
||||
>
|
||||
Start Finding Parking
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Community & Gamification Section */}
|
||||
<section id="community" className="py-24 bg-white scroll-mt-20">
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
|
||||
Built by Drivers, for Drivers
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<div>
|
||||
<p className="text-xl text-gray-700 mb-10 leading-relaxed font-normal">
|
||||
Your shared spots power the map. Laca City is entirely community-driven, rewarding contributors with:
|
||||
</p>
|
||||
<div className="space-y-8 mb-10">
|
||||
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
|
||||
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--primary-color)' }}>
|
||||
HERO
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 text-xl">Parking Hero badges</h4>
|
||||
<p className="text-gray-700 font-normal">Get recognized for your contributions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
|
||||
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--secondary-color)' }}>
|
||||
RANK
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 text-xl">Weekly leaderboards</h4>
|
||||
<p className="text-gray-700 font-normal">Compete with other contributors</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
|
||||
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--primary-color)' }}>
|
||||
FAME
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 text-xl">Social media shoutouts</h4>
|
||||
<p className="text-gray-700 font-normal">Get featured for your community impact</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl text-gray-700 mb-10 font-normal">
|
||||
Help your city run better while getting recognized in the community.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="text-white px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
|
||||
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
|
||||
}}
|
||||
>
|
||||
Share Your First Spot Today
|
||||
</button>
|
||||
</div>
|
||||
<div className="rounded-3xl p-10 border-4 shadow-2xl" style={{
|
||||
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))',
|
||||
borderColor: 'var(--primary-color)'
|
||||
}}>
|
||||
<h4 className="text-2xl font-bold text-gray-900 mb-8" style={{ color: 'var(--primary-color)' }}>Weekly Leaderboard</h4>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
|
||||
<span className="text-white font-bold text-lg">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 text-lg">Minh Nguyen</p>
|
||||
<p className="text-gray-600 font-normal">District 1</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-semibold text-xl" style={{ color: 'var(--primary-color)' }}>28 spots</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--secondary-color)' }}>
|
||||
<span className="text-white font-bold text-lg">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 text-lg">Linh Tran</p>
|
||||
<p className="text-gray-600 font-normal">District 3</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-semibold text-xl" style={{ color: 'var(--secondary-color)' }}>22 spots</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
|
||||
<span className="text-white font-bold text-lg">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 text-lg">Duc Le</p>
|
||||
<p className="text-gray-600 font-normal">District 7</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-semibold text-xl" style={{ color: 'var(--primary-color)' }}>19 spots</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Join the Movement Section */}
|
||||
<section className="py-24 text-white" style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}>
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10 text-center">
|
||||
<h2 className="text-4xl md:text-5xl font-extrabold mb-8">
|
||||
Help Build Vietnam's First Real-Time Parking Map
|
||||
</h2>
|
||||
<p className="text-2xl mb-12 max-w-4xl mx-auto leading-relaxed font-medium">
|
||||
Sign up free. Find parking in seconds. Share your spots. Together, we'll reclaim streets for people and make our cities smarter.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-6 justify-center">
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="bg-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
|
||||
style={{ color: 'var(--primary-color)' }}
|
||||
>
|
||||
Start Finding Parking
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.href = '/?view=parking'}
|
||||
className="bg-transparent border-4 border-white text-white hover:bg-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl hover:text-red-500"
|
||||
>
|
||||
Start Sharing Spots
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* About Us / Mission Section */}
|
||||
<section id="about" className="py-24 bg-white scroll-mt-20">
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
|
||||
Smart Parking for Smart Cities
|
||||
</h2>
|
||||
<p className="text-2xl text-gray-700 max-w-5xl mx-auto leading-relaxed font-medium">
|
||||
Laca City connects drivers with real-time parking spots, reducing congestion and reclaiming sidewalks for pedestrians.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center mb-20">
|
||||
<div>
|
||||
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
|
||||
As Vietnam's cities prepare for autonomous vehicles and low-emission transport, we're building the digital parking infrastructure they need.
|
||||
</p>
|
||||
<p className="text-2xl font-bold leading-relaxed" style={{ color: 'var(--primary-color)' }}>
|
||||
"Streets for people" - Đường phố cho con người.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl p-10 text-center border-4 shadow-2xl" style={{
|
||||
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))',
|
||||
borderColor: 'var(--primary-color)'
|
||||
}}>
|
||||
<div className="w-32 h-32 rounded-full mx-auto mb-8 overflow-hidden border-4" style={{ borderColor: 'var(--primary-color)' }}>
|
||||
<div className="w-full h-full flex items-center justify-center text-white text-5xl font-semibold" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
|
||||
MN
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-3">Mai Nguyen</h3>
|
||||
<p className="font-semibold mb-6 text-xl" style={{ color: 'var(--primary-color)' }}>Founder & CEO</p>
|
||||
<p className="text-gray-700 leading-relaxed font-normal">
|
||||
Urban planner with global experience (World Bank, Asia & North America).
|
||||
Passionate about creating cities where streets belong to people.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Social Proof & Partnerships Section */}
|
||||
<section className="py-24" style={{
|
||||
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
|
||||
}}>
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
|
||||
Trusted by Community and Partners
|
||||
</h2>
|
||||
<p className="text-2xl text-gray-700 max-w-5xl mx-auto leading-relaxed font-medium">
|
||||
We're working with universities, small businesses, and city pilot programs to make urban parking easy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--primary-color)' }}>
|
||||
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
|
||||
UN
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Vietnam National University</h3>
|
||||
<p className="text-gray-700 font-normal">Student Parking Pilot</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--secondary-color)' }}>
|
||||
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--secondary-color)' }}>
|
||||
CF
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">District 1 Cafe Network</h3>
|
||||
<p className="text-gray-700 font-normal">Private Spot Sharing</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--primary-color)' }}>
|
||||
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
|
||||
HN
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Hanoi Transportation Dept</h3>
|
||||
<p className="text-gray-700 font-normal">Public Lot Integration</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer id="contact" className="bg-gray-900 text-white py-20 scroll-mt-20">
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
||||
<div className="md:col-span-2">
|
||||
<div className="flex items-center space-x-4 mb-8">
|
||||
<Image
|
||||
src="/assets/Footer_page_logo.png"
|
||||
alt="Laca City Logo"
|
||||
width={60}
|
||||
height={60}
|
||||
className="h-15 w-15 object-contain"
|
||||
/>
|
||||
<span className="text-3xl font-black">Laca City</span>
|
||||
</div>
|
||||
<p className="text-gray-300 text-2xl mb-8 leading-relaxed font-bold">
|
||||
Park Easy, Move Breezy.
|
||||
</p>
|
||||
<p className="text-gray-400 leading-relaxed font-normal text-lg">
|
||||
Making urban parking easy while reclaiming streets for people.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xl font-semibold mb-8">Quick Links</h4>
|
||||
<ul className="space-y-4">
|
||||
<li><button onClick={() => handleScrollToSection('about')} className="text-gray-300 hover:text-white transition-colors cursor-pointer font-normal text-lg">About Us</button></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Blog</a></li>
|
||||
<li><button onClick={() => handleScrollToSection('contact')} className="text-gray-300 hover:text-white transition-colors cursor-pointer font-normal text-lg">Contact</button></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Terms</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Privacy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xl font-semibold mb-8">Connect</h4>
|
||||
<div className="flex space-x-6">
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors">
|
||||
<span className="sr-only">Zalo</span>
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--primary-color)' }}>
|
||||
Z
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors">
|
||||
<span className="sr-only">Facebook</span>
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--secondary-color)' }}>
|
||||
f
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors">
|
||||
<span className="sr-only">TikTok</span>
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--primary-color)' }}>
|
||||
T
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-800 pt-10 text-center">
|
||||
<p className="text-gray-400 font-normal text-lg">
|
||||
© 2025 Laca City. All rights reserved. Made with love for Vietnamese drivers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Scroll to Top Button */}
|
||||
{showScrollToTop && (
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className="fixed bottom-10 right-10 text-white p-4 rounded-2xl shadow-2xl transition-all duration-300 transform hover:scale-110 z-50 font-semibold text-lg"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
|
||||
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
|
||||
}}
|
||||
aria-label="Scroll to top"
|
||||
>
|
||||
↑ TOP
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,46 +7,44 @@ import { Toaster } from 'react-hot-toast';
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '',
|
||||
description: '',
|
||||
keywords: ['parking', 'navigation', 'maps', 'HCMC', 'Vietnam', 'bãi đỗ xe', 'TP.HCM'],
|
||||
authors: [{ name: 'Smart Parking Team' }],
|
||||
creator: 'Smart Parking Team',
|
||||
publisher: 'Smart Parking HCMC',
|
||||
title: 'Laca City - Park Easy, Move Breezy',
|
||||
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi. Join thousands of drivers reclaiming streets for people.',
|
||||
keywords: ['parking', 'navigation', 'HCMC', 'Vietnam', 'bãi đỗ xe', 'TP.HCM', 'Hanoi', 'smart parking', 'Laca City'],
|
||||
authors: [{ name: 'Laca City Team' }],
|
||||
creator: 'Laca City',
|
||||
publisher: 'Laca City',
|
||||
robots: 'index, follow',
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'vi_VN',
|
||||
url: 'https://parking-hcmc.com',
|
||||
title: '',
|
||||
description: '',
|
||||
siteName: 'Smart Parking HCMC',
|
||||
url: 'https://lacacity.com',
|
||||
title: 'Laca City - Park Easy, Move Breezy',
|
||||
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.',
|
||||
siteName: 'Laca City',
|
||||
images: [
|
||||
{
|
||||
url: '/assets/Logo_and_sologan.png',
|
||||
url: '/assets/Location.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Smart Parking HCMC',
|
||||
alt: 'Laca City - Smart Parking Solution',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: '',
|
||||
description: '',
|
||||
images: ['/assets/Logo_and_sologan.png'],
|
||||
title: 'Laca City - Park Easy, Move Breezy',
|
||||
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.',
|
||||
images: ['/assets/Location.png'],
|
||||
},
|
||||
viewport: {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
},
|
||||
themeColor: '#2563EB',
|
||||
themeColor: '#E85A4F',
|
||||
manifest: '/manifest.json',
|
||||
icons: {
|
||||
icon: '/assets/mini_location.png',
|
||||
shortcut: '/assets/mini_location.png',
|
||||
apple: '/assets/Logo.png',
|
||||
icon: '/favicon.png?v=5',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,23 +8,9 @@ import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator';
|
||||
// import { ErrorMessage } from '@/components/ui/ErrorMessage';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { useParkingSearch } from '@/hooks/useParkingSearch';
|
||||
import { useRouting } from '@/hooks/useRouting';
|
||||
import { ParkingLot, UserLocation, TransportationMode } from '@/types';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
// Dynamic import for map component (client-side only)
|
||||
const MapView = dynamic(
|
||||
() => import('@/components/map/MapView').then((mod) => mod.MapView),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="h-full flex items-center justify-center bg-gray-100 rounded-lg">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
export default function ParkingFinderPage() {
|
||||
// State management
|
||||
const [selectedParkingLot, setSelectedParkingLot] = useState<ParkingLot | null>(null);
|
||||
@@ -42,14 +28,6 @@ export default function ParkingFinderPage() {
|
||||
searchLocation
|
||||
} = useParkingSearch();
|
||||
|
||||
const {
|
||||
route,
|
||||
isLoading: routeLoading,
|
||||
error: routeError,
|
||||
calculateRoute,
|
||||
clearRoute
|
||||
} = useRouting();
|
||||
|
||||
// Handle GPS location change from simulator
|
||||
const handleLocationChange = (location: UserLocation) => {
|
||||
setUserLocation(location);
|
||||
@@ -70,35 +48,16 @@ export default function ParkingFinderPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleParkingLotSelect = async (lot: ParkingLot) => {
|
||||
const handleParkingLotSelect = (lot: ParkingLot) => {
|
||||
// If the same parking lot is selected again, deselect it
|
||||
if (selectedParkingLot && selectedParkingLot.id === lot.id) {
|
||||
setSelectedParkingLot(null);
|
||||
clearRoute();
|
||||
toast.success('Đã bỏ chọn bãi đỗ xe');
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedParkingLot(lot);
|
||||
|
||||
if (userLocation) {
|
||||
try {
|
||||
await calculateRoute(
|
||||
{ latitude: userLocation.lat, longitude: userLocation.lng },
|
||||
{ latitude: lot.lat, longitude: lot.lng },
|
||||
{ mode: 'driving' }
|
||||
);
|
||||
toast.success(`Đã tính đường đến ${lot.name}`);
|
||||
} catch (error) {
|
||||
toast.error('Không thể tính toán đường đi');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearRoute = () => {
|
||||
clearRoute();
|
||||
setSelectedParkingLot(null);
|
||||
toast.success('Đã xóa tuyến đường');
|
||||
toast.success(`Đã chọn ${lot.name}`);
|
||||
};
|
||||
|
||||
// Show error messages
|
||||
@@ -108,35 +67,35 @@ export default function ParkingFinderPage() {
|
||||
}
|
||||
}, [parkingError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (routeError) {
|
||||
toast.error(routeError);
|
||||
}
|
||||
}, [routeError]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header
|
||||
title="Smart Parking Finder - TP.HCM"
|
||||
subtitle="Chỉ hỗ trợ ô tô"
|
||||
onClearRoute={route ? handleClearRoute : undefined}
|
||||
/>
|
||||
|
||||
<main className="container mx-auto px-4 py-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-full">
|
||||
{/* Left Column - Map and Parking List */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
{/* Map Section */}
|
||||
{/* Summary Section */}
|
||||
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||
<div className="h-96">
|
||||
<MapView
|
||||
userLocation={userLocation}
|
||||
parkingLots={parkingLots}
|
||||
selectedParkingLot={selectedParkingLot}
|
||||
route={route}
|
||||
onParkingLotSelect={handleParkingLotSelect}
|
||||
isLoading={routeLoading}
|
||||
/>
|
||||
<div className="h-96 bg-gradient-to-br from-gray-50 to-blue-50 flex items-center justify-center">
|
||||
<div className="text-center p-8">
|
||||
<div className="w-24 h-24 bg-blue-100 rounded-full mx-auto mb-6 flex items-center justify-center">
|
||||
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-3">Parking Finder - HCMC</h2>
|
||||
<p className="text-gray-600 mb-4">Find and book parking spots in Ho Chi Minh City</p>
|
||||
{parkingLots.length > 0 && (
|
||||
<div className="text-sm text-gray-500">
|
||||
Found {parkingLots.length} parking locations nearby
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -199,14 +158,6 @@ export default function ParkingFinderPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{routeError && (
|
||||
<div className="fixed bottom-4 right-4 max-w-sm">
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{routeError}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { ParkingList } from '@/components/parking/ParkingList';
|
||||
import { ParkingDetails } from '@/components/parking/ParkingDetails';
|
||||
import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator';
|
||||
@@ -10,24 +11,42 @@ import { Icon } from '@/components/ui/Icon';
|
||||
// import { ErrorMessage } from '@/components/ui/ErrorMessage';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { useParkingSearch } from '@/hooks/useParkingSearch';
|
||||
import { useRouting } from '@/hooks/useRouting';
|
||||
import { ParkingLot, UserLocation, TransportationMode } from '@/types';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
// Dynamic import for map component (client-side only) - NO loading component to prevent unnecessary loading states
|
||||
const MapView = dynamic(
|
||||
() => import('@/components/map/MapView').then((mod) => mod.MapView),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => null, // Remove loading spinner to prevent map reload appearance
|
||||
}
|
||||
);
|
||||
export default function MainPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const showApp = searchParams?.get('app') === 'parking';
|
||||
|
||||
export default function ParkingFinderPage() {
|
||||
if (showApp) {
|
||||
return <ParkingFinderPage />;
|
||||
}
|
||||
|
||||
// Show Canva homepage by default
|
||||
return <CanvaHomepage />;
|
||||
}
|
||||
|
||||
function CanvaHomepage() {
|
||||
useEffect(() => {
|
||||
// Redirect to the Canva homepage in the public directory
|
||||
window.location.href = '/homepage/index.html';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-red-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading homepage...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ParkingFinderPage() {
|
||||
// State management
|
||||
const [selectedParkingLot, setSelectedParkingLot] = useState<ParkingLot | null>(null);
|
||||
const [userLocation, setUserLocation] = useState<UserLocation | null>(null);
|
||||
const [searchRadius, setSearchRadius] = useState(4000); // meters - bán kính 4km
|
||||
const [searchRadius, setSearchRadius] = useState(4000); // meters - 4km radius
|
||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
|
||||
const [gpsWindowPos, setGpsWindowPos] = useState({ x: 0, y: 20 });
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
@@ -88,14 +107,6 @@ export default function ParkingFinderPage() {
|
||||
searchLocation
|
||||
} = useParkingSearch();
|
||||
|
||||
const {
|
||||
route,
|
||||
isLoading: routeLoading,
|
||||
error: routeError,
|
||||
calculateRoute,
|
||||
clearRoute
|
||||
} = useRouting();
|
||||
|
||||
// Handle GPS location change from simulator
|
||||
const handleLocationChange = (location: UserLocation) => {
|
||||
setUserLocation(location);
|
||||
@@ -103,16 +114,16 @@ export default function ParkingFinderPage() {
|
||||
// Search for parking near the new location
|
||||
if (location) {
|
||||
searchLocation({ latitude: location.lat, longitude: location.lng });
|
||||
toast.success('Đã cập nhật vị trí GPS và tìm kiếm bãi đỗ xe gần đó');
|
||||
toast.success('GPS location updated and searched for nearby parking lots');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (userLocation) {
|
||||
searchLocation({ latitude: userLocation.lat, longitude: userLocation.lng });
|
||||
toast.success('Đã làm mới danh sách bãi đỗ xe');
|
||||
toast.success('Parking list refreshed');
|
||||
} else {
|
||||
toast.error('Vui lòng chọn vị trí GPS trước');
|
||||
toast.error('Please select GPS location first');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,25 +131,12 @@ export default function ParkingFinderPage() {
|
||||
// Toggle selection
|
||||
if (selectedParkingLot?.id === lot.id) {
|
||||
setSelectedParkingLot(null);
|
||||
clearRoute();
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedParkingLot(lot);
|
||||
|
||||
if (userLocation) {
|
||||
try {
|
||||
await calculateRoute(
|
||||
{ latitude: userLocation.lat, longitude: userLocation.lng },
|
||||
{ latitude: lot.lat, longitude: lot.lng },
|
||||
{ mode: 'driving' }
|
||||
);
|
||||
toast.success(`Đã tính đường đến ${lot.name}`);
|
||||
} catch (error) {
|
||||
console.error('Error calculating route:', error);
|
||||
toast.error('Không thể tính toán tuyến đường');
|
||||
}
|
||||
}
|
||||
setLeftSidebarOpen(false); // Close sidebar when selecting parking lot
|
||||
toast.success(`Selected ${lot.name}`);
|
||||
};
|
||||
|
||||
const handleParkingLotViewing = (lot: ParkingLot | null) => {
|
||||
@@ -146,9 +144,8 @@ export default function ParkingFinderPage() {
|
||||
};
|
||||
|
||||
const handleClearRoute = () => {
|
||||
clearRoute();
|
||||
setSelectedParkingLot(null);
|
||||
toast.success('Đã xóa tuyến đường');
|
||||
toast.success('Selection cleared');
|
||||
};
|
||||
|
||||
// Show error messages
|
||||
@@ -158,18 +155,11 @@ export default function ParkingFinderPage() {
|
||||
}
|
||||
}, [parkingError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (routeError) {
|
||||
toast.error(routeError);
|
||||
}
|
||||
}, [routeError]);
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-gray-50 flex flex-col">
|
||||
<Header
|
||||
title=""
|
||||
subtitle=""
|
||||
onClearRoute={route ? handleClearRoute : undefined}
|
||||
/>
|
||||
|
||||
<main className="flex-1 flex relative bg-white">
|
||||
@@ -206,9 +196,9 @@ export default function ParkingFinderPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 tracking-tight">
|
||||
Bãi đỗ xe gần đây
|
||||
Nearby Parking Lots
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 font-medium">Tìm kiếm thông minh</p>
|
||||
<p className="text-sm text-gray-600 font-medium">Smart Search</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -229,7 +219,7 @@ export default function ParkingFinderPage() {
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Làm mới danh sách
|
||||
Refresh List
|
||||
</button>
|
||||
|
||||
{/* Status Info Bar - Thiết kế thanh lịch đơn giản */}
|
||||
@@ -241,14 +231,14 @@ export default function ParkingFinderPage() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
|
||||
<span className="text-sm text-gray-700 font-medium">
|
||||
{parkingLots.filter(lot => lot.availableSlots > 0).length} có chỗ
|
||||
{parkingLots.filter(lot => lot.availableSlots > 0).length} available
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-px h-4 bg-gray-300"></div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500"></div>
|
||||
<span className="text-sm text-gray-700 font-medium">
|
||||
{parkingLots.filter(lot => lot.availableSlots === 0).length} đầy
|
||||
{parkingLots.filter(lot => lot.availableSlots === 0).length} full
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,7 +265,7 @@ export default function ParkingFinderPage() {
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tìm kiếm bãi đỗ xe..."
|
||||
placeholder="Search parking lots..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full px-4 py-3 pl-12 pr-10 text-sm font-medium rounded-2xl border-2 transition-all duration-300 focus:outline-none focus:ring-4 focus:ring-orange-100 focus:border-orange-300"
|
||||
@@ -320,7 +310,7 @@ export default function ParkingFinderPage() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707v4.586a1 1 0 01-.54.89l-2 1A1 1 0 0110 20v-5.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Sắp xếp:</span>
|
||||
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Sort:</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -339,7 +329,7 @@ export default function ParkingFinderPage() {
|
||||
borderColor: sortType === 'availability' ? 'var(--primary-color)' : 'rgba(232, 90, 79, 0.3)',
|
||||
border: '2px solid'
|
||||
}}
|
||||
title="Sắp xếp theo chỗ trống"
|
||||
title="Sort by availability"
|
||||
>
|
||||
<Icon
|
||||
name="car"
|
||||
@@ -363,7 +353,7 @@ export default function ParkingFinderPage() {
|
||||
borderColor: sortType === 'price' ? '#10B981' : 'rgba(16, 185, 129, 0.3)',
|
||||
border: '2px solid'
|
||||
}}
|
||||
title="Sắp xếp theo giá rẻ"
|
||||
title="Sort by price"
|
||||
>
|
||||
<Icon
|
||||
name="currency"
|
||||
@@ -394,7 +384,7 @@ export default function ParkingFinderPage() {
|
||||
: userLocation ? 'rgba(245, 158, 11, 0.3)' : '#E5E7EB',
|
||||
border: '2px solid'
|
||||
}}
|
||||
title="Sắp xếp theo khoảng cách gần nhất"
|
||||
title="Sort by nearest distance"
|
||||
>
|
||||
<Icon
|
||||
name="distance"
|
||||
@@ -419,8 +409,8 @@ export default function ParkingFinderPage() {
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">Chọn vị trí GPS</h3>
|
||||
<p className="text-gray-600 text-sm">Vui lòng chọn vị trí GPS để tìm bãi đỗ xe gần đó</p>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">Select GPS Location</h3>
|
||||
<p className="text-gray-600 text-sm">Please select a GPS location to find nearby parking lots</p>
|
||||
</div>
|
||||
) : parkingLots.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
@@ -429,8 +419,8 @@ export default function ParkingFinderPage() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.732 15c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">Không có bãi đỗ xe</h3>
|
||||
<p className="text-gray-600 text-sm">Không tìm thấy bãi đỗ xe nào gần vị trí này</p>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">No Parking Lots</h3>
|
||||
<p className="text-gray-600 text-sm">No parking lots found near this location</p>
|
||||
</div>
|
||||
) : (
|
||||
<ParkingList
|
||||
@@ -468,89 +458,34 @@ export default function ParkingFinderPage() {
|
||||
userLocation={userLocation}
|
||||
onClose={() => {
|
||||
setSelectedParkingLot(null);
|
||||
clearRoute();
|
||||
}}
|
||||
onBook={(lot) => {
|
||||
toast.success(`Đã đặt chỗ tại ${lot.name}!`);
|
||||
toast.success(`Booked parking at ${lot.name}!`);
|
||||
// Here you would typically call an API to book the parking spot
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map Section - Right */}
|
||||
{/* Right Section - Summary Information */}
|
||||
<div className="flex-1 h-full relative">
|
||||
<MapView
|
||||
userLocation={userLocation}
|
||||
parkingLots={parkingLots}
|
||||
selectedParkingLot={selectedParkingLot}
|
||||
route={route}
|
||||
onParkingLotSelect={handleParkingLotSelect}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
|
||||
{/* Map overlay info - Position based on layout */}
|
||||
{userLocation && (
|
||||
<div className="absolute bottom-6 right-24 z-10 bg-white rounded-3xl shadow-2xl p-6 border-2 border-gray-100 backdrop-blur-sm" style={{ minWidth: '280px' }}>
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
|
||||
<img
|
||||
src="/assets/Logo.png"
|
||||
alt="Logo"
|
||||
className="w-7 h-7 object-contain filter brightness-0 invert"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 tracking-tight">Parking Finder</h3>
|
||||
<p className="text-sm text-gray-600 font-medium">Bản đồ thông minh</p>
|
||||
</div>
|
||||
<div className="w-full h-full bg-gradient-to-br from-gray-50 to-blue-50 rounded-2xl flex items-center justify-center border border-gray-200">
|
||||
<div className="text-center p-8">
|
||||
<div className="w-24 h-24 bg-blue-100 rounded-full mx-auto mb-6 flex items-center justify-center">
|
||||
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{/* Current location */}
|
||||
<div className="flex items-center space-x-3 p-2 rounded-xl bg-blue-50">
|
||||
<div className="w-4 h-4 rounded-full shadow-sm" style={{ backgroundColor: '#3B82F6' }}></div>
|
||||
<span className="text-sm font-semibold text-blue-800">Vị trí hiện tại</span>
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-3">Map in Developing</h2>
|
||||
<p className="text-gray-600 mb-4">Interactive map feature coming soon</p>
|
||||
{parkingLots.length > 0 && (
|
||||
<div className="text-sm text-gray-500">
|
||||
Found {parkingLots.length} parking locations nearby
|
||||
</div>
|
||||
|
||||
{/* Parking lot status legend */}
|
||||
<div className="bg-gray-50 rounded-xl p-3 space-y-2">
|
||||
<div className="text-xs font-bold text-gray-700 mb-2">Trạng thái bãi xe:</div>
|
||||
|
||||
{/* Available parking - Green */}
|
||||
<div className="flex items-center space-x-3 p-1">
|
||||
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: 'var(--success-color)' }}></div>
|
||||
<span className="text-xs font-medium text-green-700">Còn chỗ thoáng (>70%)</span>
|
||||
</div>
|
||||
|
||||
{/* Nearly full - Yellow */}
|
||||
<div className="flex items-center space-x-3 p-1">
|
||||
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#F59E0B' }}></div>
|
||||
<span className="text-xs font-medium text-yellow-700">Sắp đầy (<30%)</span>
|
||||
</div>
|
||||
|
||||
{/* Full - Red */}
|
||||
<div className="flex items-center space-x-3 p-1">
|
||||
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#EF4444' }}></div>
|
||||
<span className="text-xs font-medium text-red-700">Hết chỗ</span>
|
||||
</div>
|
||||
|
||||
{/* Closed - Gray */}
|
||||
<div className="flex items-center space-x-3 p-1">
|
||||
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#6B7280' }}></div>
|
||||
<span className="text-xs font-medium text-gray-700">Đã đóng cửa</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Route line */}
|
||||
{route && (
|
||||
<div className="flex items-center space-x-3 p-2 rounded-xl bg-red-50">
|
||||
<div className="w-4 h-2 rounded-full shadow-sm" style={{ backgroundColor: 'var(--primary-color)' }}></div>
|
||||
<span className="text-sm font-semibold" style={{ color: 'var(--primary-color)' }}>Tuyến đường</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating GPS Window */}
|
||||
@@ -595,14 +530,14 @@ export default function ParkingFinderPage() {
|
||||
<p className="text-white text-opacity-90 font-medium" style={{
|
||||
fontSize: isMobile ? '12px' : '14px'
|
||||
}}>
|
||||
{isMobile ? 'Mô phỏng GPS' : 'Mô phỏng vị trí GPS cho TP.HCM'}
|
||||
{isMobile ? 'GPS Simulation' : 'GPS Location Simulation for Ho Chi Minh City'}
|
||||
</p>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={() => setGpsSimulatorVisible(!gpsSimulatorVisible)}
|
||||
className="p-2 rounded-xl bg-white bg-opacity-20 hover:bg-opacity-30 transition-all duration-200"
|
||||
title={gpsSimulatorVisible ? 'Ẩn GPS Simulator' : 'Hiện GPS Simulator'}
|
||||
title={gpsSimulatorVisible ? 'Hide GPS Simulator' : 'Show GPS Simulator'}
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 text-white transition-transform duration-300 ${gpsSimulatorVisible ? 'rotate-180' : 'rotate-0'}`}
|
||||
@@ -620,7 +555,7 @@ export default function ParkingFinderPage() {
|
||||
<button
|
||||
onClick={() => setGpsSimulatorVisible(!gpsSimulatorVisible)}
|
||||
className="p-2 rounded-xl bg-white bg-opacity-20 hover:bg-opacity-30 transition-all duration-200 group"
|
||||
title={gpsSimulatorVisible ? 'Ẩn GPS Simulator' : 'Hiện GPS Simulator'}
|
||||
title={gpsSimulatorVisible ? 'Hide GPS Simulator' : 'Show GPS Simulator'}
|
||||
>
|
||||
<svg
|
||||
className={`w-5 h-5 text-white transition-transform duration-300 ${gpsSimulatorVisible ? 'rotate-180' : 'rotate-0'}`}
|
||||
@@ -654,6 +589,9 @@ export default function ParkingFinderPage() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer showFullFooter={false} />
|
||||
|
||||
{/* Show errors */}
|
||||
{parkingError && (
|
||||
<div className="fixed bottom-6 right-6 max-w-sm z-50">
|
||||
@@ -662,14 +600,6 @@ export default function ParkingFinderPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{routeError && (
|
||||
<div className="fixed bottom-6 right-6 max-w-sm z-50">
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{routeError}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
113
frontend/src/components/Footer.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface FooterProps {
|
||||
showFullFooter?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Footer: React.FC<FooterProps> = ({
|
||||
showFullFooter = false,
|
||||
className = ""
|
||||
}) => {
|
||||
if (!showFullFooter) {
|
||||
return (
|
||||
<footer className={`bg-black text-white py-6 ${className}`}>
|
||||
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Image
|
||||
src="/assets/Footer_page_logo.png"
|
||||
alt="Laca City Logo"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-auto object-contain"
|
||||
/>
|
||||
<span className="text-lg font-bold">Laca City</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className={`bg-black text-white py-16 ${className}`}>
|
||||
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
|
||||
<div className="md:col-span-2">
|
||||
<div className="flex items-center space-x-4 mb-6">
|
||||
<Image
|
||||
src="/assets/Footer_page_logo.png"
|
||||
alt="Laca City Logo"
|
||||
width={60}
|
||||
height={60}
|
||||
className="h-16 w-auto object-contain"
|
||||
/>
|
||||
<span className="text-2xl font-bold">Laca City</span>
|
||||
</div>
|
||||
<p className="text-xl text-gray-200 leading-relaxed mb-6 max-w-lg">
|
||||
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
|
||||
</p>
|
||||
|
||||
{/* Social Media Links */}
|
||||
<div className="flex space-x-4">
|
||||
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
|
||||
{/* Facebook Icon */}
|
||||
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
|
||||
{/* Twitter Icon */}
|
||||
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
|
||||
{/* LinkedIn Icon */}
|
||||
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-6">Quick Links</h4>
|
||||
<ul className="space-y-3">
|
||||
<li><a href="/" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Home</a></li>
|
||||
<li><a href="/#features" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Features</a></li>
|
||||
<li><a href="/#team" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Team</a></li>
|
||||
<li><a href="/#news" className="text-gray-300 hover:text-red-500 transition-colors duration-300">News</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-6">App</h4>
|
||||
<ul className="space-y-3">
|
||||
<li><a href="/?app=parking" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Launch App</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Help</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Privacy</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Terms</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="border-t border-gray-800 pt-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between">
|
||||
<p className="text-gray-400 text-sm">
|
||||
© 2024 Laca City. All rights reserved.
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mt-2 md:mt-0">
|
||||
Made with ❤️ in Ho Chi Minh City
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -10,31 +10,31 @@ interface HCMCGPSSimulatorProps {
|
||||
|
||||
// Predefined locations near HCMC parking lots
|
||||
const simulationPoints = [
|
||||
// Trung tâm Quận 1 - gần bãi đỗ xe
|
||||
// District 1 Center - near parking lots
|
||||
{
|
||||
name: 'Vincom Center Đồng Khởi',
|
||||
location: { lat: 10.7769, lng: 106.7009 },
|
||||
description: 'Gần trung tâm thương mại Vincom'
|
||||
description: 'Near Vincom shopping center'
|
||||
},
|
||||
{
|
||||
name: 'Saigon Centre',
|
||||
location: { lat: 10.7743, lng: 106.7017 },
|
||||
description: 'Gần Saigon Centre'
|
||||
description: 'Near Saigon Centre'
|
||||
},
|
||||
{
|
||||
name: 'Landmark 81',
|
||||
location: { lat: 10.7955, lng: 106.7195 },
|
||||
description: 'Gần tòa nhà Landmark 81'
|
||||
description: 'Near Landmark 81 building'
|
||||
},
|
||||
{
|
||||
name: 'Bitexco Financial Tower',
|
||||
location: { lat: 10.7718, lng: 106.7047 },
|
||||
description: 'Gần tòa nhà Bitexco'
|
||||
description: 'Near Bitexco building'
|
||||
},
|
||||
{
|
||||
name: 'Chợ Bến Thành',
|
||||
location: { lat: 10.7729, lng: 106.6980 },
|
||||
description: 'Gần chợ Bến Thành'
|
||||
description: 'Near Ben Thanh Market'
|
||||
},
|
||||
{
|
||||
name: 'Diamond Plaza',
|
||||
@@ -240,7 +240,7 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 md:gap-3 mb-1">
|
||||
<span className="text-base md:text-lg font-bold tracking-tight" style={{ color: 'var(--primary-color)' }}>Vị trí hiện tại</span>
|
||||
<span className="text-base md:text-lg font-bold tracking-tight" style={{ color: 'var(--primary-color)' }}>Current Location</span>
|
||||
<div className="flex items-center gap-1 md:gap-2 px-2 md:px-3 py-1 rounded-full bg-white border-2" style={{ borderColor: 'var(--success-color)' }}>
|
||||
<div className="w-1.5 md:w-2 h-1.5 md:h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--success-color)' }}></div>
|
||||
<span className="text-xs font-bold" style={{ color: 'var(--success-color)' }}>LIVE</span>
|
||||
@@ -483,8 +483,8 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-left flex-1 min-w-0">
|
||||
<h5 className="text-base md:text-lg font-bold tracking-tight mb-1" style={{ color: 'var(--accent-color)' }}>Vị trí ngẫu nhiên</h5>
|
||||
<p className="text-xs md:text-sm text-gray-600 font-medium">Tạo tọa độ tự động trong TP.HCM</p>
|
||||
<h5 className="text-base md:text-lg font-bold tracking-tight mb-1" style={{ color: 'var(--accent-color)' }}>Random Location</h5>
|
||||
<p className="text-xs md:text-sm text-gray-600 font-medium">Auto-generate coordinates in HCMC</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-full" style={{ backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" style={{ color: 'var(--primary-color)' }}>
|
||||
@@ -493,7 +493,7 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
|
||||
<span className="text-xs font-bold" style={{ color: 'var(--primary-color)' }}>RANDOM</span>
|
||||
</div>
|
||||
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: 'var(--primary-color)' }}></div>
|
||||
<span className="text-xs text-gray-500 hidden md:inline">Khu vực mở rộng</span>
|
||||
<span className="text-xs text-gray-500 hidden md:inline">Extended area</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-5 md:w-6 h-5 md:h-6 rounded-full border-2 flex items-center justify-center group-hover:border-red-500 transition-colors flex-shrink-0" style={{ borderColor: 'var(--primary-color)' }}>
|
||||
|
||||
@@ -26,18 +26,12 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<div className="flex-shrink-0">
|
||||
<div className="relative">
|
||||
<Image
|
||||
src="/assets/Logo_and_sologan.png"
|
||||
src="/assets/Location.png"
|
||||
alt="Smart Parking Logo"
|
||||
width={320}
|
||||
height={80}
|
||||
className="h-18 w-auto object-contain"
|
||||
/>
|
||||
{/* Animated accent line */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 rounded-full" style={{
|
||||
background: 'linear-gradient(90deg, var(--primary-color), var(--secondary-color))',
|
||||
transform: 'scaleX(0.8)',
|
||||
transformOrigin: 'left'
|
||||
}}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -66,7 +60,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Xóa tuyến đường
|
||||
Clear Route
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -76,7 +70,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
borderColor: 'rgba(34, 197, 94, 0.3)'
|
||||
}}>
|
||||
<div className="w-3 h-3 rounded-full animate-pulse shadow-sm" style={{ backgroundColor: 'var(--success-color)' }}></div>
|
||||
<span className="text-sm font-bold" style={{ color: 'var(--success-color)' }}>Dữ liệu trực tuyến</span>
|
||||
<span className="text-sm font-bold" style={{ color: 'var(--success-color)' }}>Live Data</span>
|
||||
</div>
|
||||
|
||||
{/* City Info */}
|
||||
@@ -90,7 +84,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-bold" style={{ color: 'var(--primary-color)' }}>TP. Hồ Chí Minh</span>
|
||||
<span className="text-sm font-bold" style={{ color: 'var(--primary-color)' }}>Ho Chi Minh City</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ interface ParkingFloor {
|
||||
walkways: { x: number; y: number; width: number; height: number }[];
|
||||
}
|
||||
|
||||
// Thiết kế bãi xe đẹp và chuyên nghiệp
|
||||
// Professional and beautiful parking lot design
|
||||
const generateParkingFloorData = (floorNumber: number): ParkingFloor => {
|
||||
const slots: ParkingSlot[] = [];
|
||||
const walkways = [];
|
||||
@@ -108,7 +108,7 @@ const generateParkingFloorData = (floorNumber: number): ParkingFloor => {
|
||||
|
||||
return {
|
||||
floor: floorNumber,
|
||||
name: `Tầng ${floorNumber}`,
|
||||
name: `Floor ${floorNumber}`,
|
||||
slots,
|
||||
entrances: [
|
||||
{ x: 60, y: 10, type: 'entrance' },
|
||||
@@ -194,7 +194,7 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
|
||||
{/* Real-time indicator */}
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>Cập nhật: {lastUpdate.toLocaleTimeString()}</span>
|
||||
<span>Updated: {lastUpdate.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -202,15 +202,15 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-center p-2 bg-green-50 rounded border border-green-200">
|
||||
<div className="font-bold text-green-600">{floorStats.available}</div>
|
||||
<div className="text-green-700">Trống</div>
|
||||
<div className="text-green-700">Available</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-red-50 rounded border border-red-200">
|
||||
<div className="font-bold text-red-600">{floorStats.occupied}</div>
|
||||
<div className="text-red-700">Đã đậu</div>
|
||||
<div className="text-red-700">Occupied</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-gray-50 rounded border border-gray-200">
|
||||
<div className="font-bold text-gray-600">{floorStats.total}</div>
|
||||
<div className="text-gray-700">Tổng</div>
|
||||
<div className="text-gray-700">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,27 +221,27 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
|
||||
const SAMPLE_REVIEWS = [
|
||||
{
|
||||
id: 1,
|
||||
user: 'Nguyễn Văn A',
|
||||
user: 'John Smith',
|
||||
rating: 5,
|
||||
comment: 'Bãi xe rộng rãi, bảo vệ 24/7 rất an toàn. Giá cả hợp lý.',
|
||||
comment: 'Spacious parking lot with 24/7 security. Very safe and reasonably priced.',
|
||||
date: '2024-01-15',
|
||||
avatar: 'N'
|
||||
avatar: 'J'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: 'Trần Thị B',
|
||||
user: 'Sarah Johnson',
|
||||
rating: 4,
|
||||
comment: 'Vị trí thuận tiện, dễ tìm. Chỉ hơi xa lối ra một chút.',
|
||||
comment: 'Convenient location, easy to find. Just a bit far from the exit.',
|
||||
date: '2024-01-10',
|
||||
avatar: 'T'
|
||||
avatar: 'S'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user: 'Lê Văn C',
|
||||
user: 'Mike Davis',
|
||||
rating: 5,
|
||||
comment: 'Có sạc điện cho xe điện, rất tiện lợi!',
|
||||
comment: 'Has electric charging stations, very convenient!',
|
||||
date: '2024-01-08',
|
||||
avatar: 'L'
|
||||
avatar: 'M'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -326,8 +326,8 @@ const formatAmenities = (amenities: string[] | { [key: string]: any }): string[]
|
||||
|
||||
const amenityList: string[] = [];
|
||||
if (amenities.covered) amenityList.push('Có mái che');
|
||||
if (amenities.security) amenityList.push('Bảo vệ 24/7');
|
||||
if (amenities.ev_charging) amenityList.push('Sạc xe điện');
|
||||
if (amenities.security) amenityList.push('24/7 Security');
|
||||
if (amenities.ev_charging) amenityList.push('EV Charging');
|
||||
if (amenities.wheelchair_accessible) amenityList.push('Phù hợp xe lăn');
|
||||
if (amenities.valet_service) amenityList.push('Dịch vụ đỗ xe');
|
||||
|
||||
@@ -418,7 +418,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
{renderStars(Math.round(averageRating))}
|
||||
</div>
|
||||
<span className="text-sm font-semibold">{averageRating.toFixed(1)}</span>
|
||||
<span className="text-sm opacity-80">({SAMPLE_REVIEWS.length} đánh giá)</span>
|
||||
<span className="text-sm opacity-80">({SAMPLE_REVIEWS.length} reviews)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,13 +426,13 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
{/* Status banners */}
|
||||
{isFull && (
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-red-600 text-center py-2">
|
||||
<span className="text-sm font-bold">Bãi xe đã hết chỗ</span>
|
||||
<span className="text-sm font-bold">Parking lot is full</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isClosed && (
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gray-600 text-center py-2">
|
||||
<span className="text-sm font-bold">Bãi xe đã đóng cửa</span>
|
||||
<span className="text-sm font-bold">Parking lot is closed</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -451,7 +451,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
<div className="text-2xl font-bold mb-1" style={{ color: statusColors.textColor }}>
|
||||
{parkingLot.availableSlots}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-gray-600">chỗ trống</div>
|
||||
<div className="text-xs font-medium text-gray-600">available</div>
|
||||
<div className="text-xs text-gray-500">/ {parkingLot.totalSlots} tổng</div>
|
||||
</div>
|
||||
|
||||
@@ -505,7 +505,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
}}
|
||||
>
|
||||
{tab === 'overview' && 'Tổng quan'}
|
||||
{tab === 'reviews' && 'Đánh giá'}
|
||||
{tab === 'reviews' && 'Reviews'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -541,7 +541,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
Tiện ích
|
||||
Amenities
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{amenityList.map((amenity, index) => (
|
||||
@@ -570,7 +570,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
<div className="flex items-center justify-center gap-1 mb-2">
|
||||
{renderStars(Math.round(averageRating))}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-600">{SAMPLE_REVIEWS.length} đánh giá</div>
|
||||
<div className="text-sm font-medium text-gray-600">{SAMPLE_REVIEWS.length} reviews</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews list */}
|
||||
@@ -605,7 +605,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
<button className="w-full py-3 rounded-xl font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105" style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}>
|
||||
Viết đánh giá
|
||||
Write Review
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -722,7 +722,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
|
||||
: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}
|
||||
>
|
||||
{isFull ? 'Bãi xe đã hết chỗ' : isClosed ? 'Bãi xe đã đóng cửa' : `Đặt chỗ (${bookingDuration}h)`}
|
||||
{isFull ? 'Parking lot is full' : isClosed ? 'Parking lot is closed' : `Book Spot (${bookingDuration}h)`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -43,21 +43,21 @@ const formatDistance = (distance: number): string => {
|
||||
const getStatusColor = (availableSlots: number, totalSlots: number) => {
|
||||
const percentage = availableSlots / totalSlots;
|
||||
if (availableSlots === 0) {
|
||||
// Hết chỗ - màu đỏ
|
||||
// Full - red color
|
||||
return {
|
||||
background: 'rgba(239, 68, 68, 0.15)',
|
||||
borderColor: '#EF4444',
|
||||
textColor: '#EF4444'
|
||||
};
|
||||
} else if (percentage > 0.7) {
|
||||
// >70% chỗ trống - màu xanh lá cây
|
||||
// >70% available - green color
|
||||
return {
|
||||
background: 'rgba(34, 197, 94, 0.1)',
|
||||
borderColor: 'var(--success-color)',
|
||||
textColor: 'var(--success-color)'
|
||||
};
|
||||
} else {
|
||||
// <30% chỗ trống - màu vàng
|
||||
// <30% available - yellow color
|
||||
return {
|
||||
background: 'rgba(251, 191, 36, 0.1)',
|
||||
borderColor: '#F59E0B',
|
||||
@@ -68,11 +68,11 @@ const getStatusColor = (availableSlots: number, totalSlots: number) => {
|
||||
|
||||
const getStatusText = (availableSlots: number, totalSlots: number) => {
|
||||
if (availableSlots === 0) {
|
||||
return 'Hết chỗ';
|
||||
return 'Full';
|
||||
} else if (availableSlots / totalSlots > 0.7) {
|
||||
return `${availableSlots} chỗ trống`;
|
||||
return `${availableSlots} available`;
|
||||
} else {
|
||||
return `${availableSlots} chỗ trống (sắp hết)`;
|
||||
return `${availableSlots} left (filling up)`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,9 +201,9 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">Không tìm thấy kết quả</h3>
|
||||
<p className="text-gray-600 text-sm">Không có bãi đỗ xe nào phù hợp với từ khóa "{searchQuery}"</p>
|
||||
<p className="text-gray-500 text-xs mt-2">Thử tìm kiếm với từ khóa khác</p>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">No Results Found</h3>
|
||||
<p className="text-gray-600 text-sm">No parking lots match the keyword "{searchQuery}"</p>
|
||||
<p className="text-gray-500 text-xs mt-2">Try searching with different keywords</p>
|
||||
</div>
|
||||
) : (
|
||||
sortedLots.map((lot, index) => {
|
||||
@@ -262,13 +262,13 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
{/* Warning banners */}
|
||||
{isFull && (
|
||||
<div className="absolute -top-2 -left-2 -right-2 bg-red-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
|
||||
<span className="text-sm font-bold">🚫 BÃI XE ĐÃ HẾT CHỖ</span>
|
||||
<span className="text-sm font-bold">🚫 PARKING LOT FULL</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isClosed && (
|
||||
<div className="absolute -top-2 -left-2 -right-2 bg-gray-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
|
||||
<span className="text-sm font-bold">🔒 BÃI XE ĐÃ ĐÓNG CỬA</span>
|
||||
<span className="text-sm font-bold">🔒 PARKING LOT CLOSED</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -322,10 +322,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 font-medium">
|
||||
chỗ trống
|
||||
available
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
/ {lot.totalSlots} chỗ
|
||||
/ {lot.totalSlots} total
|
||||
</div>
|
||||
{/* Availability percentage */}
|
||||
<div className="mt-1 w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
|
||||
@@ -338,7 +338,7 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
></div>
|
||||
</div>
|
||||
<div className="text-xs mt-1" style={{ color: statusColors.textColor }}>
|
||||
{Math.round((lot.availableSlots / lot.totalSlots) * 100)}% trống
|
||||
{Math.round((lot.availableSlots / lot.totalSlots) * 100)}% available
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -350,10 +350,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
{Math.round((lot.pricePerHour || lot.hourlyRate) / 1000)}k
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 font-medium">
|
||||
mỗi giờ
|
||||
per hour
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
phí gửi xe
|
||||
parking fee
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -362,10 +362,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
--
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 font-medium">
|
||||
liên hệ
|
||||
contact
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
để biết giá
|
||||
for pricing
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -386,13 +386,13 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
</div>
|
||||
<div className={`text-sm font-medium ${isCurrentlyOpen(lot) ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{isCurrentlyOpen(lot) ? (
|
||||
lot.isOpen24Hours ? 'Luôn mở cửa' : `đến ${lot.closeTime}`
|
||||
lot.isOpen24Hours ? 'Always open' : `until ${lot.closeTime}`
|
||||
) : (
|
||||
'Đã đóng cửa'
|
||||
'Closed'
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{isCurrentlyOpen(lot) ? 'Đang mở' : '🔒 Đã đóng'}
|
||||
{isCurrentlyOpen(lot) ? 'Open now' : '🔒 Closed'}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -401,10 +401,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
|
||||
--:--
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 font-medium">
|
||||
không rõ
|
||||
unknown
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
giờ mở cửa
|
||||
opening hours
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,8 @@ const iconPaths: Record<string, string> = {
|
||||
delete: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16",
|
||||
dice: "M5 3a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2H5zm3 4a1 1 0 100 2 1 1 0 000-2zm8 0a1 1 0 100 2 1 1 0 000-2zm-8 8a1 1 0 100 2 1 1 0 000-2zm8 0a1 1 0 100 2 1 1 0 000-2z",
|
||||
location: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z",
|
||||
map: "M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7v13zM9 7l6 2-6 3zm6-3l4.553 2.276A1 1 0 0121 7.618v10.764a1 1 0 01-.553.894L15 17V4z",
|
||||
// map icon removed
|
||||
marker: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z",
|
||||
market: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2 2z M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2",
|
||||
refresh: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15",
|
||||
rocket: "M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { parkingService, routingService, healthService } from '@/services/api';
|
||||
import { parkingService, healthService } from '@/services/api';
|
||||
import {
|
||||
FindNearbyParkingRequest,
|
||||
RouteRequest,
|
||||
UpdateAvailabilityRequest
|
||||
} from '@/types';
|
||||
|
||||
@@ -14,10 +13,6 @@ export const QUERY_KEYS = {
|
||||
byId: (id: number) => ['parking', id],
|
||||
popular: (limit?: number) => ['parking', 'popular', limit],
|
||||
},
|
||||
routing: {
|
||||
route: (params: RouteRequest) => ['routing', 'route', params],
|
||||
status: ['routing', 'status'],
|
||||
},
|
||||
health: ['health'],
|
||||
} as const;
|
||||
|
||||
@@ -83,26 +78,6 @@ export function useUpdateParkingAvailability() {
|
||||
});
|
||||
}
|
||||
|
||||
// Routing hooks
|
||||
export function useRoute(request: RouteRequest, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.routing.route(request),
|
||||
queryFn: () => routingService.calculateRoute(request),
|
||||
enabled: enabled && !!request.originLat && !!request.originLng && !!request.destinationLat && !!request.destinationLng,
|
||||
staleTime: 15 * 60 * 1000, // 15 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRoutingStatus() {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.routing.status,
|
||||
queryFn: routingService.getStatus,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
refetchInterval: 60 * 1000, // Refresh every minute
|
||||
});
|
||||
}
|
||||
|
||||
// Health hooks
|
||||
export function useHealth() {
|
||||
return useQuery({
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Coordinates } from '@/types';
|
||||
|
||||
export interface RouteStep {
|
||||
instruction: string;
|
||||
distance: number;
|
||||
duration: number;
|
||||
maneuver?: string;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
id: string;
|
||||
distance: number; // in meters
|
||||
duration: number; // in seconds
|
||||
geometry: Array<[number, number]>; // [lat, lng] coordinates
|
||||
steps: RouteStep[];
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
}
|
||||
|
||||
interface RoutingState {
|
||||
route: Route | null;
|
||||
alternatives: Route[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface CalculateRouteOptions {
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
avoidTolls?: boolean;
|
||||
avoidHighways?: boolean;
|
||||
alternatives?: boolean;
|
||||
}
|
||||
|
||||
export const useRouting = () => {
|
||||
const [state, setState] = useState<RoutingState>({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const calculateRoute = useCallback(async (
|
||||
start: Coordinates,
|
||||
end: Coordinates,
|
||||
options: CalculateRouteOptions = { mode: 'driving' }
|
||||
) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
error: null
|
||||
}));
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Mock route calculation
|
||||
const distance = calculateDistance(start, end);
|
||||
const mockRoute: Route = {
|
||||
id: 'route-1',
|
||||
distance: distance * 1000, // Convert to meters
|
||||
duration: Math.round(distance * 180), // Rough estimate: 3 minutes per km for driving
|
||||
geometry: [
|
||||
[start.latitude, start.longitude],
|
||||
[end.latitude, end.longitude]
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
instruction: `Đi từ vị trí hiện tại`,
|
||||
distance: distance * 1000 * 0.1,
|
||||
duration: Math.round(distance * 18)
|
||||
},
|
||||
{
|
||||
instruction: `Đến ${end.latitude.toFixed(4)}, ${end.longitude.toFixed(4)}`,
|
||||
distance: distance * 1000 * 0.9,
|
||||
duration: Math.round(distance * 162)
|
||||
}
|
||||
],
|
||||
mode: options.mode
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
route: mockRoute,
|
||||
alternatives: []
|
||||
}));
|
||||
|
||||
return { route: mockRoute, alternatives: [] };
|
||||
} catch (error: any) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: error.message || 'Failed to calculate route'
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearRoute = useCallback(() => {
|
||||
setState({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
route: state.route,
|
||||
alternatives: state.alternatives,
|
||||
isLoading: state.isLoading,
|
||||
error: state.error,
|
||||
calculateRoute,
|
||||
clearRoute
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to calculate distance between two coordinates
|
||||
function calculateDistance(coord1: Coordinates, coord2: Coordinates): number {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const dLat = toRadians(coord2.latitude - coord1.latitude);
|
||||
const dLon = toRadians(coord2.longitude - coord1.longitude);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRadians(coord1.latitude)) *
|
||||
Math.cos(toRadians(coord2.latitude)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c; // Distance in kilometers
|
||||
}
|
||||
|
||||
function toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
@@ -3,9 +3,7 @@ import {
|
||||
FindNearbyParkingRequest,
|
||||
FindNearbyParkingResponse,
|
||||
ParkingLot,
|
||||
UpdateAvailabilityRequest,
|
||||
RouteRequest,
|
||||
RouteResponse
|
||||
UpdateAvailabilityRequest
|
||||
} from '@/types';
|
||||
|
||||
class APIClient {
|
||||
@@ -77,17 +75,6 @@ class APIClient {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Routing endpoints
|
||||
async calculateRoute(request: RouteRequest): Promise<RouteResponse> {
|
||||
const response = await this.client.post('/routing/calculate', request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getRoutingServiceStatus(): Promise<{ status: string; version?: string }> {
|
||||
const response = await this.client.get('/routing/status');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Health endpoint
|
||||
async getHealth(): Promise<{ status: string; timestamp: string }> {
|
||||
const response = await this.client.get('/health');
|
||||
@@ -108,11 +95,6 @@ export const parkingService = {
|
||||
getPopular: (limit?: number) => apiClient.getPopularParkingLots(limit),
|
||||
};
|
||||
|
||||
export const routingService = {
|
||||
calculateRoute: (request: RouteRequest) => apiClient.calculateRoute(request),
|
||||
getStatus: () => apiClient.getRoutingServiceStatus(),
|
||||
};
|
||||
|
||||
export const healthService = {
|
||||
getHealth: () => apiClient.getHealth(),
|
||||
};
|
||||
|
||||
@@ -54,37 +54,6 @@ export interface ParkingLot {
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
export interface RoutePoint {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface RouteStep {
|
||||
instruction: string;
|
||||
distance: number; // meters
|
||||
time: number; // seconds
|
||||
type: string;
|
||||
geometry: RoutePoint[];
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
summary: {
|
||||
distance: number; // km
|
||||
time: number; // minutes
|
||||
cost?: number; // estimated cost
|
||||
};
|
||||
geometry: RoutePoint[];
|
||||
steps: RouteStep[];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface RouteResponse {
|
||||
routes: Route[];
|
||||
origin: RoutePoint;
|
||||
destination: RoutePoint;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
// API Request/Response Types
|
||||
export interface FindNearbyParkingRequest {
|
||||
lat: number;
|
||||
@@ -102,17 +71,6 @@ export interface FindNearbyParkingResponse {
|
||||
searchRadius: number;
|
||||
}
|
||||
|
||||
export interface RouteRequest {
|
||||
originLat: number;
|
||||
originLng: number;
|
||||
destinationLat: number;
|
||||
destinationLng: number;
|
||||
costing?: TransportationMode;
|
||||
alternatives?: number;
|
||||
avoidHighways?: boolean;
|
||||
avoidTolls?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateAvailabilityRequest {
|
||||
availableSlots: number;
|
||||
source?: string;
|
||||
@@ -174,16 +132,8 @@ export interface ParkingState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface RouteState {
|
||||
currentRoute: Route | null;
|
||||
isCalculating: boolean;
|
||||
error: string | null;
|
||||
history: Route[];
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
parking: ParkingState;
|
||||
routing: RouteState;
|
||||
userPreferences: UserPreferences;
|
||||
ui: {
|
||||
sidebarOpen: boolean;
|
||||
@@ -214,11 +164,6 @@ export interface ParkingLotSelectEvent {
|
||||
source: 'map' | 'list' | 'search';
|
||||
}
|
||||
|
||||
export interface RouteCalculatedEvent {
|
||||
route: Route;
|
||||
duration: number; // calculation time in ms
|
||||
}
|
||||
|
||||
export interface LocationUpdateEvent {
|
||||
location: UserLocation;
|
||||
accuracy: number;
|
||||
@@ -269,15 +214,6 @@ export interface SearchAnalytics {
|
||||
timeToSelection?: number;
|
||||
}
|
||||
|
||||
export interface RouteAnalytics {
|
||||
origin: RoutePoint;
|
||||
destination: RoutePoint;
|
||||
mode: TransportationMode;
|
||||
distance: number;
|
||||
duration: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
// Configuration Types
|
||||
export interface AppConfig {
|
||||
api: {
|
||||
@@ -320,19 +256,9 @@ export interface UseParkingSearchReturn {
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
export interface UseRoutingReturn {
|
||||
route: Route | null;
|
||||
isLoading: boolean;
|
||||
error: APIError | null;
|
||||
calculateRoute: (request: RouteRequest) => Promise<void>;
|
||||
clearRoute: () => void;
|
||||
alternatives: Route[];
|
||||
}
|
||||
|
||||
// Component Props Types
|
||||
export interface HeaderProps {
|
||||
onRefresh?: () => void;
|
||||
onClearRoute?: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
@@ -340,7 +266,6 @@ export interface MapViewProps {
|
||||
userLocation: UserLocation | null;
|
||||
parkingLots: ParkingLot[];
|
||||
selectedParkingLot: ParkingLot | null;
|
||||
route: Route | null;
|
||||
onParkingLotSelect: (lot: ParkingLot) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
import L from 'leaflet';
|
||||
|
||||
// Fix for default markers in React Leaflet
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||
});
|
||||
|
||||
export interface MapBounds {
|
||||
north: number;
|
||||
south: number;
|
||||
east: number;
|
||||
west: number;
|
||||
}
|
||||
|
||||
export interface MapUtils {
|
||||
createIcon: (type: 'user' | 'parking' | 'selected') => L.Icon;
|
||||
createBounds: (coordinates: Array<{ lat: number; lng: number }>) => L.LatLngBounds;
|
||||
formatDistance: (distanceKm: number) => string;
|
||||
formatDuration: (durationSeconds: number) => string;
|
||||
getBoundsFromCoordinates: (coords: Array<[number, number]>) => MapBounds;
|
||||
}
|
||||
|
||||
// Custom icons for different marker types
|
||||
export const mapIcons = {
|
||||
user: new L.Icon({
|
||||
iconUrl: '/icons/location.svg',
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -32],
|
||||
className: 'user-location-icon',
|
||||
}),
|
||||
parking: new L.Icon({
|
||||
iconUrl: '/icons/car.svg',
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 28],
|
||||
popupAnchor: [0, -28],
|
||||
className: 'parking-icon',
|
||||
}),
|
||||
selected: new L.Icon({
|
||||
iconUrl: '/icons/target.svg',
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -32],
|
||||
className: 'selected-parking-icon',
|
||||
}),
|
||||
unavailable: new L.Icon({
|
||||
iconUrl: '/icons/warning.svg',
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 28],
|
||||
popupAnchor: [0, -28],
|
||||
className: 'unavailable-parking-icon',
|
||||
}),
|
||||
};
|
||||
|
||||
// Map configuration constants
|
||||
export const MAP_CONFIG = {
|
||||
defaultCenter: { lat: 1.3521, lng: 103.8198 }, // Singapore
|
||||
defaultZoom: 12,
|
||||
maxZoom: 18,
|
||||
minZoom: 10,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
tileLayerUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
searchRadius: 5000, // 5km in meters
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
export const mapUtils: MapUtils = {
|
||||
createIcon: (type: 'user' | 'parking' | 'selected') => {
|
||||
return mapIcons[type];
|
||||
},
|
||||
|
||||
createBounds: (coordinates: Array<{ lat: number; lng: number }>) => {
|
||||
if (coordinates.length === 0) {
|
||||
return new L.LatLngBounds(
|
||||
[MAP_CONFIG.defaultCenter.lat, MAP_CONFIG.defaultCenter.lng],
|
||||
[MAP_CONFIG.defaultCenter.lat, MAP_CONFIG.defaultCenter.lng]
|
||||
);
|
||||
}
|
||||
|
||||
const latLngs = coordinates.map(coord => new L.LatLng(coord.lat, coord.lng));
|
||||
return new L.LatLngBounds(latLngs);
|
||||
},
|
||||
|
||||
formatDistance: (distanceKm: number): string => {
|
||||
if (distanceKm < 1) {
|
||||
return `${Math.round(distanceKm * 1000)}m`;
|
||||
}
|
||||
return `${distanceKm.toFixed(1)}km`;
|
||||
},
|
||||
|
||||
formatDuration: (durationSeconds: number): string => {
|
||||
const minutes = Math.round(durationSeconds / 60);
|
||||
if (minutes < 60) {
|
||||
return `${minutes} min`;
|
||||
}
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
},
|
||||
|
||||
getBoundsFromCoordinates: (coords: Array<[number, number]>): MapBounds => {
|
||||
if (coords.length === 0) {
|
||||
return {
|
||||
north: MAP_CONFIG.defaultCenter.lat + 0.01,
|
||||
south: MAP_CONFIG.defaultCenter.lat - 0.01,
|
||||
east: MAP_CONFIG.defaultCenter.lng + 0.01,
|
||||
west: MAP_CONFIG.defaultCenter.lng - 0.01,
|
||||
};
|
||||
}
|
||||
|
||||
const lats = coords.map(coord => coord[0]);
|
||||
const lngs = coords.map(coord => coord[1]);
|
||||
|
||||
return {
|
||||
north: Math.max(...lats),
|
||||
south: Math.min(...lats),
|
||||
east: Math.max(...lngs),
|
||||
west: Math.min(...lngs),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Route styling
|
||||
export const routeStyle = {
|
||||
color: '#2563eb', // Blue
|
||||
weight: 4,
|
||||
opacity: 0.8,
|
||||
dashArray: '0',
|
||||
lineJoin: 'round' as const,
|
||||
lineCap: 'round' as const,
|
||||
};
|
||||
|
||||
export const alternativeRouteStyle = {
|
||||
color: '#6b7280', // Gray
|
||||
weight: 3,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 10',
|
||||
lineJoin: 'round' as const,
|
||||
lineCap: 'round' as const,
|
||||
};
|
||||
|
||||
// Parking lot status colors
|
||||
export const parkingStatusColors = {
|
||||
available: '#10b981', // Green
|
||||
limited: '#f59e0b', // Amber
|
||||
full: '#ef4444', // Red
|
||||
unknown: '#6b7280', // Gray
|
||||
};
|
||||
|
||||
// Helper function to get parking lot color based on availability
|
||||
export const getParkingStatusColor = (
|
||||
availableSpaces: number,
|
||||
totalSpaces: number
|
||||
): string => {
|
||||
if (totalSpaces === 0) return parkingStatusColors.unknown;
|
||||
|
||||
const occupancyRate = 1 - (availableSpaces / totalSpaces);
|
||||
|
||||
if (occupancyRate < 0.7) return parkingStatusColors.available;
|
||||
if (occupancyRate < 0.9) return parkingStatusColors.limited;
|
||||
return parkingStatusColors.full;
|
||||
};
|
||||
|
||||
// Animation utilities
|
||||
export const animateMarker = (marker: L.Marker, newPosition: L.LatLng, duration = 1000) => {
|
||||
const startPosition = marker.getLatLng();
|
||||
const startTime = Date.now();
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const currentLat = startPosition.lat + (newPosition.lat - startPosition.lat) * progress;
|
||||
const currentLng = startPosition.lng + (newPosition.lng - startPosition.lng) * progress;
|
||||
|
||||
marker.setLatLng([currentLat, currentLng]);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
};
|
||||
|
||||
// Bounds padding for better map view
|
||||
export const boundsOptions = {
|
||||
padding: [20, 20] as [number, number],
|
||||
maxZoom: 16,
|
||||
};
|
||||
@@ -4,7 +4,7 @@ This directory contains all the deployment and development scripts for Smart Par
|
||||
|
||||
## 📋 Available Scripts
|
||||
|
||||
### 🚀 Main Scripts
|
||||
### 🚀 Development Scripts
|
||||
|
||||
| Script | Purpose | Usage |
|
||||
|--------|---------|--------|
|
||||
@@ -14,6 +14,22 @@ This directory contains all the deployment and development scripts for Smart Par
|
||||
| **docker-dev.sh** | 🐳 Docker development with all services | `./scripts/docker-dev.sh` |
|
||||
| **setup.sh** | 🛠️ Initial project setup | `./scripts/setup.sh` |
|
||||
|
||||
### 🚀 Production Deployment Scripts
|
||||
|
||||
| Script | Purpose | Usage |
|
||||
|--------|---------|--------|
|
||||
| **deploy-production.sh** | 🌐 Full production deployment to VPS | `./scripts/deploy-production.sh` |
|
||||
| **deploy-update.sh** | 🔄 Quick updates (frontend/backend/static) | `./scripts/deploy-update.sh` |
|
||||
| **deploy-docker.sh** | 🐳 Docker-based production deployment | `./scripts/deploy-docker.sh` |
|
||||
| **setup-vps.sh** | 🛠️ Initial VPS server setup | Run on VPS as root |
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| **DEPLOYMENT_GUIDE.md** | 📘 Complete deployment guide |
|
||||
| **.env.example** | 🔧 Environment configuration template |
|
||||
|
||||
### 🎯 Quick Access from Root
|
||||
|
||||
From the project root directory, you can use:
|
||||
|
||||
0
scripts/docker-dev.sh
Normal file → Executable file
0
scripts/frontend-only.sh
Normal file → Executable file
2
scripts/full-dev.sh
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
``#!/bin/bash
|
||||
|
||||
# 🔄 Full Development Environment (Frontend + Backend)
|
||||
echo "🔄 Starting Full Development Environment..."
|
||||
|
||||