From bc87a88719b2335664c1365f39448b24e04c2af4 Mon Sep 17 00:00:00 2001 From: PhongPham Date: Sun, 3 Aug 2025 07:00:22 +0700 Subject: [PATCH] feat: Enhanced CSS animations, improved UI components, and project reorganization - Enhanced globals.css with comprehensive animation system - Added advanced map marker animations (GPS, parking) - Improved button and filter animations with hover effects - Added new UI components: BookingModal, ParkingDetails, WheelPicker - Reorganized project structure with better documentation - Added optimization scripts and improved development workflow - Updated deployment guides and technical documentation - Enhanced mobile responsiveness and accessibility support --- .gitignore | 125 ++ .vscode/tasks.json | 14 + Documents/API_SCHEMA.md | 1023 ++++++++++++++++ Documents/DATABASE_DESIGN.md | 839 +++++++++++++ Documents/README.md | 451 +++++++ Documents/SYSTEM_ARCHITECTURE.md | 611 +++++++++ Documents/TECHNICAL_SPECIFICATION.md | 420 +++++++ GIT_UPLOAD_GUIDE.md | 189 +++ HUONG_DAN_LOCAL.md | 0 LOCAL_DEPLOYMENT_GUIDE.md | 0 OPTIMIZATION_REPORT.md | 0 REORGANIZATION_GUIDE.md | 145 +++ frontend/next.config.optimized.js | 0 frontend/src/app/page.tsx | 150 ++- frontend/src/components/map/MapView-v2.0.tsx | 1090 ----------------- frontend/src/components/map/MapView.tsx | 412 ++++++- .../src/components/parking/BookingModal.tsx | 0 .../src/components/parking/ParkingDetails.tsx | 731 +++++++++++ .../src/components/parking/ParkingList.tsx | 45 +- .../components/parking/ParkingList.v1.0.tsx | 366 ------ frontend/src/components/ui/Icon.tsx | 6 +- frontend/src/components/ui/WheelPicker.tsx | 0 frontend/src/hooks/useParkingSearch-simple.ts | 595 --------- frontend/src/hooks/useRouting-simple.ts | 138 --- launch.sh | 77 ++ optimize.sh | 0 scripts/README.md | 201 +++ scripts/docker-dev.sh | 53 + scripts/frontend-only.sh | 23 + scripts/full-dev.sh | 58 + scripts/setup.sh | 83 ++ scripts/start.sh | 255 ++++ start-dev.sh | 104 -- start.sh | 0 34 files changed, 5851 insertions(+), 2353 deletions(-) create mode 100644 .gitignore create mode 100644 .vscode/tasks.json create mode 100644 Documents/API_SCHEMA.md create mode 100644 Documents/DATABASE_DESIGN.md create mode 100644 Documents/README.md create mode 100644 Documents/SYSTEM_ARCHITECTURE.md create mode 100644 Documents/TECHNICAL_SPECIFICATION.md create mode 100644 GIT_UPLOAD_GUIDE.md create mode 100644 HUONG_DAN_LOCAL.md create mode 100644 LOCAL_DEPLOYMENT_GUIDE.md create mode 100644 OPTIMIZATION_REPORT.md create mode 100644 REORGANIZATION_GUIDE.md create mode 100644 frontend/next.config.optimized.js delete mode 100644 frontend/src/components/map/MapView-v2.0.tsx create mode 100644 frontend/src/components/parking/BookingModal.tsx create mode 100644 frontend/src/components/parking/ParkingDetails.tsx delete mode 100644 frontend/src/components/parking/ParkingList.v1.0.tsx create mode 100644 frontend/src/components/ui/WheelPicker.tsx delete mode 100644 frontend/src/hooks/useParkingSearch-simple.ts delete mode 100644 frontend/src/hooks/useRouting-simple.ts create mode 100644 launch.sh create mode 100644 optimize.sh create mode 100644 scripts/README.md create mode 100644 scripts/docker-dev.sh create mode 100644 scripts/frontend-only.sh create mode 100644 scripts/full-dev.sh create mode 100644 scripts/setup.sh create mode 100644 scripts/start.sh mode change 100755 => 100644 start-dev.sh create mode 100644 start.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13a75ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,125 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +yarn.lock + +# Production builds +dist/ +build/ +.next/ +out/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/settings.json +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Docker +.docker/ + +# PostgreSQL data +postgres-data/ + +# Redis data +redis-data/ + +# Valhalla data +valhalla/data/ +valhalla/tiles/ + +# Backup files +*.backup +*.bak + +# Temporary files +tmp/ +temp/ + +# TypeScript +*.tsbuildinfo + +# Database +*.sqlite +*.db + +# Test files +coverage/ +test-results/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1806b23 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Frontend Development Server", + "type": "shell", + "command": "npm run dev:local", + "group": "build", + "isBackground": true, + "args": [], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Documents/API_SCHEMA.md b/Documents/API_SCHEMA.md new file mode 100644 index 0000000..8bb7d6b --- /dev/null +++ b/Documents/API_SCHEMA.md @@ -0,0 +1,1023 @@ +# 🔌 API Schema & Data Models Documentation + +## 📋 Table of Contents +1. [API Overview](#api-overview) +2. [Authentication Schema](#authentication-schema) +3. [Parking Service Schema](#parking-service-schema) +4. [Routing Service Schema](#routing-service-schema) +5. [User Management Schema](#user-management-schema) +6. [Real-time Events Schema](#real-time-events-schema) +7. [Error Handling Schema](#error-handling-schema) + +--- + +## 🌐 API Overview + +### Base Configuration +```json +{ + "apiVersion": "v1", + "baseUrl": "http://localhost:3001/api", + "contentType": "application/json", + "authentication": "Bearer JWT", + "rateLimit": "100 requests/minute per user", + "timeout": "30 seconds" +} +``` + +### HTTP Status Codes +```json +{ + "success": { + "200": "OK - Request successful", + "201": "Created - Resource created successfully", + "204": "No Content - Request successful, no response body" + }, + "clientError": { + "400": "Bad Request - Invalid request data", + "401": "Unauthorized - Authentication required", + "403": "Forbidden - Insufficient permissions", + "404": "Not Found - Resource not found", + "409": "Conflict - Resource already exists", + "422": "Unprocessable Entity - Validation failed", + "429": "Too Many Requests - Rate limit exceeded" + }, + "serverError": { + "500": "Internal Server Error - Server error", + "502": "Bad Gateway - External service error", + "503": "Service Unavailable - Service temporarily down" + } +} +``` + +--- + +## 🔐 Authentication Schema + +### Login Request +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "LoginRequest", + "required": ["email", "password"], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "User's email address", + "example": "user@example.com" + }, + "password": { + "type": "string", + "minLength": 8, + "maxLength": 128, + "description": "User's password", + "example": "securePassword123" + }, + "rememberMe": { + "type": "boolean", + "default": false, + "description": "Extended session duration" + } + } +} +``` + +### Login Response +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "LoginResponse", + "required": ["user", "tokens"], + "properties": { + "user": { + "$ref": "#/definitions/UserProfile" + }, + "tokens": { + "type": "object", + "required": ["accessToken", "refreshToken"], + "properties": { + "accessToken": { + "type": "string", + "description": "JWT access token", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "refreshToken": { + "type": "string", + "description": "JWT refresh token", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "expiresIn": { + "type": "integer", + "description": "Access token expiration in seconds", + "example": 3600 + } + } + } + } +} +``` + +### Registration Request +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "RegistrationRequest", + "required": ["email", "password", "fullName"], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "User's email address" + }, + "password": { + "type": "string", + "minLength": 8, + "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)", + "description": "Password with at least 1 lowercase, 1 uppercase, 1 digit" + }, + "fullName": { + "type": "string", + "minLength": 2, + "maxLength": 100, + "description": "User's full name" + }, + "phone": { + "type": "string", + "pattern": "^\\+?[1-9]\\d{1,14}$", + "description": "Phone number in international format" + }, + "acceptTerms": { + "type": "boolean", + "const": true, + "description": "Must accept terms and conditions" + } + } +} +``` + +--- + +## 🅿️ Parking Service Schema + +### Parking Search Request +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "ParkingSearchRequest", + "required": ["location", "radius"], + "properties": { + "location": { + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90, + "description": "Latitude coordinate" + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180, + "description": "Longitude coordinate" + } + } + }, + "radius": { + "type": "number", + "minimum": 100, + "maximum": 50000, + "description": "Search radius in meters" + }, + "filters": { + "type": "object", + "properties": { + "maxPrice": { + "type": "number", + "minimum": 0, + "description": "Maximum price per hour" + }, + "amenities": { + "type": "array", + "items": { + "type": "string", + "enum": ["covered", "security", "electric_charging", "disabled_access", "car_wash"] + }, + "description": "Required amenities" + }, + "spotTypes": { + "type": "array", + "items": { + "type": "string", + "enum": ["regular", "disabled", "electric", "compact"] + }, + "description": "Preferred spot types" + }, + "availableOnly": { + "type": "boolean", + "default": true, + "description": "Show only available parking lots" + }, + "operatingHours": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "time", + "description": "Minimum operating start time" + }, + "to": { + "type": "string", + "format": "time", + "description": "Minimum operating end time" + } + } + } + } + }, + "sorting": { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["distance", "price", "availability", "rating"], + "default": "distance" + }, + "order": { + "type": "string", + "enum": ["asc", "desc"], + "default": "asc" + } + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + } + } + } +} +``` + +### Parking Lot Schema +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "ParkingLot", + "required": ["id", "name", "address", "coordinates", "availability", "pricing"], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Unique parking lot identifier" + }, + "name": { + "type": "string", + "description": "Parking lot name" + }, + "address": { + "type": "string", + "description": "Full street address" + }, + "coordinates": { + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + } + } + }, + "availability": { + "type": "object", + "required": ["totalSpots", "availableSpots"], + "properties": { + "totalSpots": { + "type": "integer", + "minimum": 0 + }, + "availableSpots": { + "type": "integer", + "minimum": 0 + }, + "occupancyRate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Percentage of occupied spots (0-1)" + }, + "spotBreakdown": { + "type": "object", + "properties": { + "regular": {"type": "integer", "minimum": 0}, + "disabled": {"type": "integer", "minimum": 0}, + "electric": {"type": "integer", "minimum": 0}, + "compact": {"type": "integer", "minimum": 0} + } + } + } + }, + "pricing": { + "type": "object", + "required": ["hourlyRate", "currency"], + "properties": { + "hourlyRate": { + "type": "number", + "minimum": 0, + "description": "Base hourly rate" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 currency code" + }, + "discounts": { + "type": "array", + "items": { + "$ref": "#/definitions/Discount" + } + }, + "timeBasedRates": { + "type": "object", + "properties": { + "peak": { + "type": "object", + "properties": { + "rate": {"type": "number", "minimum": 0}, + "hours": {"type": "array", "items": {"type": "string", "format": "time"}} + } + }, + "offPeak": { + "type": "object", + "properties": { + "rate": {"type": "number", "minimum": 0}, + "hours": {"type": "array", "items": {"type": "string", "format": "time"}} + } + } + } + } + } + }, + "operatingHours": { + "type": "object", + "patternProperties": { + "^(monday|tuesday|wednesday|thursday|friday|saturday|sunday)$": { + "type": "object", + "properties": { + "open": {"type": "string", "format": "time"}, + "close": {"type": "string", "format": "time"}, + "is24Hours": {"type": "boolean", "default": false}, + "isClosed": {"type": "boolean", "default": false} + } + } + } + }, + "amenities": { + "type": "array", + "items": { + "type": "string", + "enum": ["covered", "security", "electric_charging", "disabled_access", "car_wash", "valet", "cctv", "lighting"] + } + }, + "distance": { + "type": "number", + "minimum": 0, + "description": "Distance from search location in meters" + }, + "estimatedWalkTime": { + "type": "integer", + "minimum": 0, + "description": "Estimated walk time in minutes" + }, + "rating": { + "type": "object", + "properties": { + "average": { + "type": "number", + "minimum": 0, + "maximum": 5 + }, + "totalReviews": { + "type": "integer", + "minimum": 0 + } + } + }, + "images": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": {"type": "string", "format": "uri"}, + "alt": {"type": "string"}, + "type": {"type": "string", "enum": ["entrance", "interior", "signage", "amenity"]} + } + } + }, + "metadata": { + "type": "object", + "properties": { + "createdAt": {"type": "string", "format": "date-time"}, + "updatedAt": {"type": "string", "format": "date-time"}, + "isActive": {"type": "boolean"}, + "lastAvailabilityUpdate": {"type": "string", "format": "date-time"} + } + } + } +} +``` + +### Reservation Request Schema +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "ReservationRequest", + "required": ["parkingLotId", "startTime", "duration"], + "properties": { + "parkingLotId": { + "type": "string", + "format": "uuid", + "description": "ID of the parking lot to reserve" + }, + "spotType": { + "type": "string", + "enum": ["regular", "disabled", "electric", "compact"], + "default": "regular", + "description": "Preferred spot type" + }, + "startTime": { + "type": "string", + "format": "date-time", + "description": "Reservation start time (ISO 8601)" + }, + "duration": { + "type": "integer", + "minimum": 30, + "maximum": 1440, + "description": "Reservation duration in minutes" + }, + "vehicleInfo": { + "type": "object", + "properties": { + "licensePlate": { + "type": "string", + "pattern": "^[A-Z0-9-]+$", + "description": "Vehicle license plate number" + }, + "make": {"type": "string"}, + "model": {"type": "string"}, + "color": {"type": "string"} + } + }, + "paymentMethod": { + "type": "string", + "enum": ["credit_card", "debit_card", "digital_wallet", "cash"], + "description": "Preferred payment method" + }, + "specialRequests": { + "type": "string", + "maxLength": 500, + "description": "Additional requests or notes" + } + } +} +``` + +--- + +## 🗺️ Routing Service Schema + +### Route Calculation Request +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "RouteRequest", + "required": ["origin", "destination"], + "properties": { + "origin": { + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": {"type": "number", "minimum": -90, "maximum": 90}, + "longitude": {"type": "number", "minimum": -180, "maximum": 180}, + "address": {"type": "string", "description": "Optional address for display"} + } + }, + "destination": { + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": {"type": "number", "minimum": -90, "maximum": 90}, + "longitude": {"type": "number", "minimum": -180, "maximum": 180}, + "address": {"type": "string", "description": "Optional address for display"} + } + }, + "transportMode": { + "type": "string", + "enum": ["driving", "walking", "cycling", "public_transit"], + "default": "driving", + "description": "Mode of transportation" + }, + "preferences": { + "type": "object", + "properties": { + "avoidTolls": {"type": "boolean", "default": false}, + "avoidHighways": {"type": "boolean", "default": false}, + "avoidFerries": {"type": "boolean", "default": false}, + "routeType": { + "type": "string", + "enum": ["fastest", "shortest", "balanced"], + "default": "fastest" + } + } + }, + "waypoints": { + "type": "array", + "items": { + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": {"type": "number", "minimum": -90, "maximum": 90}, + "longitude": {"type": "number", "minimum": -180, "maximum": 180}, + "stopDuration": {"type": "integer", "minimum": 0, "description": "Stop duration in minutes"} + } + } + }, + "alternatives": { + "type": "boolean", + "default": false, + "description": "Include alternative routes" + } + } +} +``` + +### Route Response Schema +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "RouteResponse", + "required": ["routes", "summary"], + "properties": { + "routes": { + "type": "array", + "items": { + "type": "object", + "required": ["geometry", "legs", "summary"], + "properties": { + "geometry": { + "type": "object", + "required": ["coordinates"], + "properties": { + "type": {"type": "string", "const": "LineString"}, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "items": {"type": "number"}, + "minItems": 2, + "maxItems": 2 + }, + "description": "Array of [longitude, latitude] coordinates" + } + } + }, + "legs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "distance": {"type": "number", "description": "Distance in meters"}, + "duration": {"type": "number", "description": "Duration in seconds"}, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/RouteStep" + } + } + } + } + }, + "summary": { + "type": "object", + "required": ["distance", "duration"], + "properties": { + "distance": {"type": "number", "description": "Total distance in meters"}, + "duration": {"type": "number", "description": "Total duration in seconds"}, + "trafficDelay": {"type": "number", "description": "Additional time due to traffic"}, + "tollCost": {"type": "number", "description": "Estimated toll cost"}, + "fuelCost": {"type": "number", "description": "Estimated fuel cost"} + } + } + } + } + }, + "waypoints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "location": { + "type": "array", + "items": {"type": "number"}, + "minItems": 2, + "maxItems": 2 + }, + "name": {"type": "string"}, + "waypointIndex": {"type": "integer"} + } + } + }, + "summary": { + "type": "object", + "properties": { + "totalDistance": {"type": "string", "description": "Human-readable distance"}, + "totalTime": {"type": "string", "description": "Human-readable duration"}, + "estimatedCost": {"type": "number", "description": "Total estimated cost"}, + "routeQuality": { + "type": "string", + "enum": ["excellent", "good", "fair", "poor"], + "description": "Route quality assessment" + } + } + } + } +} +``` + +--- + +## 👤 User Management Schema + +### User Profile Schema +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "UserProfile", + "required": ["id", "email", "fullName"], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Unique user identifier" + }, + "email": { + "type": "string", + "format": "email", + "description": "User's email address" + }, + "fullName": { + "type": "string", + "description": "User's full name" + }, + "phone": { + "type": "string", + "pattern": "^\\+?[1-9]\\d{1,14}$", + "description": "Phone number in international format" + }, + "avatar": { + "type": "string", + "format": "uri", + "description": "URL to user's avatar image" + }, + "preferences": { + "type": "object", + "properties": { + "defaultTransportMode": { + "type": "string", + "enum": ["driving", "walking", "cycling", "public_transit"] + }, + "defaultSearchRadius": { + "type": "number", + "minimum": 100, + "maximum": 50000 + }, + "preferredAmenities": { + "type": "array", + "items": {"type": "string"} + }, + "notifications": { + "type": "object", + "properties": { + "email": {"type": "boolean", "default": true}, + "push": {"type": "boolean", "default": true}, + "sms": {"type": "boolean", "default": false}, + "reservationReminders": {"type": "boolean", "default": true}, + "priceAlerts": {"type": "boolean", "default": false} + } + }, + "language": { + "type": "string", + "pattern": "^[a-z]{2}(-[A-Z]{2})?$", + "default": "en-US" + }, + "timezone": { + "type": "string", + "description": "IANA timezone identifier" + } + } + }, + "membership": { + "type": "object", + "properties": { + "tier": { + "type": "string", + "enum": ["basic", "premium", "enterprise"] + }, + "joinDate": {"type": "string", "format": "date-time"}, + "expiryDate": {"type": "string", "format": "date-time"}, + "benefits": { + "type": "array", + "items": {"type": "string"} + } + } + }, + "statistics": { + "type": "object", + "properties": { + "totalReservations": {"type": "integer", "minimum": 0}, + "totalSpent": {"type": "number", "minimum": 0}, + "favoriteLocations": { + "type": "array", + "items": {"type": "string", "format": "uuid"} + }, + "averageRating": {"type": "number", "minimum": 0, "maximum": 5} + } + }, + "metadata": { + "type": "object", + "properties": { + "createdAt": {"type": "string", "format": "date-time"}, + "updatedAt": {"type": "string", "format": "date-time"}, + "lastLoginAt": {"type": "string", "format": "date-time"}, + "isActive": {"type": "boolean", "default": true}, + "emailVerified": {"type": "boolean", "default": false}, + "phoneVerified": {"type": "boolean", "default": false} + } + } + } +} +``` + +--- + +## ⚡ Real-time Events Schema + +### WebSocket Event Types +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "WebSocketEvent", + "required": ["type", "timestamp"], + "properties": { + "type": { + "type": "string", + "enum": [ + "parking_availability_changed", + "reservation_confirmed", + "reservation_cancelled", + "route_updated", + "price_changed", + "user_location_updated" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "data": { + "type": "object", + "description": "Event-specific data payload" + } + } +} +``` + +### Parking Availability Event +```json +{ + "type": "parking_availability_changed", + "timestamp": "2024-08-03T10:30:00Z", + "data": { + "parkingLotId": "uuid", + "availableSpots": 15, + "totalSpots": 50, + "spotBreakdown": { + "regular": 12, + "disabled": 2, + "electric": 1, + "compact": 0 + }, + "lastUpdated": "2024-08-03T10:30:00Z" + } +} +``` + +--- + +## ❌ Error Handling Schema + +### Standard Error Response +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "ErrorResponse", + "required": ["error", "timestamp"], + "properties": { + "error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "object", + "description": "Additional error context" + }, + "field": { + "type": "string", + "description": "Field name for validation errors" + } + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "path": { + "type": "string", + "description": "API endpoint that caused the error" + }, + "requestId": { + "type": "string", + "description": "Unique request identifier for tracking" + } + } +} +``` + +### Validation Error Response +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Input validation failed", + "details": { + "violations": [ + { + "field": "email", + "message": "Must be a valid email address", + "rejectedValue": "invalid-email" + }, + { + "field": "password", + "message": "Must be at least 8 characters long", + "rejectedValue": "123" + } + ] + } + }, + "timestamp": "2024-08-03T10:30:00Z", + "path": "/api/auth/register", + "requestId": "req_123456789" +} +``` + +--- + +## 📊 Common Definitions + +### Discount Schema +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Discount", + "required": ["type", "value"], + "properties": { + "type": { + "type": "string", + "enum": ["percentage", "fixed_amount", "free_hours"] + }, + "value": {"type": "number", "minimum": 0}, + "description": {"type": "string"}, + "conditions": { + "type": "object", + "properties": { + "minimumDuration": {"type": "integer", "minimum": 0}, + "validDays": { + "type": "array", + "items": { + "type": "string", + "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + } + }, + "validHours": { + "type": "object", + "properties": { + "from": {"type": "string", "format": "time"}, + "to": {"type": "string", "format": "time"} + } + } + } + } + } +} +``` + +### Route Step Schema +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "RouteStep", + "required": ["instruction", "distance", "duration"], + "properties": { + "instruction": { + "type": "string", + "description": "Turn-by-turn navigation instruction" + }, + "distance": { + "type": "number", + "description": "Step distance in meters" + }, + "duration": { + "type": "number", + "description": "Step duration in seconds" + }, + "maneuver": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["turn", "merge", "roundabout", "continue", "arrive"] + }, + "modifier": { + "type": "string", + "enum": ["left", "right", "straight", "slight_left", "slight_right", "sharp_left", "sharp_right"] + }, + "bearingBefore": {"type": "number", "minimum": 0, "maximum": 360}, + "bearingAfter": {"type": "number", "minimum": 0, "maximum": 360} + } + }, + "geometry": { + "type": "object", + "properties": { + "coordinates": { + "type": "array", + "items": { + "type": "array", + "items": {"type": "number"}, + "minItems": 2, + "maxItems": 2 + } + } + } + }, + "streetName": {"type": "string"}, + "speedLimit": {"type": "integer", "minimum": 0} + } +} +``` + +--- + +*Last Updated: August 3, 2025* +*Version: 1.0.0* diff --git a/Documents/DATABASE_DESIGN.md b/Documents/DATABASE_DESIGN.md new file mode 100644 index 0000000..f5bee6d --- /dev/null +++ b/Documents/DATABASE_DESIGN.md @@ -0,0 +1,839 @@ +# 🗄️ Database Design & Data Structure Documentation + +## 📋 Table of Contents +1. [Database Overview](#database-overview) +2. [Entity Relationship Diagram](#entity-relationship-diagram) +3. [Table Specifications](#table-specifications) +4. [Indexes & Performance](#indexes--performance) +5. [Data Migration Strategy](#data-migration-strategy) +6. [Backup & Recovery](#backup--recovery) +7. [Scaling Considerations](#scaling-considerations) + +--- + +## 🌐 Database Overview + +### Technology Stack +```json +{ + "primary_database": "PostgreSQL 15.x", + "cache_layer": "Redis 7.x", + "orm": "TypeORM", + "connection_pooling": "pg_pool", + "backup_strategy": "Continuous WAL archiving", + "monitoring": "PostgreSQL Stats + Custom metrics" +} +``` + +### Database Characteristics +- **ACID Compliance**: Full ACID transaction support +- **Concurrency**: Multi-version concurrency control (MVCC) +- **Extensibility**: PostGIS for spatial operations +- **Performance**: Optimized indexes for geospatial queries +- **Scalability**: Read replicas and partitioning strategies + +--- + +## 🔗 Entity Relationship Diagram + +```mermaid +erDiagram + Users ||--o{ Reservations : makes + Users ||--o{ Reviews : writes + Users ||--o{ Favorites : has + Users ||--o{ Payments : processes + + ParkingLots ||--o{ ParkingSpots : contains + ParkingLots ||--o{ Reservations : receives + ParkingLots ||--o{ Reviews : receives + ParkingLots ||--o{ Favorites : featured_in + ParkingLots ||--o{ PricingRules : has + ParkingLots }|--|| Operators : managed_by + + ParkingSpots ||--o{ Reservations : reserved_for + ParkingSpots ||--o{ SpotHistory : tracks + + Reservations ||--|| Payments : triggers + Reservations ||--o{ ReservationHistory : logs + + Operators ||--o{ ParkingLots : manages + Operators ||--o{ OperatorUsers : employs + + Users { + uuid id PK + string email UK + string password_hash + string full_name + string phone + timestamp created_at + timestamp updated_at + boolean is_active + user_role role + } + + ParkingLots { + uuid id PK + string name + text address + decimal latitude + decimal longitude + integer total_spots + integer available_spots + decimal price_per_hour + jsonb operating_hours + text[] amenities + uuid operator_id FK + timestamp created_at + timestamp updated_at + boolean is_active + } + + ParkingSpots { + uuid id PK + uuid parking_lot_id FK + string spot_number + spot_type type + boolean is_occupied + boolean is_reserved + timestamp last_updated + } + + Reservations { + uuid id PK + uuid user_id FK + uuid parking_spot_id FK + timestamp start_time + timestamp end_time + decimal total_cost + reservation_status status + timestamp created_at + timestamp updated_at + } +``` + +--- + +## 📊 Table Specifications + +### 1. Users Table + +```sql +CREATE TYPE user_role AS ENUM ('customer', 'operator', 'admin'); + +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL, + phone VARCHAR(20), + avatar_url TEXT, + + -- Preferences stored as JSONB for flexibility + preferences JSONB DEFAULT '{}', + + -- Metadata + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP WITH TIME ZONE, + + -- Status fields + is_active BOOLEAN DEFAULT TRUE, + email_verified BOOLEAN DEFAULT FALSE, + phone_verified BOOLEAN DEFAULT FALSE, + role user_role DEFAULT 'customer', + + -- Constraints + CONSTRAINT valid_email CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + CONSTRAINT valid_phone CHECK (phone IS NULL OR phone ~* '^\+?[1-9]\d{1,14}$') +); + +-- Indexes +CREATE INDEX idx_users_email ON users (email); +CREATE INDEX idx_users_role_active ON users (role, is_active); +CREATE INDEX idx_users_created_at ON users (created_at); + +-- Update trigger +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON users + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); +``` + +### 2. Operators Table + +```sql +CREATE TABLE operators ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + company_name VARCHAR(255) NOT NULL, + contact_email VARCHAR(255) NOT NULL, + contact_phone VARCHAR(20), + address TEXT, + + -- Business information + business_license VARCHAR(100), + tax_id VARCHAR(50), + + -- Settings + commission_rate DECIMAL(5,4) DEFAULT 0.1000, -- 10% default + auto_approval BOOLEAN DEFAULT FALSE, + + -- Metadata + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE, + + CONSTRAINT valid_commission_rate CHECK (commission_rate >= 0 AND commission_rate <= 1) +); + +-- Indexes +CREATE INDEX idx_operators_active ON operators (is_active); +CREATE INDEX idx_operators_company_name ON operators (company_name); +``` + +### 3. Parking Lots Table + +```sql +CREATE TABLE parking_lots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + operator_id UUID REFERENCES operators(id) ON DELETE CASCADE, + + -- Basic information + name VARCHAR(255) NOT NULL, + description TEXT, + address TEXT NOT NULL, + + -- Geolocation (using decimal for precision) + latitude DECIMAL(10,8) NOT NULL, + longitude DECIMAL(11,8) NOT NULL, + + -- Capacity + total_spots INTEGER NOT NULL DEFAULT 0, + available_spots INTEGER NOT NULL DEFAULT 0, + + -- Pricing + price_per_hour DECIMAL(10,2) NOT NULL, + currency CHAR(3) DEFAULT 'USD', + + -- Operating hours stored as JSONB for flexibility + operating_hours JSONB DEFAULT '{ + "monday": {"open": "00:00", "close": "23:59", "is24Hours": true}, + "tuesday": {"open": "00:00", "close": "23:59", "is24Hours": true}, + "wednesday": {"open": "00:00", "close": "23:59", "is24Hours": true}, + "thursday": {"open": "00:00", "close": "23:59", "is24Hours": true}, + "friday": {"open": "00:00", "close": "23:59", "is24Hours": true}, + "saturday": {"open": "00:00", "close": "23:59", "is24Hours": true}, + "sunday": {"open": "00:00", "close": "23:59", "is24Hours": true} + }', + + -- Amenities as array + amenities TEXT[] DEFAULT '{}', + + -- Images + images JSONB DEFAULT '[]', + + -- Ratings + average_rating DECIMAL(3,2) DEFAULT 0, + total_reviews INTEGER DEFAULT 0, + + -- Metadata + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_availability_update TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE, + + -- Constraints + CONSTRAINT valid_coordinates CHECK ( + latitude BETWEEN -90 AND 90 AND + longitude BETWEEN -180 AND 180 + ), + CONSTRAINT valid_capacity CHECK ( + total_spots >= 0 AND + available_spots >= 0 AND + available_spots <= total_spots + ), + CONSTRAINT valid_price CHECK (price_per_hour >= 0), + CONSTRAINT valid_rating CHECK (average_rating BETWEEN 0 AND 5) +); + +-- Spatial index for location-based queries +CREATE INDEX idx_parking_lots_location ON parking_lots +USING GIST (ll_to_earth(latitude, longitude)); + +-- Regular indexes +CREATE INDEX idx_parking_lots_operator ON parking_lots (operator_id); +CREATE INDEX idx_parking_lots_active ON parking_lots (is_active); +CREATE INDEX idx_parking_lots_price ON parking_lots (price_per_hour); +CREATE INDEX idx_parking_lots_availability ON parking_lots (available_spots, is_active); +CREATE INDEX idx_parking_lots_rating ON parking_lots (average_rating DESC); + +-- Composite index for common queries +CREATE INDEX idx_parking_lots_active_location ON parking_lots (is_active, latitude, longitude); +``` + +### 4. Parking Spots Table + +```sql +CREATE TYPE spot_type AS ENUM ('regular', 'disabled', 'electric', 'compact', 'motorcycle'); + +CREATE TABLE parking_spots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + parking_lot_id UUID REFERENCES parking_lots(id) ON DELETE CASCADE, + + -- Spot identification + spot_number VARCHAR(20) NOT NULL, + floor_level INTEGER DEFAULT 0, + section VARCHAR(10), + + -- Spot characteristics + type spot_type DEFAULT 'regular', + size_category VARCHAR(20) DEFAULT 'standard', -- compact, standard, large + + -- Status + is_occupied BOOLEAN DEFAULT FALSE, + is_reserved BOOLEAN DEFAULT FALSE, + is_maintenance BOOLEAN DEFAULT FALSE, + + -- Timestamps + last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + occupied_since TIMESTAMP WITH TIME ZONE, + + -- Constraints + UNIQUE(parking_lot_id, spot_number), + CONSTRAINT valid_floor CHECK (floor_level BETWEEN -10 AND 50) +); + +-- Indexes +CREATE INDEX idx_parking_spots_lot ON parking_spots (parking_lot_id); +CREATE INDEX idx_parking_spots_status ON parking_spots (is_occupied, is_reserved, is_maintenance); +CREATE INDEX idx_parking_spots_type ON parking_spots (type); +CREATE INDEX idx_parking_spots_availability ON parking_spots (parking_lot_id, is_occupied, is_reserved, is_maintenance); +``` + +### 5. Reservations Table + +```sql +CREATE TYPE reservation_status AS ENUM ( + 'pending', + 'confirmed', + 'active', + 'completed', + 'cancelled', + 'no_show', + 'expired' +); + +CREATE TABLE reservations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + parking_spot_id UUID REFERENCES parking_spots(id) ON DELETE CASCADE, + + -- Timing + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + end_time TIMESTAMP WITH TIME ZONE NOT NULL, + actual_start_time TIMESTAMP WITH TIME ZONE, + actual_end_time TIMESTAMP WITH TIME ZONE, + + -- Pricing + base_cost DECIMAL(10,2) NOT NULL, + discount_amount DECIMAL(10,2) DEFAULT 0, + tax_amount DECIMAL(10,2) DEFAULT 0, + total_cost DECIMAL(10,2) NOT NULL, + currency CHAR(3) DEFAULT 'USD', + + -- Vehicle information + vehicle_info JSONB DEFAULT '{}', + + -- Status and notes + status reservation_status DEFAULT 'pending', + special_requests TEXT, + cancellation_reason TEXT, + + -- Metadata + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + confirmation_code VARCHAR(10) UNIQUE, + + -- Constraints + CONSTRAINT valid_duration CHECK (end_time > start_time), + CONSTRAINT valid_costs CHECK ( + base_cost >= 0 AND + discount_amount >= 0 AND + tax_amount >= 0 AND + total_cost >= 0 + ) +); + +-- Generate confirmation code +CREATE OR REPLACE FUNCTION generate_confirmation_code() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.confirmation_code IS NULL THEN + NEW.confirmation_code := upper(substring(md5(random()::text) from 1 for 8)); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER set_confirmation_code + BEFORE INSERT ON reservations + FOR EACH ROW + EXECUTE FUNCTION generate_confirmation_code(); + +-- Indexes +CREATE INDEX idx_reservations_user ON reservations (user_id); +CREATE INDEX idx_reservations_spot ON reservations (parking_spot_id); +CREATE INDEX idx_reservations_status ON reservations (status); +CREATE INDEX idx_reservations_time_range ON reservations (start_time, end_time); +CREATE INDEX idx_reservations_confirmation ON reservations (confirmation_code); + +-- Composite indexes for common queries +CREATE INDEX idx_reservations_user_status ON reservations (user_id, status); +CREATE INDEX idx_reservations_spot_time ON reservations (parking_spot_id, start_time, end_time); +``` + +### 6. Payments Table + +```sql +CREATE TYPE payment_method AS ENUM ('credit_card', 'debit_card', 'digital_wallet', 'bank_transfer', 'cash'); +CREATE TYPE payment_status AS ENUM ('pending', 'processing', 'completed', 'failed', 'refunded', 'disputed'); + +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + reservation_id UUID REFERENCES reservations(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + + -- Payment details + amount DECIMAL(10,2) NOT NULL, + currency CHAR(3) DEFAULT 'USD', + method payment_method NOT NULL, + status payment_status DEFAULT 'pending', + + -- External payment provider information + provider_name VARCHAR(50), -- stripe, paypal, etc. + provider_transaction_id VARCHAR(255), + provider_fee DECIMAL(10,2) DEFAULT 0, + + -- Payment metadata + payment_date TIMESTAMP WITH TIME ZONE, + processed_at TIMESTAMP WITH TIME ZONE, + failure_reason TEXT, + refund_amount DECIMAL(10,2) DEFAULT 0, + refund_date TIMESTAMP WITH TIME ZONE, + + -- Metadata + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT valid_amount CHECK (amount > 0), + CONSTRAINT valid_refund CHECK (refund_amount >= 0 AND refund_amount <= amount) +); + +-- Indexes +CREATE INDEX idx_payments_reservation ON payments (reservation_id); +CREATE INDEX idx_payments_user ON payments (user_id); +CREATE INDEX idx_payments_status ON payments (status); +CREATE INDEX idx_payments_method ON payments (method); +CREATE INDEX idx_payments_provider ON payments (provider_name, provider_transaction_id); +CREATE INDEX idx_payments_date ON payments (payment_date); +``` + +### 7. Reviews Table + +```sql +CREATE TABLE reviews ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + parking_lot_id UUID REFERENCES parking_lots(id) ON DELETE CASCADE, + reservation_id UUID REFERENCES reservations(id) ON DELETE SET NULL, + + -- Review content + rating INTEGER NOT NULL, + title VARCHAR(255), + comment TEXT, + + -- Review metadata + is_verified BOOLEAN DEFAULT FALSE, -- verified if linked to actual reservation + is_flagged BOOLEAN DEFAULT FALSE, + admin_response TEXT, + + -- Timestamps + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + -- Constraints + CONSTRAINT valid_rating CHECK (rating BETWEEN 1 AND 5), + UNIQUE(user_id, parking_lot_id) -- One review per user per parking lot +); + +-- Indexes +CREATE INDEX idx_reviews_parking_lot ON reviews (parking_lot_id); +CREATE INDEX idx_reviews_user ON reviews (user_id); +CREATE INDEX idx_reviews_rating ON reviews (rating); +CREATE INDEX idx_reviews_verified ON reviews (is_verified); +CREATE INDEX idx_reviews_created_at ON reviews (created_at); +``` + +--- + +## 🚀 Indexes & Performance + +### 1. Spatial Indexes for Location Queries + +```sql +-- Enable PostGIS extension for advanced spatial operations +CREATE EXTENSION IF NOT EXISTS postgis; + +-- Convert existing lat/lng to geometry for better performance +ALTER TABLE parking_lots ADD COLUMN geom GEOMETRY(POINT, 4326); + +-- Populate geometry column +UPDATE parking_lots SET geom = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326); + +-- Create spatial index +CREATE INDEX idx_parking_lots_geom ON parking_lots USING GIST (geom); + +-- Function for distance-based queries +CREATE OR REPLACE FUNCTION find_nearby_parking( + user_lat DECIMAL, + user_lng DECIMAL, + radius_meters INTEGER DEFAULT 5000 +) RETURNS TABLE( + id UUID, + name VARCHAR, + distance_meters DECIMAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + pl.id, + pl.name, + ST_Distance( + ST_SetSRID(ST_MakePoint(user_lng, user_lat), 4326)::geography, + pl.geom::geography + ) as distance_meters + FROM parking_lots pl + WHERE pl.is_active = TRUE + AND ST_DWithin( + ST_SetSRID(ST_MakePoint(user_lng, user_lat), 4326)::geography, + pl.geom::geography, + radius_meters + ) + ORDER BY distance_meters; +END; +$$ LANGUAGE plpgsql; +``` + +### 2. Partitioning Strategy + +```sql +-- Partition reservations by month for better performance +CREATE TABLE reservations_partitioned ( + LIKE reservations INCLUDING ALL +) PARTITION BY RANGE (start_time); + +-- Create partitions for current and future months +CREATE TABLE reservations_2024_08 PARTITION OF reservations_partitioned +FOR VALUES FROM ('2024-08-01') TO ('2024-09-01'); + +CREATE TABLE reservations_2024_09 PARTITION OF reservations_partitioned +FOR VALUES FROM ('2024-09-01') TO ('2024-10-01'); + +-- Automated partition creation function +CREATE OR REPLACE FUNCTION create_monthly_partition() +RETURNS void AS $$ +DECLARE + start_date DATE; + end_date DATE; + table_name TEXT; +BEGIN + start_date := DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month'); + end_date := start_date + INTERVAL '1 month'; + table_name := 'reservations_' || TO_CHAR(start_date, 'YYYY_MM'); + + EXECUTE FORMAT( + 'CREATE TABLE %I PARTITION OF reservations_partitioned + FOR VALUES FROM (%L) TO (%L)', + table_name, start_date, end_date + ); +END; +$$ LANGUAGE plpgsql; + +-- Schedule monthly partition creation +SELECT cron.schedule('create-partition', '0 0 1 * *', 'SELECT create_monthly_partition();'); +``` + +### 3. Performance Monitoring Views + +```sql +-- View for monitoring table sizes +CREATE VIEW table_sizes AS +SELECT + schemaname, + tablename, + attname, + n_distinct, + correlation, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size +FROM pg_stats +WHERE schemaname = 'public' +ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; + +-- View for monitoring index usage +CREATE VIEW index_usage AS +SELECT + schemaname, + tablename, + attname, + n_distinct, + correlation +FROM pg_stats +WHERE schemaname = 'public'; + +-- View for monitoring slow queries +CREATE VIEW slow_queries AS +SELECT + query, + calls, + total_time, + mean_time, + rows +FROM pg_stat_statements +WHERE mean_time > 100 +ORDER BY mean_time DESC; +``` + +--- + +## 🔄 Data Migration Strategy + +### 1. Migration Scripts Structure + +```sql +-- Migration: 001_initial_schema.sql +-- Description: Create initial database schema +-- Date: 2024-08-03 +-- Author: System + +BEGIN; + +-- Create enums +CREATE TYPE user_role AS ENUM ('customer', 'operator', 'admin'); +CREATE TYPE spot_type AS ENUM ('regular', 'disabled', 'electric', 'compact', 'motorcycle'); +-- ... rest of schema creation + +-- Create migration tracking table +CREATE TABLE IF NOT EXISTS schema_migrations ( + version VARCHAR(255) PRIMARY KEY, + applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO schema_migrations (version) VALUES ('001_initial_schema'); + +COMMIT; +``` + +### 2. Data Seeding + +```sql +-- Seed: 001_default_data.sql +-- Description: Insert default operational data +-- Date: 2024-08-03 + +BEGIN; + +-- Insert default operator +INSERT INTO operators (id, company_name, contact_email, contact_phone, is_active) +VALUES ( + 'b123b123-b123-b123-b123-b123b123b123', + 'City Parking Management', + 'admin@cityparking.com', + '+1234567890', + TRUE +); + +-- Insert sample parking lots +INSERT INTO parking_lots ( + id, operator_id, name, address, latitude, longitude, + total_spots, available_spots, price_per_hour, amenities +) VALUES +( + 'a123a123-a123-a123-a123-a123a123a123', + 'b123b123-b123-b123-b123-b123b123b123', + 'Downtown Parking Garage', + '123 Main St, Downtown', + 40.7128, + -74.0060, + 200, + 150, + 5.00, + ARRAY['covered', 'security', 'electric_charging'] +); + +COMMIT; +``` + +### 3. Backup Strategy + +```bash +#!/bin/bash +# backup_database.sh +# Automated database backup script + +DB_NAME="smart_parking" +BACKUP_DIR="/backups/postgresql" +DATE=$(date +%Y%m%d_%H%M%S) + +# Create full backup +pg_dump -h localhost -U postgres -d $DB_NAME \ + --verbose --format=custom \ + --file="$BACKUP_DIR/full_backup_$DATE.backup" + +# Create schema-only backup +pg_dump -h localhost -U postgres -d $DB_NAME \ + --schema-only --verbose \ + --file="$BACKUP_DIR/schema_backup_$DATE.sql" + +# Cleanup old backups (keep last 30 days) +find $BACKUP_DIR -name "*.backup" -mtime +30 -delete +find $BACKUP_DIR -name "*.sql" -mtime +30 -delete +``` + +--- + +## 📈 Scaling Considerations + +### 1. Read Replicas Configuration + +```sql +-- Master database configuration +-- postgresql.conf +wal_level = replica +max_wal_senders = 3 +wal_keep_segments = 64 +hot_standby = on + +-- Replica database queries +-- Route read-only queries to replicas +CREATE OR REPLACE FUNCTION is_read_only_query(query_text TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + RETURN query_text ~* '^(SELECT|WITH)' AND + query_text !~* '(FOR UPDATE|FOR SHARE)'; +END; +$$ LANGUAGE plpgsql; +``` + +### 2. Connection Pooling + +```javascript +// Database connection pool configuration +const poolConfig = { + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + + // Pool configuration + min: 5, // Minimum connections + max: 50, // Maximum connections + idle: 10000, // Idle timeout (10 seconds) + acquire: 60000, // Acquire timeout (60 seconds) + evict: 1000, // Eviction run interval (1 second) + + // Connection validation + validate: true, + validateTimeout: 3000, + + // Logging + logging: (sql, timing) => { + if (timing > 1000) { + console.warn(`Slow query detected: ${timing}ms - ${sql}`); + } + } +}; +``` + +### 3. Caching Strategy + +```sql +-- Materialized views for expensive aggregations +CREATE MATERIALIZED VIEW parking_lot_stats AS +SELECT + pl.id, + pl.name, + pl.total_spots, + pl.available_spots, + ROUND(AVG(r.rating), 2) as avg_rating, + COUNT(r.id) as total_reviews, + COUNT(res.id) as total_reservations +FROM parking_lots pl +LEFT JOIN reviews r ON pl.id = r.parking_lot_id +LEFT JOIN reservations res ON pl.id = res.parking_spot_id +WHERE pl.is_active = TRUE +GROUP BY pl.id, pl.name, pl.total_spots, pl.available_spots; + +-- Refresh materialized view regularly +CREATE OR REPLACE FUNCTION refresh_parking_stats() +RETURNS void AS $$ +BEGIN + REFRESH MATERIALIZED VIEW CONCURRENTLY parking_lot_stats; +END; +$$ LANGUAGE plpgsql; + +-- Schedule refresh every hour +SELECT cron.schedule('refresh-stats', '0 * * * *', 'SELECT refresh_parking_stats();'); +``` + +### 4. Database Monitoring + +```sql +-- Create monitoring functions +CREATE OR REPLACE FUNCTION database_health_check() +RETURNS TABLE( + metric VARCHAR, + value DECIMAL, + status VARCHAR +) AS $$ +BEGIN + RETURN QUERY + WITH metrics AS ( + SELECT 'active_connections' as metric, + COUNT(*)::DECIMAL as value, + CASE WHEN COUNT(*) > 80 THEN 'WARNING' ELSE 'OK' END as status + FROM pg_stat_activity + WHERE state = 'active' + + UNION ALL + + SELECT 'cache_hit_ratio' as metric, + ROUND( + 100.0 * sum(blks_hit) / (sum(blks_hit) + sum(blks_read)), 2 + ) as value, + CASE WHEN 100.0 * sum(blks_hit) / (sum(blks_hit) + sum(blks_read)) < 95 + THEN 'WARNING' ELSE 'OK' END as status + FROM pg_stat_database + + UNION ALL + + SELECT 'table_bloat' as metric, + pg_size_pretty(pg_database_size(current_database()))::DECIMAL as value, + 'INFO' as status + ) + SELECT * FROM metrics; +END; +$$ LANGUAGE plpgsql; +``` + +--- + +*Last Updated: August 3, 2025* +*Version: 1.0.0* diff --git a/Documents/README.md b/Documents/README.md new file mode 100644 index 0000000..7742195 --- /dev/null +++ b/Documents/README.md @@ -0,0 +1,451 @@ +# 📚 Smart Parking Finder - Complete Documentation Index + +## 🏗️ Project Overview + +**Smart Parking Finder** is a comprehensive real-time parking management system that revolutionizes urban parking through intelligent location-based services, interactive mapping, and seamless user experience. + +### 🎯 Project Vision +Transform urban parking from a frustrating experience into a streamlined, intelligent process that saves time, reduces traffic congestion, and optimizes parking resource utilization. + +--- + +## 📖 Documentation Structure + +### 📁 Core Documentation + +| Document | Description | Purpose | +|----------|-------------|---------| +| **[LOCAL_DEPLOYMENT_GUIDE.md](./LOCAL_DEPLOYMENT_GUIDE.md)** | Complete deployment instructions | Get the system running locally | +| **[SYSTEM_ARCHITECTURE.md](./SYSTEM_ARCHITECTURE.md)** | System design & architecture patterns | Understand how the system works | +| **[API_SCHEMA.md](./API_SCHEMA.md)** | Complete API documentation & schemas | API integration & development | +| **[DATABASE_DESIGN.md](./DATABASE_DESIGN.md)** | Database schema & design patterns | Database development & optimization | + +### 📁 Technical Documentation + +| Document | Description | Status | +|----------|-------------|--------| +| **DEVELOPMENT.md** | Development workflow & standards | ✅ Available | +| **DEPLOYMENT.md** | Production deployment guide | ✅ Available | +| **TECHNICAL_SPECIFICATION.md** | Detailed technical specifications | ✅ Available | +| **PERFORMANCE_OPTIMIZATION_REPORT.md** | Performance analysis & optimizations | ✅ Available | +| **MAPVIEW_VERSIONS.md** | MapView component evolution | ✅ Available | + +--- + +## 🛠️ Technology Stack Summary + +### Frontend Architecture +```json +{ + "framework": "Next.js 14 (App Router)", + "runtime": "React 18", + "language": "TypeScript 5.x", + "styling": "Tailwind CSS 3.x", + "mapping": "React Leaflet + OpenStreetMap", + "state_management": "React Query (TanStack Query)", + "forms": "React Hook Form", + "ui_components": "Custom + Shadcn/ui", + "testing": "Jest + React Testing Library" +} +``` + +### Backend Architecture +```json +{ + "framework": "NestJS 10", + "runtime": "Node.js 18+", + "language": "TypeScript 5.x", + "database": "PostgreSQL 15 + PostGIS", + "cache": "Redis 7", + "orm": "TypeORM", + "authentication": "JWT + Passport", + "validation": "Class Validator", + "documentation": "Swagger/OpenAPI", + "testing": "Jest + Supertest" +} +``` + +### Infrastructure & DevOps +```json +{ + "containerization": "Docker + Docker Compose", + "routing_engine": "Valhalla Routing Engine", + "mapping_data": "OpenStreetMap", + "process_management": "PM2", + "monitoring": "Health Check Endpoints", + "logging": "Winston Logger" +} +``` + +--- + +## 🚀 Quick Start Guide + +### 1. Prerequisites Check +```bash +# Verify Node.js version +node --version # Should be 18+ + +# Verify npm version +npm --version # Should be 8+ + +# Verify Docker (optional) +docker --version +``` + +### 2. Fastest Setup (Frontend Only) +```bash +# Clone and start frontend +git clone +cd Website_Demo_App +./launch.sh +# Choose option 2: Quick demo +``` + +### 3. Full Development Setup +```bash +# Start complete development environment +./launch.sh +# Choose option 3: Full development +``` + +### 4. Production-like Setup +```bash +# Start with Docker (includes database) +./scripts/docker-dev.sh +``` + +--- + +## 🏛️ System Architecture Overview + +### High-Level Architecture +``` +┌─────────────────────────────────────────────────────────────┐ +│ Client Layer │ +├─────────────────────────────────────────────────────────────┤ +│ Next.js Frontend (React 18 + TypeScript + Tailwind CSS) │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌─────────┴─────────┐ + │ API Gateway │ + │ (NestJS Router) │ + └─────────┬─────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ +┌───────▼───────┐ ┌─────────▼─────────┐ ┌───────▼───────┐ +│ Auth Service │ │ Parking Service │ │Routing Service│ +│ (JWT) │ │ (CRUD + Search) │ │ (Valhalla) │ +└───────────────┘ └───────────────────┘ └───────────────┘ + │ │ │ + └─────────────────────┼─────────────────────┘ + │ + ┌─────────▼─────────┐ + │ Data Layer │ + │ PostgreSQL + Redis│ + └───────────────────┘ +``` + +### Core Features +- 🗺️ **Interactive Mapping**: Real-time OpenStreetMap with custom markers +- 🔍 **Smart Search**: Geolocation-based parking spot discovery +- 🧭 **Route Optimization**: Multi-modal routing with Valhalla engine +- 📱 **Responsive Design**: Mobile-first, progressive web app +- ⚡ **Real-time Updates**: WebSocket-based live availability +- 🔐 **Secure Authentication**: JWT-based user management +- 💳 **Payment Integration**: Ready for payment processor integration + +--- + +## 📊 Data Models & Schema + +### Core Entities + +#### 1. Users & Authentication +```typescript +interface User { + id: string; + email: string; + fullName: string; + phone?: string; + role: 'customer' | 'operator' | 'admin'; + preferences: UserPreferences; + metadata: UserMetadata; +} +``` + +#### 2. Parking Infrastructure +```typescript +interface ParkingLot { + id: string; + name: string; + address: string; + coordinates: GeoCoordinates; + availability: AvailabilityInfo; + pricing: PricingInfo; + operatingHours: OperatingHours; + amenities: string[]; + metadata: ParkingMetadata; +} +``` + +#### 3. Reservations & Transactions +```typescript +interface Reservation { + id: string; + userId: string; + parkingSpotId: string; + timeSlot: TimeSlot; + pricing: ReservationPricing; + status: ReservationStatus; + metadata: ReservationMetadata; +} +``` + +--- + +## 🔌 API Reference + +### Authentication Endpoints +- `POST /api/auth/login` - User authentication +- `POST /api/auth/register` - User registration +- `POST /api/auth/refresh` - Token refresh +- `GET /api/auth/profile` - User profile + +### Parking Endpoints +- `GET /api/parking/search` - Search nearby parking +- `GET /api/parking/:id` - Get parking details +- `POST /api/parking/:id/reserve` - Create reservation +- `GET /api/parking/reservations` - User reservations + +### Routing Endpoints +- `POST /api/routing/calculate` - Calculate route +- `GET /api/routing/modes` - Available transport modes + +### Health & Monitoring +- `GET /api/health` - System health check +- `GET /api/metrics` - System metrics + +--- + +## 🗄️ Database Schema Summary + +### Primary Tables +1. **users** - User accounts and profiles +2. **operators** - Parking lot operators/managers +3. **parking_lots** - Physical parking locations +4. **parking_spots** - Individual parking spaces +5. **reservations** - Booking records +6. **payments** - Transaction records +7. **reviews** - User feedback and ratings + +### Key Relationships +- Users can make multiple reservations +- Parking lots contain multiple spots +- Reservations link users to specific spots +- Payments are triggered by reservations +- Reviews are tied to parking lots and users + +### Performance Optimizations +- Spatial indexes for location-based queries +- Partitioning for time-series data (reservations) +- Materialized views for expensive aggregations +- Connection pooling for database efficiency + +--- + +## 🚀 Development Workflow + +### Environment Setup +1. **Development**: `./scripts/frontend-only.sh` (Quick demo) +2. **Full Stack**: `./scripts/full-dev.sh` (Frontend + Backend) +3. **Production-like**: `./scripts/docker-dev.sh` (Docker environment) +4. **Interactive**: `./launch.sh` (Menu with all options) + +### Code Organization +``` +frontend/ +├── src/ +│ ├── app/ # Next.js App Router pages +│ ├── components/ # React components +│ ├── hooks/ # Custom React hooks +│ ├── services/ # API integration +│ ├── types/ # TypeScript type definitions +│ └── utils/ # Utility functions +backend/ +├── src/ +│ ├── modules/ # Feature modules (auth, parking, routing) +│ ├── config/ # Configuration files +│ └── database/ # Database seeds and migrations +``` + +### Development Standards +- **TypeScript**: Strict mode enabled +- **ESLint**: Code quality enforcement +- **Prettier**: Code formatting +- **Jest**: Unit and integration testing +- **Conventional Commits**: Commit message standards + +--- + +## 📈 Performance & Optimization + +### Frontend Optimizations +- **Code Splitting**: Route-based and component-based +- **Image Optimization**: Next.js Image component +- **Caching**: React Query for API state management +- **Lazy Loading**: Components and routes +- **Bundle Analysis**: Webpack bundle analyzer + +### Backend Optimizations +- **Database Indexing**: Spatial and composite indexes +- **Caching Strategy**: Redis for frequently accessed data +- **Connection Pooling**: PostgreSQL connection optimization +- **Query Optimization**: Efficient database queries +- **Rate Limiting**: API protection and resource management + +### Infrastructure Optimizations +- **Docker**: Containerized deployment +- **Health Checks**: Monitoring and alerting +- **Logging**: Structured logging with Winston +- **Process Management**: PM2 for production + +--- + +## 🔒 Security Considerations + +### Authentication & Authorization +- **JWT Tokens**: Secure token-based authentication +- **Role-based Access**: Customer, operator, admin roles +- **Password Security**: Bcrypt hashing +- **Session Management**: Refresh token rotation + +### Data Protection +- **Input Validation**: DTO validation with class-validator +- **SQL Injection Prevention**: TypeORM parameterized queries +- **CORS Configuration**: Restricted origin access +- **Rate Limiting**: API abuse prevention +- **HTTPS**: TLS encryption (production) + +### Privacy & Compliance +- **Data Minimization**: Collect only necessary data +- **User Consent**: Clear privacy policies +- **Data Retention**: Automated cleanup of old data +- **Audit Logging**: Security event tracking + +--- + +## 🌐 Deployment Options + +### Local Development +```bash +# Quick frontend demo +./scripts/frontend-only.sh + +# Full development stack +./scripts/full-dev.sh + +# Docker environment +./scripts/docker-dev.sh + +# Interactive launcher +./launch.sh +``` + +### Production Deployment +```bash +# Docker production setup +docker-compose -f docker-compose.prod.yml up -d + +# Traditional deployment +npm run build && npm run start +``` + +### Cloud Deployment +- **Vercel**: Frontend deployment (recommended) +- **Railway/Heroku**: Full-stack deployment +- **AWS/GCP**: Enterprise deployment +- **Docker**: Containerized deployment + +--- + +## 🔧 Troubleshooting + +### Common Issues +1. **Port conflicts**: Use `lsof -ti:3000` to check port usage +2. **Node version**: Ensure Node.js 18+ is installed +3. **Docker issues**: Verify Docker Desktop is running +4. **Database connection**: Check PostgreSQL service status + +### Debug Mode +```bash +# Frontend debug mode +cd frontend && npm run dev -- --debug + +# Backend debug mode +cd backend && npm run start:debug +``` + +### Logs Location +- **Frontend**: Browser console (F12) +- **Backend**: Terminal output and log files +- **Database**: PostgreSQL logs +- **Docker**: `docker-compose logs` + +--- + +## 📞 Support & Resources + +### Documentation Links +- [Local Deployment Guide](./LOCAL_DEPLOYMENT_GUIDE.md) +- [System Architecture](./SYSTEM_ARCHITECTURE.md) +- [API Documentation](./API_SCHEMA.md) +- [Database Design](./DATABASE_DESIGN.md) + +### Development Resources +- **Next.js Documentation**: https://nextjs.org/docs +- **NestJS Documentation**: https://docs.nestjs.com +- **React Leaflet**: https://react-leaflet.js.org +- **TypeORM**: https://typeorm.io +- **PostgreSQL**: https://www.postgresql.org/docs + +### Community & Support +- GitHub Issues for bug reports +- Development team for feature requests +- Stack Overflow for technical questions +- Documentation updates via pull requests + +--- + +## 🗺️ Roadmap & Future Enhancements + +### Phase 1 (Current) - Core Features ✅ +- Interactive mapping and search +- Basic reservation system +- User authentication +- Real-time availability + +### Phase 2 - Enhanced Features 🚧 +- Payment integration +- Mobile applications +- Advanced analytics +- Multi-language support + +### Phase 3 - AI & IoT Integration 📋 +- Predictive availability +- Smart parking sensors +- Machine learning optimization +- Advanced routing algorithms + +### Phase 4 - Enterprise Features 📋 +- Multi-tenant architecture +- Advanced reporting +- API marketplace +- White-label solutions + +--- + +*Last Updated: August 3, 2025* +*Version: 1.0.0* +*Project Status: Active Development* diff --git a/Documents/SYSTEM_ARCHITECTURE.md b/Documents/SYSTEM_ARCHITECTURE.md new file mode 100644 index 0000000..10ce7ab --- /dev/null +++ b/Documents/SYSTEM_ARCHITECTURE.md @@ -0,0 +1,611 @@ +# 🏗️ Smart Parking Finder - System Architecture Documentation + +## 📋 Table of Contents +1. [System Overview](#system-overview) +2. [Architecture Patterns](#architecture-patterns) +3. [Technology Stack](#technology-stack) +4. [Data Structure & Schema](#data-structure--schema) +5. [System Design](#system-design) +6. [API Documentation](#api-documentation) +7. [Database Design](#database-design) +8. [Security & Performance](#security--performance) + +--- + +## 🎯 System Overview + +### Project Description +Smart Parking Finder is a real-time parking management system that helps users find available parking spots in urban areas. The system provides interactive mapping, route optimization, and real-time availability tracking. + +### Core Features +- 🗺️ Interactive map with OpenStreetMap integration +- 🔍 Real-time parking spot search +- 🧭 GPS-based navigation and routing +- 📱 Responsive web application +- 🔄 Real-time data synchronization +- 📊 Analytics and reporting + +### System Goals +- **Performance**: Sub-second response times for searches +- **Scalability**: Support 10,000+ concurrent users +- **Reliability**: 99.9% uptime availability +- **Usability**: Intuitive interface for all user types + +--- + +## 🏛️ Architecture Patterns + +### 1. Microservices Architecture +``` +┌─────────────────────────────────────────────────────────────┐ +│ Client Layer │ +├─────────────────────────────────────────────────────────────┤ +│ Next.js Frontend (React 18 + TypeScript + Tailwind CSS) │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌─────────┴─────────┐ + │ API Gateway │ + │ (NestJS Router) │ + └─────────┬─────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ +┌───────▼───────┐ ┌─────────▼─────────┐ ┌───────▼───────┐ +│ Auth Service │ │ Parking Service │ │Routing Service│ +│ (JWT) │ │ (CRUD + Search) │ │ (Valhalla) │ +└───────────────┘ └───────────────────┘ └───────────────┘ + │ │ │ + └─────────────────────┼─────────────────────┘ + │ + ┌─────────▼─────────┐ + │ Data Layer │ + │ PostgreSQL + Redis│ + └───────────────────┘ +``` + +### 2. Layered Architecture +- **Presentation Layer**: Next.js React Components +- **Business Logic Layer**: NestJS Services & Controllers +- **Data Access Layer**: TypeORM Repositories +- **Database Layer**: PostgreSQL with Redis Cache + +### 3. Event-Driven Architecture +- Real-time updates using WebSocket connections +- Event sourcing for parking availability changes +- Message queuing for background tasks + +--- + +## 🛠️ Technology Stack + +### Frontend Stack +```json +{ + "framework": "Next.js 14", + "runtime": "React 18", + "language": "TypeScript 5.x", + "styling": "Tailwind CSS 3.x", + "mapping": "React Leaflet + OpenStreetMap", + "state": "React Query (TanStack Query)", + "forms": "React Hook Form", + "testing": "Jest + React Testing Library" +} +``` + +### Backend Stack +```json +{ + "framework": "NestJS 10", + "runtime": "Node.js 18+", + "language": "TypeScript 5.x", + "database": "PostgreSQL 15", + "cache": "Redis 7", + "orm": "TypeORM", + "authentication": "JWT + Passport", + "validation": "Class Validator", + "documentation": "Swagger/OpenAPI", + "testing": "Jest + Supertest" +} +``` + +### Infrastructure Stack +```json +{ + "containerization": "Docker + Docker Compose", + "routing": "Valhalla Routing Engine", + "mapping": "OpenStreetMap Data", + "monitoring": "Health Check Endpoints", + "logging": "Winston Logger", + "process": "PM2 Process Manager" +} +``` + +--- + +## 📊 Data Structure & Schema + +### 1. Database Schema + +#### Users Table +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL, + phone VARCHAR(20), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT true, + role user_role DEFAULT 'customer' +); + +CREATE TYPE user_role AS ENUM ('customer', 'operator', 'admin'); +``` + +#### Parking Lots Table +```sql +CREATE TABLE parking_lots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + address TEXT NOT NULL, + latitude DECIMAL(10,8) NOT NULL, + longitude DECIMAL(11,8) NOT NULL, + total_spots INTEGER NOT NULL DEFAULT 0, + available_spots INTEGER NOT NULL DEFAULT 0, + price_per_hour DECIMAL(10,2) NOT NULL, + operating_hours JSONB, + amenities TEXT[], + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT true +); + +-- Spatial index for location-based queries +CREATE INDEX idx_parking_lots_location ON parking_lots USING GIST ( + ll_to_earth(latitude, longitude) +); +``` + +#### Parking Spots Table +```sql +CREATE TABLE parking_spots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + parking_lot_id UUID REFERENCES parking_lots(id) ON DELETE CASCADE, + spot_number VARCHAR(20) NOT NULL, + spot_type spot_type DEFAULT 'regular', + is_occupied BOOLEAN DEFAULT false, + is_reserved BOOLEAN DEFAULT false, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(parking_lot_id, spot_number) +); + +CREATE TYPE spot_type AS ENUM ('regular', 'disabled', 'electric', 'compact'); +``` + +#### Reservations Table +```sql +CREATE TABLE reservations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + parking_spot_id UUID REFERENCES parking_spots(id) ON DELETE CASCADE, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL, + total_cost DECIMAL(10,2) NOT NULL, + status reservation_status DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TYPE reservation_status AS ENUM ('pending', 'confirmed', 'active', 'completed', 'cancelled'); +``` + +### 2. API Data Models + +#### Parking Lot Response Model +```typescript +interface ParkingLot { + id: string; + name: string; + address: string; + coordinates: { + latitude: number; + longitude: number; + }; + availability: { + totalSpots: number; + availableSpots: number; + occupancyRate: number; + }; + pricing: { + hourlyRate: number; + currency: string; + discounts?: Discount[]; + }; + operatingHours: { + [day: string]: { + open: string; + close: string; + is24Hours: boolean; + }; + }; + amenities: string[]; + distance?: number; + estimatedWalkTime?: number; + metadata: { + createdAt: string; + updatedAt: string; + isActive: boolean; + }; +} +``` + +#### Search Request Model +```typescript +interface ParkingSearchRequest { + location: { + latitude: number; + longitude: number; + }; + radius: number; // in meters + filters?: { + maxPrice?: number; + amenities?: string[]; + spotTypes?: SpotType[]; + availableOnly?: boolean; + }; + sorting?: { + field: 'distance' | 'price' | 'availability'; + order: 'asc' | 'desc'; + }; + pagination?: { + page: number; + limit: number; + }; +} +``` + +#### Route Response Model +```typescript +interface RouteResponse { + route: { + distance: number; // in meters + duration: number; // in seconds + coordinates: [number, number][]; // [lng, lat] pairs + }; + instructions: RouteInstruction[]; + summary: { + totalDistance: string; + totalTime: string; + estimatedCost: number; + }; +} + +interface RouteInstruction { + text: string; + distance: number; + time: number; + sign: number; + interval: [number, number]; +} +``` + +--- + +## 🎨 System Design + +### 1. Frontend Architecture + +#### Component Hierarchy +``` +App (layout.tsx) +├── Header +├── LocationDetector +├── MapView +│ ├── LeafletMap +│ ├── ParkingMarkers +│ ├── RouteLayer +│ └── LocationMarker +├── ParkingList +│ ├── ParkingCard +│ └── PaginationControls +├── BookingModal +├── TransportationSelector +└── GPSSimulator (dev only) +``` + +#### State Management +```typescript +// Global State Structure +interface AppState { + user: { + profile: UserProfile | null; + authentication: AuthState; + preferences: UserPreferences; + }; + location: { + current: Coordinates | null; + permissions: LocationPermission; + tracking: boolean; + }; + parking: { + searchResults: ParkingLot[]; + selectedLot: ParkingLot | null; + filters: SearchFilters; + loading: boolean; + error: string | null; + }; + routing: { + currentRoute: RouteResponse | null; + isCalculating: boolean; + transportMode: TransportMode; + }; + ui: { + mapCenter: Coordinates; + mapZoom: number; + sidebarOpen: boolean; + modalState: ModalState; + }; +} +``` + +### 2. Backend Architecture + +#### Service Layer Design +```typescript +// Parking Service Architecture +@Injectable() +export class ParkingService { + constructor( + private readonly parkingRepository: ParkingRepository, + private readonly cacheService: CacheService, + private readonly geoService: GeoService + ) {} + + async findNearbyParking(searchDto: ParkingSearchDto): Promise { + // 1. Check cache first + const cacheKey = this.generateCacheKey(searchDto); + const cached = await this.cacheService.get(cacheKey); + if (cached) return cached; + + // 2. Perform spatial query + const results = await this.parkingRepository.findWithinRadius({ + latitude: searchDto.latitude, + longitude: searchDto.longitude, + radius: searchDto.radius + }); + + // 3. Apply filters and sorting + const filtered = this.applyFilters(results, searchDto.filters); + const sorted = this.applySorting(filtered, searchDto.sorting); + + // 4. Cache results + await this.cacheService.set(cacheKey, sorted, 300); // 5 min cache + + return sorted; + } +} +``` + +#### Repository Pattern +```typescript +@EntityRepository(ParkingLot) +export class ParkingRepository extends Repository { + async findWithinRadius(params: SpatialQueryParams): Promise { + return this.createQueryBuilder('parking') + .select() + .addSelect(` + (6371 * acos( + cos(radians(:lat)) * cos(radians(latitude)) * + cos(radians(longitude) - radians(:lng)) + + sin(radians(:lat)) * sin(radians(latitude)) + )) AS distance + `) + .where(` + (6371 * acos( + cos(radians(:lat)) * cos(radians(latitude)) * + cos(radians(longitude) - radians(:lng)) + + sin(radians(:lat)) * sin(radians(latitude)) + )) <= :radius + `) + .andWhere('is_active = true') + .setParameters({ + lat: params.latitude, + lng: params.longitude, + radius: params.radius / 1000 // Convert to km + }) + .orderBy('distance', 'ASC') + .getMany(); + } +} +``` + +### 3. Real-time Updates + +#### WebSocket Integration +```typescript +// WebSocket Gateway for real-time updates +@WebSocketGateway({ + cors: { origin: '*' }, + transports: ['websocket', 'polling'] +}) +export class ParkingGateway implements OnGatewayConnection, OnGatewayDisconnect { + @WebSocketServer() server: Server; + + async handleConnection(client: Socket) { + // Subscribe client to location-based updates + const { latitude, longitude, radius } = client.handshake.query; + const room = this.generateLocationRoom(latitude, longitude, radius); + client.join(room); + } + + @SubscribeMessage('parkingUpdate') + async handleParkingUpdate(client: Socket, data: ParkingUpdateDto) { + // Broadcast to relevant clients + const affectedRooms = this.getAffectedRooms(data.location); + affectedRooms.forEach(room => { + this.server.to(room).emit('parkingAvailabilityChanged', { + parkingLotId: data.parkingLotId, + availableSpots: data.availableSpots, + timestamp: new Date().toISOString() + }); + }); + } +} +``` + +--- + +## 🔗 API Documentation + +### Authentication Endpoints +```typescript +POST /api/auth/login +POST /api/auth/register +POST /api/auth/refresh +DELETE /api/auth/logout +GET /api/auth/profile +PUT /api/auth/profile +``` + +### Parking Endpoints +```typescript +GET /api/parking/search +GET /api/parking/:id +POST /api/parking/:id/reserve +GET /api/parking/reservations +PUT /api/parking/reservations/:id +DELETE /api/parking/reservations/:id +``` + +### Routing Endpoints +```typescript +POST /api/routing/calculate +GET /api/routing/modes +POST /api/routing/optimize +``` + +### Health & Monitoring +```typescript +GET /api/health +GET /api/health/database +GET /api/health/cache +GET /api/metrics +``` + +--- + +## 🗄️ Database Design + +### Indexing Strategy +```sql +-- Spatial indexes for location queries +CREATE INDEX idx_parking_lots_location ON parking_lots +USING GIST (ll_to_earth(latitude, longitude)); + +-- Compound indexes for filtering +CREATE INDEX idx_parking_lots_active_price ON parking_lots (is_active, price_per_hour); +CREATE INDEX idx_parking_spots_lot_available ON parking_spots (parking_lot_id, is_occupied); + +-- Time-based indexes for reservations +CREATE INDEX idx_reservations_time_range ON reservations (start_time, end_time); +CREATE INDEX idx_reservations_user_status ON reservations (user_id, status, created_at); +``` + +### Data Partitioning +```sql +-- Partition reservations by month for better performance +CREATE TABLE reservations_2024_01 PARTITION OF reservations +FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); + +-- Automated partition management +CREATE OR REPLACE FUNCTION create_monthly_partition() +RETURNS void AS $$ +DECLARE + start_date date; + end_date date; + table_name text; +BEGIN + start_date := date_trunc('month', CURRENT_DATE + interval '1 month'); + end_date := start_date + interval '1 month'; + table_name := 'reservations_' || to_char(start_date, 'YYYY_MM'); + + EXECUTE format('CREATE TABLE %I PARTITION OF reservations + FOR VALUES FROM (%L) TO (%L)', + table_name, start_date, end_date); +END; +$$ LANGUAGE plpgsql; +``` + +--- + +## 🔒 Security & Performance + +### Security Measures +1. **Authentication**: JWT with refresh tokens +2. **Authorization**: Role-based access control (RBAC) +3. **Input Validation**: DTO validation with class-validator +4. **SQL Injection**: Protected by TypeORM parameterized queries +5. **Rate Limiting**: API rate limiting per user/IP +6. **CORS**: Configured for specific origins +7. **HTTPS**: TLS encryption in production + +### Performance Optimizations +1. **Caching**: Redis for frequently accessed data +2. **Database**: Optimized indexes and query planning +3. **CDN**: Static asset delivery optimization +4. **Compression**: Gzip compression for API responses +5. **Lazy Loading**: Component and route-based code splitting +6. **Pagination**: Efficient pagination for large datasets +7. **Connection Pooling**: Database connection optimization + +### Monitoring & Logging +```typescript +// Health Check Implementation +@Controller('health') +export class HealthController { + constructor( + private readonly healthCheckService: HealthCheckService, + private readonly databaseHealthIndicator: TypeOrmHealthIndicator, + private readonly redisHealthIndicator: RedisHealthIndicator + ) {} + + @Get() + @HealthCheck() + check() { + return this.healthCheckService.check([ + () => this.databaseHealthIndicator.pingCheck('database'), + () => this.redisHealthIndicator.checkHealth('redis'), + () => this.checkExternalServices() + ]); + } +} +``` + +--- + +## 📈 Scalability Considerations + +### Horizontal Scaling +- **Load Balancing**: Multiple application instances +- **Database Sharding**: Geographic or user-based sharding +- **Microservices**: Independent service scaling +- **CDN Integration**: Global content distribution + +### Vertical Scaling +- **Database Optimization**: Query optimization and indexing +- **Memory Management**: Efficient caching strategies +- **CPU Optimization**: Algorithmic improvements +- **Storage Optimization**: Data archiving and compression + +### Future Enhancements +- **Machine Learning**: Predictive parking availability +- **Mobile Apps**: Native iOS/Android applications +- **Payment Integration**: Online payment processing +- **IoT Integration**: Smart parking sensor integration +- **Multi-language**: Internationalization support + +--- + +*Last Updated: August 3, 2025* +*Version: 1.0.0* diff --git a/Documents/TECHNICAL_SPECIFICATION.md b/Documents/TECHNICAL_SPECIFICATION.md new file mode 100644 index 0000000..8e514bf --- /dev/null +++ b/Documents/TECHNICAL_SPECIFICATION.md @@ -0,0 +1,420 @@ +# 🚗 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/GIT_UPLOAD_GUIDE.md b/GIT_UPLOAD_GUIDE.md new file mode 100644 index 0000000..30f7c12 --- /dev/null +++ b/GIT_UPLOAD_GUIDE.md @@ -0,0 +1,189 @@ +# 🚀 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 new file mode 100644 index 0000000..e69de29 diff --git a/LOCAL_DEPLOYMENT_GUIDE.md b/LOCAL_DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..e69de29 diff --git a/OPTIMIZATION_REPORT.md b/OPTIMIZATION_REPORT.md new file mode 100644 index 0000000..e69de29 diff --git a/REORGANIZATION_GUIDE.md b/REORGANIZATION_GUIDE.md new file mode 100644 index 0000000..72e2133 --- /dev/null +++ b/REORGANIZATION_GUIDE.md @@ -0,0 +1,145 @@ +# 🧹 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/frontend/next.config.optimized.js b/frontend/next.config.optimized.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 3d0906f..f3a0e8f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -4,7 +4,9 @@ import React, { useState, useEffect } from 'react'; import dynamic from 'next/dynamic'; import { Header } from '@/components/Header'; import { ParkingList } from '@/components/parking/ParkingList'; +import { ParkingDetails } from '@/components/parking/ParkingDetails'; import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator'; +import { Icon } from '@/components/ui/Icon'; // import { ErrorMessage } from '@/components/ui/ErrorMessage'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { useParkingSearch } from '@/hooks/useParkingSearch'; @@ -31,6 +33,16 @@ export default function ParkingFinderPage() { const [isMobile, setIsMobile] = useState(false); const [sortType, setSortType] = useState<'availability' | 'price' | 'distance'>('availability'); const [gpsSimulatorVisible, setGpsSimulatorVisible] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [currentTime, setCurrentTime] = useState(new Date()); + + // Update time every minute + useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(new Date()); + }, 60000); + return () => clearInterval(timer); + }, []); // Set initial GPS window position after component mounts useEffect(() => { @@ -219,6 +231,78 @@ export default function ParkingFinderPage() { Làm mới danh sách + + {/* Status Info Bar - Thiết kế thanh lịch đơn giản */} +
+
+
+ {/* Thống kê bãi đỗ */} +
+
+
+ + {parkingLots.filter(lot => lot.availableSlots > 0).length} có chỗ + +
+
+
+
+ + {parkingLots.filter(lot => lot.availableSlots === 0).length} đầy + +
+
+ + {/* Thông tin thời tiết và giờ */} +
+
+ {currentTime.getHours() >= 6 && currentTime.getHours() < 18 ? '☀️' : '🌙'} + + {currentTime.getHours() >= 6 && currentTime.getHours() < 18 ? '28°C' : '24°C'} + +
+
+
+ {currentTime.toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' })} +
+
+
+
+
+ + {/* Search Bar */} +
+
+ 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" + style={{ + borderColor: 'rgba(232, 90, 79, 0.2)', + backgroundColor: 'rgba(255, 255, 255, 0.95)' + }} + /> + {/* Search Icon */} +
+ + + +
+ {/* Clear Button */} + {searchQuery && ( + + )} +
+
{/* Filter buttons - Below header */} @@ -236,13 +320,13 @@ export default function ParkingFinderPage() { - Sắp xếp: + Sắp xếp:
@@ -337,6 +440,7 @@ export default function ParkingFinderPage() { selectedId={selectedParkingLot?.id} userLocation={userLocation} sortType={sortType} + searchQuery={searchQuery} /> )} @@ -356,7 +460,25 @@ export default function ParkingFinderPage() { )} - {/* Map Section - Center */} + {/* Middle Column - Parking Details */} + {selectedParkingLot && ( +
+ { + setSelectedParkingLot(null); + clearRoute(); + }} + onBook={(lot) => { + toast.success(`Đã đặt chỗ tại ${lot.name}!`); + // Here you would typically call an API to book the parking spot + }} + /> +
+ )} + + {/* Map Section - Right */}
- {/* Map overlay info - Moved to bottom right */} + {/* Map overlay info - Position based on layout */} {userLocation && ( -
+
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(''); - - // 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', - }); - } - }, []); - - // Auto zoom to fit user location and selected parking lot - useEffect(() => { - if (mapRef.current && userLocation) { - if (selectedParkingLot) { - // Create bounds that include both user location and selected parking lot - const bounds = [ - [userLocation.lat, userLocation.lng], - [selectedParkingLot.lat, selectedParkingLot.lng] - ]; - - // Fit map to bounds with padding - mapRef.current.fitBounds(bounds, { - padding: [50, 50], - maxZoom: 16 - }); - } else { - // Just center on user location - mapRef.current.setView([userLocation.lat, userLocation.lng], 15); - } - } - }, [userLocation, selectedParkingLot]); - - // 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... - //
- //
- // ); - // } - - return ( -
- { - // Force invalidate size when map is ready - setTimeout(() => { - if (mapRef.current) { - mapRef.current.invalidateSize(); - } - }, 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 ( - { - onParkingLotSelect?.(lot); - } - }} - > - -
-
-
-
- - - -
-
- {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' - )} -
-
-
- - -
-
-
-
- ); - })} - - {/* 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/map/MapView.tsx b/frontend/src/components/map/MapView.tsx index 64eed85..c4cc31d 100644 --- a/frontend/src/components/map/MapView.tsx +++ b/frontend/src/components/map/MapView.tsx @@ -68,6 +68,7 @@ export const MapView: React.FC = ({ 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="; @@ -225,27 +226,122 @@ export const MapView: React.FC = ({ } }, []); - // Auto zoom to fit user location and selected parking lot + // 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(() => { - if (mapRef.current && userLocation) { - if (selectedParkingLot) { - // Create bounds that include both user location and selected parking lot - const bounds = [ - [userLocation.lat, userLocation.lng], - [selectedParkingLot.lat, selectedParkingLot.lng] - ]; - - // Fit map to bounds with padding - mapRef.current.fitBounds(bounds, { - padding: [50, 50], - maxZoom: 16 - }); + 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 { - // Just center on user location - mapRef.current.setView([userLocation.lat, userLocation.lng], 15); + console.log('Map ref not ready yet'); } - } - }, [userLocation, selectedParkingLot]); + }, 200); // Small delay to ensure map is ready + + return () => clearTimeout(timer); + }, [selectedParkingLot, userLocation]); // Calculate route when parking lot is selected useEffect(() => { @@ -742,13 +838,206 @@ export const MapView: React.FC = ({ // ); // } + // 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 && ( + + )} +
+ = ({ keyboard={true} dragging={true} attributionControl={true} + zoomAnimation={true} + fadeAnimation={true} + markerZoomAnimation={true} + inertia={true} + worldCopyJump={false} + maxBoundsViscosity={0.3} ref={mapRef} whenReady={() => { // 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); }} @@ -769,8 +1085,12 @@ export const MapView: React.FC = ({ {/* User Location Marker (GPS với hiệu ứng pulse) */} @@ -814,7 +1134,15 @@ export const MapView: React.FC = ({ )} eventHandlers={{ click: () => { + 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); } }} > @@ -948,20 +1276,34 @@ export const MapView: React.FC = ({
- +
+ + + {/* Focus camera button */} + +
diff --git a/frontend/src/components/parking/BookingModal.tsx b/frontend/src/components/parking/BookingModal.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/parking/ParkingDetails.tsx b/frontend/src/components/parking/ParkingDetails.tsx new file mode 100644 index 0000000..088fcf1 --- /dev/null +++ b/frontend/src/components/parking/ParkingDetails.tsx @@ -0,0 +1,731 @@ +'use client'; + +import React, { useState } from 'react'; +import { ParkingLot, UserLocation } from '@/types'; + +interface ParkingDetailsProps { + parkingLot: ParkingLot; + userLocation?: UserLocation | null; + onClose?: () => void; + onBook?: (lot: ParkingLot) => void; +} + +// Interface for parking slot data +interface ParkingSlot { + id: string; + status: 'available' | 'occupied' | 'reserved'; + type: 'regular' | 'ev' | 'disabled'; + x: number; + y: number; + width: number; + height: number; +} + +interface ParkingFloor { + floor: number; + name: string; + slots: ParkingSlot[]; + entrances: { x: number; y: number; type: 'entrance' | 'exit' }[]; + evStations: { x: number; y: number; id: string }[]; + walkways: { x: number; y: number; width: number; height: number }[]; +} + +// Thiết kế bãi xe đẹp và chuyên nghiệp +const generateParkingFloorData = (floorNumber: number): ParkingFloor => { + const slots: ParkingSlot[] = []; + const walkways = []; + + // Layout tối ưu: 2 khu vực, mỗi khu 2 dãy, mỗi dãy 5 chỗ + const sectionsPerFloor = 2; + const rowsPerSection = 2; + const slotsPerRow = 5; + const slotWidth = 32; + const slotHeight = 45; + const rowSpacing = 50; + const sectionSpacing = 80; + const columnSpacing = 4; + const startX = 40; + const startY = 40; + + for (let section = 0; section < sectionsPerFloor; section++) { + for (let row = 0; row < rowsPerSection; row++) { + for (let col = 0; col < slotsPerRow; col++) { + const rowLetter = String.fromCharCode(65 + (section * rowsPerSection + row)); + const slotId = `${rowLetter}${String(col + 1).padStart(2, '0')}`; + + const x = startX + col * (slotWidth + columnSpacing); + const y = startY + section * sectionSpacing + row * rowSpacing; + + // Phân bố trạng thái thực tế + const rand = Math.random(); + let status: 'available' | 'occupied' | 'reserved'; + if (rand < 0.3) status = 'occupied'; + else if (rand < 0.35) status = 'reserved'; + else status = 'available'; + + // Chỗ đặc biệt + let type: 'regular' | 'ev' | 'disabled' = 'regular'; + if (section === 0 && row === 0 && col === 0) type = 'disabled'; + if (section === 0 && row === 0 && col === slotsPerRow - 1) type = 'ev'; + if (section === 1 && row === 0 && col === slotsPerRow - 1) type = 'ev'; + + slots.push({ + id: slotId, + status, + type, + x, + y, + width: slotWidth, + height: slotHeight + }); + } + } + } + + // Hệ thống đường đi thông minh + walkways.push( + // Đường vào chính + { x: 15, y: 10, width: 200, height: 20 }, + + // Đường dọc chính (trái) + { x: 15, y: 10, width: 20, height: 240 }, + + // Đường dọc chính (phải) + { x: 195, y: 10, width: 20, height: 240 }, + + // Đường ngang giữa section 1 + { x: 15, y: 65, width: 200, height: 15 }, + + // Đường ngang giữa 2 section + { x: 15, y: 105, width: 200, height: 20 }, + + // Đường ngang giữa section 2 + { x: 15, y: 145, width: 200, height: 15 }, + + // Đường ra + { x: 15, y: 230, width: 200, height: 20 } + ); + + return { + floor: floorNumber, + name: `Tầng ${floorNumber}`, + slots, + entrances: [ + { x: 60, y: 10, type: 'entrance' }, + { x: 155, y: 230, type: 'exit' } + ], + evStations: [ + { x: 200, y: 40, id: `EV-${floorNumber}-01` }, + { x: 200, y: 120, id: `EV-${floorNumber}-02` } + ], + walkways + }; +}; + +// Parking Lot Map Component +const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) => { + const [selectedFloor, setSelectedFloor] = useState(1); + const [lastUpdate, setLastUpdate] = useState(new Date()); + + // Generate 3 floors for demo + const floors = [1, 2, 3].map(generateParkingFloorData); + const currentFloor = floors.find(f => f.floor === selectedFloor) || floors[0]; + + // Real-time update simulation + React.useEffect(() => { + const interval = setInterval(() => { + setLastUpdate(new Date()); + // Here you would typically fetch real-time data + }, 60000); // Update every 60 seconds (1 minute) instead of 10 seconds + + return () => clearInterval(interval); + }, []); + + const getSlotColor = (slot: ParkingSlot) => { + switch (slot.status) { + case 'available': + return '#22C55E'; // Green for all available slots + case 'occupied': + return '#EF4444'; // Red + case 'reserved': + return '#F59E0B'; // Amber + default: + return '#6B7280'; // Gray + } + }; + + const getSlotIcon = (slot: ParkingSlot) => { + // No special icons needed anymore + return null; + }; + + const floorStats = currentFloor.slots.reduce((acc, slot) => { + acc.total++; + if (slot.status === 'available') acc.available++; + if (slot.status === 'occupied') acc.occupied++; + return acc; + }, { total: 0, available: 0, occupied: 0 }); + + return ( +
+ {/* Floor selector */} +
+
+ {floors.map((floor) => ( + + ))} +
+ + {/* Real-time indicator */} +
+
+ Cập nhật: {lastUpdate.toLocaleTimeString()} +
+
+ + {/* Floor stats */} +
+
+
{floorStats.available}
+
Trống
+
+
+
{floorStats.occupied}
+
Đã đậu
+
+
+
{floorStats.total}
+
Tổng
+
+
+
+ ); +}; + +// Sample reviews data (in real app, this would come from API) +const SAMPLE_REVIEWS = [ + { + id: 1, + user: 'Nguyễn Văn A', + 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ý.', + date: '2024-01-15', + avatar: 'N' + }, + { + id: 2, + user: 'Trần Thị B', + rating: 4, + comment: 'Vị trí thuận tiện, dễ tìm. Chỉ hơi xa lối ra một chút.', + date: '2024-01-10', + avatar: 'T' + }, + { + id: 3, + user: 'Lê Văn C', + rating: 5, + comment: 'Có sạc điện cho xe điện, rất tiện lợi!', + date: '2024-01-08', + avatar: 'L' + } +]; + +// 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; +}; + +const formatDistance = (distance: number): string => { + if (distance < 1) { + return `${Math.round(distance * 1000)}m`; + } + return `${distance.toFixed(1)}km`; +}; + +// Check if parking lot is currently open +const isCurrentlyOpen = (lot: ParkingLot): boolean => { + if (lot.isOpen24Hours) return true; + + if (!lot.openTime || !lot.closeTime) return true; + + const now = new Date(); + const currentTime = now.getHours() * 100 + now.getMinutes(); + + const parseTime = (timeStr: string): number => { + const [hours, minutes] = timeStr.split(':').map(Number); + return hours * 100 + (minutes || 0); + }; + + const openTime = parseTime(lot.openTime); + const closeTime = parseTime(lot.closeTime); + + if (openTime <= closeTime) { + return currentTime >= openTime && currentTime <= closeTime; + } else { + return currentTime >= openTime || currentTime <= closeTime; + } +}; + +const getStatusColor = (availableSlots: number, totalSlots: number) => { + const percentage = availableSlots / totalSlots; + if (availableSlots === 0) { + return { + background: 'rgba(239, 68, 68, 0.15)', + borderColor: '#EF4444', + textColor: '#EF4444' + }; + } else if (percentage > 0.7) { + return { + background: 'rgba(34, 197, 94, 0.1)', + borderColor: 'var(--success-color)', + textColor: 'var(--success-color)' + }; + } else { + return { + background: 'rgba(251, 191, 36, 0.1)', + borderColor: '#F59E0B', + textColor: '#F59E0B' + }; + } +}; + +const formatAmenities = (amenities: string[] | { [key: string]: any }): string[] => { + if (Array.isArray(amenities)) { + return amenities; + } + + 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.wheelchair_accessible) amenityList.push('Phù hợp xe lăn'); + if (amenities.valet_service) amenityList.push('Dịch vụ đỗ xe'); + + return amenityList; +}; + +const renderStars = (rating: number) => { + return [...Array(5)].map((_, i) => ( + + + + )); +}; + +const calculateAverageRating = (reviews: typeof SAMPLE_REVIEWS) => { + if (reviews.length === 0) return 0; + return reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length; +}; + +export const ParkingDetails: React.FC = ({ + parkingLot, + userLocation, + onClose, + onBook +}) => { + const [activeTab, setActiveTab] = useState<'overview' | 'reviews'>('overview'); + const [bookingDuration, setBookingDuration] = useState(2); // hours + + const distance = userLocation + ? calculateDistance(userLocation.lat, userLocation.lng, parkingLot.lat, parkingLot.lng) + : null; + + const statusColors = getStatusColor(parkingLot.availableSlots, parkingLot.totalSlots); + const isFull = parkingLot.availableSlots === 0; + const isClosed = !isCurrentlyOpen(parkingLot); + const isDisabled = isFull || isClosed; + const amenityList = formatAmenities(parkingLot.amenities); + const averageRating = calculateAverageRating(SAMPLE_REVIEWS); + + return ( +
+ {/* Header */} +
+ {onClose && ( + + )} + +
+
+ + + +
+ +
+

