diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index 7522b09..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -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 -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 -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 /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. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index 521881c..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -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 -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 = ({ - parkingLots, - onSelect, - loading = false -}) => { - const [selectedId, setSelectedId] = useState(null); - - const handleSelect = useCallback((lot: ParkingLot) => { - setSelectedId(lot.id); - onSelect(lot); - }, [onSelect]); - - if (loading) { - return ; - } - - return ( -
- {parkingLots.map((lot) => ( - handleSelect(lot)} - /> - ))} -
- ); -}; -``` - -#### Backend Services - -```typescript -// βœ… Good: Service with proper error handling and typing -@Injectable() -export class ParkingService { - constructor( - @InjectRepository(ParkingLot) - private readonly parkingRepository: Repository, - private readonly cacheService: CacheService, - private readonly logger: Logger - ) {} - - async findNearbyParking( - dto: FindNearbyParkingDto - ): Promise { - try { - const cacheKey = `nearby:${dto.latitude}:${dto.longitude}:${dto.radius}`; - - // Check cache first - const cached = await this.cacheService.get(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( - - ); - - expect(screen.getByText('Central Mall Parking')).toBeInTheDocument(); - expect(screen.getByText('$5/hour')).toBeInTheDocument(); - }); - - it('calls onSelect when parking lot is clicked', () => { - render( - - ); - - fireEvent.click(screen.getByText('Central Mall Parking')); - expect(mockOnSelect).toHaveBeenCalledWith(mockParkingLots[0]); - }); - - it('shows loading spinner when loading', () => { - render( - - ); - - 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); - 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 ( -
-

Parking Debug Info

-
{JSON.stringify(parkingLots, null, 2)}
-
- ); -}; -``` - -### 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(({ - 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 }) => ( - - {ParkingRow} - -); -``` - -### Backend Performance - -```typescript -// Database query optimization -@Injectable() -export class OptimizedParkingService { - // Use spatial indexes - async findNearbyOptimized(dto: FindNearbyParkingDto): Promise { - 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 { - 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). diff --git a/GIT_UPLOAD_GUIDE.md b/GIT_UPLOAD_GUIDE.md deleted file mode 100644 index 30f7c12..0000000 --- a/GIT_UPLOAD_GUIDE.md +++ /dev/null @@ -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.* diff --git a/HUONG_DAN_LOCAL.md b/HUONG_DAN_LOCAL.md deleted file mode 100644 index e69de29..0000000 diff --git a/LOCAL_DEPLOYMENT_GUIDE.md b/LOCAL_DEPLOYMENT_GUIDE.md deleted file mode 100644 index e69de29..0000000 diff --git a/MAPVIEW_VERSIONS.md b/MAPVIEW_VERSIONS.md deleted file mode 100644 index 7bac59c..0000000 --- a/MAPVIEW_VERSIONS.md +++ /dev/null @@ -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 diff --git a/OPTIMIZATION_REPORT.md b/OPTIMIZATION_REPORT.md deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/REORGANIZATION_GUIDE.md b/REORGANIZATION_GUIDE.md deleted file mode 100644 index 72e2133..0000000 --- a/REORGANIZATION_GUIDE.md +++ /dev/null @@ -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.* diff --git a/TECHNICAL_SPECIFICATION.md b/TECHNICAL_SPECIFICATION.md deleted file mode 100644 index 8e514bf..0000000 --- a/TECHNICAL_SPECIFICATION.md +++ /dev/null @@ -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. diff --git a/VPS-DEPLOYMENT-GUIDE.md b/VPS-DEPLOYMENT-GUIDE.md new file mode 100644 index 0000000..6da943b --- /dev/null +++ b/VPS-DEPLOYMENT-GUIDE.md @@ -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!** diff --git a/assets/AiBootcamp_event.png b/assets/AiBootcamp_event.png new file mode 100644 index 0000000..581f45a Binary files /dev/null and b/assets/AiBootcamp_event.png differ diff --git a/assets/Background_introduction_Section.jpg b/assets/Background_introduction_Section.jpg new file mode 100644 index 0000000..6d81dfe Binary files /dev/null and b/assets/Background_introduction_Section.jpg differ diff --git a/assets/Footer_page_logo.png b/assets/Footer_page_logo.png new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/assets/Footer_page_logo.png differ diff --git a/assets/Phone_intro.png b/assets/Phone_intro.png new file mode 100644 index 0000000..dfafeb1 Binary files /dev/null and b/assets/Phone_intro.png differ diff --git a/assets/download_store.png b/assets/download_store.png new file mode 100644 index 0000000..3a652ef Binary files /dev/null and b/assets/download_store.png differ diff --git a/assets/intro_section_phone.png b/assets/intro_section_phone.png new file mode 100644 index 0000000..6ea9d37 Binary files /dev/null and b/assets/intro_section_phone.png differ diff --git a/assets/logo_partners/Ford_Philanthropy_Logo.jpeg b/assets/logo_partners/Ford_Philanthropy_Logo.jpeg new file mode 100644 index 0000000..d4688dd Binary files /dev/null and b/assets/logo_partners/Ford_Philanthropy_Logo.jpeg differ diff --git a/assets/logo_partners/GIST_logo_445_1.jpeg b/assets/logo_partners/GIST_logo_445_1.jpeg new file mode 100644 index 0000000..dd4dd4a Binary files /dev/null and b/assets/logo_partners/GIST_logo_445_1.jpeg differ diff --git a/assets/logo_partners/watson.png b/assets/logo_partners/watson.png new file mode 100644 index 0000000..4de30ea Binary files /dev/null and b/assets/logo_partners/watson.png differ diff --git a/assets/team_ava/Phong pham.png b/assets/team_ava/Phong pham.png new file mode 100644 index 0000000..b0b80b7 Binary files /dev/null and b/assets/team_ava/Phong pham.png differ diff --git a/assets/team_ava/Phung do.png b/assets/team_ava/Phung do.png new file mode 100644 index 0000000..c77293d Binary files /dev/null and b/assets/team_ava/Phung do.png differ diff --git a/assets/team_ava/mai nguyen.png b/assets/team_ava/mai nguyen.png new file mode 100644 index 0000000..4509f85 Binary files /dev/null and b/assets/team_ava/mai nguyen.png differ diff --git a/assets/team_ava/quang tue.png b/assets/team_ava/quang tue.png new file mode 100644 index 0000000..dead287 Binary files /dev/null and b/assets/team_ava/quang tue.png differ diff --git a/assets/team_photo.jpg b/assets/team_photo.jpg new file mode 100644 index 0000000..70f9f15 Binary files /dev/null and b/assets/team_photo.jpg differ diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index bbb84dd..8fd0b93 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -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'; diff --git a/backend/src/modules/routing/dto/route-request.dto.ts b/backend/src/modules/routing/dto/route-request.dto.ts deleted file mode 100644 index c72a0cd..0000000 --- a/backend/src/modules/routing/dto/route-request.dto.ts +++ /dev/null @@ -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; -} diff --git a/backend/src/modules/routing/routing.controller.ts b/backend/src/modules/routing/routing.controller.ts deleted file mode 100644 index e97704e..0000000 --- a/backend/src/modules/routing/routing.controller.ts +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/backend/src/modules/routing/routing.module.ts b/backend/src/modules/routing/routing.module.ts deleted file mode 100644 index 3ef063d..0000000 --- a/backend/src/modules/routing/routing.module.ts +++ /dev/null @@ -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 {} diff --git a/backend/src/modules/routing/routing.service.ts b/backend/src/modules/routing/routing.service.ts deleted file mode 100644 index a8c0650..0000000 --- a/backend/src/modules/routing/routing.service.ts +++ /dev/null @@ -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('VALHALLA_URL') || 'http://valhalla:8002'; - this.valhallaClient = axios.create({ - baseURL: this.valhallaUrl, - timeout: 30000, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - async calculateRoute(dto: RouteRequestDto): Promise { - 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', - }; - } - } -} diff --git a/deploy-vercel.sh b/deploy-vercel.sh deleted file mode 100755 index af45316..0000000 --- a/deploy-vercel.sh +++ /dev/null @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 68c70c3..0000000 --- a/docker-compose.yml +++ /dev/null @@ -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 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0004569..bac227f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/public/android-chrome-192x192.png b/frontend/public/android-chrome-192x192.png new file mode 100644 index 0000000..07a5166 Binary files /dev/null and b/frontend/public/android-chrome-192x192.png differ diff --git a/frontend/public/android-chrome-512x512.png b/frontend/public/android-chrome-512x512.png new file mode 100644 index 0000000..e92a6bd Binary files /dev/null and b/frontend/public/android-chrome-512x512.png differ diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/assets/AiBootcamp_event.png b/frontend/public/assets/AiBootcamp_event.png new file mode 100644 index 0000000..581f45a Binary files /dev/null and b/frontend/public/assets/AiBootcamp_event.png differ diff --git a/frontend/public/assets/Background_introduction_Section.jpg b/frontend/public/assets/Background_introduction_Section.jpg new file mode 100644 index 0000000..6d81dfe Binary files /dev/null and b/frontend/public/assets/Background_introduction_Section.jpg differ diff --git a/frontend/public/assets/Footer_page_logo.png b/frontend/public/assets/Footer_page_logo.png new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/frontend/public/assets/Footer_page_logo.png differ diff --git a/frontend/public/assets/Founding team_Laca City.jpg b/frontend/public/assets/Founding team_Laca City.jpg new file mode 100644 index 0000000..70f9f15 Binary files /dev/null and b/frontend/public/assets/Founding team_Laca City.jpg differ diff --git a/frontend/public/assets/Phone_intro.png b/frontend/public/assets/Phone_intro.png new file mode 100644 index 0000000..dfafeb1 Binary files /dev/null and b/frontend/public/assets/Phone_intro.png differ diff --git a/frontend/public/assets/download_store.png b/frontend/public/assets/download_store.png new file mode 100644 index 0000000..3a652ef Binary files /dev/null and b/frontend/public/assets/download_store.png differ diff --git a/frontend/public/assets/intro_section_phone.png b/frontend/public/assets/intro_section_phone.png new file mode 100644 index 0000000..6ea9d37 Binary files /dev/null and b/frontend/public/assets/intro_section_phone.png differ diff --git a/frontend/public/assets/logo_partners/Ford_Philanthropy_Logo.jpeg b/frontend/public/assets/logo_partners/Ford_Philanthropy_Logo.jpeg new file mode 100644 index 0000000..d4688dd Binary files /dev/null and b/frontend/public/assets/logo_partners/Ford_Philanthropy_Logo.jpeg differ diff --git a/frontend/public/assets/logo_partners/GIST_logo_445_1.jpeg b/frontend/public/assets/logo_partners/GIST_logo_445_1.jpeg new file mode 100644 index 0000000..dd4dd4a Binary files /dev/null and b/frontend/public/assets/logo_partners/GIST_logo_445_1.jpeg differ diff --git a/frontend/public/assets/logo_partners/watson.png b/frontend/public/assets/logo_partners/watson.png new file mode 100644 index 0000000..4de30ea Binary files /dev/null and b/frontend/public/assets/logo_partners/watson.png differ diff --git a/frontend/public/assets/mai_nguyen.png b/frontend/public/assets/mai_nguyen.png new file mode 100644 index 0000000..4509f85 Binary files /dev/null and b/frontend/public/assets/mai_nguyen.png differ diff --git a/frontend/public/assets/new_team_photo.jpg b/frontend/public/assets/new_team_photo.jpg new file mode 100644 index 0000000..70f9f15 Binary files /dev/null and b/frontend/public/assets/new_team_photo.jpg differ diff --git a/frontend/public/assets/phong_pham.png b/frontend/public/assets/phong_pham.png new file mode 100644 index 0000000..b0b80b7 Binary files /dev/null and b/frontend/public/assets/phong_pham.png differ diff --git a/frontend/public/assets/phung_do.png b/frontend/public/assets/phung_do.png new file mode 100644 index 0000000..c77293d Binary files /dev/null and b/frontend/public/assets/phung_do.png differ diff --git a/frontend/public/assets/quang_tue.png b/frontend/public/assets/quang_tue.png new file mode 100644 index 0000000..dead287 Binary files /dev/null and b/frontend/public/assets/quang_tue.png differ diff --git a/frontend/public/assets/team_ava/Phong pham.png b/frontend/public/assets/team_ava/Phong pham.png new file mode 100644 index 0000000..b0b80b7 Binary files /dev/null and b/frontend/public/assets/team_ava/Phong pham.png differ diff --git a/frontend/public/assets/team_ava/Phung do.png b/frontend/public/assets/team_ava/Phung do.png new file mode 100644 index 0000000..c77293d Binary files /dev/null and b/frontend/public/assets/team_ava/Phung do.png differ diff --git a/frontend/public/assets/team_ava/mai nguyen.png b/frontend/public/assets/team_ava/mai nguyen.png new file mode 100644 index 0000000..4509f85 Binary files /dev/null and b/frontend/public/assets/team_ava/mai nguyen.png differ diff --git a/frontend/public/assets/team_ava/quang tue.png b/frontend/public/assets/team_ava/quang tue.png new file mode 100644 index 0000000..dead287 Binary files /dev/null and b/frontend/public/assets/team_ava/quang tue.png differ diff --git a/frontend/public/assets/team_photo.jpg b/frontend/public/assets/team_photo.jpg new file mode 100644 index 0000000..1004a9f Binary files /dev/null and b/frontend/public/assets/team_photo.jpg differ diff --git a/frontend/public/assets/team_photo_new.jpg b/frontend/public/assets/team_photo_new.jpg new file mode 100644 index 0000000..363edbc --- /dev/null +++ b/frontend/public/assets/team_photo_new.jpg @@ -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. diff --git a/frontend/public/favicon-16x16.png b/frontend/public/favicon-16x16.png new file mode 100644 index 0000000..42bcdec Binary files /dev/null and b/frontend/public/favicon-16x16.png differ diff --git a/frontend/public/favicon-32x32.png b/frontend/public/favicon-32x32.png new file mode 100644 index 0000000..8cf2c95 Binary files /dev/null and b/frontend/public/favicon-32x32.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..6003876 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/frontend/public/favicon.png differ diff --git a/frontend/public/homepage/_assets.html b/frontend/public/homepage/_assets.html new file mode 100644 index 0000000..1afb413 Binary files /dev/null and b/frontend/public/homepage/_assets.html differ diff --git a/frontend/public/homepage/apple-touch-icon.png b/frontend/public/homepage/apple-touch-icon.png new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/frontend/public/homepage/apple-touch-icon.png differ diff --git a/frontend/public/homepage/assets/team_photo.jpg b/frontend/public/homepage/assets/team_photo.jpg new file mode 100644 index 0000000..1004a9f Binary files /dev/null and b/frontend/public/homepage/assets/team_photo.jpg differ diff --git a/frontend/public/homepage/favicon-16x16.png b/frontend/public/homepage/favicon-16x16.png new file mode 100644 index 0000000..42bcdec Binary files /dev/null and b/frontend/public/homepage/favicon-16x16.png differ diff --git a/frontend/public/homepage/favicon-32x32.png b/frontend/public/homepage/favicon-32x32.png new file mode 100644 index 0000000..8cf2c95 Binary files /dev/null and b/frontend/public/homepage/favicon-32x32.png differ diff --git a/frontend/public/homepage/favicon-test.html b/frontend/public/homepage/favicon-test.html new file mode 100644 index 0000000..6d4ba04 --- /dev/null +++ b/frontend/public/homepage/favicon-test.html @@ -0,0 +1,99 @@ + + + + + + Favicon Test - Laca City + + + + + + + + + + + + + +
+

