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
This commit is contained in:
125
.gitignore
vendored
Normal file
125
.gitignore
vendored
Normal file
@@ -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/
|
||||
14
.vscode/tasks.json
vendored
Normal file
14
.vscode/tasks.json
vendored
Normal file
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
1023
Documents/API_SCHEMA.md
Normal file
1023
Documents/API_SCHEMA.md
Normal file
File diff suppressed because it is too large
Load Diff
839
Documents/DATABASE_DESIGN.md
Normal file
839
Documents/DATABASE_DESIGN.md
Normal file
@@ -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*
|
||||
451
Documents/README.md
Normal file
451
Documents/README.md
Normal file
@@ -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 <repository>
|
||||
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*
|
||||
611
Documents/SYSTEM_ARCHITECTURE.md
Normal file
611
Documents/SYSTEM_ARCHITECTURE.md
Normal file
@@ -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<ParkingLot[]> {
|
||||
// 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<ParkingLot> {
|
||||
async findWithinRadius(params: SpatialQueryParams): Promise<ParkingLot[]> {
|
||||
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*
|
||||
420
Documents/TECHNICAL_SPECIFICATION.md
Normal file
420
Documents/TECHNICAL_SPECIFICATION.md
Normal file
@@ -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.
|
||||
189
GIT_UPLOAD_GUIDE.md
Normal file
189
GIT_UPLOAD_GUIDE.md
Normal file
@@ -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.*
|
||||
0
HUONG_DAN_LOCAL.md
Normal file
0
HUONG_DAN_LOCAL.md
Normal file
0
LOCAL_DEPLOYMENT_GUIDE.md
Normal file
0
LOCAL_DEPLOYMENT_GUIDE.md
Normal file
0
OPTIMIZATION_REPORT.md
Normal file
0
OPTIMIZATION_REPORT.md
Normal file
145
REORGANIZATION_GUIDE.md
Normal file
145
REORGANIZATION_GUIDE.md
Normal file
@@ -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.*
|
||||
0
frontend/next.config.optimized.js
Normal file
0
frontend/next.config.optimized.js
Normal file
@@ -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() {
|
||||
</svg>
|
||||
Làm mới danh sách
|
||||
</button>
|
||||
|
||||
{/* Status Info Bar - Thiết kế thanh lịch đơn giản */}
|
||||
<div className="mt-4 bg-white/80 backdrop-blur-sm rounded-xl border border-gray-200/50 shadow-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Thống kê bãi đỗ */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
|
||||
<span className="text-sm text-gray-700 font-medium">
|
||||
{parkingLots.filter(lot => lot.availableSlots > 0).length} có chỗ
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-px h-4 bg-gray-300"></div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500"></div>
|
||||
<span className="text-sm text-gray-700 font-medium">
|
||||
{parkingLots.filter(lot => lot.availableSlots === 0).length} đầy
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thông tin thời tiết và giờ */}
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-600">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>{currentTime.getHours() >= 6 && currentTime.getHours() < 18 ? '☀️' : '🌙'}</span>
|
||||
<span className="font-medium">
|
||||
{currentTime.getHours() >= 6 && currentTime.getHours() < 18 ? '28°C' : '24°C'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-px h-4 bg-gray-300"></div>
|
||||
<div className="font-medium">
|
||||
{currentTime.toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="mt-4 relative">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tìm kiếm bãi đỗ xe..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full px-4 py-3 pl-12 pr-10 text-sm font-medium rounded-2xl border-2 transition-all duration-300 focus:outline-none focus:ring-4 focus:ring-orange-100 focus:border-orange-300"
|
||||
style={{
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)'
|
||||
}}
|
||||
/>
|
||||
{/* Search Icon */}
|
||||
<div className="absolute left-4 top-1/2 transform -translate-y-1/2">
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* Clear Button */}
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 rounded-full hover:bg-gray-100 transition-colors duration-200"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter buttons - Below header */}
|
||||
@@ -236,13 +320,13 @@ export default function ParkingFinderPage() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707v4.586a1 1 0 01-.54.89l-2 1A1 1 0 0110 20v-5.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-bold" style={{ color: 'var(--accent-color)' }}>Sắp xếp:</span>
|
||||
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Sắp xếp:</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSortType('availability')}
|
||||
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all duration-300 shadow-md ${
|
||||
className={`px-3 py-3 text-sm font-bold rounded-lg transition-all duration-300 shadow-md flex items-center justify-center ${
|
||||
sortType === 'availability'
|
||||
? 'transform scale-105'
|
||||
: 'hover:transform hover:scale-105'
|
||||
@@ -255,13 +339,18 @@ export default function ParkingFinderPage() {
|
||||
borderColor: sortType === 'availability' ? 'var(--primary-color)' : 'rgba(232, 90, 79, 0.3)',
|
||||
border: '2px solid'
|
||||
}}
|
||||
title="Sắp xếp theo chỗ trống"
|
||||
>
|
||||
Chỗ trống
|
||||
<Icon
|
||||
name="car"
|
||||
size="lg"
|
||||
className={sortType === 'availability' ? 'text-white' : 'text-orange-500'}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setSortType('price')}
|
||||
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all duration-300 shadow-md ${
|
||||
className={`px-3 py-3 text-sm font-bold rounded-lg transition-all duration-300 shadow-md flex items-center justify-center ${
|
||||
sortType === 'price'
|
||||
? 'transform scale-105'
|
||||
: 'hover:transform hover:scale-105'
|
||||
@@ -274,14 +363,19 @@ export default function ParkingFinderPage() {
|
||||
borderColor: sortType === 'price' ? '#10B981' : 'rgba(16, 185, 129, 0.3)',
|
||||
border: '2px solid'
|
||||
}}
|
||||
title="Sắp xếp theo giá rẻ"
|
||||
>
|
||||
Giá rẻ
|
||||
<Icon
|
||||
name="currency"
|
||||
size="xl"
|
||||
className={sortType === 'price' ? 'text-white' : 'text-green-600'}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setSortType('distance')}
|
||||
disabled={!userLocation}
|
||||
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all duration-300 shadow-md ${
|
||||
className={`px-3 py-3 text-sm font-bold rounded-lg transition-all duration-300 shadow-md flex items-center justify-center ${
|
||||
sortType === 'distance'
|
||||
? 'transform scale-105'
|
||||
: userLocation
|
||||
@@ -290,18 +384,27 @@ export default function ParkingFinderPage() {
|
||||
}`}
|
||||
style={{
|
||||
background: sortType === 'distance'
|
||||
? 'linear-gradient(135deg, #8B5CF6, #7C3AED)'
|
||||
? 'linear-gradient(135deg, #F59E0B, #D97706)'
|
||||
: userLocation ? 'white' : '#F9FAFB',
|
||||
color: sortType === 'distance'
|
||||
? 'white'
|
||||
: userLocation ? '#7C3AED' : '#9CA3AF',
|
||||
: userLocation ? '#D97706' : '#9CA3AF',
|
||||
borderColor: sortType === 'distance'
|
||||
? '#8B5CF6'
|
||||
: userLocation ? 'rgba(139, 92, 246, 0.3)' : '#E5E7EB',
|
||||
? '#F59E0B'
|
||||
: userLocation ? 'rgba(245, 158, 11, 0.3)' : '#E5E7EB',
|
||||
border: '2px solid'
|
||||
}}
|
||||
title="Sắp xếp theo khoảng cách gần nhất"
|
||||
>
|
||||
Gần nhất
|
||||
<Icon
|
||||
name="distance"
|
||||
size="lg"
|
||||
className={
|
||||
sortType === 'distance'
|
||||
? 'text-white'
|
||||
: userLocation ? 'text-amber-600' : 'text-gray-400'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,6 +440,7 @@ export default function ParkingFinderPage() {
|
||||
selectedId={selectedParkingLot?.id}
|
||||
userLocation={userLocation}
|
||||
sortType={sortType}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -356,7 +460,25 @@ export default function ParkingFinderPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Map Section - Center */}
|
||||
{/* Middle Column - Parking Details */}
|
||||
{selectedParkingLot && (
|
||||
<div className="w-[26rem] h-full">
|
||||
<ParkingDetails
|
||||
parkingLot={selectedParkingLot}
|
||||
userLocation={userLocation}
|
||||
onClose={() => {
|
||||
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
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map Section - Right */}
|
||||
<div className="flex-1 h-full relative">
|
||||
<MapView
|
||||
userLocation={userLocation}
|
||||
@@ -367,9 +489,9 @@ export default function ParkingFinderPage() {
|
||||
className="w-full h-full"
|
||||
/>
|
||||
|
||||
{/* Map overlay info - Moved to bottom right */}
|
||||
{/* Map overlay info - Position based on layout */}
|
||||
{userLocation && (
|
||||
<div className="absolute bottom-6 right-24 bg-white rounded-3xl shadow-2xl p-6 z-10 border-2 border-gray-100 backdrop-blur-sm" style={{ minWidth: '280px' }}>
|
||||
<div className="absolute bottom-6 right-24 z-10 bg-white rounded-3xl shadow-2xl p-6 border-2 border-gray-100 backdrop-blur-sm" style={{ minWidth: '280px' }}>
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
|
||||
<img
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,7 @@ export const MapView: React.FC<MapViewProps> = ({
|
||||
const [routeInfo, setRouteInfo] = useState<{distance: number, duration: number} | null>(null);
|
||||
const [isCalculatingRoute, setIsCalculatingRoute] = useState(false);
|
||||
const [debugInfo, setDebugInfo] = useState<string>('');
|
||||
const [currentZoom, setCurrentZoom] = useState<number>(13);
|
||||
|
||||
// OpenRouteService API key
|
||||
const ORS_API_KEY = "eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6ImJmMjM5NTNiMjNlNzQzZWY4NWViMDFlYjNkNTRkNmVkIiwiaCI6Im11cm11cjY0In0=";
|
||||
@@ -225,27 +226,122 @@ export const MapView: React.FC<MapViewProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 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<MapViewProps> = ({
|
||||
// );
|
||||
// }
|
||||
|
||||
// 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 (
|
||||
<div className={`${finalClassName} map-container`} style={{ minHeight: '0', flexGrow: 1, height: '100%' }}>
|
||||
<div className={`${finalClassName} map-container relative`} style={{ minHeight: '0', flexGrow: 1, height: '100%' }}>
|
||||
{/* Custom Zoom Controls */}
|
||||
<div className="absolute top-4 right-4 z-[1000] flex flex-col gap-2">
|
||||
{/* Zoom Level Display */}
|
||||
<div
|
||||
className="px-3 py-1 bg-white rounded-lg shadow-lg text-xs font-bold text-center border-2"
|
||||
style={{
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)',
|
||||
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))',
|
||||
color: 'var(--accent-color)'
|
||||
}}
|
||||
>
|
||||
Zoom: {currentZoom.toFixed(0)}
|
||||
</div>
|
||||
{/* Zoom In */}
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
className="w-10 h-10 bg-white rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center text-gray-700 hover:text-white border-2 hover:scale-110"
|
||||
style={{
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)',
|
||||
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))';
|
||||
}}
|
||||
>
|
||||
<svg className="w-5 h-5 font-bold" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Zoom Out */}
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
className="w-10 h-10 bg-white rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center text-gray-700 hover:text-white border-2 hover:scale-110"
|
||||
style={{
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)',
|
||||
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))';
|
||||
}}
|
||||
>
|
||||
<svg className="w-5 h-5 font-bold" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M18 12H6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Zoom to User */}
|
||||
{userLocation && (
|
||||
<button
|
||||
onClick={zoomToUser}
|
||||
className="w-10 h-10 bg-white rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center text-gray-700 hover:text-white border-2 hover:scale-110"
|
||||
style={{
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)',
|
||||
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))';
|
||||
}}
|
||||
title="Zoom đến vị trí của bạn"
|
||||
>
|
||||
<svg className="w-5 h-5 font-bold" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
|
||||
<circle cx="12" cy="9" r="2.5" strokeWidth={2.5} />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Zoom to Selected Parking */}
|
||||
{selectedParkingLot && (
|
||||
<button
|
||||
onClick={zoomToSelected}
|
||||
className="w-10 h-10 bg-white rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center text-gray-700 hover:text-white border-2 hover:scale-110"
|
||||
style={{
|
||||
borderColor: 'rgba(220, 38, 38, 0.3)',
|
||||
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(254, 242, 242, 0.95))'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, #DC2626, #B91C1C)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(254, 242, 242, 0.95))';
|
||||
}}
|
||||
title="Zoom đến bãi xe đã chọn"
|
||||
>
|
||||
<svg className="w-5 h-5 font-bold" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Fit All */}
|
||||
{parkingLots.length > 0 && (
|
||||
<button
|
||||
onClick={zoomToFitAll}
|
||||
className="w-10 h-10 bg-white rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center text-gray-700 hover:text-white border-2 hover:scale-110"
|
||||
style={{
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)',
|
||||
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95))';
|
||||
}}
|
||||
title="Xem tất cả bãi xe"
|
||||
>
|
||||
<svg className="w-5 h-5 font-bold" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 8V4a2 2 0 012-2h2M4 16v4a2 2 0 002 2h2m8-20h2a2 2 0 012 2v4m0 8v4a2 2 0 01-2 2h-2" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MapContainer
|
||||
center={[center.lat, center.lng]}
|
||||
zoom={13}
|
||||
style={{ height: '100%', width: '100%', minHeight: '100%' }}
|
||||
zoomControl={true}
|
||||
zoomControl={false}
|
||||
scrollWheelZoom={true}
|
||||
doubleClickZoom={true}
|
||||
touchZoom={true}
|
||||
@@ -756,12 +1045,39 @@ export const MapView: React.FC<MapViewProps> = ({
|
||||
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<MapViewProps> = ({
|
||||
<TileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
maxZoom={18}
|
||||
maxZoom={19}
|
||||
minZoom={3}
|
||||
maxNativeZoom={18}
|
||||
tileSize={256}
|
||||
zoomOffset={0}
|
||||
detectRetina={true}
|
||||
/>
|
||||
|
||||
{/* User Location Marker (GPS với hiệu ứng pulse) */}
|
||||
@@ -814,7 +1134,15 @@ export const MapView: React.FC<MapViewProps> = ({
|
||||
)}
|
||||
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<MapViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onParkingLotSelect?.(lot)}
|
||||
className={`w-full px-4 py-3 rounded-xl text-sm font-bold transition-all duration-300 transform hover:scale-105 border-2 ${
|
||||
isSelected
|
||||
? 'bg-red-600 text-white hover:bg-red-700 border-red-700 shadow-lg shadow-red-300/50'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 border-blue-700 shadow-lg shadow-blue-300/50'
|
||||
}`}
|
||||
>
|
||||
{isSelected ? (
|
||||
<>❌ Bỏ chọn bãi đỗ xe</>
|
||||
) : (
|
||||
<>🎯 Chọn bãi đỗ xe này</>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => onParkingLotSelect?.(lot)}
|
||||
className={`flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-300 transform hover:scale-105 border-2 ${
|
||||
isSelected
|
||||
? 'bg-red-600 text-white hover:bg-red-700 border-red-700 shadow-lg shadow-red-300/50'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 border-blue-700 shadow-lg shadow-blue-300/50'
|
||||
}`}
|
||||
>
|
||||
{isSelected ? (
|
||||
<>❌ Bỏ chọn</>
|
||||
) : (
|
||||
<>🎯 Chọn bãi xe</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Focus camera button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('Focus button clicked for:', lot.name);
|
||||
moveCameraTo(lot.lat, lot.lng, 18, 1.2);
|
||||
}}
|
||||
className="px-4 py-3 rounded-xl text-sm font-bold transition-all duration-300 transform hover:scale-105 border-2 bg-green-600 text-white hover:bg-green-700 border-green-700 shadow-lg shadow-green-300/50"
|
||||
title="Di chuyển camera đến bãi xe"
|
||||
>
|
||||
📹 Focus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
0
frontend/src/components/parking/BookingModal.tsx
Normal file
0
frontend/src/components/parking/BookingModal.tsx
Normal file
731
frontend/src/components/parking/ParkingDetails.tsx
Normal file
731
frontend/src/components/parking/ParkingDetails.tsx
Normal file
@@ -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 (
|
||||
<div className="w-full space-y-4">
|
||||
{/* Floor selector */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
{floors.map((floor) => (
|
||||
<button
|
||||
key={floor.floor}
|
||||
onClick={() => setSelectedFloor(floor.floor)}
|
||||
className={`px-3 py-2 rounded-lg font-medium text-sm transition-all ${
|
||||
selectedFloor === floor.floor
|
||||
? 'text-white shadow-md'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
style={{
|
||||
background: selectedFloor === floor.floor
|
||||
? 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
: undefined
|
||||
}}
|
||||
>
|
||||
{floor.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Real-time indicator */}
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>Cập nhật: {lastUpdate.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floor stats */}
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-center p-2 bg-green-50 rounded border border-green-200">
|
||||
<div className="font-bold text-green-600">{floorStats.available}</div>
|
||||
<div className="text-green-700">Trống</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-red-50 rounded border border-red-200">
|
||||
<div className="font-bold text-red-600">{floorStats.occupied}</div>
|
||||
<div className="text-red-700">Đã đậu</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-gray-50 rounded border border-gray-200">
|
||||
<div className="font-bold text-gray-600">{floorStats.total}</div>
|
||||
<div className="text-gray-700">Tổng</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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) => (
|
||||
<svg
|
||||
key={i}
|
||||
className={`w-4 h-4 ${i < rating ? 'text-yellow-400' : 'text-gray-300'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
));
|
||||
};
|
||||
|
||||
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<ParkingDetailsProps> = ({
|
||||
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 (
|
||||
<div className="w-full h-full bg-white shadow-xl border-l-2 flex flex-col overflow-hidden" style={{ borderColor: 'rgba(232, 90, 79, 0.2)' }}>
|
||||
{/* Header */}
|
||||
<div className="relative p-6 text-white" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-white bg-opacity-20 flex items-center justify-center hover:bg-opacity-30 transition-all"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="w-16 h-16 bg-white bg-opacity-20 rounded-2xl flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-xl font-bold mb-2 leading-tight">
|
||||
{parkingLot.name}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 text-sm opacity-90 mb-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="truncate">{parkingLot.address}</span>
|
||||
{distance && (
|
||||
<>
|
||||
<span className="px-2">•</span>
|
||||
<span className="font-semibold">{formatDistance(distance)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Rating */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{renderStars(Math.round(averageRating))}
|
||||
</div>
|
||||
<span className="text-sm font-semibold">{averageRating.toFixed(1)}</span>
|
||||
<span className="text-sm opacity-80">({SAMPLE_REVIEWS.length} đánh giá)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status banners */}
|
||||
{isFull && (
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-red-600 text-center py-2">
|
||||
<span className="text-sm font-bold">Bãi xe đã hết chỗ</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isClosed && (
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gray-600 text-center py-2">
|
||||
<span className="text-sm font-bold">Bãi xe đã đóng cửa</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick stats */}
|
||||
<div className="p-4 border-b-2" style={{
|
||||
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))',
|
||||
borderBottomColor: 'rgba(232, 90, 79, 0.1)'
|
||||
}}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* Availability */}
|
||||
<div className="text-center p-3 rounded-xl shadow-sm" style={{
|
||||
backgroundColor: statusColors.background,
|
||||
border: `2px solid ${statusColors.borderColor}`
|
||||
}}>
|
||||
<div className="text-2xl font-bold mb-1" style={{ color: statusColors.textColor }}>
|
||||
{parkingLot.availableSlots}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-gray-600">chỗ trống</div>
|
||||
<div className="text-xs text-gray-500">/ {parkingLot.totalSlots} tổng</div>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
<div className="text-center p-3 rounded-xl shadow-sm border-2" style={{
|
||||
backgroundColor: 'rgba(232, 90, 79, 0.08)',
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)'
|
||||
}}>
|
||||
<div className="text-2xl font-bold mb-1" style={{ color: 'var(--primary-color)' }}>
|
||||
{(parkingLot.pricePerHour || parkingLot.hourlyRate) ?
|
||||
`${Math.round((parkingLot.pricePerHour || parkingLot.hourlyRate) / 1000)}k` : '--'}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-gray-600">mỗi giờ</div>
|
||||
<div className="text-xs text-gray-500">phí gửi xe</div>
|
||||
</div>
|
||||
|
||||
{/* Hours */}
|
||||
<div className="text-center p-3 rounded-xl shadow-sm bg-blue-50 border-2 border-blue-200">
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<div className={`w-2 h-2 rounded-full ${isCurrentlyOpen(parkingLot) ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
<div className="text-lg font-bold text-blue-600">
|
||||
{parkingLot.isOpen24Hours ? '24/7' : parkingLot.openTime || '--'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-medium text-gray-600">
|
||||
{isCurrentlyOpen(parkingLot) ? 'Đang mở' : 'Đã đóng'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{isCurrentlyOpen(parkingLot) ? 'Hoạt động' : 'Ngừng hoạt động'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b-2 bg-white" style={{ borderBottomColor: 'rgba(232, 90, 79, 0.1)' }}>
|
||||
{['overview', 'reviews'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab as any)}
|
||||
className={`flex-1 py-3 px-4 text-sm font-bold transition-all duration-300 ${
|
||||
activeTab === tab
|
||||
? 'border-b-2 text-white shadow-lg'
|
||||
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
|
||||
}`}
|
||||
style={{
|
||||
borderBottomColor: activeTab === tab ? 'var(--primary-color)' : 'transparent',
|
||||
background: activeTab === tab
|
||||
? 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
: 'transparent'
|
||||
}}
|
||||
>
|
||||
{tab === 'overview' && 'Tổng quan'}
|
||||
{tab === 'reviews' && 'Đánh giá'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Map section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-3 flex items-center gap-2" style={{ color: 'var(--accent-color)' }}>
|
||||
<div className="w-8 h-8 rounded-xl flex items-center justify-center shadow-md" style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7l6-3 5.447 2.724A1 1 0 0121 7.618v10.764a1 1 0 01-.553.894L15 17l-6 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
Sơ đồ bãi xe
|
||||
</h3>
|
||||
|
||||
<ParkingLotMap parkingLot={parkingLot} />
|
||||
</div>
|
||||
|
||||
{/* Amenities */}
|
||||
{amenityList.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-3 flex items-center gap-2" style={{ color: 'var(--accent-color)' }}>
|
||||
<div className="w-8 h-8 rounded-xl flex items-center justify-center shadow-md" style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
Tiện ích
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{amenityList.map((amenity, index) => (
|
||||
<div key={index} className="flex items-center gap-3 p-3 rounded-xl shadow-sm border" style={{
|
||||
backgroundColor: 'rgba(232, 90, 79, 0.05)',
|
||||
borderColor: 'rgba(232, 90, 79, 0.15)'
|
||||
}}>
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: 'var(--success-color)' }}></div>
|
||||
<span className="text-sm font-medium text-gray-700">{amenity}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'reviews' && (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Overall rating */}
|
||||
<div className="text-center p-4 rounded-xl shadow-sm border-2" style={{
|
||||
backgroundColor: 'rgba(232, 90, 79, 0.05)',
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)'
|
||||
}}>
|
||||
<div className="text-3xl font-bold mb-2" style={{ color: 'var(--accent-color)' }}>{averageRating.toFixed(1)}</div>
|
||||
<div className="flex items-center justify-center gap-1 mb-2">
|
||||
{renderStars(Math.round(averageRating))}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-600">{SAMPLE_REVIEWS.length} đánh giá</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews list */}
|
||||
<div className="space-y-3">
|
||||
{SAMPLE_REVIEWS.map((review) => (
|
||||
<div key={review.id} className="p-4 rounded-xl shadow-sm border" style={{
|
||||
backgroundColor: 'rgba(232, 90, 79, 0.05)',
|
||||
borderColor: 'rgba(232, 90, 79, 0.15)'
|
||||
}}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold text-white" style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}>
|
||||
{review.avatar}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold text-gray-800">{review.user}</span>
|
||||
<div className="flex gap-1">
|
||||
{renderStars(review.rating)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 mb-2">{review.comment}</p>
|
||||
<div className="text-xs text-gray-500">{review.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add review button */}
|
||||
<button className="w-full py-3 rounded-xl font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105" style={{
|
||||
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}>
|
||||
Viết đánh giá
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Booking section */}
|
||||
{onBook && (
|
||||
<div className="p-4 bg-white border-t-2" style={{ borderTopColor: 'rgba(232, 90, 79, 0.2)' }}>
|
||||
{/* Duration selector */}
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1" style={{ color: 'var(--accent-color)' }}>
|
||||
<div className="w-5 h-5 rounded flex items-center justify-center" style={{
|
||||
backgroundColor: 'var(--primary-color)'
|
||||
}}>
|
||||
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-base font-bold">Thời gian gửi xe</span>
|
||||
<span className="text-xs text-gray-500">(đơn vị giờ)</span>
|
||||
</div>
|
||||
|
||||
{/* Ultra compact time selector */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => setBookingDuration(Math.max(1, bookingDuration - 1))}
|
||||
disabled={bookingDuration <= 1}
|
||||
className={`w-4 h-4 rounded flex items-center justify-center font-bold text-xs transition-all ${
|
||||
bookingDuration <= 1
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: 'text-white'
|
||||
}`}
|
||||
style={{
|
||||
background: bookingDuration <= 1
|
||||
? '#E5E7EB'
|
||||
: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
|
||||
<div className="flex items-center rounded border text-xs" style={{
|
||||
backgroundColor: 'rgba(232, 90, 79, 0.05)',
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)'
|
||||
}}>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="24"
|
||||
value={bookingDuration}
|
||||
onChange={(e) => {
|
||||
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)' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setBookingDuration(Math.min(24, bookingDuration + 1))}
|
||||
disabled={bookingDuration >= 24}
|
||||
className={`w-4 h-4 rounded flex items-center justify-center font-bold text-xs transition-all ${
|
||||
bookingDuration >= 24
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: 'text-white'
|
||||
}`}
|
||||
style={{
|
||||
background: bookingDuration >= 24
|
||||
? '#E5E7EB'
|
||||
: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total summary */}
|
||||
{(parkingLot.pricePerHour || parkingLot.hourlyRate) && (
|
||||
<div className="mb-4 p-4 rounded-xl shadow-sm border-2" style={{
|
||||
backgroundColor: 'rgba(232, 90, 79, 0.05)',
|
||||
borderColor: 'rgba(232, 90, 79, 0.2)'
|
||||
}}>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-600">Thời gian đã chọn:</span>
|
||||
<span className="text-sm font-bold text-gray-800">{bookingDuration} giờ</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Tổng thanh toán:</span>
|
||||
<span className="text-xl font-bold" style={{ color: 'var(--primary-color)' }}>
|
||||
{Math.round(((parkingLot.pricePerHour || parkingLot.hourlyRate) * bookingDuration) / 1000)}k VND
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Book button */}
|
||||
<button
|
||||
onClick={() => onBook(parkingLot)}
|
||||
disabled={isDisabled}
|
||||
className={`
|
||||
w-full py-4 rounded-xl font-bold text-lg transition-all duration-300 transform shadow-lg
|
||||
${isDisabled
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'text-white hover:shadow-xl hover:scale-105'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
background: isDisabled
|
||||
? '#D1D5DB'
|
||||
: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
|
||||
}}
|
||||
>
|
||||
{isFull ? 'Bãi xe đã hết chỗ' : isClosed ? 'Bãi xe đã đóng cửa' : `Đặt chỗ (${bookingDuration}h)`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<ParkingListProps> = ({
|
||||
onViewing,
|
||||
selectedId,
|
||||
userLocation,
|
||||
sortType = 'availability'
|
||||
sortType = 'availability',
|
||||
searchQuery = ''
|
||||
}) => {
|
||||
const listRef = React.useRef<HTMLDivElement>(null);
|
||||
const itemRefs = React.useRef<Map<number, HTMLElement>>(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<ParkingListProps> = ({
|
||||
...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<ParkingListProps> = ({
|
||||
|
||||
return (
|
||||
<div ref={listRef} className="space-y-4 overflow-y-auto">
|
||||
{sortedLots.map((lot, index) => {
|
||||
{sortedLots.length === 0 && searchQuery.trim() ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 px-6 text-center">
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">Không tìm thấy kết quả</h3>
|
||||
<p className="text-gray-600 text-sm">Không có bãi đỗ xe nào phù hợp với từ khóa "{searchQuery}"</p>
|
||||
<p className="text-gray-500 text-xs mt-2">Thử tìm kiếm với từ khóa khác</p>
|
||||
</div>
|
||||
) : (
|
||||
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<ParkingListProps> = ({
|
||||
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<ParkingListProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
}))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<ParkingListProps> = ({
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
{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 (
|
||||
<button
|
||||
key={lot.id}
|
||||
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
|
||||
${isSelected
|
||||
? 'shadow-xl transform scale-[1.02]'
|
||||
: 'hover:shadow-lg hover:transform hover:scale-[1.01]'
|
||||
}
|
||||
${isDisabled ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}
|
||||
`}
|
||||
style={{
|
||||
background: isFull
|
||||
? 'linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.15))'
|
||||
: isClosed
|
||||
? 'linear-gradient(135deg, rgba(107, 114, 128, 0.15), rgba(75, 85, 99, 0.15))'
|
||||
: isSelected
|
||||
? 'linear-gradient(135deg, rgba(232, 90, 79, 0.08), rgba(215, 53, 2, 0.08))'
|
||||
: 'white',
|
||||
borderColor: isFull
|
||||
? '#EF4444'
|
||||
: isClosed
|
||||
? '#6B7280'
|
||||
: isSelected
|
||||
? 'var(--primary-color)'
|
||||
: 'rgba(232, 90, 79, 0.15)'
|
||||
}}
|
||||
>
|
||||
|
||||
<div className="relative z-10">
|
||||
{/* Warning banners */}
|
||||
{isFull && (
|
||||
<div className="absolute -top-2 -left-2 -right-2 bg-red-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
|
||||
<span className="text-sm font-bold">🚫 BÃI XE ĐÃ HẾT CHỖ</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isClosed && (
|
||||
<div className="absolute -top-2 -left-2 -right-2 bg-gray-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
|
||||
<span className="text-sm font-bold">🔒 BÃI XE ĐÃ ĐÓNG CỬA</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header với icon và tên đầy đủ */}
|
||||
<div className={`flex items-start gap-4 mb-4 relative ${(isFull || isClosed) ? 'mt-6' : ''}`}>
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center shadow-md flex-shrink-0" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-bold text-lg md:text-xl tracking-tight mb-2" style={{ color: 'var(--accent-color)' }}>
|
||||
{lot.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
{lot.address}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 flex-shrink-0">
|
||||
{distance && (
|
||||
<span className="text-sm font-bold text-white px-4 py-2 rounded-xl shadow-sm" style={{ backgroundColor: 'var(--primary-color)' }}>
|
||||
{formatDistance(distance)}
|
||||
</span>
|
||||
)}
|
||||
{/* Selected indicator */}
|
||||
{isSelected && (
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center shadow-lg" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thông tin chính - layout cân đối */}
|
||||
<div className="grid grid-cols-3 gap-4 p-4 rounded-xl" style={{
|
||||
backgroundColor: 'rgba(232, 90, 79, 0.05)',
|
||||
border: '2px solid rgba(232, 90, 79, 0.2)'
|
||||
}}>
|
||||
{/* Trạng thái chỗ đỗ */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: statusColors.borderColor }}></div>
|
||||
<div className="text-xl font-bold" style={{ color: statusColors.textColor }}>
|
||||
{lot.availableSlots}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 font-medium">
|
||||
chỗ trống
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
/ {lot.totalSlots} chỗ
|
||||
</div>
|
||||
{/* Availability percentage */}
|
||||
<div className="mt-1 w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${(lot.availableSlots / lot.totalSlots) * 100}%`,
|
||||
backgroundColor: statusColors.borderColor
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="text-xs mt-1" style={{ color: statusColors.textColor }}>
|
||||
{Math.round((lot.availableSlots / lot.totalSlots) * 100)}% trống
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Giá tiền */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{(lot.pricePerHour || lot.hourlyRate) ? (
|
||||
<>
|
||||
<div className="text-xl font-bold mb-1" style={{ color: 'var(--primary-color)' }}>
|
||||
{Math.round((lot.pricePerHour || lot.hourlyRate) / 1000)}k
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 font-medium">
|
||||
mỗi giờ
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
phí gửi xe
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xl font-bold mb-1 text-gray-400">
|
||||
--
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 font-medium">
|
||||
liên hệ
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
để biết giá
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Giờ hoạt động */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{(lot.openTime && lot.closeTime) || lot.isOpen24Hours ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<div className={`w-2 h-2 rounded-full ${isCurrentlyOpen(lot) ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--accent-color)' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>
|
||||
{lot.isOpen24Hours ? '24/7' : `${lot.openTime}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-sm font-medium ${isCurrentlyOpen(lot) ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{isCurrentlyOpen(lot) ? (
|
||||
lot.isOpen24Hours ? 'Luôn mở cửa' : `đến ${lot.closeTime}`
|
||||
) : (
|
||||
'Đã đóng cửa'
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{isCurrentlyOpen(lot) ? 'Đang mở' : '🔒 Đã đóng'}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-lg font-bold mb-1 text-gray-400">
|
||||
--:--
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 font-medium">
|
||||
không rõ
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
giờ mở cửa
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<string, string> = {
|
||||
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<IconProps> = ({
|
||||
|
||||
0
frontend/src/components/ui/WheelPicker.tsx
Normal file
0
frontend/src/components/ui/WheelPicker.tsx
Normal file
@@ -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<ParkingSearchState>({
|
||||
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);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Coordinates } from '@/types';
|
||||
|
||||
export interface RouteStep {
|
||||
instruction: string;
|
||||
distance: number;
|
||||
duration: number;
|
||||
maneuver?: string;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
id: string;
|
||||
distance: number; // in meters
|
||||
duration: number; // in seconds
|
||||
geometry: Array<[number, number]>; // [lat, lng] coordinates
|
||||
steps: RouteStep[];
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
}
|
||||
|
||||
interface RoutingState {
|
||||
route: Route | null;
|
||||
alternatives: Route[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface CalculateRouteOptions {
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
avoidTolls?: boolean;
|
||||
avoidHighways?: boolean;
|
||||
alternatives?: boolean;
|
||||
}
|
||||
|
||||
export const useRouting = () => {
|
||||
const [state, setState] = useState<RoutingState>({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const calculateRoute = useCallback(async (
|
||||
start: Coordinates,
|
||||
end: Coordinates,
|
||||
options: CalculateRouteOptions = { mode: 'driving' }
|
||||
) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
error: null
|
||||
}));
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Mock route calculation
|
||||
const distance = calculateDistance(start, end);
|
||||
const mockRoute: Route = {
|
||||
id: 'route-1',
|
||||
distance: distance * 1000, // Convert to meters
|
||||
duration: Math.round(distance * 180), // Rough estimate: 3 minutes per km for driving
|
||||
geometry: [
|
||||
[start.latitude, start.longitude],
|
||||
[end.latitude, end.longitude]
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
instruction: `Đi từ vị trí hiện tại`,
|
||||
distance: distance * 1000 * 0.1,
|
||||
duration: Math.round(distance * 18)
|
||||
},
|
||||
{
|
||||
instruction: `Đến ${end.latitude.toFixed(4)}, ${end.longitude.toFixed(4)}`,
|
||||
distance: distance * 1000 * 0.9,
|
||||
duration: Math.round(distance * 162)
|
||||
}
|
||||
],
|
||||
mode: options.mode
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
route: mockRoute,
|
||||
alternatives: []
|
||||
}));
|
||||
|
||||
return { route: mockRoute, alternatives: [] };
|
||||
} catch (error: any) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: error.message || 'Failed to calculate route'
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearRoute = useCallback(() => {
|
||||
setState({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
route: state.route,
|
||||
alternatives: state.alternatives,
|
||||
isLoading: state.isLoading,
|
||||
error: state.error,
|
||||
calculateRoute,
|
||||
clearRoute
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to calculate distance between two coordinates
|
||||
function calculateDistance(coord1: Coordinates, coord2: Coordinates): number {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const dLat = toRadians(coord2.latitude - coord1.latitude);
|
||||
const dLon = toRadians(coord2.longitude - coord1.longitude);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRadians(coord1.latitude)) *
|
||||
Math.cos(toRadians(coord2.latitude)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c; // Distance in kilometers
|
||||
}
|
||||
|
||||
function toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
77
launch.sh
Normal file
77
launch.sh
Normal file
@@ -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
|
||||
0
optimize.sh
Normal file
0
optimize.sh
Normal file
201
scripts/README.md
Normal file
201
scripts/README.md
Normal file
@@ -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.*
|
||||
53
scripts/docker-dev.sh
Normal file
53
scripts/docker-dev.sh
Normal file
@@ -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 ""
|
||||
23
scripts/frontend-only.sh
Normal file
23
scripts/frontend-only.sh
Normal file
@@ -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
|
||||
58
scripts/full-dev.sh
Normal file
58
scripts/full-dev.sh
Normal file
@@ -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
|
||||
83
scripts/setup.sh
Normal file
83
scripts/setup.sh
Normal file
@@ -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"
|
||||
255
scripts/start.sh
Normal file
255
scripts/start.sh
Normal file
@@ -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
|
||||
104
start-dev.sh
Executable file → Normal file
104
start-dev.sh
Executable file → Normal file
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user