+ {parkingLot.name} +

+
+ + + + + {parkingLot.address} + {distance && ( + <> + + {formatDistance(distance)} + + )} +
+ + {/* Rating */} +
+
+ {renderStars(Math.round(averageRating))} +
+ {averageRating.toFixed(1)} + ({SAMPLE_REVIEWS.length} đánh giá) +
+
+
+ + {/* Status banners */} + {isFull && ( +
+ Bãi xe đã hết chỗ +
+ )} + + {isClosed && ( +
+ Bãi xe đã đóng cửa +
+ )} +
+ + {/* Quick stats */} +
+
+ {/* Availability */} +
+
+ {parkingLot.availableSlots} +
+
chỗ trống
+
/ {parkingLot.totalSlots} tổng
+
+ + {/* Price */} +
+
+ {(parkingLot.pricePerHour || parkingLot.hourlyRate) ? + `${Math.round((parkingLot.pricePerHour || parkingLot.hourlyRate) / 1000)}k` : '--'} +
+
mỗi giờ
+
phí gửi xe
+
+ + {/* Hours */} +
+
+
+
+ {parkingLot.isOpen24Hours ? '24/7' : parkingLot.openTime || '--'} +
+
+
+ {isCurrentlyOpen(parkingLot) ? 'Đang mở' : 'Đã đóng'} +
+
+ {isCurrentlyOpen(parkingLot) ? 'Hoạt động' : 'Ngừng hoạt động'} +
+
+
+
+ + {/* Tabs */} +
+ {['overview', 'reviews'].map((tab) => ( + + ))} +
+ + {/* Content */} +
+ {activeTab === 'overview' && ( +
+ {/* Map section */} +
+

+
+ + + +
+ Sơ đồ bãi xe +

+ + +
+ + {/* Amenities */} + {amenityList.length > 0 && ( +
+

+
+ + + +
+ Tiện ích +

+
+ {amenityList.map((amenity, index) => ( +
+
+ {amenity} +
+ ))} +
+
+ )} +
+ )} + + {activeTab === 'reviews' && ( +
+ {/* Overall rating */} +
+
{averageRating.toFixed(1)}
+
+ {renderStars(Math.round(averageRating))} +
+
{SAMPLE_REVIEWS.length} đánh giá
+
+ + {/* Reviews list */} +
+ {SAMPLE_REVIEWS.map((review) => ( +
+
+
+ {review.avatar} +
+
+
+ {review.user} +
+ {renderStars(review.rating)} +
+
+

{review.comment}

+
{review.date}
+
+
+
+ ))} +
+ + {/* Add review button */} + +
+ )} +
+ + {/* Booking section */} + {onBook && ( +
+ {/* Duration selector */} +
+
+
+
+ + + +
+ Thời gian gửi xe + (đơn vị giờ) +
+ + {/* Ultra compact time selector */} +
+ + +
+ { + const value = parseInt(e.target.value) || 1; + setBookingDuration(Math.min(24, Math.max(1, value))); + }} + className="w-8 px-0.5 py-0.5 text-center text-xs font-bold border-none outline-none bg-transparent" + style={{ color: 'var(--accent-color)' }} + /> +
+ + +
+
+
+ + {/* Total summary */} + {(parkingLot.pricePerHour || parkingLot.hourlyRate) && ( +
+
+ Thời gian đã chọn: + {bookingDuration} giờ +
+
+ Tổng thanh toán: + + {Math.round(((parkingLot.pricePerHour || parkingLot.hourlyRate) * bookingDuration) / 1000)}k VND + +
+
+ )} + + {/* Book button */} + +
+ )} +
+ ); +}; diff --git a/frontend/src/components/parking/ParkingList.tsx b/frontend/src/components/parking/ParkingList.tsx index 7f40f75..11e4089 100644 --- a/frontend/src/components/parking/ParkingList.tsx +++ b/frontend/src/components/parking/ParkingList.tsx @@ -10,6 +10,7 @@ interface ParkingListProps { selectedId?: number; userLocation?: UserLocation | null; sortType?: 'availability' | 'price' | 'distance'; + searchQuery?: string; } // Calculate distance between two points using Haversine formula @@ -108,22 +109,34 @@ export const ParkingList: React.FC = ({ onViewing, selectedId, userLocation, - sortType = 'availability' + sortType = 'availability', + searchQuery = '' }) => { const listRef = React.useRef(null); const itemRefs = React.useRef>(new Map()); + // Filter and sort parking lots const sortedLots = React.useMemo(() => { + // First filter by search query + let filteredLots = parkingLots; + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase().trim(); + filteredLots = parkingLots.filter(lot => + lot.name.toLowerCase().includes(query) || + lot.address.toLowerCase().includes(query) + ); + } + // Separate parking lots into categories - const openLotsWithSpaces = parkingLots.filter(lot => + const openLotsWithSpaces = filteredLots.filter(lot => lot.availableSlots > 0 && isCurrentlyOpen(lot) ); - const closedLots = parkingLots.filter(lot => + const closedLots = filteredLots.filter(lot => !isCurrentlyOpen(lot) ); - const fullLots = parkingLots.filter(lot => + const fullLots = filteredLots.filter(lot => lot.availableSlots === 0 && isCurrentlyOpen(lot) ); @@ -167,7 +180,7 @@ export const ParkingList: React.FC = ({ ...sortLots(fullLots), ...sortLots(closedLots) ]; - }, [parkingLots, userLocation, sortType]); + }, [parkingLots, userLocation, sortType, searchQuery]); // Remove auto-viewing functionality - now only supports selection React.useEffect(() => { @@ -181,7 +194,19 @@ export const ParkingList: React.FC = ({ return (
- {sortedLots.map((lot, index) => { + {sortedLots.length === 0 && searchQuery.trim() ? ( +
+
+ + + +
+

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

+
+ ) : ( + sortedLots.map((lot, index) => { const distance = userLocation ? calculateDistance(userLocation.lat, userLocation.lng, lot.lat, lot.lng) : null; @@ -208,10 +233,10 @@ export const ParkingList: React.FC = ({ onClick={() => !isDisabled && onSelect(lot)} disabled={isDisabled} className={` - w-full p-5 md:p-6 text-left rounded-2xl border-2 transition-all duration-300 group relative overflow-hidden + w-full p-5 md:p-6 text-left rounded-2xl border-2 transition-all duration-300 group relative overflow-visible ${isSelected - ? 'shadow-xl transform scale-[1.02] z-10' - : 'hover:shadow-lg hover:transform hover:scale-[1.01]' + ? 'shadow-2xl z-10 ring-2 ring-orange-200 ring-opacity-50' + : 'hover:shadow-lg hover:ring-1 hover:ring-gray-200 hover:ring-opacity-30' } ${isDisabled ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'} `} @@ -388,7 +413,7 @@ export const ParkingList: React.FC = ({
); - })} + }))}
); }; diff --git a/frontend/src/components/parking/ParkingList.v1.0.tsx b/frontend/src/components/parking/ParkingList.v1.0.tsx deleted file mode 100644 index 9bb15c2..0000000 --- a/frontend/src/components/parking/ParkingList.v1.0.tsx +++ /dev/null @@ -1,366 +0,0 @@ -'use client'; - -import React from 'react'; -import { ParkingLot, UserLocation } from '@/types'; - -interface ParkingListProps { - parkingLots: ParkingLot[]; - onSelect: (lot: ParkingLot) => void; - selectedId?: number; - userLocation?: UserLocation | null; - sortType?: 'availability' | 'price' | 'distance'; -} - -// Calculate distance between two points using Haversine formula -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; -}; - -const formatDistance = (distance: number): string => { - if (distance < 1) { - return `${Math.round(distance * 1000)}m`; - } - return `${distance.toFixed(1)}km`; -}; - -const getStatusColor = (availableSlots: number, totalSlots: number) => { - const percentage = availableSlots / totalSlots; - if (availableSlots === 0) { - // Hết chỗ - màu đỏ - 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 - 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 - return { - background: 'rgba(251, 191, 36, 0.1)', - borderColor: '#F59E0B', - textColor: '#F59E0B' - }; - } -}; - -const getStatusText = (availableSlots: number, totalSlots: number) => { - if (availableSlots === 0) { - return 'Hết chỗ'; - } else if (availableSlots / totalSlots > 0.7) { - return `${availableSlots} chỗ trống`; - } else { - return `${availableSlots} chỗ trống (sắp hết)`; - } -}; - -// Check if parking lot is currently open -const isCurrentlyOpen = (lot: ParkingLot): boolean => { - if (lot.isOpen24Hours) return true; - - if (!lot.openTime || !lot.closeTime) return true; // Assume open if no time specified - - const now = new Date(); - const currentTime = now.getHours() * 100 + now.getMinutes(); // Format: 930 for 9:30 - - // Parse time strings (assuming format like "08:00" or "8:00") - const parseTime = (timeStr: string): number => { - const [hours, minutes] = timeStr.split(':').map(Number); - return hours * 100 + (minutes || 0); - }; - - const openTime = parseTime(lot.openTime); - const closeTime = parseTime(lot.closeTime); - - if (openTime <= closeTime) { - // Same day operation (e.g., 8:00 - 22:00) - return currentTime >= openTime && currentTime <= closeTime; - } else { - // Cross midnight operation (e.g., 22:00 - 06:00) - return currentTime >= openTime || currentTime <= closeTime; - } -}; - -export const ParkingList: React.FC = ({ - parkingLots, - onSelect, - selectedId, - userLocation, - sortType = 'availability' -}) => { - // Filter and sort parking lots - const sortedLots = React.useMemo(() => { - // Separate parking lots into categories - const openLotsWithSpaces = parkingLots.filter(lot => - lot.availableSlots > 0 && isCurrentlyOpen(lot) - ); - - const closedLots = parkingLots.filter(lot => - !isCurrentlyOpen(lot) - ); - - const fullLots = parkingLots.filter(lot => - lot.availableSlots === 0 && isCurrentlyOpen(lot) - ); - - // Sort function for each category - const sortLots = (lots: ParkingLot[]) => { - return [...lots].sort((a, b) => { - switch (sortType) { - case 'price': - // Sort by price (cheapest first) - handle cases where price might be null/undefined - const priceA = a.pricePerHour || a.hourlyRate || 999999; - const priceB = b.pricePerHour || b.hourlyRate || 999999; - return priceA - priceB; - - case 'distance': - // Sort by distance (closest first) - if (!userLocation) return 0; - const distanceA = calculateDistance(userLocation.lat, userLocation.lng, a.lat, a.lng); - const distanceB = calculateDistance(userLocation.lat, userLocation.lng, b.lat, b.lng); - return distanceA - distanceB; - - case 'availability': - default: - // Sort by available spaces (most available first) - const availabilityDiff = b.availableSlots - a.availableSlots; - if (availabilityDiff !== 0) return availabilityDiff; - - // If same availability, sort by distance as secondary criteria - if (userLocation) { - const distanceA = calculateDistance(userLocation.lat, userLocation.lng, a.lat, a.lng); - const distanceB = calculateDistance(userLocation.lat, userLocation.lng, b.lat, b.lng); - return distanceA - distanceB; - } - return a.name.localeCompare(b.name); - } - }); - }; - - // Combine all categories with priority: open with spaces > full > closed - return [ - ...sortLots(openLotsWithSpaces), - ...sortLots(fullLots), - ...sortLots(closedLots) - ]; - }, [parkingLots, userLocation, sortType]); - - return ( -
- {sortedLots.map((lot, index) => { - const distance = userLocation - ? calculateDistance(userLocation.lat, userLocation.lng, lot.lat, lot.lng) - : null; - - const isSelected = selectedId === lot.id; - const statusColors = getStatusColor(lot.availableSlots, lot.totalSlots); - const isFull = lot.availableSlots === 0; - const isClosed = !isCurrentlyOpen(lot); - const isDisabled = isFull || isClosed; - - return ( - - ); - })} -
- ); -}; diff --git a/frontend/src/components/ui/Icon.tsx b/frontend/src/components/ui/Icon.tsx index f2fa427..f600975 100644 --- a/frontend/src/components/ui/Icon.tsx +++ b/frontend/src/components/ui/Icon.tsx @@ -5,15 +5,18 @@ import React from 'react'; export interface IconProps { name: string; className?: string; - size?: 'sm' | 'md' | 'lg'; + size?: 'sm' | 'md' | 'lg' | 'xl'; } const iconPaths: Record = { airport: "M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7v13zM9 7l6-3 2 1v7l-2 1-6-3zm6-3V2a1 1 0 00-1-1H8a1 1 0 00-1 1v2l8 0z", + availability: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z", building: "M3 21h18M5 21V7l8-4v18M13 9h4v12", car: "M7 17a2 2 0 11-4 0 2 2 0 014 0zM21 17a2 2 0 11-4 0 2 2 0 014 0zM5 17H3v-6l2-5h9l4 5v6h-2m-7-6h7m-7 0l-1-3", check: "M5 13l4 4L19 7", clock: "M12 2v10l3 3m5-8a9 9 0 11-18 0 9 9 0 0118 0z", + currency: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1", + distance: "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", 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", @@ -32,6 +35,7 @@ const sizeClasses = { sm: 'h-4 w-4', md: 'h-5 w-5', lg: 'h-6 w-6', + xl: 'h-8 w-8', }; export const Icon: React.FC = ({ diff --git a/frontend/src/components/ui/WheelPicker.tsx b/frontend/src/components/ui/WheelPicker.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/hooks/useParkingSearch-simple.ts b/frontend/src/hooks/useParkingSearch-simple.ts deleted file mode 100644 index c5ae905..0000000 --- a/frontend/src/hooks/useParkingSearch-simple.ts +++ /dev/null @@ -1,595 +0,0 @@ -import { useState, useCallback } from 'react'; -import { ParkingLot, Coordinates } from '@/types'; - -interface ParkingSearchState { - parkingLots: ParkingLot[]; - loading: boolean; - error: string | null; - searchLocation: Coordinates | null; -} - -export const useParkingSearch = () => { - const [state, setState] = useState({ - parkingLots: [], - loading: false, - error: null, - searchLocation: null - }); - - // Mock parking data for Ho Chi Minh City - const mockParkingLots: ParkingLot[] = [ - { - id: 1, - name: 'Vincom Center Đồng Khởi', - address: '72 Lê Thánh Tôn, Bến Nghé, Quận 1, TP.HCM', - lat: 10.7769, - lng: 106.7009, - availableSlots: 85, - totalSlots: 250, - availableSpaces: 85, - totalSpaces: 250, - hourlyRate: 15000, - pricePerHour: 15000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'security', 'valet'], - contactInfo: { phone: '+84-28-3829-4888' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: false - }, - { - id: 2, - name: 'Saigon Centre', - address: '65 Lê Lợi, Bến Nghé, Quận 1, TP.HCM', - lat: 10.7743, - lng: 106.7017, - availableSlots: 42, - totalSlots: 180, - availableSpaces: 42, - totalSpaces: 180, - hourlyRate: 18000, - pricePerHour: 18000, - openTime: '06:00', - closeTime: '00:00', - amenities: ['covered', 'security', 'ev_charging'], - contactInfo: { phone: '+84-28-3914-4999' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: true - }, - { - id: 3, - name: 'Landmark 81 SkyBar Parking', - address: '720A Điện Biên Phủ, Bình Thạnh, TP.HCM', - lat: 10.7955, - lng: 106.7195, - availableSlots: 156, - totalSlots: 400, - availableSpaces: 156, - totalSpaces: 400, - hourlyRate: 25000, - pricePerHour: 25000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'valet', 'luxury'], - contactInfo: { phone: '+84-28-3645-1234' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: false - }, - { - id: 4, - name: 'Bitexco Financial Tower', - address: '2 Hải Triều, Bến Nghé, Quận 1, TP.HCM', - lat: 10.7718, - lng: 106.7047, - availableSlots: 28, - totalSlots: 120, - availableSpaces: 28, - totalSpaces: 120, - hourlyRate: 20000, - pricePerHour: 20000, - openTime: '06:00', - closeTime: '23:00', - amenities: ['covered', 'security', 'premium'], - contactInfo: { phone: '+84-28-3915-6666' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: false - }, - { - id: 5, - name: 'Chợ Bến Thành Underground', - address: 'Lê Lợi, Bến Nghé, Quận 1, TP.HCM', - lat: 10.7729, - lng: 106.6980, - availableSlots: 67, - totalSlots: 150, - availableSpaces: 67, - totalSpaces: 150, - hourlyRate: 12000, - pricePerHour: 12000, - openTime: '05:00', - closeTime: '22:00', - amenities: ['underground', 'security'], - contactInfo: { phone: '+84-28-3925-3145' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: false - }, - { - id: 6, - name: 'Diamond Plaza Parking', - address: '34 Lê Duẩn, Bến Nghé, Quận 1, TP.HCM', - lat: 10.7786, - lng: 106.7046, - availableSlots: 93, - totalSlots: 200, - availableSpaces: 93, - totalSpaces: 200, - hourlyRate: 16000, - pricePerHour: 16000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'security'], - contactInfo: { phone: '+84-28-3825-7750' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: false - }, - { - id: 7, - name: 'Nhà Thờ Đức Bà Parking', - address: '01 Công xã Paris, Bến Nghé, Quận 1, TP.HCM', - lat: 10.7798, - lng: 106.6991, - availableSlots: 15, - totalSlots: 60, - availableSpaces: 15, - totalSpaces: 60, - hourlyRate: 10000, - pricePerHour: 10000, - openTime: '06:00', - closeTime: '18:00', - amenities: ['outdoor', 'heritage'], - contactInfo: { phone: '+84-28-3829-3477' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: false, - isEVCharging: false - }, - { - id: 8, - name: 'Takashimaya Parking', - address: '92-94 Nam Kỳ Khởi Nghĩa, Quận 1, TP.HCM', - lat: 10.7741, - lng: 106.7008, - availableSlots: 78, - totalSlots: 220, - availableSpaces: 78, - totalSpaces: 220, - hourlyRate: 17000, - pricePerHour: 17000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'premium', 'valet'], - contactInfo: { phone: '+84-28-3822-7222' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: false - }, - - // Thêm nhiều bãi đỗ xe mới cho test bán kính 4km - { - id: 9, - name: 'Quận 2 - The Vista Parking', - address: '628C Hanoi Highway, Quận 2, TP.HCM', - lat: 10.7879, - lng: 106.7308, - availableSlots: 95, - totalSlots: 200, - availableSpaces: 95, - totalSpaces: 200, - hourlyRate: 20000, - pricePerHour: 20000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'security'], - contactInfo: { phone: '+84-28-3744-5555' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: false - }, - { - id: 10, - name: 'Quận 3 - Viện Chợ Rẫy Parking', - address: '201B Nguyễn Chí Thanh, Quận 3, TP.HCM', - lat: 10.7656, - lng: 106.6889, - availableSlots: 45, - totalSlots: 120, - availableSpaces: 45, - totalSpaces: 120, - hourlyRate: 12000, - pricePerHour: 12000, - openTime: '05:00', - closeTime: '23:00', - amenities: ['outdoor', 'security'], - contactInfo: { phone: '+84-28-3855-4321' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: false - }, - { - id: 11, - name: 'Quận 5 - Chợ Lớn Plaza', - address: '1362 Trần Hưng Đạo, Quận 5, TP.HCM', - lat: 10.7559, - lng: 106.6631, - availableSlots: 67, - totalSlots: 150, - availableSpaces: 67, - totalSpaces: 150, - hourlyRate: 10000, - pricePerHour: 10000, - openTime: '06:00', - closeTime: '22:00', - amenities: ['covered', 'budget'], - contactInfo: { phone: '+84-28-3855-7890' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: false, - isEVCharging: false - }, - { - id: 12, - name: 'Quận 7 - Phú Mỹ Hưng Midtown', - address: '20 Nguyễn Lương Bằng, Quận 7, TP.HCM', - lat: 10.7291, - lng: 106.7194, - availableSlots: 112, - totalSlots: 300, - availableSpaces: 112, - totalSpaces: 300, - hourlyRate: 22000, - pricePerHour: 22000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'premium', 'ev_charging'], - contactInfo: { phone: '+84-28-5412-3456' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: true - }, - { - id: 13, - name: 'Quận 10 - Đại học Y khoa Parking', - address: '215 Hồng Bàng, Quận 10, TP.HCM', - lat: 10.7721, - lng: 106.6698, - availableSlots: 33, - totalSlots: 80, - availableSpaces: 33, - totalSpaces: 80, - hourlyRate: 8000, - pricePerHour: 8000, - openTime: '06:00', - closeTime: '20:00', - amenities: ['outdoor', 'budget'], - contactInfo: { phone: '+84-28-3864-2222' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: false, - isEVCharging: false - }, - { - id: 14, - name: 'Bình Thạnh - Vincom Landmark', - address: '800A Điện Biên Phủ, Bình Thạnh, TP.HCM', - lat: 10.8029, - lng: 106.7208, - availableSlots: 189, - totalSlots: 450, - availableSpaces: 189, - totalSpaces: 450, - hourlyRate: 18000, - pricePerHour: 18000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'security', 'valet'], - contactInfo: { phone: '+84-28-3512-6789' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: false - }, - { - id: 15, - name: 'Gò Vấp - Emart Shopping Center', - address: '242 Lê Đức Thọ, Gò Vấp, TP.HCM', - lat: 10.8239, - lng: 106.6834, - availableSlots: 145, - totalSlots: 380, - availableSpaces: 145, - totalSpaces: 380, - hourlyRate: 15000, - pricePerHour: 15000, - openTime: '07:00', - closeTime: '22:00', - amenities: ['covered', 'security'], - contactInfo: { phone: '+84-28-3989-1234' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: false - }, - { - id: 16, - name: 'Quận 4 - Bến Vân Đồn Port', - address: '5 Bến Vân Đồn, Quận 4, TP.HCM', - lat: 10.7575, - lng: 106.7053, - availableSlots: 28, - totalSlots: 60, - availableSpaces: 28, - totalSpaces: 60, - hourlyRate: 10000, - pricePerHour: 10000, - openTime: '06:00', - closeTime: '18:00', - amenities: ['outdoor'], - contactInfo: { phone: '+84-28-3940-5678' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: false, - isEVCharging: false - }, - { - id: 17, - name: 'Quận 6 - Bình Phú Industrial', - address: '1578 Hậu Giang, Quận 6, TP.HCM', - lat: 10.7395, - lng: 106.6345, - availableSlots: 78, - totalSlots: 180, - availableSpaces: 78, - totalSpaces: 180, - hourlyRate: 8000, - pricePerHour: 8000, - openTime: '05:00', - closeTime: '22:00', - amenities: ['covered', 'budget'], - contactInfo: { phone: '+84-28-3755-9999' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: false - }, - { - id: 18, - name: 'Tân Bình - Airport Plaza', - address: '1B Hồng Hà, Tân Bình, TP.HCM', - lat: 10.8099, - lng: 106.6631, - availableSlots: 234, - totalSlots: 500, - availableSpaces: 234, - totalSpaces: 500, - hourlyRate: 30000, - pricePerHour: 30000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'premium', 'valet', 'ev_charging'], - contactInfo: { phone: '+84-28-3844-7777' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: true - }, - { - id: 19, - name: 'Phú Nhuận - Phan Xích Long', - address: '453 Phan Xích Long, Phú Nhuận, TP.HCM', - lat: 10.7984, - lng: 106.6834, - availableSlots: 56, - totalSlots: 140, - availableSpaces: 56, - totalSpaces: 140, - hourlyRate: 16000, - pricePerHour: 16000, - openTime: '06:00', - closeTime: '00:00', - amenities: ['covered', 'security'], - contactInfo: { phone: '+84-28-3844-3333' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: false - }, - { - id: 20, - name: 'Quận 8 - Phạm Hùng Boulevard', - address: '688 Phạm Hùng, Quận 8, TP.HCM', - lat: 10.7389, - lng: 106.6756, - availableSlots: 89, - totalSlots: 200, - availableSpaces: 89, - totalSpaces: 200, - hourlyRate: 12000, - pricePerHour: 12000, - openTime: '05:30', - closeTime: '23:30', - amenities: ['covered', 'security'], - contactInfo: { phone: '+84-28-3876-5432' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: false - }, - { - id: 21, - name: 'Sân bay Tân Sơn Nhất - Terminal 1', - address: 'Sân bay Tân Sơn Nhất, TP.HCM', - lat: 10.8187, - lng: 106.6520, - availableSlots: 456, - totalSlots: 800, - availableSpaces: 456, - totalSpaces: 800, - hourlyRate: 25000, - pricePerHour: 25000, - openTime: '00:00', - closeTime: '23:59', - amenities: ['covered', 'premium', 'security'], - contactInfo: { phone: '+84-28-3848-5555' }, - isActive: true, - isOpen24Hours: true, - hasCCTV: true, - isEVCharging: false - }, - { - id: 22, - name: 'Quận 12 - Tân Chánh Hiệp Market', - address: '123 Tân Chánh Hiệp, Quận 12, TP.HCM', - lat: 10.8567, - lng: 106.6289, - availableSlots: 67, - totalSlots: 150, - availableSpaces: 67, - totalSpaces: 150, - hourlyRate: 8000, - pricePerHour: 8000, - openTime: '05:00', - closeTime: '20:00', - amenities: ['outdoor', 'budget'], - contactInfo: { phone: '+84-28-3718-8888' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: false, - isEVCharging: false - }, - { - id: 23, - name: 'Thủ Đức - Khu Công Nghệ Cao', - address: 'Xa lộ Hà Nội, Thủ Đức, TP.HCM', - lat: 10.8709, - lng: 106.8034, - availableSlots: 189, - totalSlots: 350, - availableSpaces: 189, - totalSpaces: 350, - hourlyRate: 15000, - pricePerHour: 15000, - openTime: '06:00', - closeTime: '22:00', - amenities: ['covered', 'security', 'ev_charging'], - contactInfo: { phone: '+84-28-3725-9999' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: true, - isEVCharging: true - }, - { - id: 24, - name: 'Nhà Bè - Phú Xuân Industrial', - address: '89 Huỳnh Tấn Phát, Nhà Bè, TP.HCM', - lat: 10.6834, - lng: 106.7521, - availableSlots: 45, - totalSlots: 100, - availableSpaces: 45, - totalSpaces: 100, - hourlyRate: 10000, - pricePerHour: 10000, - openTime: '06:00', - closeTime: '18:00', - amenities: ['outdoor', 'budget'], - contactInfo: { phone: '+84-28-3781-2345' }, - isActive: true, - isOpen24Hours: false, - hasCCTV: false, - isEVCharging: false - } - ]; - - const searchLocation = useCallback((location: Coordinates) => { - setState(prev => ({ - ...prev, - loading: true, - error: null, - searchLocation: location - })); - - // Simulate API call delay - setTimeout(() => { - try { - // Calculate distances and add to parking lots - const lotsWithDistance = mockParkingLots.map(lot => { - const distance = calculateDistance(location, { latitude: lot.lat, longitude: lot.lng }); - return { - ...lot, - distance: distance * 1000, // Convert to meters - walkingTime: Math.round(distance * 12), // Rough estimate: 12 minutes per km - }; - }); - - // Filter by 4km radius (4000 meters) and sort by distance - const lotsWithin4km = lotsWithDistance.filter(lot => lot.distance! <= 4000); - const sortedLots = lotsWithin4km.sort((a, b) => a.distance! - b.distance!); - - setState(prev => ({ - ...prev, - loading: false, - parkingLots: sortedLots - })); - } catch (error: any) { - setState(prev => ({ - ...prev, - loading: false, - error: error.message || 'Failed to search parking lots' - })); - } - }, 1000); - }, []); - - return { - parkingLots: state.parkingLots, - error: state.error, - searchLocation - }; -}; - -// 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/hooks/useRouting-simple.ts b/frontend/src/hooks/useRouting-simple.ts deleted file mode 100644 index 13ac7a5..0000000 --- a/frontend/src/hooks/useRouting-simple.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/launch.sh b/launch.sh new file mode 100644 index 0000000..a18af6b --- /dev/null +++ b/launch.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# 🚗 Smart Parking Finder - Quick Launcher +# This script provides quick access to all available scripts + +# Colors for output +BLUE='\033[0;34m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🚗 Smart Parking Finder - Quick Launcher${NC}" +echo -e "${BLUE}=========================================${NC}" +echo "" +echo -e "${GREEN}Available scripts:${NC}" +echo -e "${YELLOW} 📋 ./scripts/start.sh${NC} - Interactive menu with all options" +echo -e "${YELLOW} 🎨 ./scripts/frontend-only.sh${NC} - Start frontend only (fastest)" +echo -e "${YELLOW} 🔄 ./scripts/full-dev.sh${NC} - Full development (frontend + backend)" +echo -e "${YELLOW} 🐳 ./scripts/docker-dev.sh${NC} - Docker development environment" +echo -e "${YELLOW} 🛠️ ./scripts/setup.sh${NC} - Initial project setup" +echo "" +echo -e "${GREEN}Quick actions:${NC}" +echo -e "${YELLOW} 1. First time setup${NC} → ./scripts/setup.sh" +echo -e "${YELLOW} 2. Quick demo${NC} → ./scripts/frontend-only.sh" +echo -e "${YELLOW} 3. Full development${NC} → ./scripts/full-dev.sh" +echo -e "${YELLOW} 4. Interactive menu${NC} → ./scripts/start.sh" +echo "" + +# Make scripts executable if they aren't already +chmod +x scripts/*.sh + +# Check if user wants to run a script directly +if [ $# -eq 0 ]; then + echo -e "${GREEN}💡 Tip: Add a number (1-4) to run directly, or just press Enter for the interactive menu${NC}" + read -p "Enter your choice (1-4) or press Enter for menu: " choice + + case $choice in + 1) + echo -e "${GREEN}Running setup...${NC}" + ./scripts/setup.sh + ;; + 2) + echo -e "${GREEN}Starting frontend only...${NC}" + ./scripts/frontend-only.sh + ;; + 3) + echo -e "${GREEN}Starting full development...${NC}" + ./scripts/full-dev.sh + ;; + 4|"") + echo -e "${GREEN}Opening interactive menu...${NC}" + ./scripts/start.sh + ;; + *) + echo -e "${GREEN}Opening interactive menu...${NC}" + ./scripts/start.sh + ;; + esac +else + case $1 in + 1|setup) + ./scripts/setup.sh + ;; + 2|frontend) + ./scripts/frontend-only.sh + ;; + 3|full) + ./scripts/full-dev.sh + ;; + 4|menu) + ./scripts/start.sh + ;; + *) + ./scripts/start.sh + ;; + esac +fi diff --git a/optimize.sh b/optimize.sh new file mode 100644 index 0000000..e69de29 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..7584bec --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,201 @@ +# 📂 Scripts Directory + +This directory contains all the deployment and development scripts for Smart Parking Finder. + +## 📋 Available Scripts + +### 🚀 Main Scripts + +| Script | Purpose | Usage | +|--------|---------|--------| +| **start.sh** | 🎯 Interactive menu with all options | `./scripts/start.sh` | +| **frontend-only.sh** | 🎨 Start frontend only (fastest) | `./scripts/frontend-only.sh` | +| **full-dev.sh** | 🔄 Full development environment | `./scripts/full-dev.sh` | +| **docker-dev.sh** | 🐳 Docker development with all services | `./scripts/docker-dev.sh` | +| **setup.sh** | 🛠️ Initial project setup | `./scripts/setup.sh` | + +### 🎯 Quick Access from Root + +From the project root directory, you can use: + +```bash +# Interactive launcher with quick options +./launch.sh + +# Direct script access +./scripts/start.sh +./scripts/frontend-only.sh +./scripts/full-dev.sh +./scripts/docker-dev.sh +./scripts/setup.sh +``` + +## 📖 Script Details + +### 1. start.sh - Interactive Menu +**Purpose**: Provides a colorful interactive menu with all deployment options. + +**Features**: +- Frontend only deployment +- Network access (other devices on WiFi) +- Global access (via ngrok) +- Full development environment +- Docker development environment + +**Usage**: +```bash +./scripts/start.sh +# Follow the interactive menu (options 1-6) +``` + +### 2. frontend-only.sh - Quick Demo +**Purpose**: Fastest way to see the application interface. + +**What it does**: +- Installs frontend dependencies (if needed) +- Starts Next.js development server on http://localhost:3000 +- No backend required + +**Usage**: +```bash +./scripts/frontend-only.sh +``` + +**Note**: Some features may not work without backend. + +### 3. full-dev.sh - Complete Development +**Purpose**: Start both frontend and backend for full functionality. + +**What it does**: +- Installs both frontend and backend dependencies +- Starts NestJS backend on http://localhost:3001 +- Starts Next.js frontend on http://localhost:3000 +- Runs both in parallel + +**Usage**: +```bash +./scripts/full-dev.sh +``` + +**Requirements**: Node.js 18+ + +### 4. docker-dev.sh - Full Environment +**Purpose**: Complete development environment with all services. + +**What it includes**: +- Frontend (Next.js) on http://localhost:3000 +- Backend (NestJS) on http://localhost:3001 +- PostgreSQL database on localhost:5432 +- Redis cache on localhost:6379 +- Valhalla routing engine on localhost:8002 + +**Usage**: +```bash +./scripts/docker-dev.sh +``` + +**Requirements**: Docker Desktop + +### 5. setup.sh - Initial Setup +**Purpose**: One-time setup for new developers. + +**What it does**: +- Checks Node.js and npm installation +- Installs frontend dependencies +- Installs backend dependencies +- Makes all scripts executable +- Provides next steps + +**Usage**: +```bash +./scripts/setup.sh +``` + +## 🔧 Script Features + +### Error Handling +All scripts include: +- ✅ Prerequisite checking (Node.js, Docker, etc.) +- ✅ Graceful error messages +- ✅ Cleanup on exit (Ctrl+C) +- ✅ Colored output for better readability + +### Auto-Installation +Scripts automatically: +- 📦 Install npm dependencies if not present +- 🔧 Make scripts executable +- ⚡ Start services in the correct order + +### Cross-Platform Support +Scripts are designed for: +- 🍎 macOS (primary) +- 🐧 Linux +- 🪟 Windows (via Git Bash/WSL) + +## 🎨 Color Coding + +Scripts use consistent color coding: +- 🔵 **Blue**: Information and headers +- 🟢 **Green**: Success messages and URLs +- 🟡 **Yellow**: Warnings and tips +- 🔴 **Red**: Errors and problems +- 🟦 **Cyan**: Special highlights + +## 🚀 Getting Started + +### First Time Setup +```bash +# 1. Run initial setup +./scripts/setup.sh + +# 2. Choose your development style: +# Quick demo +./scripts/frontend-only.sh + +# Full development +./scripts/full-dev.sh + +# Complete environment +./scripts/docker-dev.sh + +# Interactive menu +./scripts/start.sh +``` + +### Daily Development +```bash +# Most common: Start frontend only for quick testing +./scripts/frontend-only.sh + +# When working on backend: Full development +./scripts/full-dev.sh + +# When testing integrations: Docker environment +./scripts/docker-dev.sh +``` + +## 🔄 Migration from Old Scripts + +The old scripts have been reorganized: + +| Old Script | New Script | Status | +|------------|------------|---------| +| `start.sh` | `scripts/start.sh` | ✅ Enhanced with better UI | +| `start-frontend-only.sh` | `scripts/frontend-only.sh` | ✅ Renamed and improved | +| `start-dev.sh` | `scripts/docker-dev.sh` | ✅ Moved and enhanced | +| `start-local.sh` | `scripts/full-dev.sh` | ✅ Renamed for clarity | +| `start-global.sh` | Part of `scripts/start.sh` | ✅ Integrated into menu | +| `start-network.sh` | Part of `scripts/start.sh` | ✅ Integrated into menu | +| `setup.sh` | `scripts/setup.sh` | ✅ Moved and enhanced | + +## 📞 Support + +If you encounter issues with any script: +1. Check the colored error messages +2. Ensure prerequisites are installed +3. Run the setup script: `./scripts/setup.sh` +4. Check the main documentation in `Documents/README.md` + +--- + +*All scripts are located in the `scripts/` directory for better organization.* diff --git a/scripts/docker-dev.sh b/scripts/docker-dev.sh new file mode 100644 index 0000000..cc8a4dc --- /dev/null +++ b/scripts/docker-dev.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# 🐳 Smart Parking Finder - Docker Development Environment +echo "🚗 Starting Smart Parking Finder Development Environment with Docker..." + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Navigate to project root (go back one level since we're in scripts folder) +cd "$(dirname "$0")/.." + +# Check required tools +echo "Checking required tools..." +if ! command_exists docker; then + echo "❌ Docker is not installed. Please install Docker first." + echo "📥 Download from: https://www.docker.com/products/docker-desktop" + exit 1 +fi + +# Check if Docker is running +if ! docker info >/dev/null 2>&1; then + echo "❌ Docker is not running. Please start Docker and try again." + exit 1 +fi + +echo "✅ All requirements met!" +echo "" + +# Start services with docker-compose +echo "🐳 Starting services with Docker Compose..." +docker-compose up -d + +# Wait for services to be ready +echo "⏳ Waiting for services to be ready..." +sleep 10 + +echo "" +echo "🎉 Smart Parking Finder is now running!" +echo "📋 Service URLs:" +echo " 🎨 Frontend: http://localhost:3000" +echo " 🔧 Backend API: http://localhost:3001" +echo " 📊 API Docs: http://localhost:3001/api/docs" +echo " 🗄️ Database: localhost:5432" +echo " 🔴 Redis: localhost:6379" +echo " 🗺️ Valhalla: localhost:8002" +echo "" +echo "🔧 Management commands:" +echo " 📋 View logs: docker-compose logs -f" +echo " 🛑 Stop all: docker-compose down" +echo " 🗑️ Clean up: docker-compose down -v" +echo "" diff --git a/scripts/frontend-only.sh b/scripts/frontend-only.sh new file mode 100644 index 0000000..29a562b --- /dev/null +++ b/scripts/frontend-only.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# 🎨 Frontend Only Start Script +echo "🎨 Starting Smart Parking Finder Frontend..." + +# Navigate to frontend directory (go back one level since we're in scripts folder) +cd "$(dirname "$0")/../frontend" + +# Check dependencies +if [ ! -d "node_modules" ]; then + echo "📦 Installing dependencies..." + npm install +fi + +echo "🚀 Starting Next.js development server..." +echo "🌐 Frontend will run on: http://localhost:3000" +echo "" +echo "⚠️ Note: Backend is not started. Some features may not work." +echo "💡 To start backend, open new terminal and run:" +echo " cd backend && npm run start:dev" +echo "" + +npm run dev diff --git a/scripts/full-dev.sh b/scripts/full-dev.sh new file mode 100644 index 0000000..1216265 --- /dev/null +++ b/scripts/full-dev.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# 🔄 Full Development Environment (Frontend + Backend) +echo "🔄 Starting Full Development Environment..." + +# Navigate to project root +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Function to check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check Node.js +if ! command_exists node; then + echo "❌ Node.js is not installed. Please install Node.js first." + exit 1 +fi + +echo "✅ Node.js is ready" + +# Start backend in background +echo "🔧 Starting Backend on http://localhost:3001..." +cd "$PROJECT_DIR/backend" + +if [ ! -d "node_modules" ]; then + echo "📦 Installing backend dependencies..." + npm install +fi + +npm run start:dev & +BACKEND_PID=$! + +# Start frontend +echo "🎨 Starting Frontend on http://localhost:3000..." +cd "$PROJECT_DIR/frontend" + +if [ ! -d "node_modules" ]; then + echo "📦 Installing frontend dependencies..." + npm install +fi + +# Wait a bit for backend to start +sleep 3 + +npm run dev & +FRONTEND_PID=$! + +echo "🎉 Full development environment started!" +echo "Frontend: http://localhost:3000" +echo "Backend: http://localhost:3001" + +# Wait for user to exit +echo "Press Ctrl+C to stop all services..." + +# Cleanup on exit +trap "kill $BACKEND_PID $FRONTEND_PID 2>/dev/null" EXIT +wait diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..9dc4ecc --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# 🛠️ Smart Parking Finder - Project 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 + +# Navigate to project root +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# 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 +} + +print_status "🚗 Setting up Smart Parking Finder development environment..." + +# Check Node.js +if command_exists node; then + NODE_VERSION=$(node --version) + print_success "Node.js is installed: $NODE_VERSION" +else + print_error "Node.js is not installed. Please install Node.js 18+ first." + exit 1 +fi + +# Check npm +if command_exists npm; then + NPM_VERSION=$(npm --version) + print_success "npm is installed: $NPM_VERSION" +else + print_error "npm is not installed." + exit 1 +fi + +# Setup Frontend +print_status "📦 Setting up Frontend dependencies..." +cd "$PROJECT_DIR/frontend" +npm install +print_success "Frontend dependencies installed" + +# Setup Backend +print_status "📦 Setting up Backend dependencies..." +cd "$PROJECT_DIR/backend" +npm install +print_success "Backend dependencies installed" + +# Make scripts executable +print_status "🔧 Making scripts executable..." +chmod +x "$PROJECT_DIR/scripts/"*.sh +print_success "Scripts are now executable" + +print_success "🎉 Setup complete!" +echo "" +echo "🚀 To start the application:" +echo " 📱 Frontend only: ./scripts/frontend-only.sh" +echo " 🔄 Full development: ./scripts/full-dev.sh" +echo " 🐳 Docker development: ./scripts/docker-dev.sh" +echo " 🎯 Interactive menu: ./scripts/start.sh" diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..defa091 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,255 @@ +#!/bin/bash + +# 🚗 Smart Parking Finder - Unified Start Script +# This script provides multiple deployment options + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Project directory (go back one level since we're in scripts folder) +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Function to display menu +show_menu() { + echo -e "${BLUE}🚗 Smart Parking Finder - Start Options${NC}" + echo -e "${BLUE}============================================${NC}" + echo "" + echo -e "${GREEN}1.${NC} 🎨 Frontend Only (Quick Demo)" + echo -e "${GREEN}2.${NC} 🌐 Network Access (Other devices can access)" + echo -e "${GREEN}3.${NC} 🌍 Global Access (Internet access via ngrok)" + echo -e "${GREEN}4.${NC} 🔄 Full Development (Frontend + Backend)" + echo -e "${GREEN}5.${NC} 🐳 Docker Development (Complete with services)" + echo -e "${GREEN}6.${NC} ❌ Exit" + echo "" + echo -e "${YELLOW}Choose an option [1-6]:${NC} " +} + +# Function to check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to get local IP +get_local_ip() { + ifconfig | grep -E "inet.*broadcast" | head -1 | awk '{print $2}' +} + +# Function to start frontend only +start_frontend_only() { + echo -e "${BLUE}🎨 Starting Frontend Only...${NC}" + cd "$PROJECT_DIR/frontend" + + if [ ! -d "node_modules" ]; then + echo -e "${YELLOW}📦 Installing dependencies...${NC}" + npm install + fi + + echo -e "${GREEN}🚀 Starting Next.js development server...${NC}" + echo -e "${CYAN}🌐 Frontend will run on: http://localhost:3000${NC}" + echo "" + echo -e "${YELLOW}⚠️ Note: Backend is not started. Some features may not work.${NC}" + echo -e "${YELLOW}💡 To start backend, open new terminal and run:${NC}" + echo -e "${YELLOW} cd backend && npm run start:dev${NC}" + echo "" + + npm run dev +} + +# Function to start with network access +start_network_access() { + echo -e "${BLUE}🌐 Starting with Network Access...${NC}" + LOCAL_IP=$(get_local_ip) + + echo -e "${BLUE}===============================================${NC}" + echo -e "${BLUE}🌐 NETWORK ACCESS INFORMATION${NC}" + echo -e "${BLUE}===============================================${NC}" + echo -e "${GREEN}📱 Local Access: http://localhost:3000${NC}" + echo -e "${GREEN}🌍 Network Access: http://$LOCAL_IP:3000${NC}" + echo -e "${BLUE}===============================================${NC}" + echo "" + echo -e "${YELLOW}📋 To access from other devices:${NC}" + echo -e "${YELLOW} 1. Make sure devices are on the same WiFi network${NC}" + echo -e "${YELLOW} 2. Use this URL: http://$LOCAL_IP:3000${NC}" + echo -e "${YELLOW} 3. Make sure macOS Firewall allows Node.js connections${NC}" + echo "" + + cd "$PROJECT_DIR/frontend" + + if [ ! -d "node_modules" ]; then + echo -e "${YELLOW}📦 Installing dependencies...${NC}" + npm install + fi + + npm run dev +} + +# Function to start with global access +start_global_access() { + echo -e "${BLUE}🌍 Starting with Global Access...${NC}" + + if ! command_exists ngrok; then + echo -e "${YELLOW}❌ ngrok is not installed. Installing...${NC}" + if command_exists brew; then + brew install ngrok/ngrok/ngrok + else + echo -e "${RED}❌ Please install ngrok manually: https://ngrok.com/download${NC}" + exit 1 + fi + fi + + LOCAL_IP=$(get_local_ip) + + echo -e "${BLUE}===============================================${NC}" + echo -e "${BLUE}🌍 GLOBAL ACCESS DEPLOYMENT${NC}" + echo -e "${BLUE}===============================================${NC}" + echo -e "${GREEN}📱 Local Access: http://localhost:3000${NC}" + echo -e "${GREEN}🏠 Network Access: http://$LOCAL_IP:3000${NC}" + echo -e "${GREEN}🌐 Global Access: Will be shown after ngrok starts${NC}" + echo -e "${BLUE}===============================================${NC}" + echo "" + + cd "$PROJECT_DIR/frontend" + + if [ ! -d "node_modules" ]; then + echo -e "${YELLOW}📦 Installing dependencies...${NC}" + npm install + fi + + # Start frontend in background + npm run dev & + FRONTEND_PID=$! + + # Wait for frontend to start + sleep 5 + + # Start ngrok + echo -e "${CYAN}🔗 Creating global tunnel...${NC}" + ngrok http 3000 + + # Cleanup on exit + trap "kill $FRONTEND_PID" EXIT +} + +# Function to start full development +start_full_development() { + echo -e "${BLUE}🔄 Starting Full Development (Frontend + Backend)...${NC}" + + # Check Node.js + if ! command_exists node; then + echo -e "${RED}❌ Node.js is not installed. Please install Node.js first.${NC}" + exit 1 + fi + + echo -e "${GREEN}✅ Node.js is ready${NC}" + + # Start backend in background + echo -e "${BLUE}🔧 Starting Backend on http://localhost:3001...${NC}" + cd "$PROJECT_DIR/backend" + + if [ ! -d "node_modules" ]; then + echo -e "${YELLOW}📦 Installing backend dependencies...${NC}" + npm install + fi + + npm run start:dev & + BACKEND_PID=$! + + # Start frontend + echo -e "${BLUE}🎨 Starting Frontend on http://localhost:3000...${NC}" + cd "$PROJECT_DIR/frontend" + + if [ ! -d "node_modules" ]; then + echo -e "${YELLOW}📦 Installing frontend dependencies...${NC}" + npm install + fi + + # Wait a bit for backend to start + sleep 3 + + npm run dev & + FRONTEND_PID=$! + + echo -e "${GREEN}🎉 Full development environment started!${NC}" + echo -e "${GREEN}Frontend: http://localhost:3000${NC}" + echo -e "${GREEN}Backend: http://localhost:3001${NC}" + + # Wait for user to exit + echo -e "${YELLOW}Press Ctrl+C to stop all services...${NC}" + + # Cleanup on exit + trap "kill $BACKEND_PID $FRONTEND_PID 2>/dev/null" EXIT + wait +} + +# Function to start docker development +start_docker_development() { + echo -e "${BLUE}🐳 Starting Docker Development...${NC}" + + # Check Docker + if ! command_exists docker; then + echo -e "${RED}❌ Docker is not installed. Please install Docker first.${NC}" + exit 1 + fi + + if ! docker info >/dev/null 2>&1; then + echo -e "${RED}❌ Docker is not running. Please start Docker and try again.${NC}" + exit 1 + fi + + echo -e "${GREEN}✅ Docker is ready${NC}" + + cd "$PROJECT_DIR" + + echo -e "${BLUE}🚀 Starting Docker Compose...${NC}" + docker-compose up -d + + echo -e "${GREEN}🎉 Docker development environment started!${NC}" + echo -e "${GREEN}Frontend: http://localhost:3000${NC}" + echo -e "${GREEN}Backend: http://localhost:3001${NC}" + echo -e "${GREEN}Database: localhost:5432${NC}" + echo -e "${GREEN}Redis: localhost:6379${NC}" + + echo -e "${YELLOW}To stop: docker-compose down${NC}" +} + +# Main execution +while true; do + show_menu + read -p "" choice + + case $choice in + 1) + start_frontend_only + break + ;; + 2) + start_network_access + break + ;; + 3) + start_global_access + break + ;; + 4) + start_full_development + break + ;; + 5) + start_docker_development + break + ;; + 6) + echo -e "${GREEN}👋 Goodbye!${NC}" + exit 0 + ;; + *) + echo -e "${RED}❌ Invalid option. Please choose 1-6.${NC}" + echo "" + ;; + esac +done diff --git a/start-dev.sh b/start-dev.sh old mode 100755 new mode 100644 index e6652dd..e69de29 --- a/start-dev.sh +++ b/start-dev.sh @@ -1,104 +0,0 @@ -#!/bin/bash - -# Smart Parking Finder - Development Start Script -echo "🚗 Starting Smart Parking Finder Development Environment..." - -# Function to check if a command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Check required tools -echo "Checking required tools..." -if ! command_exists node; then - echo "❌ Node.js is not installed. Please install Node.js first." - exit 1 -fi - -if ! command_exists npm; then - echo "❌ npm is not installed. Please install npm first." - exit 1 -fi - -if ! command_exists docker; then - echo "❌ Docker is not installed. Please install Docker first." - exit 1 -fi - -# Check if Docker is running -if ! docker info >/dev/null 2>&1; then - echo "❌ Docker is not running. Please start Docker and try again." - exit 1 -fi - -echo "✅ All required tools are available" - -# Start infrastructure services (PostgreSQL, Redis, Valhalla) -echo "🐳 Starting infrastructure services..." -docker-compose up -d postgres redis valhalla - -# Wait for services to be ready -echo "⏳ Waiting for services to be ready..." -sleep 10 - -# Check if services are running -if docker-compose ps | grep -q "Up"; then - echo "✅ Infrastructure services are running" -else - echo "❌ Failed to start infrastructure services" - docker-compose logs - exit 1 -fi - -# Start backend in background -echo "🔧 Starting backend server..." -cd backend -npm run start:dev & -BACKEND_PID=$! -cd .. - -# Wait a bit for backend to start -sleep 5 - -# Start frontend -echo "🌐 Starting frontend server..." -cd frontend -npm run dev & -FRONTEND_PID=$! -cd .. - -echo "" -echo "🎉 Smart Parking Finder is starting up!" -echo "" -echo "📡 Backend API: http://localhost:3001" -echo " - Swagger API docs: http://localhost:3001/api" -echo " - Health check: http://localhost:3001/health" -echo "" -echo "🌐 Frontend App: http://localhost:3000" -echo "" -echo "🗄️ Database: PostgreSQL on localhost:5432" -echo " - PgAdmin: http://localhost:5050 (admin@admin.com / admin)" -echo "" -echo "⚡ Redis: localhost:6379" -echo " - Redis Commander: http://localhost:8081" -echo "" -echo "🗺️ Valhalla Routing: http://localhost:8002" -echo "" -echo "Press Ctrl+C to stop all services" - -# Function to cleanup on exit -cleanup() { - echo "" - echo "🛑 Stopping all services..." - kill $BACKEND_PID 2>/dev/null - kill $FRONTEND_PID 2>/dev/null - docker-compose down - echo "✅ All services stopped" - exit 0 -} - -# Set trap to cleanup on script exit -trap cleanup SIGINT SIGTERM - -# Keep script running -wait diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..e69de29