🎯 Favicon Test Page - Laca City

+ +
+ πŸ“‹ Instructions: +
    +
  1. Look at your browser tab - you should see the red LC logo
  2. +
  3. Try refreshing the page (Ctrl+F5 or Cmd+Shift+R)
  4. +
  5. Clear browser cache if needed
  6. +
  7. Bookmark this page to test bookmark favicon
  8. +
+
+ +
+

πŸ” Favicon Display Test

+
+ 16x16: + 16x16 favicon +
+
+ 32x32: + 32x32 favicon +
+
+ 64x64: + 64x64 favicon +
+
+ +
+

βœ… If you can see the red LC logo in the browser tab and in the images above, the favicon is working correctly!

+ ← Back to Homepage | + Go to Parking App β†’ +
+
+ + diff --git a/frontend/public/homepage/favicon.ico b/frontend/public/homepage/favicon.ico new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/frontend/public/homepage/favicon.ico differ diff --git a/frontend/public/homepage/favicon.png b/frontend/public/homepage/favicon.png new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/frontend/public/homepage/favicon.png differ diff --git a/frontend/public/homepage/index.html b/frontend/public/homepage/index.html new file mode 100644 index 0000000..6e346c6 --- /dev/null +++ b/frontend/public/homepage/index.html @@ -0,0 +1,1066 @@ + + + + + + Laca City - Smart Parking Solutions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + +
+ +
+ + +
+
+
+
+
+ +
+
+
+ +
+
+ + + Live Parking Availability + +
+

+ Park Easy,
+ + Move Breezy! + +

+

+ Download the app to experience LACA's services. +

+
+ Download on App Store and Google Play +
+
+ + +
+ Location and Parking +
+
+
+
+ + +
+ + + +
+
+ + +
+
+
+

+ What Makes Laca City Different ? +

+

+ Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility. +

+
+ +
+ +
+
+ + + +
+

Trend Predictions

+

+ Predicts best parking times by trends. +

+
+ +
+
+ + + +
+

Real-Time Alerts

+

+ Broadcasts fleeting spots in real time. +

+
+ +
+
+ + + +
+

Community Network

+

+ Follow reliable spot-sharers. +

+
+
+
+
+ + +
+
+
+

+ Meet Our Team +

+

+ The passionate innovators behind Laca City's smart parking revolution +

+
+ +
+ +
+ +
+
+ +
+
+ +
+ Laca City Founding Team +
+
+
+ + +
+ +
+
+
+
+ + + +
+
+
+

Our Story

+

+ Laca City connects drivers to real-time parking, easing congestion, cutting emissions, and reclaiming sidewalks. +

+
+
+
+ + +
+
+
+
+ + + +
+
+
+

Founded by Mai Nguyen

+

+ After witnessing Vietnam's urban parking chaos, we're building smart, scalable solutions for today's citiesβ€”and the autonomous mobility of tomorrow. +

+
+
+
+ + +
+
+
+
+ + + + +
+
+
+

Our Vision

+

+ Transforming urban mobility through innovative technology and sustainable solutions for smarter cities. +

+
+
+
+ + +
+
+
+
+ + + +
+

Smart Solutions

+
+

Real-time parking availability and seamless user experience

+
+ +
+
+
+ + + +
+

Sustainable Future

+
+

Reducing emissions and building greener urban environments

+
+
+
+
+ + +
+
+

+ Key Team Members +

+

+ Meet the dedicated professionals driving innovation at Laca City +

+
+ + +
+ +
+
+ +
+ +
+
+
+ + + Mai Nguyen + + +
+ Founder +
+
+

Mai Nguyen

+

Founder

+

+ MIT-trained urban planner with global experience (City of Boston, World Bank), guiding Laca City's strategy for sustainable, impactful urban tech. +

+ + +
+
+ + +
+
+ +
+ +
+
+
+ + + Phung Do + + +
+ Operations +
+
+

Phung Do

+

Co-Founder & CPO/COO

+

+ Mapping expert (Mercedes-Benz, Grab, Be Group) and OpenStreetMap Vietnam admin, building geospatial systems that make parking smarter and streets more efficient. +

+ + +
+
+ + +
+
+ +
+ +
+
+
+ + + Quang Tue + + +
+ Product +
+
+

Tue Le-quang

+

CMO

+

+ Data journalist turned storyteller (VnExpress, Fulbright), connecting Laca City to people through maps, media, and metrics. +

+ + +
+
+ + +
+
+ +
+ +
+
+
+ + + Phong Pham + + +
+ Tech Lead +
+
+

Phong Pham

+

Head of Development

+

+ Physicist and lead coder, designing ML engines and backend systems that turn raw data into seamless urban mobility experiences. +

+ + +
+
+
+
+
+
+
+ + +
+
+ +
+

+ Latest News +

+

+ Stay updated with our events +

+
+ + +
+ +
+ Google LACA City AI Bootcamp Event +
+ + +
+

+ Laca City in Google AI Bootcamp HCMC +

+
+
+
+
+ + +
+
+
+ +
+ +
+ Ford Philanthropy +
+ + +
+ GIST +
+ + +
+ Watson +
+
+
+
+
+ + + +
+ + + + + + + + + + + + + + + diff --git a/frontend/public/homepage/index_backup.html b/frontend/public/homepage/index_backup.html new file mode 100644 index 0000000..abd1a18 --- /dev/null +++ b/frontend/public/homepage/index_backup.html @@ -0,0 +1,71 @@ + + + + + + Laca City - Smart Parking Solutions + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/frontend/public/homepage/index_new.html b/frontend/public/homepage/index_new.html new file mode 100644 index 0000000..c956cc1 --- /dev/null +++ b/frontend/public/homepage/index_new.html @@ -0,0 +1,746 @@ + + + + + + Laca City - Smart Parking Solutions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + Live Parking Availability + +
+

+ Smart Parking
+ + Made Simple + +

+

+ Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation. +

+ +
+ + +
+
+ Location and Parking +
+
+
+ + +
+ + + +
+
+ + +
+
+
+
+
500+
+
Parking Locations
+
+
+
10K+
+
Happy Users
+
+
+
24/7
+
Available Support
+
+
+
+
+ + +
+
+
+

+ Why Choose Laca City? +

+

+ Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility. +

+
+ +
+ +
+
+ + + +
+

Real-Time Availability

+

+ Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data. +

+
+ +
+
+ + + +
+

Smart Pricing

+

+ Transparent, competitive pricing with advance booking discounts and flexible payment options. +

+
+ +
+
+ + + +
+

GPS Navigation

+

+ Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions. +

+
+ +
+
+ + + +
+

Secure Booking

+

+ Safe and secure reservation system with instant confirmation and QR code access. +

+
+ +
+
+ + + +
+

Quick Access

+

+ Fast booking process takes less than 2 minutes from search to confirmation. +

+
+ +
+
+ + + +
+

24/7 Support

+

+ Round-the-clock customer support to help you with any parking-related queries or issues. +

+
+
+
+
+ + +
+
+
+

+ How It Works +

+

+ Simple, fast, and efficient parking solution in just three easy steps. +

+
+ +
+
+
+ 1 +
+ + + +
+
+

Search Location

+

+ Enter your destination or use GPS to find nearby parking spots with real-time availability. +

+
+ +
+
+ 2 +
+ + + +
+
+

Select & Book

+

+ Choose your preferred parking spot, select duration, and confirm your booking instantly. +

+
+ +
+
+ 3 +
+ + + +
+
+

Navigate & Park

+

+ Follow GPS navigation to your reserved spot and use QR code for easy access. +

+
+
+ + + +
+
+ + +
+
+
+

+ Ready to Transform Your
+ + Parking Experience? + +

+

+ Join thousands of satisfied users who have made parking stress-free with Laca City. +

+ +
+
+
+ + +
+
+
+
+
+ Laca City Logo +
+

Laca City

+

Smart Parking Solutions

+
+
+

+ Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City. +

+
+ +
+

Quick Links

+ +
+ +
+

Contact Info

+
+
+ + + + Ho Chi Minh City, Vietnam +
+
+ + + + info@lacacity.com +
+
+ + + + +84 123 456 789 +
+
+
+
+ +
+

+ Β© 2025 Laca City. All rights reserved. +

+ +
+
+
+
+ + + + + + + + + + + + + + + diff --git a/frontend/public/homepage/laca-favicon.png b/frontend/public/homepage/laca-favicon.png new file mode 100644 index 0000000..22422dc Binary files /dev/null and b/frontend/public/homepage/laca-favicon.png differ diff --git a/frontend/public/homepage/redesigned.html b/frontend/public/homepage/redesigned.html new file mode 100644 index 0000000..81fdc83 --- /dev/null +++ b/frontend/public/homepage/redesigned.html @@ -0,0 +1,776 @@ + + + + + + Laca City - Smart Parking Solutions + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + Live Parking Availability + +
+

+ Smart Parking
+ + Made Simple + +

+

+ Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation. +

+ +
+ + +
+
+ Location and Parking +
+
+
+ + +
+ + + +
+
+ + +
+
+
+
+
500+
+
Parking Locations
+
+
+
10K+
+
Happy Users
+
+
+
24/7
+
Available Support
+
+
+
+
+ + +
+
+
+

+ Why Choose Laca City? +

+

+ Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility. +

+
+ +
+ +
+
+ + + +
+

Real-Time Availability

+

+ Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data. +

+
+ + +
+
+ + + +
+

Smart Pricing

+

+ Transparent, competitive pricing with advance booking discounts and flexible payment options. +

+
+ + +
+
+ + + +
+

GPS Navigation

+

+ Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions. +

+
+ + +
+
+ + + +
+

Secure Booking

+

+ Safe and secure reservation system with instant confirmation and QR code access. +

+
+ + +
+
+ + + +
+

Quick Access

+

+ Fast booking process takes less than 2 minutes from search to confirmation. +

+
+ + +
+
+ + + +
+

24/7 Support

+

+ Round-the-clock customer support to help you with any parking-related queries or issues. +

+
+
+
+
+ + +
+
+
+

+ How It Works +

+

+ Simple, fast, and efficient parking solution in just three easy steps. +

+
+ +
+ +
+
+ 1 +
+ + + +
+
+

Search Location

+

+ Enter your destination or use GPS to find nearby parking spots with real-time availability. +

+
+ + +
+
+ 2 +
+ + + +
+
+

Select & Book

+

+ Choose your preferred parking spot, select duration, and confirm your booking instantly. +

+
+ + +
+
+ 3 +
+ + + +
+
+

Navigate & Park

+

+ Follow GPS navigation to your reserved spot and use QR code for easy access. +

+
+
+ + + +
+
+ + +
+
+
+

+ Ready to Transform Your
+ + Parking Experience? + +

+

+ Join thousands of satisfied users who have made parking stress-free with Laca City. +

+ +
+
+
+ + +
+
+
+ +
+
+ Laca City Logo +
+

Laca City

+

Smart Parking Solutions

+
+
+

+ Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City. +

+ +
+ + +
+

Quick Links

+ +
+ + +
+

Contact Info

+
+
+ + + + Ho Chi Minh City, Vietnam +
+
+ + + + info@lacacity.com +
+
+ + + + +84 123 456 789 +
+
+
+
+ + +
+

+ Β© 2025 Laca City. All rights reserved. +

+ +
+
+
+ + + + + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..8330545 --- /dev/null +++ b/frontend/public/manifest.json @@ -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 +} diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..a25388c --- /dev/null +++ b/frontend/public/robots.txt @@ -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/ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 615bc46..2d73a19 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -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% { diff --git a/frontend/src/app/homepage.tsx b/frontend/src/app/homepage.tsx new file mode 100644 index 0000000..bfc53e1 --- /dev/null +++ b/frontend/src/app/homepage.tsx @@ -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 ( +
+ {/* Navigation */} + + + {/* Hero Section */} +
+
+
+
+

+ Park Easy, Move Breezy +

+

+ Find and share parking spots in seconds. Save time, fuel, and reduce stress in Ho Chi Minh City & Hanoi. +

+
+ + +
+

+ Join thousands of drivers reimagining streets for people, making urban driving easy and sustainable. +

+
+
+
+ Laca City App Interface +
+ Your city's parking assistant +
+
+
+
+
+
+ + {/* Problem & Story Section */} +
+
+
+

+ Tired of Circling for Parking? +

+
+
+
+

+ Urban drivers in Vietnam spend 15–30 minutes per trip searching for parkingβ€”burning fuel, wasting time, and adding to congestion. +

+

+ 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. +

+

+ Laca City was born to end this struggleβ€”making parking easy while reclaiming streets for people. +

+
+
+
+
+
+ ! +
+

Before Laca City

+

15-30 minutes circling, wasting fuel, stress

+
+
+
+ βœ“ +
+

With Laca City

+

Instant parking, happy drivers

+
+
+
+
+
+
+ + {/* How It Works Section */} +
+
+
+

+ Find. Share. Drive Happy. +

+
+
+ {/* Step 1 */} +
+
+ 1 +
+

Find Parking Instantly

+

Open Laca City to see real-time parking spots near you.

+
+ + {/* Step 2 */} +
+
+ 2 +
+

Share a Spot

+

Add public, private, or peer-to-peer parking to help other drivers.

+
+ + {/* Step 3 */} +
+
+ 3 +
+

Get Recognition

+

Earn badges and leaderboard positions for contributing to a smarter city.

+
+
+
+ +
+
+
+ + {/* Community & Gamification Section */} +
+
+
+

+ Built by Drivers, for Drivers +

+
+
+
+

+ Your shared spots power the map. Laca City is entirely community-driven, rewarding contributors with: +

+
+
+
+ HERO +
+
+

Parking Hero badges

+

Get recognized for your contributions

+
+
+
+
+ RANK +
+
+

Weekly leaderboards

+

Compete with other contributors

+
+
+
+
+ FAME +
+
+

Social media shoutouts

+

Get featured for your community impact

+
+
+
+

+ Help your city run better while getting recognized in the community. +

+ +
+
+

Weekly Leaderboard

+
+
+
+
+ 1 +
+
+

Minh Nguyen

+

District 1

+
+
+ 28 spots +
+
+
+
+ 2 +
+
+

Linh Tran

+

District 3

+
+
+ 22 spots +
+
+
+
+ 3 +
+
+

Duc Le

+

District 7

+
+
+ 19 spots +
+
+
+
+
+
+ + {/* Join the Movement Section */} +
+
+

+ Help Build Vietnam's First Real-Time Parking Map +

+

+ Sign up free. Find parking in seconds. Share your spots. Together, we'll reclaim streets for people and make our cities smarter. +

+
+ + +
+
+
+ + {/* About Us / Mission Section */} +
+
+
+

+ Smart Parking for Smart Cities +

+

+ Laca City connects drivers with real-time parking spots, reducing congestion and reclaiming sidewalks for pedestrians. +

+
+
+
+

+ As Vietnam's cities prepare for autonomous vehicles and low-emission transport, we're building the digital parking infrastructure they need. +

+

+ "Streets for people" - Đường phα»‘ cho con người. +

+
+
+
+
+ MN +
+
+

Mai Nguyen

+

Founder & CEO

+

+ Urban planner with global experience (World Bank, Asia & North America). + Passionate about creating cities where streets belong to people. +

+
+
+
+
+ + {/* Social Proof & Partnerships Section */} +
+
+
+

+ Trusted by Community and Partners +

+

+ We're working with universities, small businesses, and city pilot programs to make urban parking easy. +

+
+
+
+
+ UN +
+

Vietnam National University

+

Student Parking Pilot

+
+
+
+ CF +
+

District 1 Cafe Network

+

Private Spot Sharing

+
+
+
+ HN +
+

Hanoi Transportation Dept

+

Public Lot Integration

+
+
+
+
+ + {/* Footer */} +
+
+
+
+
+ Laca City Logo + Laca City +
+

+ Park Easy, Move Breezy. +

+

+ Making urban parking easy while reclaiming streets for people. +

+
+
+

Quick Links

+
    +
  • +
  • Blog
  • +
  • +
  • Terms
  • +
  • Privacy
  • +
+
+ +
+
+

+ Β© 2025 Laca City. All rights reserved. Made with love for Vietnamese drivers. +

+
+
+
+ + {/* Scroll to Top Button */} + {showScrollToTop && ( + + )} +
+ ); +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index f8ec337..3775dde 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -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', }, }; diff --git a/frontend/src/app/page-hcmc.tsx b/frontend/src/app/page-hcmc.tsx index 40b6341..7892827 100644 --- a/frontend/src/app/page-hcmc.tsx +++ b/frontend/src/app/page-hcmc.tsx @@ -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: () => ( -
- -
- ), - } -); - export default function ParkingFinderPage() { // State management const [selectedParkingLot, setSelectedParkingLot] = useState(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 (
{/* Left Column - Map and Parking List */}
- {/* Map Section */} + {/* Summary Section */}
-
- +
+
+
+ + + + +
+

Parking Finder - HCMC

+

Find and book parking spots in Ho Chi Minh City

+ {parkingLots.length > 0 && ( +
+ Found {parkingLots.length} parking locations nearby +
+ )} +
@@ -199,14 +158,6 @@ export default function ParkingFinderPage() {
)} - - {routeError && ( -
-
- {routeError} -
-
- )}
); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f3a0e8f..cf5cea3 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -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 ; + } + + // Show Canva homepage by default + return ; +} + +function CanvaHomepage() { + useEffect(() => { + // Redirect to the Canva homepage in the public directory + window.location.href = '/homepage/index.html'; + }, []); + + return ( +
+
+
+

Loading homepage...

+
+
+ ); +} + +function ParkingFinderPage() { // State management const [selectedParkingLot, setSelectedParkingLot] = useState(null); const [userLocation, setUserLocation] = useState(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 (
@@ -206,9 +196,9 @@ export default function ParkingFinderPage() {

- BΓ£i Δ‘α»— xe gαΊ§n Δ‘Γ’y + Nearby Parking Lots

-

Tìm kiếm thông minh

+

Smart Search

@@ -229,7 +219,7 @@ export default function ParkingFinderPage() { - LΓ m mα»›i danh sΓ‘ch + Refresh List {/* Status Info Bar - ThiαΊΏt kαΊΏ thanh lα»‹ch Δ‘Ζ‘n giαΊ£n */} @@ -241,14 +231,14 @@ export default function ParkingFinderPage() {
- {parkingLots.filter(lot => lot.availableSlots > 0).length} cΓ³ chα»— + {parkingLots.filter(lot => lot.availableSlots > 0).length} available
- {parkingLots.filter(lot => lot.availableSlots === 0).length} Δ‘αΊ§y + {parkingLots.filter(lot => lot.availableSlots === 0).length} full
@@ -275,7 +265,7 @@ export default function ParkingFinderPage() {
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() {
- SαΊ―p xαΊΏp: + Sort:
@@ -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" >
-

Chọn vα»‹ trΓ­ GPS

-

Vui lΓ²ng chọn vα»‹ trΓ­ GPS để tΓ¬m bΓ£i Δ‘α»— xe gαΊ§n Δ‘Γ³

+

Select GPS Location

+

Please select a GPS location to find nearby parking lots

) : parkingLots.length === 0 ? (
@@ -429,8 +419,8 @@ export default function ParkingFinderPage() {
-

KhΓ΄ng cΓ³ bΓ£i Δ‘α»— xe

-

KhΓ΄ng tΓ¬m thαΊ₯y bΓ£i Δ‘α»— xe nΓ o gαΊ§n vα»‹ trΓ­ nΓ y

+

No Parking Lots

+

No parking lots found near this location

) : ( { 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 }} /> )} - {/* Map Section - Right */} + {/* Right Section - Summary Information */}
- - - {/* Map overlay info - Position based on layout */} - {userLocation && ( -
-
-
- Logo -
-
-

Parking Finder

-

BαΊ£n Δ‘α»“ thΓ΄ng minh

-
+
+
+
+ + + +
-
- {/* Current location */} -
-
- Vα»‹ trΓ­ hiện tαΊ‘i +

Map in Developing

+

Interactive map feature coming soon

+ {parkingLots.length > 0 && ( +
+ Found {parkingLots.length} parking locations nearby
- - {/* Parking lot status legend */} -
-
TrαΊ‘ng thΓ‘i bΓ£i xe:
- - {/* Available parking - Green */} -
-
- CΓ²n chα»— thoΓ‘ng (>70%) -
- - {/* Nearly full - Yellow */} -
-
- SαΊ―p Δ‘αΊ§y (<30%) -
- - {/* Full - Red */} -
-
- HαΊΏt chα»— -
- - {/* Closed - Gray */} -
-
- Đã Δ‘Γ³ng cα»­a -
-
- - {/* Route line */} - {route && ( -
-
- TuyαΊΏn đường -
- )} -
+ )}
- )} +
{/* Floating GPS Window */} @@ -595,14 +530,14 @@ export default function ParkingFinderPage() {

- {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'}

{isMobile && (
)} - - {routeError && ( -
-
- {routeError} -
-
- )}
); } diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..8e0fec7 --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -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 = ({ + showFullFooter = false, + className = "" +}) => { + if (!showFullFooter) { + return ( +
+
+
+
+ Laca City Logo + Laca City +
+
+
+
+ ); + } + + return ( + + ); +}; diff --git a/frontend/src/components/HCMCGPSSimulator.tsx b/frontend/src/components/HCMCGPSSimulator.tsx index 914c498..cfd15e3 100644 --- a/frontend/src/components/HCMCGPSSimulator.tsx +++ b/frontend/src/components/HCMCGPSSimulator.tsx @@ -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 = ({
- Vα»‹ trΓ­ hiện tαΊ‘i + Current Location
LIVE @@ -483,8 +483,8 @@ export const HCMCGPSSimulator: React.FC = ({
-
Vα»‹ trΓ­ ngαΊ«u nhiΓͺn
-

TαΊ‘o tọa Δ‘α»™ tα»± Δ‘α»™ng trong TP.HCM

+
Random Location
+

Auto-generate coordinates in HCMC

@@ -493,7 +493,7 @@ export const HCMCGPSSimulator: React.FC = ({ RANDOM
- Khu vα»±c mở rα»™ng + Extended area
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index affb93a..ef8e545 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -26,18 +26,12 @@ export const Header: React.FC = ({
Smart Parking Logo - {/* Animated accent line */} -
)} @@ -66,7 +60,7 @@ export const Header: React.FC = ({ - XΓ³a tuyαΊΏn đường + Clear Route )} @@ -76,7 +70,7 @@ export const Header: React.FC = ({ borderColor: 'rgba(34, 197, 94, 0.3)' }}>
- Dα»― liệu trα»±c tuyαΊΏn + Live Data
{/* City Info */} @@ -90,7 +84,7 @@ export const Header: React.FC = ({
- TP. Hα»“ ChΓ­ Minh + Ho Chi Minh City
diff --git a/frontend/src/components/map/MapView.tsx b/frontend/src/components/map/MapView.tsx deleted file mode 100644 index c4cc31d..0000000 --- a/frontend/src/components/map/MapView.tsx +++ /dev/null @@ -1,1432 +0,0 @@ -'use client'; - -import React, { useEffect, useRef, useState } from 'react'; -import dynamic from 'next/dynamic'; -import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; -import { ParkingLot, UserLocation } from '@/types'; - -// Import Leaflet type for proper typing -let L: any; -if (typeof window !== 'undefined') { - L = require('leaflet'); -} - -interface MapViewProps { - userLocation?: UserLocation | null; - parkingLots?: ParkingLot[]; - selectedParkingLot?: ParkingLot | null; - route?: any; - onParkingLotSelect?: (lot: ParkingLot) => void; - isLoading?: boolean; - className?: string; -} - -// Dynamically import Leaflet components to avoid SSR issues -const MapContainer = dynamic( - () => import('react-leaflet').then((mod) => mod.MapContainer), - { - ssr: false, - loading: () => ( -
- -
- ) - } -); - -const TileLayer = dynamic( - () => import('react-leaflet').then((mod) => mod.TileLayer), - { ssr: false } -); - -const Marker = dynamic( - () => import('react-leaflet').then((mod) => mod.Marker), - { ssr: false } -); - -const Popup = dynamic( - () => import('react-leaflet').then((mod) => mod.Popup), - { ssr: false } -); - -const Polyline = dynamic( - () => import('react-leaflet').then((mod) => mod.Polyline), - { ssr: false } -); - -export const MapView: React.FC = ({ - userLocation, - parkingLots = [], - selectedParkingLot, - route, - onParkingLotSelect, - isLoading = false, - className = '' -}) => { - const mapRef = useRef(null); - const [routeCoordinates, setRouteCoordinates] = useState<[number, number][]>([]); - const [routeInfo, setRouteInfo] = useState<{distance: number, duration: number} | null>(null); - const [isCalculatingRoute, setIsCalculatingRoute] = useState(false); - const [debugInfo, setDebugInfo] = useState(''); - const [currentZoom, setCurrentZoom] = useState(13); - - // OpenRouteService API key - const ORS_API_KEY = "eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6ImJmMjM5NTNiMjNlNzQzZWY4NWViMDFlYjNkNTRkNmVkIiwiaCI6Im11cm11cjY0In0="; - - // Function to calculate route using OpenRouteService - const calculateRouteToParking = async (startLat: number, startLng: number, endLat: number, endLng: number) => { - console.log('Starting route calculation from:', { startLat, startLng }, 'to:', { endLat, endLng }); - - setIsCalculatingRoute(true); - - try { - // Try different API endpoint formats - let response = await fetch('https://api.openrouteservice.org/v2/directions/driving-car/geojson', { - method: 'POST', - headers: { - 'Authorization': ORS_API_KEY, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - coordinates: [[startLng, startLat], [endLng, endLat]], - instructions: true, - language: 'vi', - }), - }); - - // If first attempt fails, try alternative endpoint - if (!response.ok) { - console.log('First endpoint failed, trying alternative...'); - response = await fetch('https://api.openrouteservice.org/v2/directions/driving-car', { - method: 'POST', - headers: { - 'Authorization': ORS_API_KEY, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - coordinates: [[startLng, startLat], [endLng, endLat]], - format: 'json', - instructions: true, - language: 'vi', - }), - }); - } - - console.log('API Response status:', response.status); - - if (!response.ok) { - const errorText = await response.text(); - console.error('API Error response:', errorText); - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - console.log('API Response data:', data); - console.log('API Response structure:', JSON.stringify(data, null, 2)); - - // Check for both GeoJSON format and direct route format - let route = null; - let coordinates = null; - let routeDistance = 0; - let routeDuration = 0; - - if (data.features && data.features.length > 0) { - // GeoJSON format response - console.log('Processing GeoJSON format response'); - route = data.features[0]; - coordinates = route.geometry.coordinates; - - const properties = route.properties; - routeDistance = Math.round(properties.segments[0].distance / 1000 * 10) / 10; - routeDuration = Math.round(properties.segments[0].duration / 60); - } else if (data.routes && data.routes.length > 0) { - // Direct routes format response - console.log('Processing direct routes format response'); - route = data.routes[0]; - coordinates = route.geometry.coordinates || route.geometry; - - routeDistance = Math.round(route.summary.distance / 1000 * 10) / 10; - routeDuration = Math.round(route.summary.duration / 60); - } else { - console.error('No route found in response. Response structure:', { - hasFeatures: !!data.features, - featuresLength: data.features?.length, - hasRoutes: !!data.routes, - routesLength: data.routes?.length, - keys: Object.keys(data) - }); - setRouteCoordinates([]); - setRouteInfo(null); - setDebugInfo('No route found in API response'); - return; - } - - if (coordinates && coordinates.length > 0) { - console.log('Raw coordinates count:', coordinates.length); - console.log('First few coordinates:', coordinates.slice(0, 3)); - - // Convert from [lng, lat] to [lat, lng] for Leaflet - const leafletCoords: [number, number][] = coordinates.map((coord: [number, number]) => [coord[1], coord[0]]); - - console.log('Converted Leaflet coordinates count:', leafletCoords.length); - console.log('First few Leaflet coords:', leafletCoords.slice(0, 3)); - - setRouteCoordinates(leafletCoords); - - setRouteInfo({ - distance: routeDistance, - duration: routeDuration - }); - - console.log('Route calculated successfully:', { - distance: routeDistance, - duration: routeDuration, - coordinates: leafletCoords.length - }); - - setDebugInfo(`Route found: ${leafletCoords.length} points, ${routeDistance}km`); - } else { - console.error('No coordinates found in route'); - setRouteCoordinates([]); - setRouteInfo(null); - setDebugInfo('No coordinates found in route'); - } - } catch (error) { - console.error('Error calculating route:', error); - setRouteCoordinates([]); - setRouteInfo(null); - setDebugInfo(`Route error: ${error instanceof Error ? error.message : 'Unknown error'}`); - } finally { - setIsCalculatingRoute(false); - } - }; - - // Clear route when parking lot is deselected - const clearRoute = () => { - setRouteCoordinates([]); - setRouteInfo(null); - setDebugInfo('Route cleared'); - }; - - // Default center (Ho Chi Minh City) - const defaultCenter = { lat: 10.7769, lng: 106.7009 }; - const center = userLocation || defaultCenter; - - useEffect(() => { - // Fix for Leaflet default markers in Next.js - if (typeof window !== 'undefined') { - const L = require('leaflet'); - - delete (L.Icon.Default.prototype as any)._getIconUrl; - L.Icon.Default.mergeOptions({ - iconRetinaUrl: '/icons/marker-icon-2x.png', - iconUrl: '/icons/marker-icon.png', - shadowUrl: '/icons/marker-shadow.png', - }); - } - }, []); - - // Calculate distance between two points - const calculateDistance = (lat1: number, lng1: number, lat2: number, lng2: number): number => { - const R = 6371; // Earth's radius in kilometers - const dLat = (lat2 - lat1) * (Math.PI / 180); - const dLng = (lng2 - lng1) * (Math.PI / 180); - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(lat1 * (Math.PI / 180)) * - Math.cos(lat2 * (Math.PI / 180)) * - Math.sin(dLng / 2) * - Math.sin(dLng / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; - }; - - // Smart zoom calculation based on distance with enhanced levels - const calculateOptimalZoom = (distance: number): number => { - if (distance < 0.2) return 18; // Very very close - max detail - if (distance < 0.5) return 17; // Very close - detailed view - if (distance < 1) return 16; // Close - street level - if (distance < 2) return 15; // Nearby - neighborhood - if (distance < 5) return 14; // Medium distance - district - if (distance < 10) return 13; // Far - city area - if (distance < 20) return 12; // Very far - wider area - return 11; // Extremely far - metropolitan area - }; - - // Debug effect to track selectedParkingLot changes - useEffect(() => { - console.log('Selected parking lot changed:', { - previous: 'tracked in state', - current: selectedParkingLot?.name || 'None', - id: selectedParkingLot?.id || 'None' - }); - }, [selectedParkingLot]); - - // Smart camera movement to selected parking station - useEffect(() => { - console.log('Camera movement effect triggered:', { - mapReady: !!mapRef.current, - selectedParkingLot: selectedParkingLot?.name || 'None', - userLocation: !!userLocation - }); - - // Add a small delay to ensure map is fully ready - const timer = setTimeout(() => { - if (mapRef.current) { - if (selectedParkingLot) { - console.log('Moving camera to selected parking lot:', selectedParkingLot.name); - - if (userLocation) { - // Calculate distance between user and parking station - const distance = calculateDistance( - userLocation.lat, - userLocation.lng, - selectedParkingLot.lat, - selectedParkingLot.lng - ); - - console.log('Distance between user and parking:', distance, 'km'); - - // If parking station is far from user, show both locations - if (distance > 2) { - const bounds = [ - [userLocation.lat, userLocation.lng], - [selectedParkingLot.lat, selectedParkingLot.lng] - ]; - - // Calculate optimal padding based on distance - const padding = distance < 5 ? [80, 80] : distance < 10 ? [60, 60] : [40, 40]; - const maxZoom = calculateOptimalZoom(distance); - - console.log('Using fitBounds with padding:', padding, 'maxZoom:', maxZoom); - - // Fit map to bounds with smart padding and max zoom - mapRef.current.fitBounds(bounds, { - padding: padding, - maxZoom: maxZoom, - animate: true, - duration: 1.5 - }); - } else { - // If parking station is close, center between user and parking - const centerLat = (userLocation.lat + selectedParkingLot.lat) / 2; - const centerLng = (userLocation.lng + selectedParkingLot.lng) / 2; - - console.log('Using setView to center point:', { centerLat, centerLng }); - - mapRef.current.setView([centerLat, centerLng], 16, { - animate: true, - duration: 1.2 - }); - } - } else { - // No user location, just focus on parking station - console.log('No user location, focusing only on parking station'); - mapRef.current.setView([selectedParkingLot.lat, selectedParkingLot.lng], 17, { - animate: true, - duration: 1.0 - }); - } - } else if (userLocation) { - // No parking station selected, center on user location - console.log('No parking selected, centering on user location'); - mapRef.current.setView([userLocation.lat, userLocation.lng], 15, { - animate: true, - duration: 0.8 - }); - } - } else { - console.log('Map ref not ready yet'); - } - }, 200); // Small delay to ensure map is ready - - return () => clearTimeout(timer); - }, [selectedParkingLot, userLocation]); - - // Calculate route when parking lot is selected - useEffect(() => { - console.log('Route calculation effect triggered:', { - userLocation: !!userLocation, - selectedParkingLot: !!selectedParkingLot, - userLocationCoords: userLocation ? { lat: userLocation.lat, lng: userLocation.lng } : null, - selectedParkingCoords: selectedParkingLot ? { lat: selectedParkingLot.lat, lng: selectedParkingLot.lng } : null - }); - - if (userLocation && selectedParkingLot) { - console.log('Calling calculateRouteToParking...'); - calculateRouteToParking( - userLocation.lat, - userLocation.lng, - selectedParkingLot.lat, - selectedParkingLot.lng - ); - } else { - console.log('Clearing route - missing location or parking lot'); - clearRoute(); - } - }, [userLocation, selectedParkingLot]); - - // Create GPS marker icon with enhanced 3D pulsing effect - const createGPSIcon = () => { - if (typeof window === 'undefined') return null; - - const L = require('leaflet'); - - const iconHtml = ` -
- -
- - -
- - -
-
-
-
-
- - -
- - -
-
-
-
-
-
- - - `; - - return L.divIcon({ - html: iconHtml, - className: 'gps-marker-icon-enhanced', - iconSize: [50, 50], - iconAnchor: [25, 25], - popupAnchor: [0, -25] - }); - }; - - // Create parking lot icon with enhanced 3D design and status indicators - const createParkingIcon = ( - isSelected: boolean = false, - availableSpaces: number = 0, - totalSpaces: number = 1, - isDimmed: boolean = false - ) => { - if (typeof window === 'undefined') return null; - - const L = require('leaflet'); - - // Determine status color and info based on availability - const getStatusInfo = () => { - const occupancyRate = 1 - (availableSpaces / totalSpaces); - if (occupancyRate < 0.3) return { - bg: '#10B981', - label: 'Nhiều chα»—', - glow: 'rgba(16, 185, 129, 0.4)', - statusColor: '#10B981' - }; - if (occupancyRate < 0.7) return { - bg: '#F59E0B', - label: 'CΓ²n Γ­t', - glow: 'rgba(245, 158, 11, 0.4)', - statusColor: '#F59E0B' - }; - if (occupancyRate < 0.9) return { - bg: '#EF4444', - label: 'GαΊ§n hαΊΏt', - glow: 'rgba(239, 68, 68, 0.4)', - statusColor: '#EF4444' - }; - return { - bg: '#6B7280', - label: 'HαΊΏt chα»—', - glow: 'rgba(107, 114, 128, 0.4)', - statusColor: '#6B7280' - }; - }; - - const status = getStatusInfo(); - - // Apply dimming effect when not selected and another parking is selected - const baseOpacity = isDimmed ? 0.3 : 1; - const pulseEffect = isDimmed ? '' : (isSelected ? 'animation: pulse-parking-selection 1.2s ease-in-out infinite;' : ''); - - // Enhanced selection effect for highlighted parking - const selectionEffect = isSelected ? ` - -
- - -
- ` : ''; - - const iconHtml = ` -
- ${selectionEffect} - - -
- - -
- -
- - - -
-
- - -
- - -
- ${availableSpaces}/${totalSpaces} -
- - ${isSelected ? ` - -
- β˜… CHỌN -
- ` : ''} -
- - - `; - - return L.divIcon({ - html: iconHtml, - className: 'parking-marker-icon-enhanced', - iconSize: [38, 38], - iconAnchor: [19, 19], - popupAnchor: [0, -19] - }); - }; - - useEffect(() => { - // Force map to resize when component mounts or updates - if (mapRef.current) { - setTimeout(() => { - mapRef.current?.invalidateSize(); - }, 100); - } - }, [userLocation, parkingLots]); - - const defaultClassName = 'w-full h-full min-h-0'; - const finalClassName = className || defaultClassName; - - // Remove loading state to prevent unnecessary map reloads - // if (isLoading) { - // return ( - //
- //
- // - // Loading map... - //
- //
- // ); - // } - - // Custom zoom functions - const zoomIn = () => { - if (mapRef.current) { - const currentZoom = mapRef.current.getZoom(); - mapRef.current.setZoom(Math.min(currentZoom + 1, 18), { animate: true }); - } - }; - - const zoomOut = () => { - if (mapRef.current) { - const currentZoom = mapRef.current.getZoom(); - mapRef.current.setZoom(Math.max(currentZoom - 1, 3), { animate: true }); - } - }; - - const zoomToUser = () => { - if (mapRef.current && userLocation) { - mapRef.current.setView([userLocation.lat, userLocation.lng], 16, { - animate: true, - duration: 1.0 - }); - } - }; - - const zoomToFitAll = () => { - if (mapRef.current && parkingLots.length > 0) { - const bounds = parkingLots.map(lot => [lot.lat, lot.lng]); - if (userLocation) { - bounds.push([userLocation.lat, userLocation.lng]); - } - mapRef.current.fitBounds(bounds, { - padding: [40, 40], - maxZoom: 15, - animate: true - }); - } - }; - - const zoomToSelected = () => { - if (mapRef.current && selectedParkingLot) { - console.log('zoomToSelected called for:', selectedParkingLot.name); - // Focus directly on the selected parking station - mapRef.current.setView([selectedParkingLot.lat, selectedParkingLot.lng], 18, { - animate: true, - duration: 1.2 - }); - } - }; - - // Helper function to move camera with better error handling - const moveCameraTo = (lat: number, lng: number, zoom: number = 17, duration: number = 1.2) => { - if (!mapRef.current) { - console.warn('Map ref not available for camera movement'); - return; - } - - try { - console.log('Moving camera to:', { lat, lng, zoom }); - mapRef.current.setView([lat, lng], zoom, { - animate: true, - duration: duration, - easeLinearity: 0.1 - }); - } catch (error) { - console.error('Error moving camera:', error); - } - }; - - return ( -
- {/* Custom Zoom Controls */} -
- {/* Zoom Level Display */} -
- Zoom: {currentZoom.toFixed(0)} -
- {/* Zoom In */} - - - {/* Zoom Out */} - - - {/* Zoom to User */} - {userLocation && ( - - )} - - {/* Zoom to Selected Parking */} - {selectedParkingLot && ( - - )} - - {/* Fit All */} - {parkingLots.length > 0 && ( - - )} -
- - { - // Force invalidate size when map is ready - setTimeout(() => { - if (mapRef.current) { - console.log('Map is ready, invalidating size...'); - mapRef.current.invalidateSize(); - - // Add zoom event listener - mapRef.current.on('zoomend', () => { - if (mapRef.current) { - setCurrentZoom(mapRef.current.getZoom()); - } - }); - - // Add moveend listener for debugging - mapRef.current.on('moveend', () => { - if (mapRef.current) { - const center = mapRef.current.getCenter(); - console.log('Map moved to:', { lat: center.lat, lng: center.lng, zoom: mapRef.current.getZoom() }); - } - }); - - // Set initial zoom level - setCurrentZoom(mapRef.current.getZoom()); - - console.log('Map setup complete'); - } - }, 100); - }} - > - - - {/* User Location Marker (GPS vα»›i hiệu α»©ng pulse) */} - {userLocation && ( - - -
-
-
- πŸ“ Vα»‹ trΓ­ cα»§a tΓ΄i -
-
-
🌐 {userLocation.lat.toFixed(6)}, {userLocation.lng.toFixed(6)}
- {userLocation.accuracy && ( -
🎯 Độ chΓ­nh xΓ‘c: Β±{userLocation.accuracy}m
- )} -
⏰ Cập nhật: {new Date(userLocation.timestamp || Date.now()).toLocaleTimeString('vi-VN')}
-
-
-
-
- )} - - {/* Parking Lot Markers (vα»›i thiαΊΏt kαΊΏ mα»›i) */} - {parkingLots.map((lot, index) => { - const isSelected = selectedParkingLot?.id === lot.id; - const isDimmed = !!(selectedParkingLot && selectedParkingLot.id !== lot.id); - - return ( - { - console.log('Parking marker clicked:', lot.name); - - // First select the parking lot - onParkingLotSelect?.(lot); - - // Then smoothly move camera to the parking lot with a slight delay - setTimeout(() => { - moveCameraTo(lot.lat, lot.lng, 17, 1.5); - }, 300); - } - }} - > - -
-
-
-
- - - -
-
- {lot.name} - {isSelected && ( -
-
- 🎯 ĐÃ CHỌN -
- )} -
-
- -
- πŸ“ {lot.address} -
- - {/* Route information when selected and route is available */} - {isSelected && routeInfo && ( -
-
-
- - - -
- πŸ›£οΈ Chỉ đường -
-
-
- KhoαΊ£ng cΓ‘ch: -
{routeInfo.distance} km
-
-
- Thời gian: -
{routeInfo.duration} phΓΊt
-
-
-
- )} - - {/* Loading route calculation */} - {isSelected && isCalculatingRoute && ( -
-
-
- 🧭 Đang tΓ­nh toΓ‘n đường Δ‘i... -
-
- )} - -
-
-
TrαΊ‘ng thΓ‘i
-
lot.totalSlots * 0.3 ? 'text-green-600' : - lot.availableSlots > 0 ? 'text-orange-600' : 'text-red-600' - }`}> - {lot.availableSlots > lot.totalSlots * 0.3 ? ( - <>🟒 CΓ²n nhiều chα»— - ) : lot.availableSlots > 0 ? ( - <>🟑 SαΊ―p hαΊΏt chα»— - ) : ( - <>πŸ”΄ Đã hαΊΏt chα»— - )} -
-
- -
-
Chα»— trα»‘ng
-
- {lot.availableSlots}/{lot.totalSlots} -
-
- -
-
GiΓ‘ thuΓͺ
-
- πŸ’° {lot.pricePerHour || lot.hourlyRate}/giờ -
-
- -
-
KhoαΊ£ng cΓ‘ch
-
- {routeInfo ? ( - <>�️ {routeInfo.distance} km - ) : userLocation ? ( - <>οΏ½πŸš— {( - Math.sqrt( - Math.pow(lot.lat - userLocation.lat, 2) + - Math.pow(lot.lng - userLocation.lng, 2) - ) * 111 - ).toFixed(1)} km - ) : ( - 'N/A' - )} -
-
-
- -
- - - {/* Focus camera button */} - -
-
-
-
-
- ); - })} - - {/* Route Polyline - Display route when available with enhanced visibility */} - {routeCoordinates.length > 0 && ( - <> - {/* Outer glow effect - largest */} - - {/* Middle glow effect */} - - {/* Background shadow line for depth */} - - {/* Main route line - bright and thick */} - - {/* Animated dashed overlay for movement effect */} - - {/* Top center highlight line */} - - - )} - - {/* Debug info for route coordinates */} - {debugInfo && ( -
- Debug: {debugInfo} -
- Coordinates: {routeCoordinates.length} -
- Selected: {selectedParkingLot?.name || 'None'} -
- )} -
- - {/* CSS for route animation */} - -
- ); -}; diff --git a/frontend/src/components/parking/ParkingDetails.tsx b/frontend/src/components/parking/ParkingDetails.tsx index 088fcf1..a9f17dd 100644 --- a/frontend/src/components/parking/ParkingDetails.tsx +++ b/frontend/src/components/parking/ParkingDetails.tsx @@ -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 */}
- CαΊ­p nhαΊ­t: {lastUpdate.toLocaleTimeString()} + Updated: {lastUpdate.toLocaleTimeString()}
@@ -202,15 +202,15 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
{floorStats.available}
-
Trα»‘ng
+
Available
{floorStats.occupied}
-
Đã Δ‘αΊ­u
+
Occupied
{floorStats.total}
-
Tα»•ng
+
Total
@@ -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 = ({ {renderStars(Math.round(averageRating))} {averageRating.toFixed(1)} - ({SAMPLE_REVIEWS.length} Δ‘Γ‘nh giΓ‘) + ({SAMPLE_REVIEWS.length} reviews) @@ -426,13 +426,13 @@ export const ParkingDetails: React.FC = ({ {/* Status banners */} {isFull && (
- BΓ£i xe Δ‘Γ£ hαΊΏt chα»— + Parking lot is full
)} {isClosed && (
- BΓ£i xe Δ‘Γ£ Δ‘Γ³ng cα»­a + Parking lot is closed
)} @@ -451,7 +451,7 @@ export const ParkingDetails: React.FC = ({
{parkingLot.availableSlots}
-
chα»— trα»‘ng
+
available
/ {parkingLot.totalSlots} tα»•ng
@@ -505,7 +505,7 @@ export const ParkingDetails: React.FC = ({ }} > {tab === 'overview' && 'Tα»•ng quan'} - {tab === 'reviews' && 'ĐÑnh giΓ‘'} + {tab === 'reviews' && 'Reviews'} ))} @@ -541,7 +541,7 @@ export const ParkingDetails: React.FC = ({ - Tiện Γ­ch + Amenities
{amenityList.map((amenity, index) => ( @@ -570,7 +570,7 @@ export const ParkingDetails: React.FC = ({
{renderStars(Math.round(averageRating))}
-
{SAMPLE_REVIEWS.length} Δ‘Γ‘nh giΓ‘
+
{SAMPLE_REVIEWS.length} reviews
{/* Reviews list */} @@ -605,7 +605,7 @@ export const ParkingDetails: React.FC = ({ )} @@ -722,7 +722,7 @@ export const ParkingDetails: React.FC = ({ : '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)`} )} diff --git a/frontend/src/components/parking/ParkingList.tsx b/frontend/src/components/parking/ParkingList.tsx index 11e4089..8ca1097 100644 --- a/frontend/src/components/parking/ParkingList.tsx +++ b/frontend/src/components/parking/ParkingList.tsx @@ -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 = ({ -

KhΓ΄ng tΓ¬m thαΊ₯y kαΊΏt quαΊ£

-

KhΓ΄ng cΓ³ bΓ£i Δ‘α»— xe nΓ o phΓΉ hợp vα»›i tα»« khΓ³a "{searchQuery}"

-

Thử tìm kiếm với từ khóa khÑc

+

No Results Found

+

No parking lots match the keyword "{searchQuery}"

+

Try searching with different keywords

) : ( sortedLots.map((lot, index) => { @@ -262,13 +262,13 @@ export const ParkingList: React.FC = ({ {/* Warning banners */} {isFull && (
- 🚫 BΓƒI XE Đà HαΊΎT CHα»– + 🚫 PARKING LOT FULL
)} {isClosed && (
- πŸ”’ BΓƒI XE Đà ĐÓNG CỬA + πŸ”’ PARKING LOT CLOSED
)} @@ -322,10 +322,10 @@ export const ParkingList: React.FC = ({
- chα»— trα»‘ng + available
- / {lot.totalSlots} chα»— + / {lot.totalSlots} total
{/* Availability percentage */}
@@ -338,7 +338,7 @@ export const ParkingList: React.FC = ({ >
- {Math.round((lot.availableSlots / lot.totalSlots) * 100)}% trα»‘ng + {Math.round((lot.availableSlots / lot.totalSlots) * 100)}% available
@@ -350,10 +350,10 @@ export const ParkingList: React.FC = ({ {Math.round((lot.pricePerHour || lot.hourlyRate) / 1000)}k
- mα»—i giờ + per hour
- phΓ­ gα»­i xe + parking fee
) : ( @@ -362,10 +362,10 @@ export const ParkingList: React.FC = ({ --
- liΓͺn hệ + contact
- để biαΊΏt giΓ‘ + for pricing
)} @@ -386,13 +386,13 @@ export const ParkingList: React.FC = ({
{isCurrentlyOpen(lot) ? ( - lot.isOpen24Hours ? 'LuΓ΄n mở cα»­a' : `Δ‘αΊΏn ${lot.closeTime}` + lot.isOpen24Hours ? 'Always open' : `until ${lot.closeTime}` ) : ( - 'Đã Δ‘Γ³ng cα»­a' + 'Closed' )}
- {isCurrentlyOpen(lot) ? 'Đang mở' : 'πŸ”’ Đã Δ‘Γ³ng'} + {isCurrentlyOpen(lot) ? 'Open now' : 'πŸ”’ Closed'}
) : ( @@ -401,10 +401,10 @@ export const ParkingList: React.FC = ({ --:--
- khΓ΄ng rΓ΅ + unknown
- giờ mở cửa + opening hours
)} diff --git a/frontend/src/components/ui/Icon.tsx b/frontend/src/components/ui/Icon.tsx index f600975..ba30798 100644 --- a/frontend/src/components/ui/Icon.tsx +++ b/frontend/src/components/ui/Icon.tsx @@ -20,7 +20,8 @@ const iconPaths: Record = { 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", diff --git a/frontend/src/hooks/api.ts b/frontend/src/hooks/api.ts index d581c28..9c52b22 100644 --- a/frontend/src/hooks/api.ts +++ b/frontend/src/hooks/api.ts @@ -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({ diff --git a/frontend/src/hooks/useRouting.ts b/frontend/src/hooks/useRouting.ts deleted file mode 100644 index 13ac7a5..0000000 --- a/frontend/src/hooks/useRouting.ts +++ /dev/null @@ -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({ - 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); -} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1da747f..a7c97ae 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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 { - 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(), }; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 664fe2e..706bec5 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -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; - 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; } diff --git a/frontend/src/utils/map.ts b/frontend/src/utils/map.ts deleted file mode 100644 index 6597e1b..0000000 --- a/frontend/src/utils/map.ts +++ /dev/null @@ -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: '© OpenStreetMap 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, -}; diff --git a/optimize.sh b/optimize.sh deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/README.md b/scripts/README.md index 7584bec..67048dd 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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: diff --git a/scripts/docker-dev.sh b/scripts/docker-dev.sh old mode 100644 new mode 100755 diff --git a/scripts/frontend-only.sh b/scripts/frontend-only.sh old mode 100644 new mode 100755 diff --git a/scripts/full-dev.sh b/scripts/full-dev.sh old mode 100644 new mode 100755 index 1216265..ec0c8c8 --- a/scripts/full-dev.sh +++ b/scripts/full-dev.sh @@ -1,4 +1,4 @@ -#!/bin/bash +``#!/bin/bash # πŸ”„ Full Development Environment (Frontend + Backend) echo "πŸ”„ Starting Full Development Environment..." diff --git a/scripts/setup.sh b/scripts/setup.sh old mode 100644 new mode 100755 diff --git a/scripts/start.sh b/scripts/start.sh old mode 100644 new mode 100755 diff --git a/setup.sh b/setup.sh deleted file mode 100755 index a903da0..0000000 --- a/setup.sh +++ /dev/null @@ -1,288 +0,0 @@ -#!/bin/bash - -# Smart Parking Finder - Development Setup Script -# This script sets up the complete development environment - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Function to print colored output -print_status() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Function to check if command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Check prerequisites -check_prerequisites() { - print_status "Checking prerequisites..." - - local missing_deps=() - - if ! command_exists docker; then - missing_deps+=("docker") - fi - - if ! command_exists docker-compose; then - missing_deps+=("docker-compose") - fi - - if ! command_exists node; then - missing_deps+=("node") - fi - - if ! command_exists npm; then - missing_deps+=("npm") - fi - - if [ ${#missing_deps[@]} -ne 0 ]; then - print_error "Missing required dependencies: ${missing_deps[*]}" - echo "" - echo "Please install the following:" - echo "- Docker: https://docs.docker.com/get-docker/" - echo "- Docker Compose: https://docs.docker.com/compose/install/" - echo "- Node.js 18+: https://nodejs.org/" - exit 1 - fi - - print_success "All prerequisites are installed!" -} - -# Create directory structure -create_structure() { - print_status "Creating project structure..." - - # Create main directories - mkdir -p {frontend,backend,valhalla/custom_files} - - # Create subdirectories - mkdir -p frontend/{src,public,components,pages} - mkdir -p backend/{src,test,database} - - print_success "Project structure created!" -} - -# Download OSM data -download_osm_data() { - print_status "Setting up OSM data for Valhalla..." - - if [ ! -f "valhalla/custom_files/vietnam-latest.osm.pbf" ]; then - read -p "Do you want to download Vietnam OSM data now? (y/N): " download_osm - - if [[ $download_osm =~ ^[Yy]$ ]]; then - print_status "Downloading Vietnam OSM data (~100MB)..." - cd valhalla - ./download-osm-data.sh - cd .. - else - print_warning "OSM data not downloaded. Valhalla may not work properly." - print_warning "You can download it later by running: cd valhalla && ./download-osm-data.sh" - fi - else - print_success "OSM data already exists!" - fi -} - -# Setup environment files -setup_environment() { - print_status "Setting up environment files..." - - # Frontend environment - if [ ! -f "frontend/.env.local" ]; then - cat > frontend/.env.local << EOF -# Frontend Environment Variables -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 -NEXT_PUBLIC_DEFAULT_LAT=10.7769 -NEXT_PUBLIC_DEFAULT_LNG=106.7009 -EOF - print_success "Created frontend/.env.local" - fi - - # Backend environment - if [ ! -f "backend/.env" ]; then - cat > backend/.env << EOF -# Backend Environment Variables -NODE_ENV=development -PORT=3001 -DATABASE_URL=postgresql://parking_user:parking_pass@localhost:5432/parking_db -REDIS_URL=redis://localhost:6379 -VALHALLA_URL=http://localhost:8002 -JWT_SECRET=your-development-jwt-secret-$(date +%s) -JWT_EXPIRATION=24h -CORS_ORIGIN=http://localhost:3000 -API_PREFIX=api -EOF - print_success "Created backend/.env" - fi -} - -# Setup Docker services -setup_docker() { - print_status "Setting up Docker services..." - - # Check if Docker is running - if ! docker info >/dev/null 2>&1; then - print_error "Docker is not running. Please start Docker first." - exit 1 - fi - - # Pull images - print_status "Pulling Docker images..." - docker-compose pull postgres redis - - # Start infrastructure services - print_status "Starting infrastructure services..." - docker-compose up -d postgres redis - - # Wait for services to be ready - print_status "Waiting for services to be ready..." - sleep 10 - - # Check if services are healthy - if docker-compose ps postgres | grep -q "healthy\|Up"; then - print_success "PostgreSQL is ready!" - else - print_warning "PostgreSQL may still be starting..." - fi - - if docker-compose ps redis | grep -q "healthy\|Up"; then - print_success "Redis is ready!" - else - print_warning "Redis may still be starting..." - fi -} - -# Setup Valhalla -setup_valhalla() { - print_status "Setting up Valhalla routing engine..." - - if [ ! -f "valhalla/custom_files/vietnam-latest.osm.pbf" ]; then - print_warning "No OSM data found. Skipping Valhalla setup." - print_warning "Download OSM data first: cd valhalla && ./download-osm-data.sh" - return - fi - - print_status "Building and starting Valhalla (this may take 10-30 minutes)..." - docker-compose up -d valhalla - - print_status "Valhalla is processing OSM data. This may take a while..." - print_status "You can check progress with: docker-compose logs -f valhalla" -} - -# Install dependencies -install_dependencies() { - print_status "Installing Node.js dependencies..." - - # Frontend dependencies - if [ -f "frontend/package.json" ]; then - print_status "Installing frontend dependencies..." - cd frontend && npm install && cd .. - print_success "Frontend dependencies installed!" - else - print_warning "No frontend/package.json found. Skipping frontend dependencies." - fi - - # Backend dependencies - if [ -f "backend/package.json" ]; then - print_status "Installing backend dependencies..." - cd backend && npm install && cd .. - print_success "Backend dependencies installed!" - else - print_warning "No backend/package.json found. Skipping backend dependencies." - fi -} - -# Setup database -setup_database() { - print_status "Setting up database..." - - # Wait for PostgreSQL to be ready - print_status "Waiting for PostgreSQL to be ready..." - timeout=60 - while ! docker-compose exec -T postgres pg_isready -U parking_user -d parking_db >/dev/null 2>&1; do - if [ $timeout -le 0 ]; then - print_error "PostgreSQL is not ready after 60 seconds" - exit 1 - fi - sleep 1 - ((timeout--)) - done - - print_success "PostgreSQL is ready!" - - # Run migrations (if backend exists) - if [ -f "backend/package.json" ]; then - print_status "Running database migrations..." - cd backend - # npm run migration:run # Uncomment when migrations exist - cd .. - print_success "Database migrations completed!" - fi -} - -# Main setup function -main() { - echo "" - echo "πŸš— Smart Parking Finder - Development Setup" - echo "===========================================" - echo "" - - check_prerequisites - create_structure - setup_environment - download_osm_data - setup_docker - install_dependencies - setup_database - setup_valhalla - - echo "" - echo "πŸŽ‰ Setup completed successfully!" - echo "" - echo "Next steps:" - echo "1. Start the development servers:" - echo " - Frontend: cd frontend && npm run dev" - echo " - Backend: cd backend && npm run start:dev" - echo "" - echo "2. Access the applications:" - echo " - Frontend: http://localhost:3000" - echo " - Backend API: http://localhost:3001" - echo " - Database (pgAdmin): http://localhost:5050 (with --profile tools)" - echo " - Redis (Commander): http://localhost:8081 (with --profile tools)" - echo " - Valhalla: http://localhost:8002/status" - echo "" - echo "3. Useful commands:" - echo " - View logs: docker-compose logs -f [service]" - echo " - Stop services: docker-compose down" - echo " - Restart services: docker-compose restart [service]" - echo " - Start with tools: docker-compose --profile tools up -d" - echo "" - echo "πŸ’‘ If Valhalla is still processing data, wait for it to complete" - echo " Check status: curl http://localhost:8002/status" -} - -# Run main function -main "$@" diff --git a/start-backend-global.sh b/start-backend-global.sh deleted file mode 100644 index c29a641..0000000 --- a/start-backend-global.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Smart Parking Finder - Backend Global Access -echo "🌐 Starting Backend for Global Access..." - -# Start backend -cd backend -npm run start:dev & -BACKEND_PID=$! - -# Wait for backend to start -sleep 3 - -# Start ngrok for backend -echo "πŸ”— Creating global tunnel for backend..." -ngrok http 3001 & -BACKEND_NGROK_PID=$! - -echo "βœ… Backend is now accessible globally!" -echo "πŸ“‹ Update frontend API URL with the ngrok URL" - -# Cleanup function -cleanup() { - echo "πŸ›‘ Stopping backend services..." - kill $BACKEND_PID 2>/dev/null - kill $BACKEND_NGROK_PID 2>/dev/null - exit -} - -trap cleanup INT -wait diff --git a/start-dev.sh b/start-dev.sh deleted file mode 100644 index e69de29..0000000 diff --git a/start-global.sh b/start-global.sh deleted file mode 100755 index e786de9..0000000 --- a/start-global.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -# Smart Parking Finder - Global Access Script -echo "🌍 Starting Smart Parking Finder for GLOBAL ACCESS..." - -# Function to check if a command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Check if ngrok is installed -if ! command_exists ngrok; then - echo "❌ ngrok is not installed. Installing..." - brew install ngrok/ngrok/ngrok -fi - -# Get local IP for reference -LOCAL_IP=$(ifconfig | grep -E "inet.*broadcast" | head -1 | awk '{print $2}') - -echo "===============================================" -echo "🌍 GLOBAL ACCESS DEPLOYMENT" -echo "===============================================" -echo "πŸ“± Local Access: http://localhost:3000" -echo "🏠 Network Access: http://$LOCAL_IP:3000" -echo "🌐 Global Access: Will be shown after ngrok starts" -echo "===============================================" -echo "" -echo "πŸš€ Starting development server..." -echo "" - -# Navigate to frontend directory -cd frontend - -# Start Next.js in background with network access -echo "πŸ“¦ Starting Next.js server..." -npm run dev & -NEXTJS_PID=$! - -# Wait for Next.js to start -echo "⏳ Waiting for Next.js to start..." -sleep 5 - -# Start ngrok tunnel -echo "🌐 Starting ngrok tunnel..." -echo "" -echo "===============================================" -echo "πŸ”— GLOBAL ACCESS URLS:" -echo "===============================================" - -# Start ngrok and capture the URL -ngrok http 3000 --log=stdout & -NGROK_PID=$! - -# Function to cleanup on exit -cleanup() { - echo "" - echo "πŸ›‘ Stopping services..." - kill $NEXTJS_PID 2>/dev/null - kill $NGROK_PID 2>/dev/null - exit -} - -# Trap Ctrl+C -trap cleanup INT - -# Keep script running -echo "" -echo "🎯 Your app is now accessible globally!" -echo "πŸ“‹ Share the ngrok URL with anyone in the world" -echo "⚠️ Note: Free ngrok has session limits" -echo "" -echo "Press Ctrl+C to stop all services" -echo "" - -# Wait for processes -wait diff --git a/start-network.sh b/start-network.sh deleted file mode 100755 index 2aaeae9..0000000 --- a/start-network.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# Smart Parking Finder - Network Access Script -echo "πŸš— Starting Smart Parking Finder for Network Access..." - -# Get local IP address -LOCAL_IP=$(ifconfig | grep -E "inet.*broadcast" | head -1 | awk '{print $2}') - -echo "===============================================" -echo "🌐 NETWORK ACCESS INFORMATION" -echo "===============================================" -echo "πŸ“± Local Access: http://localhost:3000" -echo "🌍 Network Access: http://$LOCAL_IP:3000" -echo "===============================================" -echo "" -echo "πŸ“‹ To access from other devices:" -echo " 1. Make sure devices are on the same WiFi network" -echo " 2. Use this URL: http://$LOCAL_IP:3000" -echo " 3. Make sure macOS Firewall allows Node.js connections" -echo "" -echo "πŸ”₯ Starting development server..." -echo "" - -# Navigate to frontend directory -cd frontend - -# Start Next.js with network access -npm run dev diff --git a/start.sh b/start.sh deleted file mode 100644 index e69de29..0000000 diff --git a/valhalla/Dockerfile b/valhalla/Dockerfile deleted file mode 100644 index 27308e9..0000000 --- a/valhalla/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Valhalla Routing Engine Dockerfile -FROM ghcr.io/gis-ops/docker-valhalla/valhalla:latest - -# Set working directory -WORKDIR /app - -# Create necessary directories -RUN mkdir -p /custom_files /data/valhalla - -# Copy configuration file -COPY valhalla.json /valhalla.json - -# Copy OSM data files (if they exist) -COPY custom_files/ /custom_files/ - -# Set proper permissions -RUN chown -R valhalla:valhalla /data/valhalla /custom_files - -# Expose the service port -EXPOSE 8002 - -# Health check -HEALTHCHECK --interval=60s --timeout=30s --start-period=300s --retries=3 \ - CMD curl -f http://localhost:8002/status || exit 1 - -# Start Valhalla service -CMD ["valhalla_service", "/valhalla.json"] diff --git a/valhalla/README.md b/valhalla/README.md deleted file mode 100644 index ff85aa5..0000000 --- a/valhalla/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# Valhalla Routing Engine - -This directory contains the configuration and setup for the Valhalla routing engine. - -## πŸš€ Quick Setup - -### 1. Download OSM Data - -Download OpenStreetMap data for your region from [Geofabrik](https://download.geofabrik.de/): - -```bash -# For Vietnam/Southeast Asia -wget https://download.geofabrik.de/asia/vietnam-latest.osm.pbf -P custom_files/ - -# For smaller regions (Ho Chi Minh City area) -wget https://download.geofabrik.de/asia/vietnam-latest.osm.pbf -P custom_files/ -``` - -### 2. Build and Run - -```bash -# Build Valhalla container -docker-compose up -d valhalla - -# Check status -curl http://localhost:8002/status -``` - -## πŸ“Š API Endpoints - -### Route Calculation -```bash -# POST /route -curl -X POST http://localhost:8002/route \ - -H "Content-Type: application/json" \ - -d '{ - "locations": [ - {"lat": 10.7769, "lon": 106.7009}, - {"lat": 10.7796, "lon": 106.7019} - ], - "costing": "auto", - "directions_options": { - "units": "kilometers" - } - }' -``` - -### Locate Nearby Roads -```bash -# POST /locate -curl -X POST http://localhost:8002/locate \ - -H "Content-Type: application/json" \ - -d '{ - "locations": [ - {"lat": 10.7769, "lon": 106.7009} - ], - "costing": "auto" - }' -``` - -### Health Check -```bash -# GET /status -curl http://localhost:8002/status -``` - -## βš™οΈ Configuration - -The `valhalla.json` file contains the routing engine configuration: - -- **Costing models**: auto, bicycle, pedestrian -- **Data sources**: OpenStreetMap -- **Service endpoints**: route, locate, status -- **Logging**: Configurable log levels - -## πŸ—ΊοΈ Supported Regions - -Current OSM data includes: -- Vietnam (complete) -- Southeast Asia (partial) -- Custom boundary areas - -To add new regions: -1. Download `.osm.pbf` files to `custom_files/` -2. Restart the container -3. Wait for data processing to complete - -## πŸ”§ Performance Tuning - -### Memory Configuration -```json -{ - "mjolnir": { - "tile_dir": "/data/valhalla", - "max_cache_size": 1000000000 - } -} -``` - -### Costing Options -```json -{ - "costing_options": { - "auto": { - "maneuver_penalty": 5, - "gate_cost": 30, - "toll_booth_cost": 15 - } - } -} -``` - -## πŸ“ˆ Monitoring - -### Health Checks -- Container status: `docker ps` -- Service health: `curl http://localhost:8002/status` -- Logs: `docker logs valhalla` - -### Performance Metrics -- Route calculation time -- Memory usage -- Cache hit rate -- Request throughput - -## πŸ› Troubleshooting - -### Common Issues - -1. **Container won't start** - - Check OSM data files in `custom_files/` - - Verify memory allocation (minimum 2GB) - - Check port conflicts - -2. **Slow route calculation** - - Increase cache size in configuration - - Optimize OSM data for your region - - Add more memory to container - -3. **Routes not found** - - Verify coordinates are within OSM data bounds - - Check road connectivity - - Try different costing models - -### Debug Mode -```bash -# Enable debug logging -docker-compose up valhalla --build -docker logs -f valhalla -``` - -## πŸ”„ Updates - -### Update OSM Data -```bash -# Download new data -wget https://download.geofabrik.de/asia/vietnam-latest.osm.pbf -P custom_files/ - -# Rebuild container -docker-compose down valhalla -docker-compose up -d valhalla --build -``` - -### Update Valhalla Version -```bash -# Update base image in Dockerfile -# Rebuild container -docker-compose build valhalla --no-cache -``` diff --git a/valhalla/download-osm-data.sh b/valhalla/download-osm-data.sh deleted file mode 100755 index e12d494..0000000 --- a/valhalla/download-osm-data.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Download OSM data for Vietnam/Southeast Asia -# This script downloads OpenStreetMap data files for use with Valhalla - -set -e - -# Create custom_files directory if it doesn't exist -mkdir -p custom_files - -echo "πŸ—ΊοΈ Downloading OpenStreetMap data for Vietnam..." - -# Download Vietnam OSM data from Geofabrik -VIETNAM_URL="https://download.geofabrik.de/asia/vietnam-latest.osm.pbf" -VIETNAM_FILE="custom_files/vietnam-latest.osm.pbf" - -if [ ! -f "$VIETNAM_FILE" ]; then - echo "πŸ“₯ Downloading Vietnam OSM data..." - wget -O "$VIETNAM_FILE" "$VIETNAM_URL" - echo "βœ… Downloaded Vietnam OSM data" -else - echo "πŸ“ Vietnam OSM data already exists" -fi - -# Optional: Download other regional data -read -p "Do you want to download additional regional data? (y/N): " download_more - -if [[ $download_more =~ ^[Yy]$ ]]; then - echo "Available regions:" - echo "1. Southeast Asia (larger file, ~2GB)" - echo "2. Cambodia" - echo "3. Laos" - echo "4. Thailand" - echo "5. Myanmar" - - read -p "Select region (1-5): " region_choice - - case $region_choice in - 1) - REGION_URL="https://download.geofabrik.de/asia/southeast-asia-latest.osm.pbf" - REGION_FILE="custom_files/southeast-asia-latest.osm.pbf" - ;; - 2) - REGION_URL="https://download.geofabrik.de/asia/cambodia-latest.osm.pbf" - REGION_FILE="custom_files/cambodia-latest.osm.pbf" - ;; - 3) - REGION_URL="https://download.geofabrik.de/asia/laos-latest.osm.pbf" - REGION_FILE="custom_files/laos-latest.osm.pbf" - ;; - 4) - REGION_URL="https://download.geofabrik.de/asia/thailand-latest.osm.pbf" - REGION_FILE="custom_files/thailand-latest.osm.pbf" - ;; - 5) - REGION_URL="https://download.geofabrik.de/asia/myanmar-latest.osm.pbf" - REGION_FILE="custom_files/myanmar-latest.osm.pbf" - ;; - *) - echo "Invalid selection" - exit 1 - ;; - esac - - if [ ! -f "$REGION_FILE" ]; then - echo "πŸ“₯ Downloading additional regional data..." - wget -O "$REGION_FILE" "$REGION_URL" - echo "βœ… Downloaded additional regional data" - else - echo "πŸ“ Regional data already exists" - fi -fi - -echo "" -echo "πŸŽ‰ OSM data download complete!" -echo "πŸ“ Files saved to: custom_files/" -ls -lh custom_files/ - -echo "" -echo "πŸš€ Next steps:" -echo "1. Run: docker-compose up -d valhalla" -echo "2. Wait for data processing to complete (may take 10-30 minutes)" -echo "3. Test: curl http://localhost:8002/status" - -echo "" -echo "πŸ’‘ Tips:" -echo "- Larger OSM files take longer to process but provide better coverage" -echo "- Vietnam data (~100MB) is sufficient for most Vietnamese locations" -echo "- Southeast Asia data (~2GB) covers the entire region" diff --git a/valhalla/valhalla.json b/valhalla/valhalla.json deleted file mode 100644 index 2ca2127..0000000 --- a/valhalla/valhalla.json +++ /dev/null @@ -1,354 +0,0 @@ -{ - "mjolnir": { - "tile_dir": "/data/valhalla", - "admin": "/data/valhalla/admin.sqlite", - "timezone": "/data/valhalla/tz_world.sqlite", - "max_cache_size": 1000000000, - "global_synchronized_cache": false, - "tile_extract": "/data/valhalla/tiles.tar", - "logging": { - "type": "std_out", - "color": true, - "level": "INFO" - } - }, - "loki": { - "actions": ["locate", "route", "height", "sources_to_targets", "optimized_route", "isochrone", "trace_route", "trace_attributes", "expansion"], - "logging": { - "type": "std_out", - "color": true, - "level": "INFO" - }, - "service_defaults": { - "minimum_reachability": 50, - "radius": 0, - "search_cutoff": 35000, - "node_snap_tolerance": 5, - "street_side_tolerance": 5, - "street_side_max_distance": 1000, - "heading_tolerance": 60 - } - }, - "thor": { - "logging": { - "type": "std_out", - "color": true, - "level": "INFO" - }, - "source_to_target_algorithm": "select_optimal" - }, - "odin": { - "logging": { - "type": "std_out", - "color": true, - "level": "INFO" - } - }, - "meili": { - "customizable": ["turn_penalty_factor", "max_route_distance_factor", "max_route_time_factor"], - "mode": "auto", - "grid": { - "cache_size": 100240, - "size": 500 - }, - "default": { - "sigma_z": 4.07, - "gps_accuracy": 5.0, - "beta": 3, - "max_route_distance_factor": 5, - "max_route_time_factor": 5, - "breakage_distance": 2000, - "max_search_radius": 100, - "interpolation_distance": 10, - "search_radius": 15.0, - "max_candidates": 8, - "turn_penalty_factor": 0 - }, - "auto": { - "turn_penalty_factor": 200, - "search_radius": 15 - }, - "pedestrian": { - "turn_penalty_factor": 100, - "search_radius": 25 - }, - "bicycle": { - "turn_penalty_factor": 140 - }, - "logging": { - "type": "std_out", - "color": true, - "level": "INFO" - } - }, - "httpd": { - "service": { - "listen": "0.0.0.0:8002", - "loopback": "0.0.0.0:8002", - "interrupt": "ipc:///tmp/interrupt" - } - }, - "statsd": { - "host": "localhost", - "port": 8125, - "prefix": "valhalla" - }, - "service_limits": { - "auto": { - "max_distance": 5000000.0, - "max_locations": 20, - "max_matrix_distance": 400000.0, - "max_matrix_location_pairs": 2500 - }, - "auto_shorter": { - "max_distance": 5000000.0, - "max_locations": 20, - "max_matrix_distance": 400000.0, - "max_matrix_location_pairs": 2500 - }, - "bicycle": { - "max_distance": 500000.0, - "max_locations": 50, - "max_matrix_distance": 200000.0, - "max_matrix_location_pairs": 2500 - }, - "bus": { - "max_distance": 5000000.0, - "max_locations": 50, - "max_matrix_distance": 400000.0, - "max_matrix_location_pairs": 2500 - }, - "hov": { - "max_distance": 5000000.0, - "max_locations": 20, - "max_matrix_distance": 400000.0, - "max_matrix_location_pairs": 2500 - }, - "motorcycle": { - "max_distance": 5000000.0, - "max_locations": 20, - "max_matrix_distance": 400000.0, - "max_matrix_location_pairs": 2500 - }, - "motor_scooter": { - "max_distance": 500000.0, - "max_locations": 50, - "max_matrix_distance": 200000.0, - "max_matrix_location_pairs": 2500 - }, - "pedestrian": { - "max_distance": 250000.0, - "max_locations": 50, - "min_transit_walking_distance": 1, - "max_transit_walking_distance": 10000, - "max_matrix_distance": 200000.0, - "max_matrix_location_pairs": 2500 - }, - "bikeshare": { - "max_distance": 500000.0, - "max_locations": 50, - "max_matrix_distance": 200000.0, - "max_matrix_location_pairs": 2500 - }, - "taxi": { - "max_distance": 5000000.0, - "max_locations": 20, - "max_matrix_distance": 400000.0, - "max_matrix_location_pairs": 2500 - }, - "max_avoid_locations": 128, - "max_reachability": 100, - "max_radius": 200, - "max_timedep_distance": 500000, - "max_alternates": 2, - "max_exclude_locations": 50, - "max_exclude_polygons_length": 10000, - "max_walkway_shape": 100000, - "skadi": { - "max_shape": 750000, - "min_resample": 10.0 - }, - "isochrone": { - "max_contours": 4, - "max_time_contour": 120, - "max_distance_contour": 25000, - "max_locations": 1 - }, - "trace": { - "max_distance": 200000.0, - "max_gps_accuracy": 100.0, - "max_search_radius": 100, - "max_shape": 16000, - "max_best_paths": 4, - "max_best_paths_shape": 100, - "max_alternates": 3, - "max_alternates_shape": 100 - }, - "transit": { - "max_distance": 500000.0, - "max_locations": 50, - "max_matrix_distance": 200000.0, - "max_matrix_location_pairs": 2500 - }, - "status": { - "allow_verbose": false - } - }, - "costing_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": 0.5, - "use_highways": 1.0, - "use_tolls": 0.5, - "use_tracks": 0.0, - "use_living_streets": 0.25, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "private_access_factor": 9.0, - "exclude_unpaved": 1.0, - "include_hov2": false, - "include_hov3": false, - "include_hot": false - }, - "auto_shorter": { - "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": 0.5, - "use_highways": 1.0, - "use_tolls": 0.5, - "use_tracks": 0.0, - "use_living_streets": 0.25, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "private_access_factor": 9.0, - "exclude_unpaved": 1.0, - "include_hov2": false, - "include_hov3": false, - "include_hot": false - }, - "motorcycle": { - "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": 0.5, - "use_highways": 1.0, - "use_tolls": 0.5, - "use_tracks": 0.8, - "use_living_streets": 0.25, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "private_access_factor": 9.0, - "exclude_unpaved": 1.0 - }, - "bus": { - "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": 0.3, - "use_highways": 1.0, - "use_tolls": 0.5, - "use_tracks": 0.0, - "use_living_streets": 0.25, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "private_access_factor": 9.0, - "exclude_unpaved": 1.0 - }, - "taxi": { - "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": 0.5, - "use_highways": 1.0, - "use_tolls": 0.5, - "use_tracks": 0.0, - "use_living_streets": 0.25, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "private_access_factor": 9.0, - "exclude_unpaved": 1.0 - }, - "hov": { - "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": 0.5, - "use_highways": 1.0, - "use_tolls": 0.5, - "use_tracks": 0.0, - "use_living_streets": 0.25, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "private_access_factor": 9.0, - "exclude_unpaved": 1.0 - }, - "bicycle": { - "maneuver_penalty": 5, - "gate_cost": 30, - "gate_penalty": 300, - "private_access_penalty": 450, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "use_ferry": 0.5, - "use_living_streets": 0.6, - "use_tracks": 0.85, - "cycling_speed": 25.0, - "use_roads": 0.5, - "use_hills": 0.5, - "avoid_bad_surfaces": 0.25 - }, - "pedestrian": { - "maneuver_penalty": 5, - "gate_cost": 30, - "gate_penalty": 300, - "private_access_penalty": 450, - "service_penalty": 15, - "service_factor": 1.0, - "closure_factor": 9.0, - "use_ferry": 1.0, - "use_living_streets": 0.6, - "use_tracks": 0.85, - "walking_speed": 5.1, - "walkway_factor": 1.0, - "sidewalk_factor": 1.0, - "alley_factor": 2.0, - "driveway_factor": 5.0, - "step_penalty": 0.0, - "max_hiking_difficulty": 6 - } - } -} diff --git a/vps-deploy.sh b/vps-deploy.sh new file mode 100755 index 0000000..97cb838 --- /dev/null +++ b/vps-deploy.sh @@ -0,0 +1,760 @@ +#!/bin/bash + +# πŸš€ Laca City Website - Complete VPS Deployment Script +# This script sets up your entire website on a VPS with domain configuration +# Run this script on your VPS after uploading your project files + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Configuration Variables (Modify these before running) +DOMAIN_NAME="" # e.g., "yourdomain.com" +EMAIL="" # Your email for SSL certificate +PROJECT_NAME="laca-city" +DB_NAME="laca_city_db" +DB_USER="laca_admin" +DB_PASSWORD="" # Will be generated if empty +FRONTEND_PORT=3000 +BACKEND_PORT=3001 +NGINX_CONFIG_NAME="laca-city" + +# System Configuration +UBUNTU_VERSION=$(lsb_release -rs 2>/dev/null || echo "unknown") +CURRENT_USER=$(whoami) +PROJECT_DIR="/var/www/$PROJECT_NAME" +BACKUP_DIR="/var/backups/$PROJECT_NAME" + +# Function to print colored output +print_header() { + echo -e "\n${PURPLE}========================================${NC}" + echo -e "${PURPLE}$1${NC}" + echo -e "${PURPLE}========================================${NC}\n" +} + +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_step() { + echo -e "${CYAN}[STEP]${NC} $1" +} + +# Function to check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to prompt for user input +prompt_input() { + local prompt="$1" + local var_name="$2" + local default="$3" + + if [ -n "$default" ]; then + read -p "$prompt [$default]: " input + eval "$var_name=\${input:-$default}" + else + read -p "$prompt: " input + eval "$var_name=\$input" + fi +} + +# Function to generate random password +generate_password() { + openssl rand -base64 32 | tr -d "=+/" | cut -c1-25 +} + +# Function to create systemd service +create_systemd_service() { + local service_name="$1" + local description="$2" + local exec_start="$3" + local working_dir="$4" + local user="$5" + + sudo tee "/etc/systemd/system/$service_name.service" > /dev/null < /dev/null </dev/null | { cat; echo "0 3 * * * /usr/bin/certbot renew --quiet"; } | sudo crontab - + + print_success "SSL certificate configured and auto-renewal setup" +} + +# Function to setup PostgreSQL +setup_postgresql() { + print_step "Setting up PostgreSQL..." + + sudo apt update + sudo apt install -y postgresql postgresql-contrib + + # Start and enable PostgreSQL + sudo systemctl start postgresql + sudo systemctl enable postgresql + + # Generate database password if not provided + if [ -z "$DB_PASSWORD" ]; then + DB_PASSWORD=$(generate_password) + print_status "Generated database password: $DB_PASSWORD" + fi + + # Create database and user + sudo -u postgres psql </dev/null || true + sudo chown -R "$CURRENT_USER:$CURRENT_USER" "$PROJECT_DIR" + + cd "$PROJECT_DIR" + + # Install dependencies + print_status "Installing frontend dependencies..." + cd "$PROJECT_DIR/frontend" + npm ci --production + + print_status "Installing backend dependencies..." + cd "$PROJECT_DIR/backend" + npm ci --production + + print_success "Project dependencies installed" +} + +# Function to create environment files +create_env_files() { + print_step "Creating environment files..." + + # Backend environment + cat > "$PROJECT_DIR/backend/.env" < "$PROJECT_DIR/frontend/.env.production" < ecosystem.config.js < /dev/null < /dev/null < "\$BACKUP_PATH/database.sql" + +# Backup application files +cp -r "$PROJECT_DIR" "\$BACKUP_PATH/app" + +# Backup Nginx configuration +cp -r /etc/nginx/sites-available/$NGINX_CONFIG_NAME "\$BACKUP_PATH/nginx.conf" + +# Compress backup +cd "$BACKUP_DIR" +tar -czf "backup_\$BACKUP_DATE.tar.gz" "backup_\$BACKUP_DATE" +rm -rf "backup_\$BACKUP_DATE" + +# Keep only last 7 days of backups +find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete + +echo "Backup completed: backup_\$BACKUP_DATE.tar.gz" +EOF + + sudo chmod +x "/usr/local/bin/backup-$PROJECT_NAME" + + # Setup daily backup cron job + (crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/backup-$PROJECT_NAME") | crontab - + + print_success "Backup script created and scheduled" +} + +# Function to setup deployment script +create_deployment_script() { + print_step "Creating deployment script for future updates..." + + tee "$PROJECT_DIR/deploy-update.sh" > /dev/null </dev/null; then + print_error "This script requires sudo access. Please ensure you can run sudo commands." + exit 1 + fi + + # Collect configuration + if [ -z "$DOMAIN_NAME" ]; then + prompt_input "Enter your domain name (e.g., yourdomain.com)" DOMAIN_NAME + fi + + if [ -z "$EMAIL" ]; then + prompt_input "Enter your email for SSL certificate" EMAIL + fi + + if [ -z "$DB_PASSWORD" ]; then + prompt_input "Enter database password (leave empty to generate)" DB_PASSWORD + fi + + print_status "Configuration:" + print_status "Domain: $DOMAIN_NAME" + print_status "Email: $EMAIL" + print_status "Project Directory: $PROJECT_DIR" + print_status "Frontend Port: $FRONTEND_PORT" + print_status "Backend Port: $BACKEND_PORT" + + read -p "Continue with deployment? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + print_status "Deployment cancelled." + exit 0 + fi + + # Start deployment + print_header "STARTING DEPLOYMENT" + + # Update system + print_step "Updating system packages..." + sudo apt update && sudo apt upgrade -y + + # Install basic tools + print_step "Installing basic tools..." + sudo apt install -y curl wget git unzip software-properties-common apt-transport-https ca-certificates gnupg lsb-release openssl + + # Setup firewall + setup_firewall + + # Install Node.js + install_nodejs + + # Setup databases + setup_postgresql + setup_redis + + # Setup project + setup_project + + # Create environment files + create_env_files + + # Build applications + build_applications + + # Setup Nginx + setup_nginx + + # Setup SSL + setup_ssl + + # Setup PM2 + setup_pm2 + + # Setup monitoring + setup_monitoring + + # Create backup script + create_backup_script + + # Create deployment script + create_deployment_script + + # Final checks + final_checks + + print_header "πŸŽ‰ DEPLOYMENT COMPLETED SUCCESSFULLY!" + + echo -e "\n${GREEN}Your Laca City website is now deployed!${NC}\n" + echo -e "🌐 Website URL: ${CYAN}https://$DOMAIN_NAME${NC}" + echo -e "πŸ“Š PM2 Monitor: ${CYAN}pm2 monit${NC}" + echo -e "πŸ“ Logs: ${CYAN}pm2 logs${NC}" + echo -e "πŸ”„ Update: ${CYAN}$PROJECT_DIR/deploy-update.sh${NC}" + echo -e "πŸ’Ύ Backup: ${CYAN}/usr/local/bin/backup-$PROJECT_NAME${NC}" + + echo -e "\n${YELLOW}Important Information:${NC}" + echo -e "πŸ“§ Database Password: ${RED}$DB_PASSWORD${NC} (save this!)" + echo -e "πŸ“ Project Directory: $PROJECT_DIR" + echo -e "πŸ“ Backup Directory: $BACKUP_DIR" + echo -e "πŸ”§ Nginx Config: /etc/nginx/sites-available/$NGINX_CONFIG_NAME" + + echo -e "\n${BLUE}Useful Commands:${NC}" + echo -e " pm2 list # View running processes" + echo -e " pm2 restart all # Restart all applications" + echo -e " pm2 logs # View application logs" + echo -e " sudo systemctl status nginx # Check Nginx status" + echo -e " sudo certbot renew --dry-run # Test SSL renewal" + + print_success "Setup completed! Your website should be accessible at https://$DOMAIN_NAME" +} + +# Run main function +main "$@"