🚀 Complete Laca City Website with VPS Deployment

- Added complete Next.js frontend with responsive design
- Added NestJS backend with PostgreSQL and Redis
- Added comprehensive VPS deployment script (vps-deploy.sh)
- Added deployment guide and documentation
- Added all assets and static files
- Configured SSL, Nginx, PM2, and monitoring
- Ready for production deployment on any VPS
This commit is contained in:
2025-08-12 07:06:15 +07:00
parent bc87a88719
commit 51f2505839
111 changed files with 4967 additions and 6447 deletions

View File

@@ -1,510 +0,0 @@
# 🚀 Deployment Guide
This guide covers different deployment strategies for the Smart Parking Finder application.
## 📋 Table of Contents
1. [Development Deployment](#development-deployment)
2. [Production Deployment](#production-deployment)
3. [Cloud Deployment Options](#cloud-deployment-options)
4. [Environment Configuration](#environment-configuration)
5. [Monitoring & Logging](#monitoring--logging)
6. [Backup & Recovery](#backup--recovery)
7. [Troubleshooting](#troubleshooting)
## 🛠️ Development Deployment
### Quick Start
```bash
# 1. Clone and setup
git clone <repository-url>
cd smart-parking-finder
./setup.sh
# 2. Start development environment
docker-compose up -d
# 3. Start development servers
cd frontend && npm run dev &
cd backend && npm run start:dev &
```
### Development Services
- **Frontend**: http://localhost:3000
- **Backend API**: http://localhost:3001
- **PostgreSQL**: localhost:5432
- **Redis**: localhost:6379
- **Valhalla**: http://localhost:8002
- **pgAdmin**: http://localhost:5050 (with `--profile tools`)
## 🏭 Production Deployment
### Docker Compose Production
```bash
# 1. Create production environment
cp docker-compose.yml docker-compose.prod.yml
# 2. Update production configuration
# Edit docker-compose.prod.yml with production settings
# 3. Deploy
docker-compose -f docker-compose.prod.yml up -d
```
### Production Docker Compose
```yaml
version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
ports:
- "80:3000"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_API_URL=https://api.yourparking.com
restart: unless-stopped
backend:
build:
context: ./backend
dockerfile: Dockerfile.prod
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
depends_on:
- frontend
- backend
restart: unless-stopped
postgres:
image: postgis/postgis:15-3.3
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
valhalla:
build: ./valhalla
volumes:
- valhalla_data:/data
restart: unless-stopped
volumes:
postgres_data:
redis_data:
valhalla_data:
```
## ☁️ Cloud Deployment Options
### 1. DigitalOcean Droplet
**Recommended for small to medium deployments**
```bash
# 1. Create Droplet (4GB RAM minimum for Valhalla)
doctl compute droplet create parking-app \
--size s-2vcpu-4gb \
--image ubuntu-22-04-x64 \
--region sgp1
# 2. Install Docker
ssh root@your-droplet-ip
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
# 3. Deploy application
git clone <repository-url>
cd smart-parking-finder
docker-compose -f docker-compose.prod.yml up -d
```
### 2. AWS EC2 + RDS
**Recommended for scalable production**
```bash
# 1. Launch EC2 instance (t3.medium minimum)
# 2. Setup RDS PostgreSQL with PostGIS
# 3. Setup ElastiCache Redis
# 4. Deploy application containers
# User data script for EC2:
#!/bin/bash
yum update -y
yum install -y docker git
systemctl start docker
systemctl enable docker
usermod -a -G docker ec2-user
# Clone and deploy
git clone <repository-url> /opt/parking-app
cd /opt/parking-app
docker-compose -f docker-compose.aws.yml up -d
```
### 3. Google Cloud Platform
**Using Cloud Run and Cloud SQL**
```bash
# 1. Build and push images
gcloud builds submit --tag gcr.io/PROJECT_ID/parking-frontend ./frontend
gcloud builds submit --tag gcr.io/PROJECT_ID/parking-backend ./backend
# 2. Deploy to Cloud Run
gcloud run deploy parking-frontend \
--image gcr.io/PROJECT_ID/parking-frontend \
--platform managed \
--region asia-southeast1
gcloud run deploy parking-backend \
--image gcr.io/PROJECT_ID/parking-backend \
--platform managed \
--region asia-southeast1
```
### 4. Kubernetes Deployment
**For large-scale deployments**
```yaml
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: parking-finder
---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: parking-finder
data:
DATABASE_HOST: "postgres-service"
REDIS_HOST: "redis-service"
VALHALLA_URL: "http://valhalla-service:8002"
---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-deployment
namespace: parking-finder
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: your-registry/parking-frontend:latest
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: app-config
```
## 🔧 Environment Configuration
### Production Environment Variables
```bash
# .env.production
NODE_ENV=production
# Database
DATABASE_URL=postgresql://user:pass@db-host:5432/parking_db
POSTGRES_SSL=true
# Redis
REDIS_URL=redis://redis-host:6379
REDIS_SSL=true
# Security
JWT_SECRET=your-super-secure-jwt-secret-256-bit
JWT_EXPIRATION=1h
CORS_ORIGIN=https://yourparking.com
# APIs
VALHALLA_URL=http://valhalla:8002
MAP_TILES_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png
# Monitoring
SENTRY_DSN=your-sentry-dsn
LOG_LEVEL=info
# Performance
REDIS_CACHE_TTL=3600
DB_POOL_SIZE=10
API_RATE_LIMIT=100
```
### SSL Configuration
```nginx
# nginx/nginx.conf
server {
listen 443 ssl http2;
server_name yourparking.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# Frontend
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Backend API
location /api {
proxy_pass http://backend:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket support
location /ws {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
## 📊 Monitoring & Logging
### Docker Logging
```yaml
# docker-compose.monitoring.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
node-exporter:
image: prom/node-exporter
ports:
- "9100:9100"
volumes:
grafana_data:
```
### Application Monitoring
```typescript
// backend/src/monitoring/metrics.ts
import { createPrometheusMetrics } from '@prometheus/client';
export const metrics = {
httpRequests: new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status']
}),
routeCalculationTime: new Histogram({
name: 'route_calculation_duration_seconds',
help: 'Route calculation duration',
buckets: [0.1, 0.5, 1, 2, 5]
}),
databaseQueries: new Counter({
name: 'database_queries_total',
help: 'Total database queries',
labelNames: ['operation', 'table']
})
};
```
## 💾 Backup & Recovery
### Database Backup
```bash
#!/bin/bash
# backup.sh
# Variables
BACKUP_DIR="/opt/backups"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="parking_db"
# Create backup
docker-compose exec postgres pg_dump \
-U parking_user \
-h localhost \
-d $DB_NAME \
--clean \
--if-exists \
--create \
> "$BACKUP_DIR/db_backup_$DATE.sql"
# Compress backup
gzip "$BACKUP_DIR/db_backup_$DATE.sql"
# Keep only last 7 days
find $BACKUP_DIR -name "db_backup_*.sql.gz" -mtime +7 -delete
echo "Backup completed: db_backup_$DATE.sql.gz"
```
### Automated Backup with Cron
```bash
# Add to crontab: crontab -e
# Daily backup at 2 AM
0 2 * * * /opt/parking-app/scripts/backup.sh >> /var/log/backup.log 2>&1
# Weekly full system backup
0 3 * * 0 /opt/parking-app/scripts/full-backup.sh >> /var/log/backup.log 2>&1
```
## 🔍 Troubleshooting
### Common Issues
1. **Valhalla not starting**
```bash
# Check OSM data
ls -la valhalla/custom_files/
# Check logs
docker-compose logs valhalla
# Verify memory allocation
docker stats valhalla
```
2. **Database connection issues**
```bash
# Test connection
docker-compose exec postgres psql -U parking_user -d parking_db
# Check network
docker network ls
docker network inspect parking-finder_parking-network
```
3. **High memory usage**
```bash
# Monitor services
docker stats
# Optimize Valhalla cache
# Edit valhalla.json: reduce max_cache_size
```
### Health Checks
```bash
#!/bin/bash
# health-check.sh
echo "=== Health Check ==="
# Frontend
curl -f http://localhost:3000 || echo "❌ Frontend down"
# Backend
curl -f http://localhost:3001/health || echo "❌ Backend down"
# Database
docker-compose exec postgres pg_isready -U parking_user || echo "❌ Database down"
# Redis
docker-compose exec redis redis-cli ping || echo "❌ Redis down"
# Valhalla
curl -f http://localhost:8002/status || echo "❌ Valhalla down"
echo "=== Check complete ==="
```
### Performance Optimization
```yaml
# docker-compose.optimized.yml
services:
backend:
deploy:
resources:
limits:
memory: 1G
cpus: '0.5'
reservations:
memory: 512M
cpus: '0.25'
valhalla:
deploy:
resources:
limits:
memory: 4G
cpus: '2'
reservations:
memory: 2G
cpus: '1'
```
---
For additional support, refer to the [main README](../README.md) or contact the development team.

View File

@@ -1,750 +0,0 @@
# 🛠️ Development Guide
This guide covers the development workflow, coding standards, and best practices for the Smart Parking Finder application.
## 📋 Table of Contents
1. [Development Setup](#development-setup)
2. [Project Structure](#project-structure)
3. [Development Workflow](#development-workflow)
4. [Coding Standards](#coding-standards)
5. [Testing Strategy](#testing-strategy)
6. [Debugging](#debugging)
7. [Performance Guidelines](#performance-guidelines)
8. [Contributing](#contributing)
## 🚀 Development Setup
### Prerequisites
- Node.js 18+ and npm
- Docker and Docker Compose
- Git
- VS Code (recommended)
### Initial Setup
```bash
# 1. Clone repository
git clone <repository-url>
cd smart-parking-finder
# 2. Run automated setup
./setup.sh
# 3. Start development environment
docker-compose up -d
# 4. Install dependencies
cd frontend && npm install
cd ../backend && npm install
# 5. Start development servers
npm run dev:all # Starts both frontend and backend
```
### Environment Configuration
```bash
# .env.development
NODE_ENV=development
# Database
DATABASE_URL=postgresql://parking_user:parking_pass@localhost:5432/parking_db
# Redis
REDIS_URL=redis://localhost:6379
# Valhalla
VALHALLA_URL=http://localhost:8002
# Development
DEBUG=true
LOG_LEVEL=debug
HOT_RELOAD=true
```
## 📁 Project Structure
```
smart-parking-finder/
├── frontend/ # Next.js frontend application
│ ├── src/
│ │ ├── app/ # App router pages
│ │ ├── components/ # Reusable UI components
│ │ ├── hooks/ # Custom React hooks
│ │ ├── services/ # API service layers
│ │ ├── types/ # TypeScript type definitions
│ │ └── utils/ # Utility functions
│ ├── public/ # Static assets
│ └── tests/ # Frontend tests
├── backend/ # NestJS backend application
│ ├── src/
│ │ ├── modules/ # Feature modules
│ │ ├── common/ # Shared utilities
│ │ ├── config/ # Configuration files
│ │ └── database/ # Database related files
│ └── tests/ # Backend tests
├── valhalla/ # Routing engine setup
├── scripts/ # Development scripts
└── docs/ # Documentation
```
### Frontend Architecture
```
frontend/src/
├── app/ # Next.js 14 App Router
│ ├── (dashboard)/ # Route groups
│ ├── api/ # API routes
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/ # UI Components
│ ├── ui/ # Base UI components
│ ├── forms/ # Form components
│ ├── map/ # Map-related components
│ └── parking/ # Parking-specific components
├── hooks/ # Custom hooks
│ ├── useGeolocation.ts
│ ├── useParking.ts
│ └── useRouting.ts
├── services/ # API services
│ ├── api.ts # Base API client
│ ├── parkingService.ts
│ └── routingService.ts
└── types/ # TypeScript definitions
├── parking.ts
├── routing.ts
└── user.ts
```
### Backend Architecture
```
backend/src/
├── modules/ # Feature modules
│ ├── auth/ # Authentication
│ ├── parking/ # Parking management
│ ├── routing/ # Route calculation
│ └── users/ # User management
├── common/ # Shared code
│ ├── decorators/ # Custom decorators
│ ├── filters/ # Exception filters
│ ├── guards/ # Auth guards
│ └── pipes/ # Validation pipes
├── config/ # Configuration
│ ├── database.config.ts
│ ├── redis.config.ts
│ └── app.config.ts
└── database/ # Database files
├── migrations/ # Database migrations
├── seeds/ # Seed data
└── entities/ # TypeORM entities
```
## 🔄 Development Workflow
### Git Workflow
```bash
# 1. Create feature branch
git checkout -b feature/parking-search-improvements
# 2. Make changes with atomic commits
git add .
git commit -m "feat(parking): add distance-based search filtering"
# 3. Push and create PR
git push origin feature/parking-search-improvements
```
### Commit Message Convention
```
type(scope): description
Types:
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- style: Code style changes
- refactor: Code refactoring
- test: Adding tests
- chore: Maintenance tasks
Examples:
feat(map): add real-time parking availability indicators
fix(routing): resolve incorrect distance calculations
docs(api): update parking endpoint documentation
```
### Development Scripts
```json
{
"scripts": {
"dev": "next dev",
"dev:backend": "cd backend && npm run start:dev",
"dev:all": "concurrently \"npm run dev\" \"npm run dev:backend\"",
"build": "next build",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit"
}
}
```
## 📝 Coding Standards
### TypeScript Configuration
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/services/*": ["./src/services/*"],
"@/types/*": ["./src/types/*"]
}
}
}
```
### ESLint Configuration
```json
// .eslintrc.json
{
"extends": [
"next/core-web-vitals",
"@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"prefer-const": "error",
"no-var": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
```
### Code Style Guidelines
#### Frontend Components
```typescript
// ✅ Good: Functional component with proper typing
interface ParkingListProps {
parkingLots: ParkingLot[];
onSelect: (lot: ParkingLot) => void;
loading?: boolean;
}
export const ParkingList: React.FC<ParkingListProps> = ({
parkingLots,
onSelect,
loading = false
}) => {
const [selectedId, setSelectedId] = useState<string | null>(null);
const handleSelect = useCallback((lot: ParkingLot) => {
setSelectedId(lot.id);
onSelect(lot);
}, [onSelect]);
if (loading) {
return <LoadingSpinner />;
}
return (
<div className="parking-list">
{parkingLots.map((lot) => (
<ParkingCard
key={lot.id}
lot={lot}
isSelected={selectedId === lot.id}
onClick={() => handleSelect(lot)}
/>
))}
</div>
);
};
```
#### Backend Services
```typescript
// ✅ Good: Service with proper error handling and typing
@Injectable()
export class ParkingService {
constructor(
@InjectRepository(ParkingLot)
private readonly parkingRepository: Repository<ParkingLot>,
private readonly cacheService: CacheService,
private readonly logger: Logger
) {}
async findNearbyParking(
dto: FindNearbyParkingDto
): Promise<ParkingLot[]> {
try {
const cacheKey = `nearby:${dto.latitude}:${dto.longitude}:${dto.radius}`;
// Check cache first
const cached = await this.cacheService.get<ParkingLot[]>(cacheKey);
if (cached) {
return cached;
}
// Query database with spatial index
const lots = await this.parkingRepository
.createQueryBuilder('lot')
.where(
'ST_DWithin(lot.location::geography, ST_Point(:lng, :lat)::geography, :radius)',
{
lng: dto.longitude,
lat: dto.latitude,
radius: dto.radius
}
)
.andWhere('lot.isActive = :isActive', { isActive: true })
.orderBy(
'ST_Distance(lot.location::geography, ST_Point(:lng, :lat)::geography)',
'ASC'
)
.limit(dto.limit || 20)
.getMany();
// Cache results
await this.cacheService.set(cacheKey, lots, 300); // 5 minutes
return lots;
} catch (error) {
this.logger.error('Failed to find nearby parking', error);
throw new InternalServerErrorException('Failed to find nearby parking');
}
}
}
```
## 🧪 Testing Strategy
### Test Structure
```
tests/
├── unit/ # Unit tests
├── integration/ # Integration tests
├── e2e/ # End-to-end tests
└── fixtures/ # Test data
```
### Frontend Testing
```typescript
// components/ParkingList.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ParkingList } from './ParkingList';
import { mockParkingLots } from '../../../tests/fixtures/parking';
describe('ParkingList', () => {
const mockOnSelect = jest.fn();
beforeEach(() => {
mockOnSelect.mockClear();
});
it('renders parking lots correctly', () => {
render(
<ParkingList
parkingLots={mockParkingLots}
onSelect={mockOnSelect}
/>
);
expect(screen.getByText('Central Mall Parking')).toBeInTheDocument();
expect(screen.getByText('$5/hour')).toBeInTheDocument();
});
it('calls onSelect when parking lot is clicked', () => {
render(
<ParkingList
parkingLots={mockParkingLots}
onSelect={mockOnSelect}
/>
);
fireEvent.click(screen.getByText('Central Mall Parking'));
expect(mockOnSelect).toHaveBeenCalledWith(mockParkingLots[0]);
});
it('shows loading spinner when loading', () => {
render(
<ParkingList
parkingLots={[]}
onSelect={mockOnSelect}
loading={true}
/>
);
expect(screen.getByRole('status')).toBeInTheDocument();
});
});
```
### Backend Testing
```typescript
// parking/parking.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ParkingService } from './parking.service';
import { ParkingLot } from './entities/parking-lot.entity';
import { mockRepository } from '../../tests/mocks/repository.mock';
describe('ParkingService', () => {
let service: ParkingService;
let repository: any;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ParkingService,
{
provide: getRepositoryToken(ParkingLot),
useValue: mockRepository,
},
],
}).compile();
service = module.get<ParkingService>(ParkingService);
repository = module.get(getRepositoryToken(ParkingLot));
});
describe('findNearbyParking', () => {
it('should return nearby parking lots', async () => {
const mockLots = [/* mock data */];
repository.createQueryBuilder().getMany.mockResolvedValue(mockLots);
const result = await service.findNearbyParking({
latitude: 1.3521,
longitude: 103.8198,
radius: 1000,
});
expect(result).toEqual(mockLots);
expect(repository.createQueryBuilder).toHaveBeenCalled();
});
});
});
```
### Integration Testing
```typescript
// parking/parking.controller.integration.spec.ts
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../app.module';
describe('ParkingController (Integration)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
it('/parking/nearby (POST)', () => {
return request(app.getHttpServer())
.post('/parking/nearby')
.send({
latitude: 1.3521,
longitude: 103.8198,
radius: 1000,
})
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('data');
expect(Array.isArray(res.body.data)).toBe(true);
});
});
afterAll(async () => {
await app.close();
});
});
```
## 🐛 Debugging
### Frontend Debugging
```typescript
// Development debugging utilities
export const debugLog = (message: string, data?: any): void => {
if (process.env.NODE_ENV === 'development') {
console.log(`[DEBUG] ${message}`, data);
}
};
// React Developer Tools
// Component debugging
export const ParkingDebugger: React.FC = () => {
const [parkingLots, setParkingLots] = useLocalStorage('debug:parking', []);
useEffect(() => {
// Log component updates
debugLog('ParkingDebugger mounted');
return () => {
debugLog('ParkingDebugger unmounted');
};
}, []);
return (
<div className="debug-panel">
<h3>Parking Debug Info</h3>
<pre>{JSON.stringify(parkingLots, null, 2)}</pre>
</div>
);
};
```
### Backend Debugging
```typescript
// Logger configuration
import { Logger } from '@nestjs/common';
@Injectable()
export class DebugService {
private readonly logger = new Logger(DebugService.name);
logRequest(req: Request, res: Response, next: NextFunction): void {
const { method, originalUrl, body, query } = req;
this.logger.debug(`${method} ${originalUrl}`, {
body,
query,
timestamp: new Date().toISOString(),
});
next();
}
logDatabaseQuery(query: string, parameters?: any[]): void {
this.logger.debug('Database Query', {
query,
parameters,
timestamp: new Date().toISOString(),
});
}
}
```
### VS Code Debug Configuration
```json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Frontend",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/frontend",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"]
},
{
"name": "Debug Backend",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/backend",
"program": "${workspaceFolder}/backend/dist/main.js",
"env": {
"NODE_ENV": "development"
},
"console": "integratedTerminal",
"restart": true,
"protocol": "inspector"
}
]
}
```
## ⚡ Performance Guidelines
### Frontend Performance
```typescript
// Use React.memo for expensive components
export const ParkingMap = React.memo<ParkingMapProps>(({
parkingLots,
onMarkerClick
}) => {
// Component implementation
});
// Optimize re-renders with useMemo
const filteredLots = useMemo(() => {
return parkingLots.filter(lot =>
lot.availableSpaces > 0 &&
lot.distance <= maxDistance
);
}, [parkingLots, maxDistance]);
// Virtual scrolling for large lists
import { FixedSizeList as List } from 'react-window';
const VirtualizedParkingList: React.FC = ({ items }) => (
<List
height={600}
itemCount={items.length}
itemSize={120}
itemData={items}
>
{ParkingRow}
</List>
);
```
### Backend Performance
```typescript
// Database query optimization
@Injectable()
export class OptimizedParkingService {
// Use spatial indexes
async findNearbyOptimized(dto: FindNearbyParkingDto): Promise<ParkingLot[]> {
return this.parkingRepository.query(`
SELECT *
FROM parking_lots
WHERE ST_DWithin(
location::geography,
ST_Point($1, $2)::geography,
$3
)
AND available_spaces > 0
ORDER BY location <-> ST_Point($1, $2)
LIMIT $4
`, [dto.longitude, dto.latitude, dto.radius, dto.limit]);
}
// Implement caching
@Cacheable('parking:nearby', 300) // 5 minutes
async findNearbyCached(dto: FindNearbyParkingDto): Promise<ParkingLot[]> {
return this.findNearbyOptimized(dto);
}
}
// Connection pooling
export const databaseConfig: TypeOrmModuleOptions = {
type: 'postgres',
url: process.env.DATABASE_URL,
extra: {
max: 20, // maximum number of connections
connectionTimeoutMillis: 2000,
idleTimeoutMillis: 30000,
},
};
```
## 🤝 Contributing
### Pull Request Process
1. **Fork and Clone**
```bash
git clone https://github.com/your-username/smart-parking-finder.git
cd smart-parking-finder
git remote add upstream https://github.com/original/smart-parking-finder.git
```
2. **Create Feature Branch**
```bash
git checkout -b feature/your-feature-name
```
3. **Development**
- Follow coding standards
- Write tests for new features
- Update documentation
4. **Submit PR**
- Ensure all tests pass
- Update CHANGELOG.md
- Provide clear description
### Code Review Guidelines
- **Code Quality**: Follows TypeScript best practices
- **Testing**: Adequate test coverage (>80%)
- **Performance**: No performance regressions
- **Documentation**: Updated documentation
- **Security**: No security vulnerabilities
### Issue Templates
```markdown
## Bug Report
**Describe the bug**
A clear description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
What you expected to happen.
**Screenshots**
If applicable, add screenshots.
**Environment:**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
```
---
For more information, see the [README](../README.md) and [Technical Specification](../TECHNICAL_SPECIFICATION.md).

View File

@@ -1,189 +0,0 @@
# 🚀 Git Upload Guide for Smart Parking Finder
## 📋 Instructions to Upload to Gitea Repository
### 1. Navigate to Project Directory
```bash
cd /Users/phongworking/Desktop/Working/Laca_city/Website_Demo_App
```
### 2. Initialize Git Repository (if not already done)
```bash
# Check if Git is already initialized
git status
# If not initialized, run:
git init
```
### 3. Configure Git User (if not set globally)
```bash
git config user.name "Phong Pham"
git config user.email "your-email@example.com"
```
### 4. Add All Files to Git
```bash
# Add all files to staging
git add .
# Check what will be committed
git status
```
### 5. Create Initial Commit
```bash
git commit -m "🚀 Initial commit: Smart Parking Finder
✨ Features:
- Next.js 14 frontend with React 18 & TypeScript
- NestJS backend with PostgreSQL & Redis
- Interactive OpenStreetMap with React Leaflet
- Real-time parking search and reservations
- Docker development environment
- Comprehensive documentation
📁 Project Structure:
- /frontend - Next.js application
- /backend - NestJS API server
- /scripts - Deployment and development scripts
- /Documents - Complete documentation
- /valhalla - Routing engine configuration
🛠️ Quick Start:
- ./launch.sh - Interactive launcher
- ./scripts/setup.sh - Initial setup
- ./scripts/frontend-only.sh - Quick demo
- ./scripts/full-dev.sh - Full development
- ./scripts/docker-dev.sh - Docker environment
📚 Documentation:
- Complete system architecture
- API schemas and database design
- Deployment guides and troubleshooting
- Performance optimization reports"
```
### 6. Add Remote Repository
```bash
# Add your Gitea repository as remote origin
git remote add origin https://gitea.phongprojects.id.vn/phongpham/Laca-City.git
# Verify remote is added
git remote -v
```
### 7. Set Main Branch and Push
```bash
# Set main branch
git branch -M main
# Push to remote repository
git push -u origin main
```
### 8. Verify Upload
```bash
# Check if push was successful
git status
# View commit history
git log --oneline -5
# Check remote tracking
git branch -vv
```
## 🔧 Troubleshooting
### If Authentication is Required:
```bash
# Option 1: Use personal access token
git remote set-url origin https://your-username:your-token@gitea.phongprojects.id.vn/phongpham/Laca-City.git
# Option 2: Use SSH (if SSH key is configured)
git remote set-url origin git@gitea.phongprojects.id.vn:phongpham/Laca-City.git
```
### If Repository Already Exists:
```bash
# Force push (use with caution)
git push -u origin main --force
# Or pull first then push
git pull origin main --allow-unrelated-histories
git push -u origin main
```
### If Large Files Need to be Excluded:
The `.gitignore` file has been created to exclude:
- `node_modules/`
- `.next/`
- `dist/`
- `.env` files
- Database files
- Log files
- Cache files
## 📊 Repository Structure After Upload
Your Gitea repository will contain:
```
Laca-City/
├── 📁 Documents/ # Complete documentation
├── 📁 scripts/ # Deployment scripts
├── 📁 frontend/ # Next.js application
├── 📁 backend/ # NestJS API
├── 📁 valhalla/ # Routing engine
├── 📁 assets/ # Static assets
├── 🚀 launch.sh # Quick launcher
├── 🐳 docker-compose.yml # Docker configuration
├── 📋 .gitignore # Git ignore rules
└── 🧹 REORGANIZATION_GUIDE.md
```
## 🎯 Next Steps After Upload
1. **Clone on other machines:**
```bash
git clone https://gitea.phongprojects.id.vn/phongpham/Laca-City.git
cd Laca-City
./scripts/setup.sh
```
2. **Development workflow:**
```bash
# Make changes
git add .
git commit -m "Description of changes"
git push
```
3. **Branching strategy:**
```bash
# Create feature branch
git checkout -b feature/new-feature
# After development
git push -u origin feature/new-feature
# Create pull request in Gitea
```
## 🔒 Security Notes
- ✅ `.gitignore` excludes sensitive files (`.env`, `node_modules`)
- ✅ No database credentials in repository
- ✅ No API keys or secrets committed
- ⚠️ Remember to set environment variables in production
## 📞 Support
If you encounter issues:
1. Check network connectivity to gitea.phongprojects.id.vn
2. Verify repository permissions in Gitea web interface
3. Ensure Git credentials are correct
4. Check if repository size limits are exceeded
---
*Run these commands in your terminal to upload the complete Smart Parking Finder project to your Gitea repository.*

View File

View File

@@ -1,104 +0,0 @@
# 📦 MAPVIEW VERSION HISTORY
## 🎯 MapView v2.0 - Global Deployment Ready
**Ngày:** 20/07/2025
**File:** `MapView-v2.0.tsx`
### ✨ **TÍNH NĂNG CHÍNH:**
#### 🗺️ **Auto-Zoom Intelligence**
- **Smart Bounds Fitting:** Tự động zoom để hiển thị vừa GPS và parking đã chọn
- **Adaptive Padding:** 50px padding cho visual balance tối ưu
- **Max Zoom Control:** Giới hạn zoom level 16 để tránh quá gần
- **Dynamic Centering:** Center trên user location khi không chọn parking
#### 🎨 **Enhanced Visual Design**
- **3D GPS Marker:** Multi-layer pulsing với gradient effects
- **Advanced Parking Icons:** Status-based colors với availability indicators
- **Enhanced Selection Effects:** Highlighted states với animation
- **Dimming System:** Non-selected parkings được làm mờ khi có selection
#### 🛣️ **Professional Route Display**
- **Multi-layer Route:** 6 layers với glow, shadow, main, animated dash
- **Real-time Calculation:** OpenRouteService API integration
- **Visual Route Info:** Distance & duration display trong popup
- **Animated Flow:** CSS animations cho movement effect
#### 📱 **Production Optimizations**
- **SSR Safe:** Dynamic imports cho Leaflet components
- **Performance:** Optimized re-renders và memory management
- **Error Handling:** Robust route calculation với fallback
- **Global Ready:** Deployed và tested trên Vercel
### 🔧 **TECHNICAL SPECS:**
```typescript
// Core Features
- Auto-zoom với fitBounds()
- Enhanced marker systems
- Route calculation API
- Status-based styling
- Animation frameworks
// Performance
- Dynamic imports
- Optimized effects
- Memory management
- Error boundaries
```
### 🌍 **DEPLOYMENT STATUS:**
-**Production Build:** Successful
-**Vercel Deploy:** https://whatever-ctk2auuxr-phong12hexdockworks-projects.vercel.app
-**Global Access:** Worldwide availability
-**HTTPS Ready:** Secure connections
-**CDN Optimized:** Fast loading globally
### 🎯 **USE CASES:**
1. **Smart Parking Discovery:** Auto-zoom to show user + nearby parking
2. **Route Planning:** Visual route với distance/time info
3. **Status Monitoring:** Real-time parking availability
4. **Global Access:** Use from anywhere in the world
### 📊 **PERFORMANCE METRICS:**
- **Bundle Size:** 22.8 kB optimized
- **First Load:** 110 kB total
- **Build Time:** ~1 minute
- **Global Latency:** <200ms via CDN
---
## 🏗️ **PREVIOUS VERSIONS:**
### MapView v1.x
- Basic Leaflet integration
- Simple markers
- Local development only
- No auto-zoom features
---
## 🚀 **DEPLOYMENT COMMANDS:**
```bash
# Local development
npm run dev
# Production build
npm run build
# Global deployment
./deploy-vercel.sh
# Alternative global access
./start-global.sh # ngrok tunnel
```
## 📝 **NOTES:**
- Version 2.0 marks the first globally accessible release
- All major build errors resolved for production
- Auto-zoom feature is the key differentiator
- Route calculation adds professional UX
- Enhanced visuals provide premium feel
**Status:** PRODUCTION READY - GLOBALLY ACCESSIBLE

View File

View File

View File

@@ -1,145 +0,0 @@
# 🧹 Project Cleanup & Organization Completed
## ✅ New Project Structure
```
Website_Demo_App/
├── 📁 Documents/ # All documentation files
│ ├── README.md # Main project documentation
│ ├── LOCAL_DEPLOYMENT_GUIDE.md
│ ├── SYSTEM_ARCHITECTURE.md
│ ├── API_SCHEMA.md
│ ├── DATABASE_DESIGN.md
│ ├── DEVELOPMENT.md
│ ├── DEPLOYMENT.md
│ ├── TECHNICAL_SPECIFICATION.md
│ ├── PERFORMANCE_OPTIMIZATION_REPORT.md
│ └── MAPVIEW_VERSIONS.md
├── 📁 scripts/ # All deployment scripts
│ ├── README.md # Scripts documentation
│ ├── start.sh # Interactive menu with all options
│ ├── frontend-only.sh # Quick frontend demo
│ ├── full-dev.sh # Full development environment
│ ├── docker-dev.sh # Docker development
│ └── setup.sh # Initial project setup
├── 📁 frontend/ # Next.js application
├── 📁 backend/ # NestJS application
├── 📁 valhalla/ # Routing engine
├── 📁 assets/ # Static assets
├── 🚀 launch.sh # Quick launcher script
└── 🐳 docker-compose.yml # Docker configuration
```
## 🎯 How to Use the New Structure
### 1. Quick Start Options
```bash
# Interactive launcher (recommended for new users)
./launch.sh
# Direct script access
./scripts/start.sh # Interactive menu
./scripts/frontend-only.sh # Quick demo
./scripts/full-dev.sh # Full development
./scripts/docker-dev.sh # Docker environment
./scripts/setup.sh # Initial setup
```
### 2. First Time Setup
```bash
# 1. Initial setup
./scripts/setup.sh
# 2. Start development
./launch.sh # Choose option 2 for quick demo
```
### 3. Daily Development
```bash
# Most common: Quick frontend demo
./scripts/frontend-only.sh
# Full stack development
./scripts/full-dev.sh
# Complete environment with database
./scripts/docker-dev.sh
```
## 📋 Script Organization Benefits
### ✅ Cleaner Root Directory
- Only essential files in root
- All scripts organized in `/scripts/` folder
- All documentation in `/Documents/` folder
### ✅ Better Script Names
- `frontend-only.sh` (instead of `start-frontend-only.sh`)
- `full-dev.sh` (instead of `start-local.sh`)
- `docker-dev.sh` (instead of `start-dev.sh`)
- Clear, concise naming convention
### ✅ Enhanced Functionality
- Interactive launcher (`launch.sh`) in root for quick access
- Comprehensive menu system in `scripts/start.sh`
- Better error handling and colored output
- Auto-dependency installation
### ✅ Improved Documentation
- Centralized documentation in `/Documents/`
- Scripts documentation in `/scripts/README.md`
- Clear usage examples and troubleshooting
## 🔄 Migration Guide
If you were using old scripts, here's the mapping:
| Old Command | New Command | Notes |
|-------------|-------------|-------|
| `./start.sh` | `./scripts/start.sh` | Enhanced interactive menu |
| `./start-frontend-only.sh` | `./scripts/frontend-only.sh` | Renamed for clarity |
| `./start-dev.sh` | `./scripts/docker-dev.sh` | Docker environment |
| `./start-local.sh` | `./scripts/full-dev.sh` | Full development |
| `./setup.sh` | `./scripts/setup.sh` | Moved to scripts folder |
## 🚀 Quick Commands Reference
```bash
# Setup (first time only)
./scripts/setup.sh
# Quick demo
./scripts/frontend-only.sh
# Full development
./scripts/full-dev.sh
# Docker environment
./scripts/docker-dev.sh
# Interactive menu
./scripts/start.sh
# Quick launcher
./launch.sh
```
## 📚 Documentation Access
All documentation is now organized in the `Documents/` folder:
- **Main docs**: `Documents/README.md`
- **Deployment**: `Documents/LOCAL_DEPLOYMENT_GUIDE.md`
- **Architecture**: `Documents/SYSTEM_ARCHITECTURE.md`
- **API**: `Documents/API_SCHEMA.md`
- **Database**: `Documents/DATABASE_DESIGN.md`
- **Scripts**: `scripts/README.md`
---
*Project reorganization completed for better maintainability and cleaner structure.*

View File

@@ -1,420 +0,0 @@
# 🚗 Smart Parking Finder - Technical Specification
## 📋 Project Overview
A responsive web application that helps users find and navigate to the nearest available parking lots using OpenStreetMap and Valhalla Routing Engine with real-time availability and turn-by-turn navigation.
## 🎯 Core Features
### 🔍 Location & Discovery
- GPS-based user location detection
- Interactive map with nearby parking lots
- Real-time availability display
- Distance and direction calculation
- Smart parking suggestions
### 🗺️ Navigation & Routing
- Valhalla-powered route generation
- Turn-by-turn directions
- Visual route display on map
- Estimated arrival time
- Alternative route options
### 📊 Parking Information
- Name, address, and contact details
- Real-time available slots
- Pricing per hour
- Operating hours
- Amenities and features
## 🏗️ System Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend API │ │ Database │
│ (Next.js) │◄──►│ (NestJS) │◄──►│ PostgreSQL + │
│ │ │ │ │ PostGIS │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐
│ Valhalla Engine │
│ (Docker) │
└─────────────────┘
```
## 🔧 Technology Stack
### Frontend
- **Framework**: Next.js 14 with TypeScript
- **Map Library**: React Leaflet + OpenStreetMap
- **UI Framework**: Tailwind CSS with custom branding
- **State Management**: React Query + Zustand
- **HTTP Client**: Axios with interceptors
- **PWA Support**: Next.js PWA plugin
### Backend
- **Framework**: NestJS with TypeScript
- **Database ORM**: TypeORM with PostGIS
- **Caching**: Redis for route caching
- **API Documentation**: Swagger/OpenAPI
- **Authentication**: JWT + Passport.js
- **Rate Limiting**: Express rate limiter
### Infrastructure
- **Routing Engine**: Valhalla (Docker)
- **Database**: PostgreSQL 15 + PostGIS 3.3
- **Deployment**: Docker Compose
- **Monitoring**: Prometheus + Grafana
- **CDN**: CloudFlare for static assets
## 📊 Database Schema
```sql
-- Parking lots table
CREATE TABLE parking_lots (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address TEXT NOT NULL,
location GEOGRAPHY(POINT, 4326) NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
hourly_rate DECIMAL(10,2),
open_time TIME,
close_time TIME,
available_slots INTEGER DEFAULT 0,
total_slots INTEGER NOT NULL,
amenities JSONB DEFAULT '{}',
contact_info JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Spatial index for location queries
CREATE INDEX idx_parking_lots_location ON parking_lots USING GIST (location);
-- Users table (for favorites, history)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE,
name VARCHAR(255),
preferences JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
-- Parking history
CREATE TABLE parking_history (
id SERIAL PRIMARY KEY,
user_id UUID REFERENCES users(id),
parking_lot_id INTEGER REFERENCES parking_lots(id),
visit_date TIMESTAMP DEFAULT NOW(),
duration_minutes INTEGER,
rating INTEGER CHECK (rating >= 1 AND rating <= 5)
);
-- Real-time parking updates
CREATE TABLE parking_updates (
id SERIAL PRIMARY KEY,
parking_lot_id INTEGER REFERENCES parking_lots(id),
available_slots INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT NOW(),
source VARCHAR(50) DEFAULT 'sensor'
);
```
## 🚀 API Endpoints
### Parking Discovery
```typescript
// GET /api/parking/nearby
interface NearbyParkingRequest {
lat: number;
lng: number;
radius?: number; // meters, default 4000
maxResults?: number; // default 20
priceRange?: [number, number];
amenities?: string[];
}
interface NearbyParkingResponse {
parkingLots: ParkingLot[];
userLocation: { lat: number; lng: number };
searchRadius: number;
}
```
### Route Planning
```typescript
// POST /api/routing/calculate
interface RouteRequest {
origin: { lat: number; lng: number };
destination: { lat: number; lng: number };
costing: 'auto' | 'bicycle' | 'pedestrian';
alternatives?: number;
}
interface RouteResponse {
routes: Route[];
summary: {
distance: number; // km
time: number; // minutes
cost: number; // estimated fuel cost
};
}
```
### Real-time Updates
```typescript
// WebSocket: /ws/parking-updates
interface ParkingUpdate {
parkingLotId: number;
availableSlots: number;
timestamp: string;
confidence: number; // 0-1
}
```
## 🎨 Brand Integration
Based on the existing assets in `/assets/`:
- **Logo**: Use Logo.png for header branding
- **Logo with Slogan**: Use Logo_and_sologan.png for splash screen
- **Location Icons**: Integrate Location.png and mini_location.png for map markers
### Color Palette
```css
:root {
--primary: #E85A4F; /* LACA Red */
--secondary: #D73502; /* Darker Red */
--accent: #8B2635; /* Deep Red */
--success: #22C55E; /* Green for available */
--warning: #F59E0B; /* Amber for limited */
--danger: #EF4444; /* Red for unavailable */
--neutral: #6B7280; /* Gray */
}
```
## 📱 UI/UX Design
### Layout Structure
```
┌─────────────────────────────────────────┐
│ Header [Logo] [Search] [Profile] │
├─────────────────┬───────────────────────┤
│ Sidebar │ Map View │
│ - Filters │ - User location │
│ - Parking List │ - Parking markers │
│ - Selected Info │ - Route overlay │
│ - Directions │ - Controls │
└─────────────────┴───────────────────────┘
```
### Responsive Breakpoints
- **Mobile**: < 768px (full-screen map with drawer)
- **Tablet**: 768px - 1024px (split view)
- **Desktop**: > 1024px (sidebar + map)
## 🐳 Docker Configuration
### Valhalla Setup
```dockerfile
# Dockerfile.valhalla
FROM ghcr.io/gis-ops/docker-valhalla/valhalla:latest
# Copy OSM data
COPY ./osm-data/*.pbf /custom_files/
# Configuration
COPY valhalla.json /valhalla.json
EXPOSE 8002
CMD ["valhalla_service", "/valhalla.json"]
```
### Docker Compose
```yaml
version: '3.8'
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://backend:3001
depends_on:
- backend
backend:
build: ./backend
ports:
- "3001:3001"
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/parking_db
- REDIS_URL=redis://redis:6379
- VALHALLA_URL=http://valhalla:8002
depends_on:
- postgres
- redis
- valhalla
postgres:
image: postgis/postgis:15-3.3
environment:
- POSTGRES_DB=parking_db
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
valhalla:
build:
context: .
dockerfile: Dockerfile.valhalla
ports:
- "8002:8002"
volumes:
- ./valhalla-data:/valhalla-data
volumes:
postgres_data:
```
## 🔐 Security Considerations
### Frontend Security
- Content Security Policy (CSP)
- HTTPS enforcement
- API key protection
- Input sanitization
### Backend Security
- Rate limiting per IP
- JWT token validation
- SQL injection prevention
- CORS configuration
### Infrastructure Security
- Database encryption at rest
- SSL/TLS certificates
- Network segmentation
- Regular security updates
## 📈 Performance Optimization
### Frontend Optimization
- Code splitting by routes
- Image optimization with Next.js
- Service worker for caching
- Lazy loading for map components
### Backend Optimization
- Database query optimization
- Redis caching for frequent requests
- Connection pooling
- Response compression
### Database Optimization
- Spatial indexes for geo queries
- Query result caching
- Read replicas for scaling
- Partitioning for large datasets
## 🚀 Deployment Strategy
### Development
```bash
# Local development setup
docker-compose -f docker-compose.dev.yml up -d
npm run dev:frontend
npm run dev:backend
```
### Production
```bash
# Production deployment
docker-compose -f docker-compose.prod.yml up -d
```
### CI/CD Pipeline
1. **Build**: Docker images for each service
2. **Test**: Unit tests, integration tests, E2E tests
3. **Deploy**: Blue-green deployment strategy
4. **Monitor**: Health checks and performance metrics
## 📊 Monitoring & Analytics
### Application Metrics
- Response times
- Error rates
- User engagement
- Route calculation performance
### Business Metrics
- Popular parking locations
- Peak usage times
- User retention
- Revenue per parking lot
## 🔄 Future Enhancements
### Phase 2 Features
- Parking reservations
- Payment integration
- User reviews and ratings
- Push notifications for parking alerts
### Phase 3 Features
- AI-powered parking predictions
- Electric vehicle charging stations
- Multi-language support
- Offline mode with cached data
## 📋 Implementation Timeline
### Week 1-2: Foundation
- Project setup and infrastructure
- Database schema and migrations
- Basic API endpoints
### Week 3-4: Core Features
- Map integration with Leaflet
- Parking lot display and search
- User location detection
### Week 5-6: Navigation
- Valhalla integration
- Route calculation and display
- Turn-by-turn directions
### Week 7-8: Polish
- UI/UX improvements
- Performance optimization
- Testing and bug fixes
### Week 9-10: Deployment
- Production setup
- CI/CD pipeline
- Monitoring and analytics
## 🏁 Success Metrics
### Technical KPIs
- Page load time < 2 seconds
- Route calculation < 3 seconds
- 99.9% uptime
- Zero security vulnerabilities
### User Experience KPIs
- User retention > 60%
- Average session time > 5 minutes
- Route accuracy > 95%
- User satisfaction score > 4.5/5
This comprehensive specification provides a solid foundation for building a world-class parking finder application with modern web technologies and best practices.

251
VPS-DEPLOYMENT-GUIDE.md Normal file
View File

@@ -0,0 +1,251 @@
# 🚀 Laca City Website - VPS Deployment Guide
This guide will help you deploy your Laca City website to a VPS with a custom domain using a single script.
## 📋 Prerequisites
### VPS Requirements
- **OS**: Ubuntu 20.04+ or Debian 11+
- **RAM**: Minimum 2GB (4GB recommended)
- **Storage**: Minimum 20GB SSD
- **CPU**: 2+ cores recommended
- **Root/Sudo access**: Required
### Domain Requirements
- Domain name pointed to your VPS IP address
- DNS A record: `yourdomain.com``your-vps-ip`
- DNS A record: `www.yourdomain.com``your-vps-ip`
### Before You Start
- [ ] VPS is running Ubuntu/Debian
- [ ] You have SSH access to your VPS
- [ ] Domain DNS is properly configured
- [ ] You have a valid email address for SSL certificate
## 🎯 Quick Deployment
### Step 1: Upload Files to VPS
```bash
# Option 1: Using SCP
scp -r /path/to/your/project your-user@your-vps-ip:/home/your-user/
# Option 2: Using rsync
rsync -avz --progress /path/to/your/project/ your-user@your-vps-ip:/home/your-user/project/
# Option 3: Using Git (if your project is on GitHub)
git clone https://github.com/your-username/your-repo.git
```
### Step 2: Run the Deployment Script
```bash
# SSH into your VPS
ssh your-user@your-vps-ip
# Navigate to your project directory
cd /path/to/your/project
# Run the deployment script
./vps-deploy.sh
```
### Step 3: Follow the Interactive Setup
The script will ask you for:
- **Domain name**: e.g., `lacacity.com`
- **Email**: For SSL certificate (e.g., `admin@lacacity.com`)
- **Database password**: Leave empty to auto-generate
## 🛠️ What the Script Does
### System Setup
- ✅ Updates Ubuntu/Debian packages
- ✅ Installs Node.js 18.x and npm
- ✅ Installs PM2 process manager
- ✅ Configures UFW firewall
- ✅ Installs and configures Nginx
- ✅ Sets up Let's Encrypt SSL certificates
### Database Setup
- ✅ Installs and configures PostgreSQL
- ✅ Creates database and user
- ✅ Installs and configures Redis
### Application Deployment
- ✅ Installs frontend and backend dependencies
- ✅ Creates production environment files
- ✅ Builds Next.js frontend and NestJS backend
- ✅ Configures PM2 for process management
- ✅ Sets up Nginx reverse proxy
### Monitoring & Maintenance
- ✅ Sets up log rotation
- ✅ Creates automated backup scripts
- ✅ Configures SSL auto-renewal
- ✅ Creates deployment update scripts
## 🔧 Post-Deployment Commands
### Check Application Status
```bash
# View PM2 processes
pm2 list
pm2 monit
# View application logs
pm2 logs
# Check Nginx status
sudo systemctl status nginx
# Check database status
sudo systemctl status postgresql
```
### Application Management
```bash
# Restart applications
pm2 restart all
# Stop applications
pm2 stop all
# Reload Nginx configuration
sudo systemctl reload nginx
# View SSL certificate info
sudo certbot certificates
```
### Maintenance
```bash
# Manual backup
sudo /usr/local/bin/backup-laca-city
# Deploy updates (after uploading new code)
./deploy-update.sh
# View system resources
htop
```
## 🌐 Accessing Your Website
After successful deployment:
- **Website**: `https://yourdomain.com`
- **API**: `https://yourdomain.com/api`
- **SSL**: Automatically configured with Let's Encrypt
## 📁 Important File Locations
```
/var/www/laca-city/ # Main application directory
├── frontend/ # Next.js frontend
├── backend/ # NestJS backend
├── ecosystem.config.js # PM2 configuration
└── deploy-update.sh # Update deployment script
/etc/nginx/sites-available/laca-city # Nginx configuration
/var/log/pm2/ # Application logs
/var/backups/laca-city/ # Backup directory
```
## 🔒 Security Features
- ✅ HTTPS with automatic SSL renewal
- ✅ Firewall configured (UFW)
- ✅ Security headers in Nginx
- ✅ Database with encrypted password
- ✅ Environment variables for sensitive data
## 🚨 Troubleshooting
### Common Issues
**Domain not accessible:**
```bash
# Check DNS
nslookup yourdomain.com
# Check Nginx
sudo nginx -t
sudo systemctl status nginx
# Check SSL
sudo certbot certificates
```
**Applications not running:**
```bash
# Check PM2 status
pm2 list
pm2 logs
# Restart applications
pm2 restart all
```
**Database connection issues:**
```bash
# Check PostgreSQL
sudo systemctl status postgresql
# Test database connection
sudo -u postgres psql -d laca_city_db
```
### Getting Help
If you encounter issues:
1. Check the logs: `pm2 logs`
2. Verify all services are running
3. Check firewall settings: `sudo ufw status`
4. Verify DNS configuration
## 🔄 Updating Your Website
To deploy updates to your website:
1. Upload new files to VPS
2. Run the update script: `./deploy-update.sh`
The update script will:
- Create a backup
- Install new dependencies
- Rebuild applications
- Restart services
- Reload Nginx
## 💾 Backup & Recovery
### Automated Backups
- Daily backups at 2 AM
- Keeps 7 days of backups
- Location: `/var/backups/laca-city/`
### Manual Backup
```bash
sudo /usr/local/bin/backup-laca-city
```
### Restore from Backup
```bash
# Extract backup
cd /var/backups/laca-city
tar -xzf backup_YYYYMMDD_HHMMSS.tar.gz
# Restore database
sudo -u postgres psql -d laca_city_db < backup_YYYYMMDD_HHMMSS/database.sql
```
## 📞 Support
For additional support or questions about the deployment process, please check:
- Application logs: `pm2 logs`
- System logs: `sudo journalctl -f`
- Nginx logs: `sudo tail -f /var/log/nginx/error.log`
---
**🎉 Congratulations! Your Laca City website is now live and running on your VPS with a custom domain and SSL certificate!**

BIN
assets/AiBootcamp_event.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

BIN
assets/Footer_page_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/Phone_intro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

BIN
assets/download_store.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
assets/team_photo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

View File

@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import { DatabaseConfig } from './config/database.config';
import { ParkingModule } from './modules/parking/parking.module';
import { RoutingModule } from './modules/routing/routing.module';
import { UsersModule } from './modules/users/users.module';
import { AuthModule } from './modules/auth/auth.module';
import { HealthModule } from './modules/health/health.module';

View File

@@ -1,124 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional, IsEnum, Min, Max } from 'class-validator';
import { Transform } from 'class-transformer';
export class RouteRequestDto {
@ApiProperty({
description: 'Origin latitude',
example: 1.3521,
minimum: -90,
maximum: 90
})
@IsNumber()
@Min(-90)
@Max(90)
@Transform(({ value }) => parseFloat(value))
originLat: number;
@ApiProperty({
description: 'Origin longitude',
example: 103.8198,
minimum: -180,
maximum: 180
})
@IsNumber()
@Min(-180)
@Max(180)
@Transform(({ value }) => parseFloat(value))
originLng: number;
@ApiProperty({
description: 'Destination latitude',
example: 1.3500,
minimum: -90,
maximum: 90
})
@IsNumber()
@Min(-90)
@Max(90)
@Transform(({ value }) => parseFloat(value))
destinationLat: number;
@ApiProperty({
description: 'Destination longitude',
example: 103.8150,
minimum: -180,
maximum: 180
})
@IsNumber()
@Min(-180)
@Max(180)
@Transform(({ value }) => parseFloat(value))
destinationLng: number;
@ApiProperty({
description: 'Transportation mode',
example: 'auto',
enum: ['auto', 'bicycle', 'pedestrian'],
required: false
})
@IsOptional()
@IsEnum(['auto', 'bicycle', 'pedestrian'])
costing?: 'auto' | 'bicycle' | 'pedestrian' = 'auto';
@ApiProperty({
description: 'Number of alternative routes',
example: 2,
minimum: 0,
maximum: 3,
required: false
})
@IsOptional()
@IsNumber()
@Min(0)
@Max(3)
@Transform(({ value }) => parseInt(value))
alternatives?: number = 1;
@ApiProperty({
description: 'Avoid highways',
example: false,
required: false
})
@IsOptional()
avoidHighways?: boolean = false;
@ApiProperty({
description: 'Avoid tolls',
example: false,
required: false
})
@IsOptional()
avoidTolls?: boolean = false;
}
export interface RoutePoint {
lat: number;
lng: number;
}
export interface RouteStep {
instruction: string;
distance: number; // meters
time: number; // seconds
type: string;
geometry: RoutePoint[];
}
export interface Route {
summary: {
distance: number; // km
time: number; // minutes
cost?: number; // estimated cost
};
geometry: RoutePoint[];
steps: RouteStep[];
confidence: number;
}
export interface RouteResponse {
routes: Route[];
origin: RoutePoint;
destination: RoutePoint;
requestId: string;
}

View File

@@ -1,69 +0,0 @@
import {
Controller,
Post,
Get,
Body,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
} from '@nestjs/swagger';
import { ThrottlerGuard } from '@nestjs/throttler';
import { RoutingService } from './routing.service';
import { RouteRequestDto, RouteResponse } from './dto/route-request.dto';
@ApiTags('Routing')
@Controller('routing')
@UseGuards(ThrottlerGuard)
export class RoutingController {
constructor(private readonly routingService: RoutingService) {}
@Post('calculate')
@ApiOperation({
summary: 'Calculate route between two points',
description: 'Generate turn-by-turn directions using Valhalla routing engine'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Successfully calculated route',
type: 'object',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid route parameters',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'No route found between the specified locations',
})
@ApiResponse({
status: HttpStatus.SERVICE_UNAVAILABLE,
description: 'Routing service unavailable',
})
async calculateRoute(@Body() dto: RouteRequestDto): Promise<RouteResponse> {
return this.routingService.calculateRoute(dto);
}
@Get('status')
@ApiOperation({
summary: 'Check routing service status',
description: 'Check if the Valhalla routing service is healthy and responsive'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Service status information',
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'healthy' },
version: { type: 'string', example: '3.1.0' },
},
},
})
async getServiceStatus(): Promise<{ status: string; version?: string }> {
return this.routingService.getServiceStatus();
}
}

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { RoutingController } from './routing.controller';
import { RoutingService } from './routing.service';
@Module({
controllers: [RoutingController],
providers: [RoutingService],
exports: [RoutingService],
})
export class RoutingModule {}

View File

@@ -1,232 +0,0 @@
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
import { RouteRequestDto, RouteResponse, Route, RoutePoint, RouteStep } from './dto/route-request.dto';
@Injectable()
export class RoutingService {
private readonly logger = new Logger(RoutingService.name);
private readonly valhallaClient: AxiosInstance;
private readonly valhallaUrl: string;
constructor(private configService: ConfigService) {
this.valhallaUrl = this.configService.get<string>('VALHALLA_URL') || 'http://valhalla:8002';
this.valhallaClient = axios.create({
baseURL: this.valhallaUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
}
async calculateRoute(dto: RouteRequestDto): Promise<RouteResponse> {
try {
this.logger.debug(`Calculating route from ${dto.originLat},${dto.originLng} to ${dto.destinationLat},${dto.destinationLng}`);
const requestId = this.generateRequestId();
const valhallaRequest = this.buildValhallaRequest(dto);
const response = await this.valhallaClient.post('/route', valhallaRequest);
if (!response.data || !response.data.trip) {
throw new Error('Invalid response from Valhalla routing engine');
}
const routes = this.parseValhallaResponse(response.data);
return {
routes,
origin: { lat: dto.originLat, lng: dto.originLng },
destination: { lat: dto.destinationLat, lng: dto.destinationLng },
requestId,
};
} catch (error) {
this.logger.error('Failed to calculate route', error);
if (error.response?.status === 400) {
throw new HttpException(
'Invalid route request parameters',
HttpStatus.BAD_REQUEST,
);
}
if (error.response?.status === 404) {
throw new HttpException(
'No route found between the specified locations',
HttpStatus.NOT_FOUND,
);
}
throw new HttpException(
'Route calculation service unavailable',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
}
private buildValhallaRequest(dto: RouteRequestDto) {
const locations = [
{ lat: dto.originLat, lon: dto.originLng },
{ lat: dto.destinationLat, lon: dto.destinationLng },
];
const costingOptions = this.getCostingOptions(dto);
return {
locations,
costing: dto.costing,
costing_options: costingOptions,
directions_options: {
units: 'kilometers',
language: 'en-US',
narrative: true,
alternates: dto.alternatives || 1,
},
format: 'json',
shape_match: 'edge_walk',
encoded_polyline: true,
};
}
private getCostingOptions(dto: RouteRequestDto) {
const options: any = {};
if (dto.costing === 'auto') {
options.auto = {
maneuver_penalty: 5,
gate_cost: 30,
gate_penalty: 300,
private_access_penalty: 450,
toll_booth_cost: 15,
toll_booth_penalty: 0,
ferry_cost: 300,
use_ferry: dto.avoidTolls ? 0 : 1,
use_highways: dto.avoidHighways ? 0 : 1,
use_tolls: dto.avoidTolls ? 0 : 1,
};
} else if (dto.costing === 'bicycle') {
options.bicycle = {
maneuver_penalty: 5,
gate_penalty: 300,
use_roads: 0.5,
use_hills: 0.5,
use_ferry: 1,
avoid_bad_surfaces: 0.25,
};
} else if (dto.costing === 'pedestrian') {
options.pedestrian = {
walking_speed: 5.1,
walkway_factor: 1,
sidewalk_factor: 1,
alley_factor: 2,
driveway_factor: 5,
step_penalty: 0,
use_ferry: 1,
use_living_streets: 0.6,
};
}
return options;
}
private parseValhallaResponse(data: any): Route[] {
const trip = data.trip;
if (!trip || !trip.legs || trip.legs.length === 0) {
return [];
}
const route: Route = {
summary: {
distance: Math.round(trip.summary.length * 100) / 100, // km
time: Math.round(trip.summary.time / 60 * 100) / 100, // minutes
cost: this.estimateFuelCost(trip.summary.length, 'auto'),
},
geometry: this.decodePolyline(trip.shape),
steps: this.parseManeuvers(trip.legs[0].maneuvers),
confidence: 0.95,
};
return [route];
}
private parseManeuvers(maneuvers: any[]): RouteStep[] {
return maneuvers.map(maneuver => ({
instruction: maneuver.instruction,
distance: Math.round(maneuver.length * 1000), // convert km to meters
time: maneuver.time, // seconds
type: maneuver.type?.toString() || 'unknown',
geometry: [], // Would need additional processing for step-by-step geometry
}));
}
private decodePolyline(encoded: string): RoutePoint[] {
// Simplified polyline decoding - in production, use a proper polyline library
const points: RoutePoint[] = [];
let index = 0;
let lat = 0;
let lng = 0;
while (index < encoded.length) {
let result = 1;
let shift = 0;
let b: number;
do {
b = encoded.charCodeAt(index++) - 63 - 1;
result += b << shift;
shift += 5;
} while (b >= 0x1f);
lat += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
result = 1;
shift = 0;
do {
b = encoded.charCodeAt(index++) - 63 - 1;
result += b << shift;
shift += 5;
} while (b >= 0x1f);
lng += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
points.push({
lat: lat / 1e5,
lng: lng / 1e5,
});
}
return points;
}
private estimateFuelCost(distanceKm: number, costing: string): number {
if (costing !== 'auto') return 0;
const fuelEfficiency = 10; // km per liter
const fuelPricePerLiter = 1.5; // USD
return Math.round((distanceKm / fuelEfficiency) * fuelPricePerLiter * 100) / 100;
}
private generateRequestId(): string {
return `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async getServiceStatus(): Promise<{ status: string; version?: string }> {
try {
const response = await this.valhallaClient.get('/status');
return {
status: 'healthy',
version: response.data?.version,
};
} catch (error) {
this.logger.error('Valhalla service health check failed', error);
return {
status: 'unhealthy',
};
}
}
}

View File

@@ -1,46 +0,0 @@
#!/bin/bash
# Smart Parking Finder - Vercel Deployment Script
echo "🚀 Deploying Smart Parking Finder to Vercel..."
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if Vercel CLI is installed
if ! command_exists vercel; then
echo "📦 Installing Vercel CLI..."
npm install -g vercel
fi
# Navigate to frontend directory
cd frontend
echo "==============================================="
echo "🌐 VERCEL DEPLOYMENT"
echo "==============================================="
echo "🎯 This will deploy your app to a global URL"
echo "🆓 Free tier with custom domain support"
echo "⚡ Automatic HTTPS and CDN"
echo "==============================================="
echo ""
# Build the project
echo "🔨 Building project..."
npm run build
# Deploy to Vercel
echo "🚀 Deploying to Vercel..."
echo "📋 Follow the prompts to:"
echo " 1. Login to Vercel"
echo " 2. Link to your project"
echo " 3. Configure deployment settings"
echo ""
vercel --prod
echo ""
echo "✅ Deployment complete!"
echo "🌍 Your app is now accessible globally!"
echo "📊 Check deployment status: https://vercel.com/dashboard"

View File

@@ -1,154 +0,0 @@
version: '3.8'
services:
# Frontend - Next.js Application
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- NEXT_PUBLIC_API_URL=http://localhost:3001
- NEXT_PUBLIC_MAP_TILES_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
- NEXT_PUBLIC_VALHALLA_URL=http://localhost:8002
volumes:
- ./frontend:/app
- /app/node_modules
- /app/.next
depends_on:
- backend
networks:
- parking-network
# Backend - NestJS API
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://parking_user:parking_pass@postgres:5432/parking_db
- REDIS_URL=redis://redis:6379
- VALHALLA_URL=http://valhalla:8002
- JWT_SECRET=your-development-jwt-secret-change-in-production
- JWT_EXPIRATION=24h
volumes:
- ./backend:/app
- /app/node_modules
- /app/dist
depends_on:
- postgres
- redis
- valhalla
networks:
- parking-network
# PostgreSQL Database with PostGIS
postgres:
image: postgis/postgis:15-3.3
environment:
- POSTGRES_DB=parking_db
- POSTGRES_USER=parking_user
- POSTGRES_PASSWORD=parking_pass
- POSTGRES_INITDB_ARGS="--encoding=UTF-8"
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backend/database/init:/docker-entrypoint-initdb.d
networks:
- parking-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U parking_user -d parking_db"]
interval: 30s
timeout: 10s
retries: 3
# Redis Cache
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- parking-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Valhalla Routing Engine
valhalla:
build:
context: ./valhalla
dockerfile: Dockerfile
ports:
- "8002:8002"
volumes:
- valhalla_data:/data
- ./valhalla/custom_files:/custom_files
environment:
- VALHALLA_CONFIG=/valhalla.json
networks:
- parking-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8002/status"]
interval: 60s
timeout: 30s
retries: 3
start_period: 300s # Wait 5 minutes for initial setup
# Optional: pgAdmin for database management
pgadmin:
image: dpage/pgadmin4:latest
environment:
- PGADMIN_DEFAULT_EMAIL=admin@parking.local
- PGADMIN_DEFAULT_PASSWORD=admin123
- PGADMIN_CONFIG_SERVER_MODE=False
ports:
- "5050:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
depends_on:
- postgres
networks:
- parking-network
profiles:
- tools # Only start with: docker-compose --profile tools up
# Optional: Redis Commander for cache management
redis-commander:
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- "8081:8081"
depends_on:
- redis
networks:
- parking-network
profiles:
- tools # Only start with: docker-compose --profile tools up
volumes:
postgres_data:
driver: local
redis_data:
driver: local
valhalla_data:
driver: local
pgadmin_data:
driver: local
networks:
parking-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16

View File

@@ -67,9 +67,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -190,31 +190,31 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
"integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.2",
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz",
"integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
"integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.2"
"@floating-ui/dom": "^1.7.3"
},
"peerDependencies": {
"react": ">=16.8.0",
@@ -367,15 +367,15 @@
}
},
"node_modules/@next/env": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.30.tgz",
"integrity": "sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.31.tgz",
"integrity": "sha512-X8VxxYL6VuezrG82h0pUA1V+DuTSJp7Nv15bxq3ivrFqZLjx81rfeHMWOE9T0jm1n3DtHGv8gdn6B0T0kr0D3Q==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.30.tgz",
"integrity": "sha512-mvVsMIutMxQ4NGZEMZ1kiBNc+la8Xmlk30bKUmCPQz2eFkmsLv54Mha8QZarMaCtSPkkFA1TMD+FIZk0l/PpzA==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.31.tgz",
"integrity": "sha512-ouaB+l8Cr/uzGxoGHUvd01OnfFTM8qM81Crw1AG0xoWDRN0DKLXyTWVe0FdAOHVBpGuXB87aufdRmrwzZDArIw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -383,9 +383,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz",
"integrity": "sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.31.tgz",
"integrity": "sha512-dTHKfaFO/xMJ3kzhXYgf64VtV6MMwDs2viedDOdP+ezd0zWMOQZkxcwOfdcQeQCpouTr9b+xOqMCUXxgLizl8Q==",
"cpu": [
"arm64"
],
@@ -399,9 +399,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz",
"integrity": "sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.31.tgz",
"integrity": "sha512-iSavebQgeMukUAfjfW8Fi2Iz01t95yxRl2w2wCzjD91h5In9la99QIDKcKSYPfqLjCgwz3JpIWxLG6LM/sxL4g==",
"cpu": [
"x64"
],
@@ -415,9 +415,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz",
"integrity": "sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.31.tgz",
"integrity": "sha512-XJb3/LURg1u1SdQoopG6jDL2otxGKChH2UYnUTcby4izjM0il7ylBY5TIA7myhvHj9lG5pn9F2nR2s3i8X9awQ==",
"cpu": [
"arm64"
],
@@ -431,9 +431,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz",
"integrity": "sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.31.tgz",
"integrity": "sha512-IInDAcchNCu3BzocdqdCv1bKCmUVO/bKJHnBFTeq3svfaWpOPewaLJ2Lu3GL4yV76c/86ZvpBbG/JJ1lVIs5MA==",
"cpu": [
"arm64"
],
@@ -447,9 +447,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz",
"integrity": "sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.31.tgz",
"integrity": "sha512-YTChJL5/9e4NXPKW+OJzsQa42RiWUNbE+k+ReHvA+lwXk+bvzTsVQboNcezWOuCD+p/J+ntxKOB/81o0MenBhw==",
"cpu": [
"x64"
],
@@ -463,9 +463,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz",
"integrity": "sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.31.tgz",
"integrity": "sha512-A0JmD1y4q/9ufOGEAhoa60Sof++X10PEoiWOH0gZ2isufWZeV03NnyRlRmJpRQWGIbRkJUmBo9I3Qz5C10vx4w==",
"cpu": [
"x64"
],
@@ -479,9 +479,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz",
"integrity": "sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.31.tgz",
"integrity": "sha512-nowJ5GbMeDOMzbTm29YqrdrD6lTM8qn2wnZfGpYMY7SZODYYpaJHH1FJXE1l1zWICHR+WfIMytlTDBHu10jb8A==",
"cpu": [
"arm64"
],
@@ -495,9 +495,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz",
"integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.31.tgz",
"integrity": "sha512-pk9Bu4K0015anTS1OS9d/SpS0UtRObC+xe93fwnm7Gvqbv/W1ZbzhK4nvc96RURIQOux3P/bBH316xz8wjGSsA==",
"cpu": [
"ia32"
],
@@ -511,9 +511,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz",
"integrity": "sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.31.tgz",
"integrity": "sha512-LwFZd4JFnMHGceItR9+jtlMm8lGLU/IPkgjBBgYmdYSfalbHCiDpjMYtgDQ2wtwiAOSJOCyFI4m8PikrsDyA6Q==",
"cpu": [
"x64"
],
@@ -1401,23 +1401,10 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"version": "5.83.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
"integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -1425,12 +1412,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"version": "5.84.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz",
"integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.83.0"
"@tanstack/query-core": "5.83.1"
},
"funding": {
"type": "github",
@@ -1514,17 +1501,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
"integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/type-utils": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/type-utils": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -1538,7 +1525,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.37.0",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@@ -1554,16 +1541,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1579,14 +1566,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
"integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.37.0",
"@typescript-eslint/types": "^8.37.0",
"@typescript-eslint/tsconfig-utils": "^8.38.0",
"@typescript-eslint/types": "^8.38.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1601,14 +1588,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
"integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0"
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1619,9 +1606,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
"integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1636,15 +1623,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
"integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -1661,9 +1648,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
"integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1675,16 +1662,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
"integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.37.0",
"@typescript-eslint/tsconfig-utils": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/project-service": "8.38.0",
"@typescript-eslint/tsconfig-utils": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1730,16 +1717,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
"integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0"
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1754,13 +1741,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
"integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/types": "8.38.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -2426,13 +2413,13 @@
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -2624,9 +2611,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001731",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"funding": [
{
"type": "opencollective",
@@ -3005,9 +2992,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.187",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
"version": "1.5.194",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==",
"dev": true,
"license": "ISC"
},
@@ -3271,13 +3258,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.30.tgz",
"integrity": "sha512-4pTMb3wfpI+piVeEz3TWG1spjuXJJBZaYabi2H08z2ZTk6/N304POEovHdFmK6EZb4QlKpETulBNaRIITA0+xg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.31.tgz",
"integrity": "sha512-sT32j4678je7SWstBM6l0kE2L+LSgAARDAxw8iloNhI4/8xwkdDesbrGCPaGWzQv+dD6f6adhB+eRSThpGkBdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "14.2.30",
"@next/eslint-plugin-next": "14.2.31",
"@rushstack/eslint-patch": "^1.3.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
@@ -3784,9 +3771,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
@@ -5178,12 +5165,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "14.2.30",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.30.tgz",
"integrity": "sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.31.tgz",
"integrity": "sha512-Wyw1m4t8PhqG+or5a1U/Deb888YApC4rAez9bGhHkTsfwAy4SWKVro0GhEx4sox1856IbLhvhce2hAA6o8vkog==",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.30",
"@next/env": "14.2.31",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
@@ -5198,15 +5185,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.30",
"@next/swc-darwin-x64": "14.2.30",
"@next/swc-linux-arm64-gnu": "14.2.30",
"@next/swc-linux-arm64-musl": "14.2.30",
"@next/swc-linux-x64-gnu": "14.2.30",
"@next/swc-linux-x64-musl": "14.2.30",
"@next/swc-win32-arm64-msvc": "14.2.30",
"@next/swc-win32-ia32-msvc": "14.2.30",
"@next/swc-win32-x64-msvc": "14.2.30"
"@next/swc-darwin-arm64": "14.2.31",
"@next/swc-darwin-x64": "14.2.31",
"@next/swc-linux-arm64-gnu": "14.2.31",
"@next/swc-linux-arm64-musl": "14.2.31",
"@next/swc-linux-x64-gnu": "14.2.31",
"@next/swc-linux-x64-musl": "14.2.31",
"@next/swc-win32-arm64-msvc": "14.2.31",
"@next/swc-win32-ia32-msvc": "14.2.31",
"@next/swc-win32-x64-msvc": "14.2.31"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -5693,7 +5680,7 @@
"postcss": "^8.2.14"
}
},
"node_modules/postcss-selector-parser": {
"node_modules/postcss-nested/node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
@@ -5706,6 +5693,19 @@
"node": ">=4"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@@ -5796,9 +5796,9 @@
}
},
"node_modules/react-hook-form": {
"version": "7.60.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz",
"integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==",
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -6791,6 +6791,19 @@
}
}
},
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

View File

@@ -0,0 +1 @@
The image attachment you provided shows four team members sitting together on a couch in a friendly, professional setting. Since I cannot directly extract the binary image data from the attachment, I'll guide you to manually save this image.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
frontend/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Favicon Test - Laca City</title>
<!-- Favicons -->
<link rel="icon" href="favicon.ico?v=3" type="image/x-icon">
<link rel="shortcut icon" href="favicon.ico?v=3">
<link rel="icon" href="favicon-16x16.png?v=3" sizes="16x16" type="image/png">
<link rel="icon" href="favicon-32x32.png?v=3" sizes="32x32" type="image/png">
<link rel="icon" href="favicon.png?v=3" sizes="192x192" type="image/png">
<link rel="apple-touch-icon" href="apple-touch-icon.png?v=3" sizes="180x180">
<meta name="theme-color" content="#E85A4F">
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #E85A4F;
text-align: center;
}
.favicon-test {
border: 2px solid #E85A4F;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
background: #fff8f8;
}
.favicon-display {
display: flex;
align-items: center;
gap: 15px;
margin: 10px 0;
}
.favicon-display img {
border: 1px solid #ccc;
padding: 5px;
background: white;
}
.instructions {
background: #e8f4f8;
padding: 15px;
border-radius: 5px;
border-left: 4px solid #2196F3;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🎯 Favicon Test Page - Laca City</h1>
<div class="instructions">
<strong>📋 Instructions:</strong>
<ol>
<li>Look at your browser tab - you should see the red LC logo</li>
<li>Try refreshing the page (Ctrl+F5 or Cmd+Shift+R)</li>
<li>Clear browser cache if needed</li>
<li>Bookmark this page to test bookmark favicon</li>
</ol>
</div>
<div class="favicon-test">
<h3>🔍 Favicon Display Test</h3>
<div class="favicon-display">
<span><strong>16x16:</strong></span>
<img src="favicon-16x16.png?v=3" width="16" height="16" alt="16x16 favicon">
</div>
<div class="favicon-display">
<span><strong>32x32:</strong></span>
<img src="favicon-32x32.png?v=3" width="32" height="32" alt="32x32 favicon">
</div>
<div class="favicon-display">
<span><strong>64x64:</strong></span>
<img src="favicon.png?v=3" width="64" height="64" alt="64x64 favicon">
</div>
</div>
<div style="text-align: center; margin-top: 30px;">
<p>✅ If you can see the red LC logo in the browser tab and in the images above, the favicon is working correctly!</p>
<a href="../" style="color: #E85A4F; text-decoration: none; font-weight: bold;">← Back to Homepage</a> |
<a href="/?app=parking" style="color: #E85A4F; text-decoration: none; font-weight: bold;">Go to Parking App →</a>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,746 @@
<!DOCTYPE html>
<html lang="vi-VN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laca City - Smart Parking Solutions</title>
<meta name="description" content="Laca City provides smart parking solutions for Ho Chi Minh City with real-time availability and easy booking.">
<meta name="app-name" content="laca_city_parking"/>
<!-- Keep original Canva assets for compatibility -->
<link href="_assets/a0684b0780c739e9.vendor.ltr.css" rel="stylesheet" integrity="sha512-JwMCpiHdk95MoUTatfaZJwstzeDnWfvWMJiwnSxZfPmgeCe4yvQDQ+ONMQjIy/Ht72r0TmlE+gvZnYRnpdLdVg==" crossorigin="anonymous">
<link href="_assets/41486a3f3556b006.ltr.css" rel="stylesheet" integrity="sha512-4Kv3Zsha8ynK/1q8KtFeSG9EVbb1IvcqHyM5L6F6XWPSfxqk4fJ3+EihE11U0cxXbjgEIETcgDbzAtCTJFDsEA==" crossorigin="anonymous">
<link href="_assets/static_font_4.ltr.css" rel="stylesheet">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Favicons -->
<link rel="shortcut icon" href="_assets/images/2d0b56e7e51cf11036ad8734bdb67e2d.png">
<link rel="icon" href="_assets/images/e53c4bd8da5e491d9ab09e7cf0daf874.png" sizes="192x192">
<link rel="apple-touch-icon" href="_assets/images/725b756a69a7d4c235070e51acd85560.png" sizes="180x180">
<!-- Meta tags -->
<meta name="referrer" content="strict-origin-when-cross-origin">
<meta property="og:title" content="Laca City - Smart Parking Solutions">
<meta property="og:type" content="website">
<meta property="og:description" content="Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.">
<!-- TailwindCSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Preserve original contextmenu protection -->
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
document.addEventListener('contextmenu', (e) => {
const isMedia = ['img', 'image', 'video', 'svg', 'picture'].some(
tagName => tagName.localeCompare(e.target.tagName, undefined, { sensitivity: 'base' }) === 0,
);
isMedia && e.preventDefault();
});
</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
const lang = navigator.language ? navigator.language : 'en';
// Preserve original Canva footer functionality
window.canva_installFooter = (container) => {
if (!(container instanceof HTMLDivElement)) {
return;
}
fetch('_footer?lang=' + encodeURIComponent(lang)).then(response => {
if (response.status !== 200) {
return;
}
response.text().then(footerStr => {
container.innerHTML = '';
const div = document.createElement('div');
div.innerHTML = footerStr;
for (const child of [...div.children]) {
if (child.tagName.toLowerCase() !== 'script') {
container.append(child);
}
}
});
});
}
</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
window.C_CAPTCHA_IMPLEMENTATION = 'RECAPTCHA';
</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">
window.C_CAPTCHA_KEY = '6LdpNmIrAAAAAHQVezN3pBAfDjQQ2qUpo881f24o';
</script>
<!-- Custom styles for the redesigned homepage -->
<style>
:root {
--primary-color: #E85A4F;
--secondary-color: #D73502;
--accent-color: #C73E1D;
--success-color: #10B981;
--warning-color: #F59E0B;
--error-color: #EF4444;
--neutral-100: #F3F4F6;
--neutral-200: #E5E7EB;
--neutral-300: #D1D5DB;
--neutral-800: #1F2937;
--neutral-900: #111827;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
color: var(--neutral-800);
overflow-x: hidden;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 8px 25px rgba(232, 90, 79, 0.3);
border: none;
cursor: pointer;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(232, 90, 79, 0.4);
}
.btn-secondary {
background: white;
color: var(--primary-color);
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
border: 2px solid var(--primary-color);
cursor: pointer;
}
.btn-secondary:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.floating-animation {
animation: floating 6s ease-in-out infinite;
}
@keyframes floating {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.fade-in-up {
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease;
}
.fade-in-up.visible {
opacity: 1;
transform: translateY(0);
}
.section-padding {
padding: 100px 0;
}
.hero-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, var(--primary-color) 100%);
}
.feature-card {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid var(--neutral-200);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.feature-card:hover {
transform: translateY(-8px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.stats-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 30px;
text-align: center;
color: white;
}
.mobile-menu {
transform: translateX(100%);
transition: transform 0.3s ease;
}
.mobile-menu.open {
transform: translateX(0);
}
@media (max-width: 768px) {
.section-padding {
padding: 60px 0;
}
}
.pulse-ring {
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
@keyframes pulse-ring {
0% { transform: scale(.33); }
80%, 100% { opacity: 0; }
}
.scroll-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
transform-origin: left;
transform: scaleX(0);
z-index: 9999;
transition: transform 0.3s ease;
}
/* Hide original Canva content but preserve functionality */
#root {
display: none;
}
</style>
</head>
<body>
<!-- Scroll Progress Indicator -->
<div class="scroll-indicator" id="scrollIndicator"></div>
<!-- Original Canva functionality preserved but hidden -->
<div id="root" style="display: none;"></div>
<!-- New Redesigned Content -->
<div id="redesigned-homepage">
<!-- Navigation -->
<nav class="fixed top-0 w-full z-50 bg-white/90 backdrop-blur-md border-b border-gray-200/50 transition-all duration-300" id="navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-20">
<!-- Logo -->
<div class="flex items-center space-x-3">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div class="hidden sm:block">
<h1 class="text-2xl font-bold text-gray-900">Laca City</h1>
<p class="text-sm text-gray-600 font-medium">Smart Parking</p>
</div>
</div>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center space-x-8">
<a href="#home" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Home</a>
<a href="#features" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Features</a>
<a href="#how-it-works" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">How It Works</a>
<a href="#contact" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Contact</a>
<a href="?app=parking" class="btn-primary">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden">
<button id="mobileMenuBtn" class="text-gray-700 hover:text-red-600 focus:outline-none">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobileMenu" class="mobile-menu md:hidden fixed inset-y-0 right-0 w-64 bg-white shadow-xl z-50">
<div class="p-6">
<div class="flex justify-end mb-8">
<button id="closeMobileMenu" class="text-gray-700 hover:text-red-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-6">
<a href="#home" class="block text-gray-700 hover:text-red-600 font-medium">Home</a>
<a href="#features" class="block text-gray-700 hover:text-red-600 font-medium">Features</a>
<a href="#how-it-works" class="block text-gray-700 hover:text-red-600 font-medium">How It Works</a>
<a href="#contact" class="block text-gray-700 hover:text-red-600 font-medium">Contact</a>
<a href="?app=parking" class="btn-primary w-full justify-center">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<section id="home" class="hero-gradient min-h-screen flex items-center relative overflow-hidden">
<!-- Background patterns -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-20 left-10 w-20 h-20 bg-white rounded-full pulse-ring"></div>
<div class="absolute top-40 right-20 w-16 h-16 bg-white rounded-full pulse-ring" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-40 left-20 w-12 h-12 bg-white rounded-full pulse-ring" style="animation-delay: 1s;"></div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 relative z-10">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<!-- Hero Content -->
<div class="hero-content text-white fade-in-up">
<div class="mb-6">
<span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-white/20 backdrop-blur-sm border border-white/30">
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
Live Parking Availability
</span>
</div>
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
Smart Parking <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Made Simple
</span>
</h1>
<p class="text-xl md:text-2xl mb-8 text-white/90 leading-relaxed">
Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
Find Parking Now
</a>
<a href="#features" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Learn More
</a>
</div>
</div>
<!-- Hero Image using existing assets -->
<div class="relative lg:h-96 fade-in-up floating-animation">
<div class="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent rounded-3xl backdrop-blur-sm"></div>
<img src="../assets/Location.png" alt="Location and Parking" class="w-full h-full object-contain relative z-10 drop-shadow-2xl">
</div>
</div>
</div>
<!-- Scroll indicator -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white animate-bounce">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</div>
</section>
<!-- Stats Section -->
<section class="relative -mt-20 z-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-3 gap-6">
<div class="stats-card">
<div class="text-4xl font-bold mb-2">500+</div>
<div class="text-white/80">Parking Locations</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">10K+</div>
<div class="text-white/80">Happy Users</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">24/7</div>
<div class="text-white/80">Available Support</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="section-padding bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Why Choose <span style="color: var(--primary-color);">Laca City</span>?
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility.
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Feature Cards -->
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Real-Time Availability</h3>
<p class="text-gray-600 leading-relaxed">
Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Smart Pricing</h3>
<p class="text-gray-600 leading-relaxed">
Transparent, competitive pricing with advance booking discounts and flexible payment options.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">GPS Navigation</h3>
<p class="text-gray-600 leading-relaxed">
Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Secure Booking</h3>
<p class="text-gray-600 leading-relaxed">
Safe and secure reservation system with instant confirmation and QR code access.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Quick Access</h3>
<p class="text-gray-600 leading-relaxed">
Fast booking process takes less than 2 minutes from search to confirmation.
</p>
</div>
<div class="feature-card fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">24/7 Support</h3>
<p class="text-gray-600 leading-relaxed">
Round-the-clock customer support to help you with any parking-related queries or issues.
</p>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section id="how-it-works" class="section-padding bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
How It <span style="color: var(--primary-color);">Works</span>
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Simple, fast, and efficient parking solution in just three easy steps.
</p>
</div>
<div class="grid md:grid-cols-3 gap-12">
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">1</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Search Location</h3>
<p class="text-gray-600 leading-relaxed">
Enter your destination or use GPS to find nearby parking spots with real-time availability.
</p>
</div>
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">2</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Select & Book</h3>
<p class="text-gray-600 leading-relaxed">
Choose your preferred parking spot, select duration, and confirm your booking instantly.
</p>
</div>
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">3</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Navigate & Park</h3>
<p class="text-gray-600 leading-relaxed">
Follow GPS navigation to your reserved spot and use QR code for easy access.
</p>
</div>
</div>
<!-- CTA -->
<div class="text-center mt-16 fade-in-up">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
Start Parking Now
</a>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="section-padding hero-gradient">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-white mb-6">
Ready to Transform Your <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Parking Experience?
</span>
</h2>
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
Join thousands of satisfied users who have made parking stress-free with Laca City.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
Launch Mobile App
</a>
<a href="#contact" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
</svg>
Contact Support
</a>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer id="contact" class="bg-gray-900 text-white section-padding">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-4 gap-8 mb-12">
<div class="col-span-2">
<div class="flex items-center space-x-3 mb-6">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div>
<h3 class="text-2xl font-bold">Laca City</h3>
<p class="text-gray-400">Smart Parking Solutions</p>
</div>
</div>
<p class="text-gray-300 leading-relaxed mb-6 max-w-md">
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
</p>
</div>
<div>
<h4 class="text-lg font-semibold mb-6">Quick Links</h4>
<ul class="space-y-3">
<li><a href="#home" class="text-gray-300 hover:text-white transition-colors duration-300">Home</a></li>
<li><a href="#features" class="text-gray-300 hover:text-white transition-colors duration-300">Features</a></li>
<li><a href="#how-it-works" class="text-gray-300 hover:text-white transition-colors duration-300">How It Works</a></li>
<li><a href="?app=parking" class="text-gray-300 hover:text-white transition-colors duration-300">Launch App</a></li>
</ul>
</div>
<div>
<h4 class="text-lg font-semibold mb-6">Contact Info</h4>
<div class="space-y-3">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
<span class="text-gray-300">Ho Chi Minh City, Vietnam</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<span class="text-gray-300">info@lacacity.com</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
<span class="text-gray-300">+84 123 456 789</span>
</div>
</div>
</div>
</div>
<div class="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-400 text-sm">
© 2025 Laca City. All rights reserved.
</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Privacy Policy</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Terms of Service</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Cookie Policy</a>
</div>
</div>
</div>
</footer>
</div>
<!-- Original Canva scripts preserved -->
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">document.documentElement.classList.replace('adaptive', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');</script>
<script nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658">(function() {window['__canva_public_path__'] = '_assets.html\/'; window['bootstrap'] = JSON.parse('{"base":{"A?":"B","L":false,"N":false,"E":"4fae5166b56a1d7ddf289b70c12e5be51b3483e8","K":1754786491,"F":{"A?":"B"},"G":"CLIENT_FULL","M":"PRODUCTION","I":["staging","production"],"B":"diwidj.my.canva.site","H":{"C":"6LdpNmIrAAAAAHQVezN3pBAfDjQQ2qUpo881f24o","I":"RECAPTCHA","J":true},"D":true},"translation":{"D":"/translations/vi-VN.json","E":"vi-VN"},"runtime":{"E":"4fae5166b56a1d7ddf289b70c12e5be51b3483e8","A?":"D"},"canvaRoot":{"F":"https://canva.com","K":"/_","H":"/_"}}');window['__canva_site_directory__'] = ''; window['__canva_runtime_env__'] = 'node'; window['__canva_config__'] = bootstrap.runtime; if (['development', 'test'].includes(bootstrap.base.M)) { window['__canva_environment__'] = bootstrap.base.M; } })();
</script>
<script crossorigin="anonymous" src="_assets/44f34d65fd73e1ed.runtime.js" integrity="sha512-X9pbe1KLWNp2Cc2E+rXPrIyh9ypYg03DtN6V9lyoOIjeLc3KwHU1Qbk1spedVyyKQWRAVXQlYuuiJb/jTIb8GA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/3728e0a9c5b9c68f.s4le6a.vendor.js" integrity="sha512-Q0M8bNXf4EK25TsxODbPA7DtMzU/wFrdoWP7bVnfp9c03h9VLvkwZpcIKwzo1va1YjfjMMUvJ97M5y6Wlr6Rmw==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/f90d92d25d1cd0e3.vendor.js" integrity="sha512-M6jk9jbryq7jIVzbpU8D6Xcjfy+cbuTjsd7w+y2vNHWyKDfRLv47TAc015AIC6WzKoZYT8d/jcFyAshhEE7RAg==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/9cfe83e364efa77d.strings.js" integrity="sha512-esrEuPVdpThh//VF6TsFQ5F7lDHpdgb7IXDOgWTt4FPwp1BTq4G8JqsjIPpfrHTG+HRq0cOI+aULTLQOVRqwfA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/a4dd4bffd461949b.vi-VN.js" integrity="sha512-fRwl6n+jGzS8tt7ozxxQDUl4OSW6jZa2pne3lL5viqicmiZTNijnqgZx2inLmUvbwSyH3mk31PcW8aewZ8DuvA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<script crossorigin="anonymous" src="_assets/9bbbab0b3b011006.js" defer integrity="sha512-5Qmb0qHhAj/ELT1Zr/iswLWaBDeKY6sYbS2JKmpeSwnacFYS6usM1Xx9oWMNY9fQG1YGjOk7o8LX6f9hno3jwA==" nonce="656f2f39-c6e5-4f19-9fd1-98a29006c658"></script>
<!-- Custom JavaScript for the redesigned homepage -->
<script>
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Mobile menu toggle
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenu = document.getElementById('mobileMenu');
const closeMobileMenu = document.getElementById('closeMobileMenu');
if (mobileMenuBtn && mobileMenu && closeMobileMenu) {
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.add('open');
});
closeMobileMenu.addEventListener('click', () => {
mobileMenu.classList.remove('open');
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
mobileMenu.classList.remove('open');
}
});
}
// Fade in animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, observerOptions);
document.querySelectorAll('.fade-in-up').forEach(el => {
observer.observe(el);
});
// Navbar background on scroll
const navbar = document.getElementById('navbar');
if (navbar) {
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
navbar.classList.add('bg-white/95');
navbar.classList.remove('bg-white/90');
} else {
navbar.classList.add('bg-white/90');
navbar.classList.remove('bg-white/95');
}
});
}
// Scroll progress indicator
const scrollIndicator = document.getElementById('scrollIndicator');
if (scrollIndicator) {
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset;
const docHeight = document.body.scrollHeight - window.innerHeight;
const scrollPercent = (scrollTop / docHeight) * 100;
scrollIndicator.style.transform = `scaleX(${scrollPercent / 100})`;
});
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,776 @@
<!DOCTYPE html>
<html lang="vi-VN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laca City - Smart Parking Solutions</title>
<meta name="description" content="Laca City provides smart parking solutions for Ho Chi Minh City with real-time availability and easy booking.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Favicons -->
<link rel="shortcut icon" href="_assets/images/2d0b56e7e51cf11036ad8734bdb67e2d.png">
<link rel="icon" href="_assets/images/e53c4bd8da5e491d9ab09e7cf0daf874.png" sizes="192x192">
<link rel="apple-touch-icon" href="_assets/images/725b756a69a7d4c235070e51acd85560.png" sizes="180x180">
<!-- TailwindCSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Custom styles -->
<style>
:root {
--primary-color: #E85A4F;
--secondary-color: #D73502;
--accent-color: #C73E1D;
--success-color: #10B981;
--warning-color: #F59E0B;
--error-color: #EF4444;
--neutral-100: #F3F4F6;
--neutral-200: #E5E7EB;
--neutral-300: #D1D5DB;
--neutral-800: #1F2937;
--neutral-900: #111827;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
color: var(--neutral-800);
overflow-x: hidden;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-8px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 8px 25px rgba(232, 90, 79, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(232, 90, 79, 0.4);
}
.btn-secondary {
background: white;
color: var(--primary-color);
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
border: 2px solid var(--primary-color);
}
.btn-secondary:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.floating-animation {
animation: floating 6s ease-in-out infinite;
}
@keyframes floating {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.fade-in-up {
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease;
}
.fade-in-up.visible {
opacity: 1;
transform: translateY(0);
}
.section-padding {
padding: 100px 0;
}
.hero-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, var(--primary-color) 100%);
}
.feature-card {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid var(--neutral-200);
}
.stats-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 30px;
text-align: center;
color: white;
}
.mobile-menu {
transform: translateX(100%);
transition: transform 0.3s ease;
}
.mobile-menu.open {
transform: translateX(0);
}
@media (max-width: 768px) {
.section-padding {
padding: 60px 0;
}
.hero-content h1 {
font-size: 2.5rem !important;
line-height: 1.2 !important;
}
}
.pulse-ring {
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(.33);
}
80%, 100% {
opacity: 0;
}
}
.scroll-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
transform-origin: left;
transform: scaleX(0);
z-index: 9999;
transition: transform 0.3s ease;
}
</style>
</head>
<body>
<!-- Scroll Progress Indicator -->
<div class="scroll-indicator" id="scrollIndicator"></div>
<!-- Navigation -->
<nav class="fixed top-0 w-full z-50 bg-white/90 backdrop-blur-md border-b border-gray-200/50 transition-all duration-300" id="navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-20">
<!-- Logo -->
<div class="flex items-center space-x-3">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div class="hidden sm:block">
<h1 class="text-2xl font-bold text-gray-900">Laca City</h1>
<p class="text-sm text-gray-600 font-medium">Smart Parking</p>
</div>
</div>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center space-x-8">
<a href="#home" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Home</a>
<a href="#features" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Features</a>
<a href="#how-it-works" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">How It Works</a>
<a href="#contact" class="text-gray-700 hover:text-red-600 font-medium transition-colors duration-300">Contact</a>
<a href="?app=parking" class="btn-primary">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden">
<button id="mobileMenuBtn" class="text-gray-700 hover:text-red-600 focus:outline-none">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobileMenu" class="mobile-menu md:hidden fixed inset-y-0 right-0 w-64 bg-white shadow-xl z-50">
<div class="p-6">
<div class="flex justify-end mb-8">
<button id="closeMobileMenu" class="text-gray-700 hover:text-red-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-6">
<a href="#home" class="block text-gray-700 hover:text-red-600 font-medium">Home</a>
<a href="#features" class="block text-gray-700 hover:text-red-600 font-medium">Features</a>
<a href="#how-it-works" class="block text-gray-700 hover:text-red-600 font-medium">How It Works</a>
<a href="#contact" class="block text-gray-700 hover:text-red-600 font-medium">Contact</a>
<a href="?app=parking" class="btn-primary w-full justify-center">
Launch App
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<section id="home" class="hero-gradient min-h-screen flex items-center relative overflow-hidden">
<!-- Background patterns -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-20 left-10 w-20 h-20 bg-white rounded-full pulse-ring"></div>
<div class="absolute top-40 right-20 w-16 h-16 bg-white rounded-full pulse-ring" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-40 left-20 w-12 h-12 bg-white rounded-full pulse-ring" style="animation-delay: 1s;"></div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 relative z-10">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<!-- Hero Content -->
<div class="hero-content text-white fade-in-up">
<div class="mb-6">
<span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-white/20 backdrop-blur-sm border border-white/30">
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
Live Parking Availability
</span>
</div>
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
Smart Parking <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Made Simple
</span>
</h1>
<p class="text-xl md:text-2xl mb-8 text-white/90 leading-relaxed">
Find, reserve, and pay for parking in Ho Chi Minh City with real-time availability and smart navigation.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
Find Parking Now
</a>
<a href="#features" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Learn More
</a>
</div>
</div>
<!-- Hero Image -->
<div class="relative lg:h-96 fade-in-up floating-animation">
<div class="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent rounded-3xl backdrop-blur-sm"></div>
<img src="../assets/Location.png" alt="Location and Parking" class="w-full h-full object-contain relative z-10 drop-shadow-2xl">
</div>
</div>
</div>
<!-- Scroll indicator -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white animate-bounce">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</div>
</section>
<!-- Stats Section -->
<section class="relative -mt-20 z-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-3 gap-6">
<div class="stats-card">
<div class="text-4xl font-bold mb-2">500+</div>
<div class="text-white/80">Parking Locations</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">10K+</div>
<div class="text-white/80">Happy Users</div>
</div>
<div class="stats-card">
<div class="text-4xl font-bold mb-2">24/7</div>
<div class="text-white/80">Available Support</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="section-padding bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Why Choose <span style="color: var(--primary-color);">Laca City</span>?
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Experience the future of parking with our smart, efficient, and user-friendly platform designed for modern urban mobility.
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Feature 1 -->
<div class="feature-card card-hover fade-in-up">
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Real-Time Availability</h3>
<p class="text-gray-600 leading-relaxed">
Get live updates on parking space availability across Ho Chi Minh City with accurate, real-time data.
</p>
</div>
<!-- Feature 2 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.2s;">
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Smart Pricing</h3>
<p class="text-gray-600 leading-relaxed">
Transparent, competitive pricing with advance booking discounts and flexible payment options.
</p>
</div>
<!-- Feature 3 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.4s;">
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">GPS Navigation</h3>
<p class="text-gray-600 leading-relaxed">
Smart navigation system guides you directly to your reserved parking spot with turn-by-turn directions.
</p>
</div>
<!-- Feature 4 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.6s;">
<div class="w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Secure Booking</h3>
<p class="text-gray-600 leading-relaxed">
Safe and secure reservation system with instant confirmation and QR code access.
</p>
</div>
<!-- Feature 5 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 0.8s;">
<div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Quick Access</h3>
<p class="text-gray-600 leading-relaxed">
Fast booking process takes less than 2 minutes from search to confirmation.
</p>
</div>
<!-- Feature 6 -->
<div class="feature-card card-hover fade-in-up" style="animation-delay: 1s;">
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">24/7 Support</h3>
<p class="text-gray-600 leading-relaxed">
Round-the-clock customer support to help you with any parking-related queries or issues.
</p>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section id="how-it-works" class="section-padding bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20 fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
How It <span style="color: var(--primary-color);">Works</span>
</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Simple, fast, and efficient parking solution in just three easy steps.
</p>
</div>
<div class="grid md:grid-cols-3 gap-12">
<!-- Step 1 -->
<div class="text-center fade-in-up">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">1</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Search Location</h3>
<p class="text-gray-600 leading-relaxed">
Enter your destination or use GPS to find nearby parking spots with real-time availability.
</p>
</div>
<!-- Step 2 -->
<div class="text-center fade-in-up" style="animation-delay: 0.3s;">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">2</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Select & Book</h3>
<p class="text-gray-600 leading-relaxed">
Choose your preferred parking spot, select duration, and confirm your booking instantly.
</p>
</div>
<!-- Step 3 -->
<div class="text-center fade-in-up" style="animation-delay: 0.6s;">
<div class="relative mx-auto w-24 h-24 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mb-8">
<span class="text-3xl font-bold text-white">3</span>
<div class="absolute -top-2 -right-2 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Navigate & Park</h3>
<p class="text-gray-600 leading-relaxed">
Follow GPS navigation to your reserved spot and use QR code for easy access.
</p>
</div>
</div>
<!-- CTA -->
<div class="text-center mt-16 fade-in-up">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
Start Parking Now
</a>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="section-padding hero-gradient">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="fade-in-up">
<h2 class="text-4xl md:text-5xl font-bold text-white mb-6">
Ready to Transform Your <br>
<span class="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
Parking Experience?
</span>
</h2>
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
Join thousands of satisfied users who have made parking stress-free with Laca City.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="?app=parking" class="btn-primary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
Launch Mobile App
</a>
<a href="#contact" class="btn-secondary text-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
</svg>
Contact Support
</a>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer id="contact" class="bg-gray-900 text-white section-padding">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-4 gap-8 mb-12">
<!-- Company Info -->
<div class="col-span-2">
<div class="flex items-center space-x-3 mb-6">
<img src="../assets/Logo.png" alt="Laca City Logo" class="h-12 w-auto">
<div>
<h3 class="text-2xl font-bold">Laca City</h3>
<p class="text-gray-400">Smart Parking Solutions</p>
</div>
</div>
<p class="text-gray-300 leading-relaxed mb-6 max-w-md">
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
</p>
<div class="flex space-x-4">
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>
</a>
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z"/>
</svg>
</a>
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-red-600 transition-colors duration-300">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</div>
<!-- Quick Links -->
<div>
<h4 class="text-lg font-semibold mb-6">Quick Links</h4>
<ul class="space-y-3">
<li><a href="#home" class="text-gray-300 hover:text-white transition-colors duration-300">Home</a></li>
<li><a href="#features" class="text-gray-300 hover:text-white transition-colors duration-300">Features</a></li>
<li><a href="#how-it-works" class="text-gray-300 hover:text-white transition-colors duration-300">How It Works</a></li>
<li><a href="?app=parking" class="text-gray-300 hover:text-white transition-colors duration-300">Launch App</a></li>
</ul>
</div>
<!-- Contact Info -->
<div>
<h4 class="text-lg font-semibold mb-6">Contact Info</h4>
<div class="space-y-3">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
</svg>
<span class="text-gray-300">Ho Chi Minh City, Vietnam</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<span class="text-gray-300">info@lacacity.com</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
<span class="text-gray-300">+84 123 456 789</span>
</div>
</div>
</div>
</div>
<!-- Bottom bar -->
<div class="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-400 text-sm">
© 2025 Laca City. All rights reserved.
</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Privacy Policy</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Terms of Service</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors duration-300">Cookie Policy</a>
</div>
</div>
</div>
</footer>
<!-- Scripts -->
<script>
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Mobile menu toggle
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenu = document.getElementById('mobileMenu');
const closeMobileMenu = document.getElementById('closeMobileMenu');
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.add('open');
});
closeMobileMenu.addEventListener('click', () => {
mobileMenu.classList.remove('open');
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
mobileMenu.classList.remove('open');
}
});
// Fade in animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, observerOptions);
document.querySelectorAll('.fade-in-up').forEach(el => {
observer.observe(el);
});
// Navbar background on scroll
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
navbar.classList.add('bg-white/95');
navbar.classList.remove('bg-white/90');
} else {
navbar.classList.add('bg-white/90');
navbar.classList.remove('bg-white/95');
}
});
// Scroll progress indicator
const scrollIndicator = document.getElementById('scrollIndicator');
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset;
const docHeight = document.body.scrollHeight - window.innerHeight;
const scrollPercent = (scrollTop / docHeight) * 100;
scrollIndicator.style.transform = `scaleX(${scrollPercent / 100})`;
});
// Add some interactive elements
document.addEventListener('DOMContentLoaded', () => {
// Add hover effects to cards
const cards = document.querySelectorAll('.card-hover');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-8px) scale(1.02)';
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'translateY(0) scale(1)';
});
});
// Add click animation to buttons
const buttons = document.querySelectorAll('.btn-primary, .btn-secondary');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
// Create ripple effect
const ripple = document.createElement('span');
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
ripple.classList.add('ripple');
button.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
});
});
// Add CSS for ripple effect
const style = document.createElement('style');
style.textContent = `
.btn-primary, .btn-secondary {
position: relative;
overflow: hidden;
}
.ripple {
position: absolute;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
pointer-events: none;
transform: scale(0);
animation: ripple-animation 0.6s linear;
}
@keyframes ripple-animation {
to {
transform: scale(4);
opacity: 0;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
{
"name": "Laca City - Smart Parking",
"short_name": "Laca City",
"description": "Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#E85A4F",
"orientation": "portrait-primary",
"icons": [
{
"src": "/favicon.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/favicon.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["navigation", "travel", "utilities"],
"lang": "en",
"dir": "ltr",
"scope": "/",
"related_applications": [],
"prefer_related_applications": false
}

View File

@@ -0,0 +1,14 @@
User-agent: *
Allow: /
# Sitemap
Sitemap: https://yourdomain.com/sitemap.xml
# Specific paths
Allow: /homepage/
Allow: /assets/
Allow: /?app=parking
# Disallow admin or private sections (if any)
# Disallow: /admin/
# Disallow: /private/

View File

@@ -2,20 +2,6 @@
@tailwind components;
@tailwind utilities;
/* Import Leaflet CSS */
@import 'leaflet/dist/leaflet.css';
/* Leaflet container fixes for Next.js and full-screen rendering */
.leaflet-container {
height: 100% !important;
width: 100% !important;
z-index: 1 !important;
}
.leaflet-control-container {
z-index: 1000 !important;
}
/* Full screen layout fixes */
html, body {
height: 100%;
@@ -28,146 +14,62 @@ html, body {
height: 100%;
}
/* Map container specific fixes */
.map-container {
height: 100% !important;
width: 100% !important;
min-height: 400px !important;
position: relative;
}
.map-container .leaflet-container {
height: 100% !important;
width: 100% !important;
min-height: inherit !important;
}
/* Ensure proper flex behavior for full-screen maps */
/* Ensure proper flex behavior for full-screen layouts */
.flex-1 {
min-height: 0;
min-width: 0;
}
/* Custom Map Marker Animations */
/* GPS Marker Animations */
@keyframes pulse-gps {
0% {
transform: scale(0.8);
opacity: 0.6;
}
50% {
transform: scale(1.2);
opacity: 0.2;
}
100% {
transform: scale(0.8);
opacity: 0.6;
}
/* Global custom variables */
:root {
--primary-color: #e85a4f;
--secondary-color: #d2001c;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
}
@keyframes blink-gps {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0.3;
}
/* Custom scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
/* Parking Marker Animations */
@keyframes pulse-parking {
0% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.1);
opacity: 0.4;
}
100% {
transform: scale(1);
opacity: 0.8;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
/* Custom marker classes */
.gps-marker-icon,
.gps-marker-icon-enhanced {
background: transparent !important;
border: none !important;
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
/* Parking Finder Button Animations */
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-6px);
}
100% {
transform: translateY(0px);
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
@keyframes pulse-glow {
0% {
box-shadow: 0 10px 30px rgba(232, 90, 79, 0.4), 0 0 20px rgba(232, 90, 79, 0.3);
}
50% {
box-shadow: 0 15px 40px rgba(232, 90, 79, 0.6), 0 0 30px rgba(232, 90, 79, 0.5);
}
100% {
box-shadow: 0 10px 30px rgba(232, 90, 79, 0.4), 0 0 20px rgba(232, 90, 79, 0.3);
}
/* Loading spinner animation */
@keyframes spin {
to { transform: rotate(360deg); }
}
.parking-finder-button {
animation: float 3s ease-in-out infinite, pulse-glow 2s ease-in-out infinite;
.animate-spin {
animation: spin 1s linear infinite;
}
.parking-finder-button:hover {
animation: none;
/* Smooth transitions for better UX */
button, input, select, textarea, .interactive {
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.parking-marker-icon,
.parking-marker-icon-enhanced {
background: transparent !important;
border: none !important;
}
/* Enhanced popup styles with animation */
.leaflet-popup-content-wrapper {
border-radius: 16px !important;
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15),
0 10px 20px rgba(0, 0, 0, 0.1) !important;
border: 1px solid rgba(0, 0, 0, 0.05) !important;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95) !important;
animation: popup-appear 0.3s ease-out;
}
.leaflet-popup-content {
margin: 20px !important;
line-height: 1.6 !important;
font-size: 14px !important;
}
.leaflet-popup-tip {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
@keyframes popup-appear {
0% {
opacity: 0;
transform: scale(0.8) translateY(10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
/* Focus styles for accessibility */
button:focus,
input:focus,
select:focus,
textarea:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* Enhanced Filter Box Animations */
@@ -273,24 +175,29 @@ html, body {
}
}
/* Custom pulse animation for selected elements */
@keyframes selected-pulse {
0% {
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
/* Animation utilities */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
70% {
box-shadow: 0 0 0 10px rgba(220, 38, 38, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0);
to {
opacity: 1;
transform: translateY(0);
}
}
/* Hover effects for markers */
.leaflet-marker-icon:hover {
z-index: 1000 !important;
filter: brightness(1.1) saturate(1.2);
transition: all 0.2s ease-in-out;
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
.animate-slide-up {
animation: slide-up 0.3s ease-out;
}
/* Enhanced animations for GPS simulator */
@@ -326,79 +233,11 @@ html, body {
}
}
.marker-loading {
.loading-animation {
animation: spin-slow 2s linear infinite;
}
/* Enhanced mobile responsiveness for markers */
@media (max-width: 768px) {
.leaflet-popup-content-wrapper {
max-width: 280px !important;
}
.leaflet-popup-content {
margin: 12px !important;
font-size: 14px !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.gps-marker-icon,
.parking-marker-icon {
filter: contrast(1.5) saturate(1.2);
}
}
/* Reduce motion for accessibility */
@media (prefers-reduced-motion: reduce) {
.gps-marker-icon *,
.parking-marker-icon * {
animation: none !important;
}
}
/* Fix for Leaflet attribution */
.leaflet-control-attribution {
font-size: 10px !important;
}
/* Custom marker styles */
.custom-div-icon {
background: none !important;
border: none !important;
}
.leaflet-pane {
z-index: 1;
}
.leaflet-control-zoom {
z-index: 2;
}
.leaflet-control-attribution {
z-index: 2;
}
/* Custom CSS Variables */
:root {
--primary-color: #E85A4F;
--secondary-color: #D73502;
--accent-color: #8B2635;
--success-color: #22C55E;
--warning-color: #F59E0B;
--danger-color: #EF4444;
--neutral-color: #6B7280;
}
/* Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
@@ -410,95 +249,26 @@ body {
background-color: #ffffff;
}
/* Custom Scrollbar */
/* Custom Scrollbar (unified) */
::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Leaflet Map Overrides */
.leaflet-container {
height: 100%;
width: 100%;
border-radius: 0.5rem;
}
.leaflet-popup-content-wrapper {
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.leaflet-popup-tip {
background: white;
}
.leaflet-control-zoom {
border-radius: 0.5rem !important;
border: none !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important;
}
.leaflet-control-zoom a {
border-radius: 0.25rem !important;
border: none !important;
background-color: white !important;
color: #374151 !important;
font-weight: 600;
transition: all 0.2s ease;
}
.leaflet-control-zoom a:hover {
background-color: #f3f4f6 !important;
color: var(--primary-color) !important;
}
/* Custom Map Marker Styles */
.parking-marker {
background: white;
border: 2px solid var(--primary-color);
border-radius: 50%;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: var(--primary-color);
transition: all 0.2s ease;
}
.parking-marker:hover {
transform: scale(1.1);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.parking-marker.available {
border-color: var(--success-color);
color: var(--success-color);
}
.parking-marker.limited {
border-color: var(--warning-color);
color: var(--warning-color);
}
.parking-marker.full {
border-color: var(--danger-color);
color: var(--danger-color);
}
/* Animation Classes */
@keyframes shimmer {
0% {

View File

@@ -0,0 +1,618 @@
'use client';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
export default function Homepage() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [showScrollToTop, setShowScrollToTop] = useState(false);
const handleScrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
const headerOffset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
setMobileMenuOpen(false);
}
};
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
useEffect(() => {
const checkScrollTop = () => {
if (!showScrollToTop && window.pageYOffset > 400) {
setShowScrollToTop(true);
} else if (showScrollToTop && window.pageYOffset <= 400) {
setShowScrollToTop(false);
}
};
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (mobileMenuOpen && !target.closest('nav')) {
setMobileMenuOpen(false);
}
};
window.addEventListener('scroll', checkScrollTop);
document.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('scroll', checkScrollTop);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showScrollToTop, mobileMenuOpen]);
return (
<div className="min-h-screen bg-white">
{/* Navigation */}
<nav className="bg-white shadow-lg border-b-4 sticky top-0 z-50" style={{ borderBottomColor: 'var(--primary-color)' }}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="flex items-center justify-between h-20">
<div className="flex items-center space-x-4">
<div className="relative">
<Image
src="/assets/Logo_and_sologan.png"
alt="Laca City Logo"
width={280}
height={70}
className="h-16 w-auto object-contain"
/>
</div>
</div>
<div className="hidden lg:flex space-x-10">
<button
onClick={() => handleScrollToSection('about')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
About Us
</button>
<button
onClick={() => handleScrollToSection('how-it-works')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
How It Works
</button>
<button
onClick={() => handleScrollToSection('community')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
Community
</button>
<button
onClick={() => handleScrollToSection('contact')}
className="text-gray-700 hover:text-white hover:bg-primary-500 px-4 py-2 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
>
Contact
</button>
</div>
{/* Mobile menu button */}
<div className="lg:hidden">
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-white bg-primary-500 p-3 rounded-xl hover:shadow-xl transition-all duration-300 font-bold"
>
MENU
</button>
</div>
<div className="hidden lg:flex space-x-4">
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-white text-gray-700 border-2 border-gray-300 hover:border-primary-500 hover:text-primary-600 px-6 py-3 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-lg"
>
Open App
</button>
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-8 py-3 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 8px 25px rgba(232, 90, 79, 0.3)'
}}
>
Get Started
</button>
</div>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="lg:hidden bg-white border-t-2 shadow-xl" style={{ borderTopColor: 'var(--primary-color)' }}>
<div className="px-6 py-6 space-y-3">
<button
onClick={() => handleScrollToSection('about')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
About Us
</button>
<button
onClick={() => handleScrollToSection('how-it-works')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
How It Works
</button>
<button
onClick={() => handleScrollToSection('community')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
Community
</button>
<button
onClick={() => handleScrollToSection('contact')}
className="block w-full text-left px-4 py-3 text-gray-700 hover:text-white hover:bg-primary-500 rounded-xl font-medium transition-all duration-300"
>
Contact
</button>
<div className="pt-4 border-t-2 border-gray-200">
<button
onClick={() => window.location.href = '/?view=parking'}
className="block w-full text-white px-6 py-4 rounded-xl font-medium transition-all duration-300 transform hover:scale-105 shadow-xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 8px 25px rgba(232, 90, 79, 0.3)'
}}
>
Get Started
</button>
</div>
</div>
</div>
)}
</nav>
{/* Hero Section */}
<section className="relative overflow-hidden" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
}}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10 py-24 md:py-32">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div className="text-center lg:text-left">
<h1 className="text-5xl md:text-6xl lg:text-7xl font-extrabold leading-tight mb-8" style={{ color: 'var(--primary-color)' }}>
Park Easy, Move Breezy
</h1>
<h2 className="text-2xl md:text-3xl text-gray-700 mb-10 leading-relaxed font-medium">
Find and share parking spots in seconds. Save time, fuel, and reduce stress in Ho Chi Minh City & Hanoi.
</h2>
<div className="flex flex-col sm:flex-row gap-6 justify-center lg:justify-start mb-10">
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
>
Start Finding Parking
</button>
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-white text-gray-700 border-3 border-primary-500 hover:text-white hover:bg-primary-500 px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
>
Share a Spot
</button>
</div>
<p className="text-gray-600 text-xl leading-relaxed font-normal">
Join thousands of drivers reimagining streets for people, making urban driving easy and sustainable.
</p>
</div>
<div className="relative">
<div className="bg-white rounded-3xl shadow-2xl p-8 transform rotate-2 hover:rotate-0 transition-transform duration-500 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<Image
src="/assets/Location.png"
alt="Laca City App Interface"
width={600}
height={500}
className="w-full h-auto rounded-2xl"
/>
<div className="absolute -top-6 -right-6 text-white px-6 py-3 rounded-2xl text-lg font-semibold shadow-2xl" style={{ background: 'var(--primary-color)' }}>
Your city's parking assistant
</div>
</div>
</div>
</div>
</div>
</section>
{/* Problem & Story Section */}
<section className="py-24 bg-white">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Tired of Circling for Parking?
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div>
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
Urban drivers in Vietnam spend 1530 minutes per trip searching for parking—burning fuel, wasting time, and adding to congestion.
</p>
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
When founder Mai Nguyen returned to Vietnam, she saw sidewalks turned into parking lots, forcing pedestrians into the street. Delivery drivers and gig workers spend hours searching for spots, day after day.
</p>
<p className="text-xl text-gray-700 leading-relaxed font-medium" style={{ color: 'var(--primary-color)' }}>
Laca City was born to end this struggle—making parking easy while reclaiming streets for people.
</p>
</div>
<div className="relative">
<div className="grid grid-cols-2 gap-6">
<div className="bg-red-50 border-4 rounded-2xl p-8 text-center transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
<span className="text-white font-bold text-2xl">!</span>
</div>
<h4 className="font-semibold mb-3 text-xl" style={{ color: 'var(--primary-color)' }}>Before Laca City</h4>
<p className="font-medium" style={{ color: 'var(--primary-color)' }}>15-30 minutes circling, wasting fuel, stress</p>
</div>
<div className="bg-green-50 border-4 rounded-2xl p-8 text-center transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--secondary-color)' }}>
<span className="text-white font-bold text-2xl">✓</span>
</div>
<h4 className="font-semibold mb-3 text-xl" style={{ color: 'var(--secondary-color)' }}>With Laca City</h4>
<p className="font-medium" style={{ color: 'var(--secondary-color)' }}>Instant parking, happy drivers</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* How It Works Section */}
<section id="how-it-works" className="py-24 scroll-mt-20" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
}}>
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Find. Share. Drive Happy.
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 mb-16">
{/* Step 1 */}
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
1
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--primary-color)' }}>Find Parking Instantly</h3>
<p className="text-gray-700 font-normal text-lg">Open Laca City to see real-time parking spots near you.</p>
</div>
{/* Step 2 */}
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
2
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--primary-color)' }}>Share a Spot</h3>
<p className="text-gray-700 font-normal text-lg">Add public, private, or peer-to-peer parking to help other drivers.</p>
</div>
{/* Step 3 */}
<div className="text-center bg-white rounded-3xl p-10 shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-105 border-4" style={{ borderColor: 'var(--secondary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--secondary-color)' }}>
3
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-6" style={{ color: 'var(--secondary-color)' }}>Get Recognition</h3>
<p className="text-gray-700 font-normal text-lg">Earn badges and leaderboard positions for contributing to a smarter city.</p>
</div>
</div>
<div className="text-center">
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
>
Start Finding Parking
</button>
</div>
</div>
</section>
{/* Community & Gamification Section */}
<section id="community" className="py-24 bg-white scroll-mt-20">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Built by Drivers, for Drivers
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div>
<p className="text-xl text-gray-700 mb-10 leading-relaxed font-normal">
Your shared spots power the map. Laca City is entirely community-driven, rewarding contributors with:
</p>
<div className="space-y-8 mb-10">
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--primary-color)' }}>
HERO
</div>
<div>
<h4 className="font-semibold text-gray-900 text-xl">Parking Hero badges</h4>
<p className="text-gray-700 font-normal">Get recognized for your contributions</p>
</div>
</div>
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--secondary-color)' }}>
RANK
</div>
<div>
<h4 className="font-semibold text-gray-900 text-xl">Weekly leaderboards</h4>
<p className="text-gray-700 font-normal">Compete with other contributors</p>
</div>
</div>
<div className="flex items-center space-x-6 p-6 rounded-2xl border-4 transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex-shrink-0 w-16 h-16 rounded-2xl flex items-center justify-center text-white font-bold text-2xl" style={{ background: 'var(--primary-color)' }}>
FAME
</div>
<div>
<h4 className="font-semibold text-gray-900 text-xl">Social media shoutouts</h4>
<p className="text-gray-700 font-normal">Get featured for your community impact</p>
</div>
</div>
</div>
<p className="text-xl text-gray-700 mb-10 font-normal">
Help your city run better while getting recognized in the community.
</p>
<button
onClick={() => window.location.href = '/?view=parking'}
className="text-white px-10 py-5 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
>
Share Your First Spot Today
</button>
</div>
<div className="rounded-3xl p-10 border-4 shadow-2xl" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))',
borderColor: 'var(--primary-color)'
}}>
<h4 className="text-2xl font-bold text-gray-900 mb-8" style={{ color: 'var(--primary-color)' }}>Weekly Leaderboard</h4>
<div className="space-y-6">
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
<span className="text-white font-bold text-lg">1</span>
</div>
<div>
<p className="font-semibold text-gray-900 text-lg">Minh Nguyen</p>
<p className="text-gray-600 font-normal">District 1</p>
</div>
</div>
<span className="font-semibold text-xl" style={{ color: 'var(--primary-color)' }}>28 spots</span>
</div>
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--secondary-color)', backgroundColor: 'rgba(210, 0, 28, 0.1)' }}>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--secondary-color)' }}>
<span className="text-white font-bold text-lg">2</span>
</div>
<div>
<p className="font-semibold text-gray-900 text-lg">Linh Tran</p>
<p className="text-gray-600 font-normal">District 3</p>
</div>
</div>
<span className="font-semibold text-xl" style={{ color: 'var(--secondary-color)' }}>22 spots</span>
</div>
<div className="flex items-center justify-between p-6 border-4 rounded-2xl transform hover:scale-105 transition-transform duration-300" style={{ borderColor: 'var(--primary-color)', backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--primary-color)' }}>
<span className="text-white font-bold text-lg">3</span>
</div>
<div>
<p className="font-semibold text-gray-900 text-lg">Duc Le</p>
<p className="text-gray-600 font-normal">District 7</p>
</div>
</div>
<span className="font-semibold text-xl" style={{ color: 'var(--primary-color)' }}>19 spots</span>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Join the Movement Section */}
<section className="py-24 text-white" style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
}}>
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10 text-center">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8">
Help Build Vietnam's First Real-Time Parking Map
</h2>
<p className="text-2xl mb-12 max-w-4xl mx-auto leading-relaxed font-medium">
Sign up free. Find parking in seconds. Share your spots. Together, we'll reclaim streets for people and make our cities smarter.
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center">
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl"
style={{ color: 'var(--primary-color)' }}
>
Start Finding Parking
</button>
<button
onClick={() => window.location.href = '/?view=parking'}
className="bg-transparent border-4 border-white text-white hover:bg-white px-12 py-6 rounded-2xl font-semibold text-xl transition-all duration-300 transform hover:scale-110 shadow-2xl hover:text-red-500"
>
Start Sharing Spots
</button>
</div>
</div>
</section>
{/* About Us / Mission Section */}
<section id="about" className="py-24 bg-white scroll-mt-20">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Smart Parking for Smart Cities
</h2>
<p className="text-2xl text-gray-700 max-w-5xl mx-auto leading-relaxed font-medium">
Laca City connects drivers with real-time parking spots, reducing congestion and reclaiming sidewalks for pedestrians.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center mb-20">
<div>
<p className="text-xl text-gray-700 mb-8 leading-relaxed font-normal">
As Vietnam's cities prepare for autonomous vehicles and low-emission transport, we're building the digital parking infrastructure they need.
</p>
<p className="text-2xl font-bold leading-relaxed" style={{ color: 'var(--primary-color)' }}>
"Streets for people" - Đường phố cho con người.
</p>
</div>
<div className="rounded-3xl p-10 text-center border-4 shadow-2xl" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))',
borderColor: 'var(--primary-color)'
}}>
<div className="w-32 h-32 rounded-full mx-auto mb-8 overflow-hidden border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-full h-full flex items-center justify-center text-white text-5xl font-semibold" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
MN
</div>
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-3">Mai Nguyen</h3>
<p className="font-semibold mb-6 text-xl" style={{ color: 'var(--primary-color)' }}>Founder & CEO</p>
<p className="text-gray-700 leading-relaxed font-normal">
Urban planner with global experience (World Bank, Asia & North America).
Passionate about creating cities where streets belong to people.
</p>
</div>
</div>
</div>
</section>
{/* Social Proof & Partnerships Section */}
<section className="py-24" style={{
background: 'linear-gradient(135deg, rgba(232, 90, 79, 0.05), rgba(215, 53, 2, 0.05))'
}}>
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="text-center mb-20">
<h2 className="text-4xl md:text-5xl font-extrabold mb-8" style={{ color: 'var(--primary-color)' }}>
Trusted by Community and Partners
</h2>
<p className="text-2xl text-gray-700 max-w-5xl mx-auto leading-relaxed font-medium">
We're working with universities, small businesses, and city pilot programs to make urban parking easy.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
UN
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Vietnam National University</h3>
<p className="text-gray-700 font-normal">Student Parking Pilot</p>
</div>
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--secondary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--secondary-color)' }}>
CF
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">District 1 Cafe Network</h3>
<p className="text-gray-700 font-normal">Private Spot Sharing</p>
</div>
<div className="bg-white rounded-3xl p-10 shadow-2xl text-center transform hover:scale-105 transition-transform duration-300 border-4" style={{ borderColor: 'var(--primary-color)' }}>
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mx-auto mb-8 text-white text-3xl font-semibold" style={{ background: 'var(--primary-color)' }}>
HN
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Hanoi Transportation Dept</h3>
<p className="text-gray-700 font-normal">Public Lot Integration</p>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer id="contact" className="bg-gray-900 text-white py-20 scroll-mt-20">
<div className="max-w-6xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
<div className="md:col-span-2">
<div className="flex items-center space-x-4 mb-8">
<Image
src="/assets/Footer_page_logo.png"
alt="Laca City Logo"
width={60}
height={60}
className="h-15 w-15 object-contain"
/>
<span className="text-3xl font-black">Laca City</span>
</div>
<p className="text-gray-300 text-2xl mb-8 leading-relaxed font-bold">
Park Easy, Move Breezy.
</p>
<p className="text-gray-400 leading-relaxed font-normal text-lg">
Making urban parking easy while reclaiming streets for people.
</p>
</div>
<div>
<h4 className="text-xl font-semibold mb-8">Quick Links</h4>
<ul className="space-y-4">
<li><button onClick={() => handleScrollToSection('about')} className="text-gray-300 hover:text-white transition-colors cursor-pointer font-normal text-lg">About Us</button></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Blog</a></li>
<li><button onClick={() => handleScrollToSection('contact')} className="text-gray-300 hover:text-white transition-colors cursor-pointer font-normal text-lg">Contact</button></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Terms</a></li>
<li><a href="#" className="text-gray-300 hover:text-white transition-colors font-normal text-lg">Privacy</a></li>
</ul>
</div>
<div>
<h4 className="text-xl font-semibold mb-8">Connect</h4>
<div className="flex space-x-6">
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<span className="sr-only">Zalo</span>
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--primary-color)' }}>
Z
</div>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<span className="sr-only">Facebook</span>
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--secondary-color)' }}>
f
</div>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<span className="sr-only">TikTok</span>
<div className="w-12 h-12 rounded-2xl flex items-center justify-center font-semibold text-xl text-white" style={{ background: 'var(--primary-color)' }}>
T
</div>
</a>
</div>
</div>
</div>
<div className="border-t border-gray-800 pt-10 text-center">
<p className="text-gray-400 font-normal text-lg">
© 2025 Laca City. All rights reserved. Made with love for Vietnamese drivers.
</p>
</div>
</div>
</footer>
{/* Scroll to Top Button */}
{showScrollToTop && (
<button
onClick={scrollToTop}
className="fixed bottom-10 right-10 text-white p-4 rounded-2xl shadow-2xl transition-all duration-300 transform hover:scale-110 z-50 font-semibold text-lg"
style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
boxShadow: '0 12px 30px rgba(232, 90, 79, 0.4)'
}}
aria-label="Scroll to top"
>
TOP
</button>
)}
</div>
);
}

View File

@@ -7,46 +7,44 @@ import { Toaster } from 'react-hot-toast';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: '',
description: '',
keywords: ['parking', 'navigation', 'maps', 'HCMC', 'Vietnam', 'bãi đỗ xe', 'TP.HCM'],
authors: [{ name: 'Smart Parking Team' }],
creator: 'Smart Parking Team',
publisher: 'Smart Parking HCMC',
title: 'Laca City - Park Easy, Move Breezy',
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi. Join thousands of drivers reclaiming streets for people.',
keywords: ['parking', 'navigation', 'HCMC', 'Vietnam', 'bãi đỗ xe', 'TP.HCM', 'Hanoi', 'smart parking', 'Laca City'],
authors: [{ name: 'Laca City Team' }],
creator: 'Laca City',
publisher: 'Laca City',
robots: 'index, follow',
openGraph: {
type: 'website',
locale: 'vi_VN',
url: 'https://parking-hcmc.com',
title: '',
description: '',
siteName: 'Smart Parking HCMC',
url: 'https://lacacity.com',
title: 'Laca City - Park Easy, Move Breezy',
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.',
siteName: 'Laca City',
images: [
{
url: '/assets/Logo_and_sologan.png',
url: '/assets/Location.png',
width: 1200,
height: 630,
alt: 'Smart Parking HCMC',
alt: 'Laca City - Smart Parking Solution',
},
],
},
twitter: {
card: 'summary_large_image',
title: '',
description: '',
images: ['/assets/Logo_and_sologan.png'],
title: 'Laca City - Park Easy, Move Breezy',
description: 'Find and share parking in seconds. Save time, fuel, and stress in Ho Chi Minh City & Hanoi.',
images: ['/assets/Location.png'],
},
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
},
themeColor: '#2563EB',
themeColor: '#E85A4F',
manifest: '/manifest.json',
icons: {
icon: '/assets/mini_location.png',
shortcut: '/assets/mini_location.png',
apple: '/assets/Logo.png',
icon: '/favicon.png?v=5',
},
};

View File

@@ -8,23 +8,9 @@ import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator';
// import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { useParkingSearch } from '@/hooks/useParkingSearch';
import { useRouting } from '@/hooks/useRouting';
import { ParkingLot, UserLocation, TransportationMode } from '@/types';
import toast from 'react-hot-toast';
// Dynamic import for map component (client-side only)
const MapView = dynamic(
() => import('@/components/map/MapView').then((mod) => mod.MapView),
{
ssr: false,
loading: () => (
<div className="h-full flex items-center justify-center bg-gray-100 rounded-lg">
<LoadingSpinner size="lg" />
</div>
),
}
);
export default function ParkingFinderPage() {
// State management
const [selectedParkingLot, setSelectedParkingLot] = useState<ParkingLot | null>(null);
@@ -42,14 +28,6 @@ export default function ParkingFinderPage() {
searchLocation
} = useParkingSearch();
const {
route,
isLoading: routeLoading,
error: routeError,
calculateRoute,
clearRoute
} = useRouting();
// Handle GPS location change from simulator
const handleLocationChange = (location: UserLocation) => {
setUserLocation(location);
@@ -70,35 +48,16 @@ export default function ParkingFinderPage() {
}
};
const handleParkingLotSelect = async (lot: ParkingLot) => {
const handleParkingLotSelect = (lot: ParkingLot) => {
// If the same parking lot is selected again, deselect it
if (selectedParkingLot && selectedParkingLot.id === lot.id) {
setSelectedParkingLot(null);
clearRoute();
toast.success('Đã bỏ chọn bãi đỗ xe');
return;
}
setSelectedParkingLot(lot);
if (userLocation) {
try {
await calculateRoute(
{ latitude: userLocation.lat, longitude: userLocation.lng },
{ latitude: lot.lat, longitude: lot.lng },
{ mode: 'driving' }
);
toast.success(`Đã tính đường đến ${lot.name}`);
} catch (error) {
toast.error('Không thể tính toán đường đi');
}
}
};
const handleClearRoute = () => {
clearRoute();
setSelectedParkingLot(null);
toast.success('Đã xóa tuyến đường');
toast.success(`Đã chọn ${lot.name}`);
};
// Show error messages
@@ -108,35 +67,35 @@ export default function ParkingFinderPage() {
}
}, [parkingError]);
useEffect(() => {
if (routeError) {
toast.error(routeError);
}
}, [routeError]);
return (
<div className="min-h-screen bg-gray-50">
<Header
title="Smart Parking Finder - TP.HCM"
subtitle="Chỉ hỗ trợ ô tô"
onClearRoute={route ? handleClearRoute : undefined}
/>
<main className="container mx-auto px-4 py-6">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-full">
{/* Left Column - Map and Parking List */}
<div className="lg:col-span-3 space-y-6">
{/* Map Section */}
{/* Summary Section */}
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
<div className="h-96">
<MapView
userLocation={userLocation}
parkingLots={parkingLots}
selectedParkingLot={selectedParkingLot}
route={route}
onParkingLotSelect={handleParkingLotSelect}
isLoading={routeLoading}
/>
<div className="h-96 bg-gradient-to-br from-gray-50 to-blue-50 flex items-center justify-center">
<div className="text-center p-8">
<div className="w-24 h-24 bg-blue-100 rounded-full mx-auto mb-6 flex items-center justify-center">
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-800 mb-3">Parking Finder - HCMC</h2>
<p className="text-gray-600 mb-4">Find and book parking spots in Ho Chi Minh City</p>
{parkingLots.length > 0 && (
<div className="text-sm text-gray-500">
Found {parkingLots.length} parking locations nearby
</div>
)}
</div>
</div>
</div>
@@ -199,14 +158,6 @@ export default function ParkingFinderPage() {
</div>
</div>
)}
{routeError && (
<div className="fixed bottom-4 right-4 max-w-sm">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{routeError}
</div>
</div>
)}
</main>
</div>
);

View File

@@ -1,8 +1,9 @@
'use client';
import React, { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { useSearchParams } from 'next/navigation';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { ParkingList } from '@/components/parking/ParkingList';
import { ParkingDetails } from '@/components/parking/ParkingDetails';
import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator';
@@ -10,24 +11,42 @@ import { Icon } from '@/components/ui/Icon';
// import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { useParkingSearch } from '@/hooks/useParkingSearch';
import { useRouting } from '@/hooks/useRouting';
import { ParkingLot, UserLocation, TransportationMode } from '@/types';
import toast from 'react-hot-toast';
// Dynamic import for map component (client-side only) - NO loading component to prevent unnecessary loading states
const MapView = dynamic(
() => import('@/components/map/MapView').then((mod) => mod.MapView),
{
ssr: false,
loading: () => null, // Remove loading spinner to prevent map reload appearance
}
);
export default function MainPage() {
const searchParams = useSearchParams();
const showApp = searchParams?.get('app') === 'parking';
export default function ParkingFinderPage() {
if (showApp) {
return <ParkingFinderPage />;
}
// Show Canva homepage by default
return <CanvaHomepage />;
}
function CanvaHomepage() {
useEffect(() => {
// Redirect to the Canva homepage in the public directory
window.location.href = '/homepage/index.html';
}, []);
return (
<div className="h-screen bg-white flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-red-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">Loading homepage...</p>
</div>
</div>
);
}
function ParkingFinderPage() {
// State management
const [selectedParkingLot, setSelectedParkingLot] = useState<ParkingLot | null>(null);
const [userLocation, setUserLocation] = useState<UserLocation | null>(null);
const [searchRadius, setSearchRadius] = useState(4000); // meters - bán kính 4km
const [searchRadius, setSearchRadius] = useState(4000); // meters - 4km radius
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
const [gpsWindowPos, setGpsWindowPos] = useState({ x: 0, y: 20 });
const [isMobile, setIsMobile] = useState(false);
@@ -88,14 +107,6 @@ export default function ParkingFinderPage() {
searchLocation
} = useParkingSearch();
const {
route,
isLoading: routeLoading,
error: routeError,
calculateRoute,
clearRoute
} = useRouting();
// Handle GPS location change from simulator
const handleLocationChange = (location: UserLocation) => {
setUserLocation(location);
@@ -103,16 +114,16 @@ export default function ParkingFinderPage() {
// Search for parking near the new location
if (location) {
searchLocation({ latitude: location.lat, longitude: location.lng });
toast.success('Đã cập nhật vị trí GPS và tìm kiếm bãi đỗ xe gần đó');
toast.success('GPS location updated and searched for nearby parking lots');
}
};
const handleRefresh = () => {
if (userLocation) {
searchLocation({ latitude: userLocation.lat, longitude: userLocation.lng });
toast.success('Đã làm mới danh sách bãi đỗ xe');
toast.success('Parking list refreshed');
} else {
toast.error('Vui lòng chọn vị trí GPS trước');
toast.error('Please select GPS location first');
}
};
@@ -120,25 +131,12 @@ export default function ParkingFinderPage() {
// Toggle selection
if (selectedParkingLot?.id === lot.id) {
setSelectedParkingLot(null);
clearRoute();
return;
}
setSelectedParkingLot(lot);
if (userLocation) {
try {
await calculateRoute(
{ latitude: userLocation.lat, longitude: userLocation.lng },
{ latitude: lot.lat, longitude: lot.lng },
{ mode: 'driving' }
);
toast.success(`Đã tính đường đến ${lot.name}`);
} catch (error) {
console.error('Error calculating route:', error);
toast.error('Không thể tính toán tuyến đường');
}
}
setLeftSidebarOpen(false); // Close sidebar when selecting parking lot
toast.success(`Selected ${lot.name}`);
};
const handleParkingLotViewing = (lot: ParkingLot | null) => {
@@ -146,9 +144,8 @@ export default function ParkingFinderPage() {
};
const handleClearRoute = () => {
clearRoute();
setSelectedParkingLot(null);
toast.success('Đã xóa tuyến đường');
toast.success('Selection cleared');
};
// Show error messages
@@ -158,18 +155,11 @@ export default function ParkingFinderPage() {
}
}, [parkingError]);
useEffect(() => {
if (routeError) {
toast.error(routeError);
}
}, [routeError]);
return (
<div className="h-screen bg-gray-50 flex flex-col">
<Header
title=""
subtitle=""
onClearRoute={route ? handleClearRoute : undefined}
/>
<main className="flex-1 flex relative bg-white">
@@ -206,9 +196,9 @@ export default function ParkingFinderPage() {
</div>
<div>
<h2 className="text-xl font-bold text-gray-900 tracking-tight">
Bãi đ xe gần đây
Nearby Parking Lots
</h2>
<p className="text-sm text-gray-600 font-medium">Tìm kiếm thông minh</p>
<p className="text-sm text-gray-600 font-medium">Smart Search</p>
</div>
</div>
<div className="flex items-center space-x-2">
@@ -229,7 +219,7 @@ export default function ParkingFinderPage() {
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Làm mới danh sách
Refresh List
</button>
{/* Status Info Bar - Thiết kế thanh lịch đơn giản */}
@@ -241,14 +231,14 @@ export default function ParkingFinderPage() {
<div className="flex items-center space-x-2">
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
<span className="text-sm text-gray-700 font-medium">
{parkingLots.filter(lot => lot.availableSlots > 0).length} chỗ
{parkingLots.filter(lot => lot.availableSlots > 0).length} available
</span>
</div>
<div className="w-px h-4 bg-gray-300"></div>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 rounded-full bg-red-500"></div>
<span className="text-sm text-gray-700 font-medium">
{parkingLots.filter(lot => lot.availableSlots === 0).length} đy
{parkingLots.filter(lot => lot.availableSlots === 0).length} full
</span>
</div>
</div>
@@ -275,7 +265,7 @@ export default function ParkingFinderPage() {
<div className="relative">
<input
type="text"
placeholder="Tìm kiếm bãi đỗ xe..."
placeholder="Search parking lots..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-3 pl-12 pr-10 text-sm font-medium rounded-2xl border-2 transition-all duration-300 focus:outline-none focus:ring-4 focus:ring-orange-100 focus:border-orange-300"
@@ -320,7 +310,7 @@ export default function ParkingFinderPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707v4.586a1 1 0 01-.54.89l-2 1A1 1 0 0110 20v-5.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
</div>
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Sắp xếp:</span>
<span className="text-lg font-bold" style={{ color: 'var(--accent-color)' }}>Sort:</span>
</div>
<div className="flex gap-2">
@@ -339,7 +329,7 @@ export default function ParkingFinderPage() {
borderColor: sortType === 'availability' ? 'var(--primary-color)' : 'rgba(232, 90, 79, 0.3)',
border: '2px solid'
}}
title="Sắp xếp theo chỗ trống"
title="Sort by availability"
>
<Icon
name="car"
@@ -363,7 +353,7 @@ export default function ParkingFinderPage() {
borderColor: sortType === 'price' ? '#10B981' : 'rgba(16, 185, 129, 0.3)',
border: '2px solid'
}}
title="Sắp xếp theo giá rẻ"
title="Sort by price"
>
<Icon
name="currency"
@@ -394,7 +384,7 @@ export default function ParkingFinderPage() {
: userLocation ? 'rgba(245, 158, 11, 0.3)' : '#E5E7EB',
border: '2px solid'
}}
title="Sắp xếp theo khoảng cách gần nhất"
title="Sort by nearest distance"
>
<Icon
name="distance"
@@ -419,8 +409,8 @@ export default function ParkingFinderPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">Chọn vị trí GPS</h3>
<p className="text-gray-600 text-sm">Vui lòng chọn vị trí GPS đ tìm bãi đ xe gần đó</p>
<h3 className="text-lg font-bold text-gray-900 mb-2">Select GPS Location</h3>
<p className="text-gray-600 text-sm">Please select a GPS location to find nearby parking lots</p>
</div>
) : parkingLots.length === 0 ? (
<div className="text-center py-12">
@@ -429,8 +419,8 @@ export default function ParkingFinderPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.732 15c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">Không bãi đ xe</h3>
<p className="text-gray-600 text-sm">Không tìm thấy bãi đ xe nào gần vị trí này</p>
<h3 className="text-lg font-bold text-gray-900 mb-2">No Parking Lots</h3>
<p className="text-gray-600 text-sm">No parking lots found near this location</p>
</div>
) : (
<ParkingList
@@ -468,89 +458,34 @@ export default function ParkingFinderPage() {
userLocation={userLocation}
onClose={() => {
setSelectedParkingLot(null);
clearRoute();
}}
onBook={(lot) => {
toast.success(`Đã đặt chỗ tại ${lot.name}!`);
toast.success(`Booked parking at ${lot.name}!`);
// Here you would typically call an API to book the parking spot
}}
/>
</div>
)}
{/* Map Section - Right */}
{/* Right Section - Summary Information */}
<div className="flex-1 h-full relative">
<MapView
userLocation={userLocation}
parkingLots={parkingLots}
selectedParkingLot={selectedParkingLot}
route={route}
onParkingLotSelect={handleParkingLotSelect}
className="w-full h-full"
/>
{/* Map overlay info - Position based on layout */}
{userLocation && (
<div className="absolute bottom-6 right-24 z-10 bg-white rounded-3xl shadow-2xl p-6 border-2 border-gray-100 backdrop-blur-sm" style={{ minWidth: '280px' }}>
<div className="flex items-center space-x-4 mb-4">
<div className="w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg" style={{ background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))' }}>
<img
src="/assets/Logo.png"
alt="Logo"
className="w-7 h-7 object-contain filter brightness-0 invert"
/>
</div>
<div>
<h3 className="text-lg font-bold text-gray-900 tracking-tight">Parking Finder</h3>
<p className="text-sm text-gray-600 font-medium">Bản đ thông minh</p>
</div>
<div className="w-full h-full bg-gradient-to-br from-gray-50 to-blue-50 rounded-2xl flex items-center justify-center border border-gray-200">
<div className="text-center p-8">
<div className="w-24 h-24 bg-blue-100 rounded-full mx-auto mb-6 flex items-center justify-center">
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div className="space-y-2">
{/* Current location */}
<div className="flex items-center space-x-3 p-2 rounded-xl bg-blue-50">
<div className="w-4 h-4 rounded-full shadow-sm" style={{ backgroundColor: '#3B82F6' }}></div>
<span className="text-sm font-semibold text-blue-800">Vị trí hiện tại</span>
<h2 className="text-2xl font-bold text-gray-800 mb-3">Map in Developing</h2>
<p className="text-gray-600 mb-4">Interactive map feature coming soon</p>
{parkingLots.length > 0 && (
<div className="text-sm text-gray-500">
Found {parkingLots.length} parking locations nearby
</div>
{/* Parking lot status legend */}
<div className="bg-gray-50 rounded-xl p-3 space-y-2">
<div className="text-xs font-bold text-gray-700 mb-2">Trạng thái bãi xe:</div>
{/* Available parking - Green */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: 'var(--success-color)' }}></div>
<span className="text-xs font-medium text-green-700">Còn chỗ thoáng (&gt;70%)</span>
</div>
{/* Nearly full - Yellow */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#F59E0B' }}></div>
<span className="text-xs font-medium text-yellow-700">Sắp đy (&lt;30%)</span>
</div>
{/* Full - Red */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#EF4444' }}></div>
<span className="text-xs font-medium text-red-700">Hết chỗ</span>
</div>
{/* Closed - Gray */}
<div className="flex items-center space-x-3 p-1">
<div className="w-3 h-3 rounded-full shadow-sm" style={{ backgroundColor: '#6B7280' }}></div>
<span className="text-xs font-medium text-gray-700">Đã đóng cửa</span>
</div>
</div>
{/* Route line */}
{route && (
<div className="flex items-center space-x-3 p-2 rounded-xl bg-red-50">
<div className="w-4 h-2 rounded-full shadow-sm" style={{ backgroundColor: 'var(--primary-color)' }}></div>
<span className="text-sm font-semibold" style={{ color: 'var(--primary-color)' }}>Tuyến đưng</span>
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
{/* Floating GPS Window */}
@@ -595,14 +530,14 @@ export default function ParkingFinderPage() {
<p className="text-white text-opacity-90 font-medium" style={{
fontSize: isMobile ? '12px' : '14px'
}}>
{isMobile ? 'Mô phỏng GPS' : 'Mô phỏng vị trí GPS cho TP.HCM'}
{isMobile ? 'GPS Simulation' : 'GPS Location Simulation for Ho Chi Minh City'}
</p>
</div>
{isMobile && (
<button
onClick={() => setGpsSimulatorVisible(!gpsSimulatorVisible)}
className="p-2 rounded-xl bg-white bg-opacity-20 hover:bg-opacity-30 transition-all duration-200"
title={gpsSimulatorVisible ? 'Ẩn GPS Simulator' : 'Hiện GPS Simulator'}
title={gpsSimulatorVisible ? 'Hide GPS Simulator' : 'Show GPS Simulator'}
>
<svg
className={`w-4 h-4 text-white transition-transform duration-300 ${gpsSimulatorVisible ? 'rotate-180' : 'rotate-0'}`}
@@ -620,7 +555,7 @@ export default function ParkingFinderPage() {
<button
onClick={() => setGpsSimulatorVisible(!gpsSimulatorVisible)}
className="p-2 rounded-xl bg-white bg-opacity-20 hover:bg-opacity-30 transition-all duration-200 group"
title={gpsSimulatorVisible ? 'Ẩn GPS Simulator' : 'Hiện GPS Simulator'}
title={gpsSimulatorVisible ? 'Hide GPS Simulator' : 'Show GPS Simulator'}
>
<svg
className={`w-5 h-5 text-white transition-transform duration-300 ${gpsSimulatorVisible ? 'rotate-180' : 'rotate-0'}`}
@@ -654,6 +589,9 @@ export default function ParkingFinderPage() {
</div>
</main>
{/* Footer */}
<Footer showFullFooter={false} />
{/* Show errors */}
{parkingError && (
<div className="fixed bottom-6 right-6 max-w-sm z-50">
@@ -662,14 +600,6 @@ export default function ParkingFinderPage() {
</div>
</div>
)}
{routeError && (
<div className="fixed bottom-6 right-6 max-w-sm z-50">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{routeError}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,113 @@
'use client';
import React from 'react';
import Image from 'next/image';
interface FooterProps {
showFullFooter?: boolean;
className?: string;
}
export const Footer: React.FC<FooterProps> = ({
showFullFooter = false,
className = ""
}) => {
if (!showFullFooter) {
return (
<footer className={`bg-black text-white py-6 ${className}`}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="flex items-center justify-center">
<div className="flex items-center space-x-4">
<Image
src="/assets/Footer_page_logo.png"
alt="Laca City Logo"
width={40}
height={40}
className="h-10 w-auto object-contain"
/>
<span className="text-lg font-bold">Laca City</span>
</div>
</div>
</div>
</footer>
);
}
return (
<footer className={`bg-black text-white py-16 ${className}`}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-10">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
<div className="md:col-span-2">
<div className="flex items-center space-x-4 mb-6">
<Image
src="/assets/Footer_page_logo.png"
alt="Laca City Logo"
width={60}
height={60}
className="h-16 w-auto object-contain"
/>
<span className="text-2xl font-bold">Laca City</span>
</div>
<p className="text-xl text-gray-200 leading-relaxed mb-6 max-w-lg">
Revolutionizing urban parking with smart technology, real-time data, and user-friendly solutions for Ho Chi Minh City.
</p>
{/* Social Media Links */}
<div className="flex space-x-4">
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
{/* Facebook Icon */}
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
{/* Twitter Icon */}
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
</svg>
</a>
<a href="#" className="group bg-gray-800 hover:bg-red-500 p-3 rounded-xl transition-all duration-300 transform hover:scale-110">
{/* LinkedIn Icon */}
<svg className="w-6 h-6 text-gray-300 group-hover:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</div>
<div>
<h4 className="text-lg font-semibold mb-6">Quick Links</h4>
<ul className="space-y-3">
<li><a href="/" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Home</a></li>
<li><a href="/#features" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Features</a></li>
<li><a href="/#team" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Team</a></li>
<li><a href="/#news" className="text-gray-300 hover:text-red-500 transition-colors duration-300">News</a></li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-6">App</h4>
<ul className="space-y-3">
<li><a href="/?app=parking" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Launch App</a></li>
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Help</a></li>
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Privacy</a></li>
<li><a href="#" className="text-gray-300 hover:text-red-500 transition-colors duration-300">Terms</a></li>
</ul>
</div>
</div>
{/* Copyright */}
<div className="border-t border-gray-800 pt-8">
<div className="flex flex-col md:flex-row items-center justify-between">
<p className="text-gray-400 text-sm">
© 2024 Laca City. All rights reserved.
</p>
<p className="text-gray-400 text-sm mt-2 md:mt-0">
Made with in Ho Chi Minh City
</p>
</div>
</div>
</div>
</footer>
);
};

View File

@@ -10,31 +10,31 @@ interface HCMCGPSSimulatorProps {
// Predefined locations near HCMC parking lots
const simulationPoints = [
// Trung tâm Quận 1 - gần bãi đỗ xe
// District 1 Center - near parking lots
{
name: 'Vincom Center Đồng Khởi',
location: { lat: 10.7769, lng: 106.7009 },
description: 'Gần trung tâm thương mại Vincom'
description: 'Near Vincom shopping center'
},
{
name: 'Saigon Centre',
location: { lat: 10.7743, lng: 106.7017 },
description: 'Gần Saigon Centre'
description: 'Near Saigon Centre'
},
{
name: 'Landmark 81',
location: { lat: 10.7955, lng: 106.7195 },
description: 'Gần tòa nhà Landmark 81'
description: 'Near Landmark 81 building'
},
{
name: 'Bitexco Financial Tower',
location: { lat: 10.7718, lng: 106.7047 },
description: 'Gần tòa nhà Bitexco'
description: 'Near Bitexco building'
},
{
name: 'Chợ Bến Thành',
location: { lat: 10.7729, lng: 106.6980 },
description: 'Gần chợ Bến Thành'
description: 'Near Ben Thanh Market'
},
{
name: 'Diamond Plaza',
@@ -240,7 +240,7 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 md:gap-3 mb-1">
<span className="text-base md:text-lg font-bold tracking-tight" style={{ color: 'var(--primary-color)' }}>Vị trí hiện tại</span>
<span className="text-base md:text-lg font-bold tracking-tight" style={{ color: 'var(--primary-color)' }}>Current Location</span>
<div className="flex items-center gap-1 md:gap-2 px-2 md:px-3 py-1 rounded-full bg-white border-2" style={{ borderColor: 'var(--success-color)' }}>
<div className="w-1.5 md:w-2 h-1.5 md:h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--success-color)' }}></div>
<span className="text-xs font-bold" style={{ color: 'var(--success-color)' }}>LIVE</span>
@@ -483,8 +483,8 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
</svg>
</div>
<div className="text-left flex-1 min-w-0">
<h5 className="text-base md:text-lg font-bold tracking-tight mb-1" style={{ color: 'var(--accent-color)' }}>Vị trí ngẫu nhiên</h5>
<p className="text-xs md:text-sm text-gray-600 font-medium">Tạo tọa đ tự đng trong TP.HCM</p>
<h5 className="text-base md:text-lg font-bold tracking-tight mb-1" style={{ color: 'var(--accent-color)' }}>Random Location</h5>
<p className="text-xs md:text-sm text-gray-600 font-medium">Auto-generate coordinates in HCMC</p>
<div className="flex items-center gap-2 mt-2">
<div className="flex items-center gap-1 px-2 py-1 rounded-full" style={{ backgroundColor: 'rgba(232, 90, 79, 0.1)' }}>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" style={{ color: 'var(--primary-color)' }}>
@@ -493,7 +493,7 @@ export const HCMCGPSSimulator: React.FC<HCMCGPSSimulatorProps> = ({
<span className="text-xs font-bold" style={{ color: 'var(--primary-color)' }}>RANDOM</span>
</div>
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: 'var(--primary-color)' }}></div>
<span className="text-xs text-gray-500 hidden md:inline">Khu vực mở rộng</span>
<span className="text-xs text-gray-500 hidden md:inline">Extended area</span>
</div>
</div>
<div className="w-5 md:w-6 h-5 md:h-6 rounded-full border-2 flex items-center justify-center group-hover:border-red-500 transition-colors flex-shrink-0" style={{ borderColor: 'var(--primary-color)' }}>

View File

@@ -26,18 +26,12 @@ export const Header: React.FC<HeaderProps> = ({
<div className="flex-shrink-0">
<div className="relative">
<Image
src="/assets/Logo_and_sologan.png"
src="/assets/Location.png"
alt="Smart Parking Logo"
width={320}
height={80}
className="h-18 w-auto object-contain"
/>
{/* Animated accent line */}
<div className="absolute bottom-0 left-0 right-0 h-1 rounded-full" style={{
background: 'linear-gradient(90deg, var(--primary-color), var(--secondary-color))',
transform: 'scaleX(0.8)',
transformOrigin: 'left'
}}></div>
</div>
</div>
)}
@@ -66,7 +60,7 @@ export const Header: React.FC<HeaderProps> = ({
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
</svg>
Xóa tuyến đưng
Clear Route
</button>
)}
@@ -76,7 +70,7 @@ export const Header: React.FC<HeaderProps> = ({
borderColor: 'rgba(34, 197, 94, 0.3)'
}}>
<div className="w-3 h-3 rounded-full animate-pulse shadow-sm" style={{ backgroundColor: 'var(--success-color)' }}></div>
<span className="text-sm font-bold" style={{ color: 'var(--success-color)' }}>Dữ liệu trực tuyến</span>
<span className="text-sm font-bold" style={{ color: 'var(--success-color)' }}>Live Data</span>
</div>
{/* City Info */}
@@ -90,7 +84,7 @@ export const Header: React.FC<HeaderProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<span className="text-sm font-bold" style={{ color: 'var(--primary-color)' }}>TP. Hồ Chí Minh</span>
<span className="text-sm font-bold" style={{ color: 'var(--primary-color)' }}>Ho Chi Minh City</span>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@ interface ParkingFloor {
walkways: { x: number; y: number; width: number; height: number }[];
}
// Thiết kế bãi xe đẹp và chuyên nghiệp
// Professional and beautiful parking lot design
const generateParkingFloorData = (floorNumber: number): ParkingFloor => {
const slots: ParkingSlot[] = [];
const walkways = [];
@@ -108,7 +108,7 @@ const generateParkingFloorData = (floorNumber: number): ParkingFloor => {
return {
floor: floorNumber,
name: `Tầng ${floorNumber}`,
name: `Floor ${floorNumber}`,
slots,
entrances: [
{ x: 60, y: 10, type: 'entrance' },
@@ -194,7 +194,7 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
{/* Real-time indicator */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span>Cập nhật: {lastUpdate.toLocaleTimeString()}</span>
<span>Updated: {lastUpdate.toLocaleTimeString()}</span>
</div>
</div>
@@ -202,15 +202,15 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-center p-2 bg-green-50 rounded border border-green-200">
<div className="font-bold text-green-600">{floorStats.available}</div>
<div className="text-green-700">Trống</div>
<div className="text-green-700">Available</div>
</div>
<div className="text-center p-2 bg-red-50 rounded border border-red-200">
<div className="font-bold text-red-600">{floorStats.occupied}</div>
<div className="text-red-700">Đã đu</div>
<div className="text-red-700">Occupied</div>
</div>
<div className="text-center p-2 bg-gray-50 rounded border border-gray-200">
<div className="font-bold text-gray-600">{floorStats.total}</div>
<div className="text-gray-700">Tổng</div>
<div className="text-gray-700">Total</div>
</div>
</div>
</div>
@@ -221,27 +221,27 @@ const ParkingLotMap: React.FC<{ parkingLot: ParkingLot }> = ({ parkingLot }) =>
const SAMPLE_REVIEWS = [
{
id: 1,
user: 'Nguyễn Văn A',
user: 'John Smith',
rating: 5,
comment: 'Bãi xe rộng rãi, bảo vệ 24/7 rất an toàn. Giá cả hợp lý.',
comment: 'Spacious parking lot with 24/7 security. Very safe and reasonably priced.',
date: '2024-01-15',
avatar: 'N'
avatar: 'J'
},
{
id: 2,
user: 'Trần Thị B',
user: 'Sarah Johnson',
rating: 4,
comment: 'Vị trí thuận tin, dễ tìm. Chỉ hơi xa lối ra một chút.',
comment: 'Convenient location, easy to find. Just a bit far from the exit.',
date: '2024-01-10',
avatar: 'T'
avatar: 'S'
},
{
id: 3,
user: 'Lê Văn C',
user: 'Mike Davis',
rating: 5,
comment: 'Có sạc điện cho xe điện, rất tiện lợi!',
comment: 'Has electric charging stations, very convenient!',
date: '2024-01-08',
avatar: 'L'
avatar: 'M'
}
];
@@ -326,8 +326,8 @@ const formatAmenities = (amenities: string[] | { [key: string]: any }): string[]
const amenityList: string[] = [];
if (amenities.covered) amenityList.push('Có mái che');
if (amenities.security) amenityList.push('Bảo vệ 24/7');
if (amenities.ev_charging) amenityList.push('Sạc xe điện');
if (amenities.security) amenityList.push('24/7 Security');
if (amenities.ev_charging) amenityList.push('EV Charging');
if (amenities.wheelchair_accessible) amenityList.push('Phù hợp xe lăn');
if (amenities.valet_service) amenityList.push('Dịch vụ đỗ xe');
@@ -418,7 +418,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
{renderStars(Math.round(averageRating))}
</div>
<span className="text-sm font-semibold">{averageRating.toFixed(1)}</span>
<span className="text-sm opacity-80">({SAMPLE_REVIEWS.length} đánh giá)</span>
<span className="text-sm opacity-80">({SAMPLE_REVIEWS.length} reviews)</span>
</div>
</div>
</div>
@@ -426,13 +426,13 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
{/* Status banners */}
{isFull && (
<div className="absolute bottom-0 left-0 right-0 bg-red-600 text-center py-2">
<span className="text-sm font-bold">Bãi xe đã hết chỗ</span>
<span className="text-sm font-bold">Parking lot is full</span>
</div>
)}
{isClosed && (
<div className="absolute bottom-0 left-0 right-0 bg-gray-600 text-center py-2">
<span className="text-sm font-bold">Bãi xe đã đóng cửa</span>
<span className="text-sm font-bold">Parking lot is closed</span>
</div>
)}
</div>
@@ -451,7 +451,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<div className="text-2xl font-bold mb-1" style={{ color: statusColors.textColor }}>
{parkingLot.availableSlots}
</div>
<div className="text-xs font-medium text-gray-600">chỗ trống</div>
<div className="text-xs font-medium text-gray-600">available</div>
<div className="text-xs text-gray-500">/ {parkingLot.totalSlots} tổng</div>
</div>
@@ -505,7 +505,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
}}
>
{tab === 'overview' && 'Tổng quan'}
{tab === 'reviews' && 'Đánh giá'}
{tab === 'reviews' && 'Reviews'}
</button>
))}
</div>
@@ -541,7 +541,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
</div>
Tiện ích
Amenities
</h3>
<div className="grid grid-cols-1 gap-2">
{amenityList.map((amenity, index) => (
@@ -570,7 +570,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<div className="flex items-center justify-center gap-1 mb-2">
{renderStars(Math.round(averageRating))}
</div>
<div className="text-sm font-medium text-gray-600">{SAMPLE_REVIEWS.length} đánh giá</div>
<div className="text-sm font-medium text-gray-600">{SAMPLE_REVIEWS.length} reviews</div>
</div>
{/* Reviews list */}
@@ -605,7 +605,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
<button className="w-full py-3 rounded-xl font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105" style={{
background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
}}>
Viết đánh giá
Write Review
</button>
</div>
)}
@@ -722,7 +722,7 @@ export const ParkingDetails: React.FC<ParkingDetailsProps> = ({
: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))'
}}
>
{isFull ? 'Bãi xe đã hết chỗ' : isClosed ? 'Bãi xe đã đóng cửa' : `Đặt chỗ (${bookingDuration}h)`}
{isFull ? 'Parking lot is full' : isClosed ? 'Parking lot is closed' : `Book Spot (${bookingDuration}h)`}
</button>
</div>
)}

View File

@@ -43,21 +43,21 @@ const formatDistance = (distance: number): string => {
const getStatusColor = (availableSlots: number, totalSlots: number) => {
const percentage = availableSlots / totalSlots;
if (availableSlots === 0) {
// Hết chỗ - màu đỏ
// Full - red color
return {
background: 'rgba(239, 68, 68, 0.15)',
borderColor: '#EF4444',
textColor: '#EF4444'
};
} else if (percentage > 0.7) {
// >70% chỗ trống - màu xanh lá cây
// >70% available - green color
return {
background: 'rgba(34, 197, 94, 0.1)',
borderColor: 'var(--success-color)',
textColor: 'var(--success-color)'
};
} else {
// <30% chỗ trống - màu vàng
// <30% available - yellow color
return {
background: 'rgba(251, 191, 36, 0.1)',
borderColor: '#F59E0B',
@@ -68,11 +68,11 @@ const getStatusColor = (availableSlots: number, totalSlots: number) => {
const getStatusText = (availableSlots: number, totalSlots: number) => {
if (availableSlots === 0) {
return 'Hết chỗ';
return 'Full';
} else if (availableSlots / totalSlots > 0.7) {
return `${availableSlots} chỗ trống`;
return `${availableSlots} available`;
} else {
return `${availableSlots} chỗ trống (sắp hết)`;
return `${availableSlots} left (filling up)`;
}
};
@@ -201,9 +201,9 @@ export const ParkingList: React.FC<ParkingListProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">Không tìm thấy kết quả</h3>
<p className="text-gray-600 text-sm">Không bãi đ xe nào phù hợp với từ khóa "{searchQuery}"</p>
<p className="text-gray-500 text-xs mt-2">Thử tìm kiếm với từ khóa khác</p>
<h3 className="text-lg font-bold text-gray-900 mb-2">No Results Found</h3>
<p className="text-gray-600 text-sm">No parking lots match the keyword "{searchQuery}"</p>
<p className="text-gray-500 text-xs mt-2">Try searching with different keywords</p>
</div>
) : (
sortedLots.map((lot, index) => {
@@ -262,13 +262,13 @@ export const ParkingList: React.FC<ParkingListProps> = ({
{/* Warning banners */}
{isFull && (
<div className="absolute -top-2 -left-2 -right-2 bg-red-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
<span className="text-sm font-bold">🚫 BÃI XE ĐÃ HẾT CHỖ</span>
<span className="text-sm font-bold">🚫 PARKING LOT FULL</span>
</div>
)}
{isClosed && (
<div className="absolute -top-2 -left-2 -right-2 bg-gray-500 text-white text-center py-2 rounded-t-xl shadow-lg z-20">
<span className="text-sm font-bold">🔒 BÃI XE ĐÃ ĐÓNG CỬA</span>
<span className="text-sm font-bold">🔒 PARKING LOT CLOSED</span>
</div>
)}
@@ -322,10 +322,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
</div>
</div>
<div className="text-sm text-gray-500 font-medium">
chỗ trống
available
</div>
<div className="text-xs text-gray-400">
/ {lot.totalSlots} chỗ
/ {lot.totalSlots} total
</div>
{/* Availability percentage */}
<div className="mt-1 w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
@@ -338,7 +338,7 @@ export const ParkingList: React.FC<ParkingListProps> = ({
></div>
</div>
<div className="text-xs mt-1" style={{ color: statusColors.textColor }}>
{Math.round((lot.availableSlots / lot.totalSlots) * 100)}% trống
{Math.round((lot.availableSlots / lot.totalSlots) * 100)}% available
</div>
</div>
@@ -350,10 +350,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
{Math.round((lot.pricePerHour || lot.hourlyRate) / 1000)}k
</div>
<div className="text-sm text-gray-500 font-medium">
mỗi giờ
per hour
</div>
<div className="text-xs text-gray-400">
p gửi xe
parking fee
</div>
</>
) : (
@@ -362,10 +362,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
--
</div>
<div className="text-xs text-gray-400 font-medium">
liên hệ
contact
</div>
<div className="text-xs text-gray-400">
đ biết giá
for pricing
</div>
</>
)}
@@ -386,13 +386,13 @@ export const ParkingList: React.FC<ParkingListProps> = ({
</div>
<div className={`text-sm font-medium ${isCurrentlyOpen(lot) ? 'text-green-600' : 'text-red-600'}`}>
{isCurrentlyOpen(lot) ? (
lot.isOpen24Hours ? 'Luôn mở cửa' : `đến ${lot.closeTime}`
lot.isOpen24Hours ? 'Always open' : `until ${lot.closeTime}`
) : (
'Đã đóng cửa'
'Closed'
)}
</div>
<div className="text-xs text-gray-400">
{isCurrentlyOpen(lot) ? 'Đang mở' : '🔒 Đã đóng'}
{isCurrentlyOpen(lot) ? 'Open now' : '🔒 Closed'}
</div>
</>
) : (
@@ -401,10 +401,10 @@ export const ParkingList: React.FC<ParkingListProps> = ({
--:--
</div>
<div className="text-xs text-gray-400 font-medium">
không
unknown
</div>
<div className="text-xs text-gray-400">
giờ mở cửa
opening hours
</div>
</>
)}

View File

@@ -20,7 +20,8 @@ const iconPaths: Record<string, string> = {
delete: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16",
dice: "M5 3a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2H5zm3 4a1 1 0 100 2 1 1 0 000-2zm8 0a1 1 0 100 2 1 1 0 000-2zm-8 8a1 1 0 100 2 1 1 0 000-2zm8 0a1 1 0 100 2 1 1 0 000-2z",
location: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z",
map: "M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7v13zM9 7l6 2-6 3zm6-3l4.553 2.276A1 1 0 0121 7.618v10.764a1 1 0 01-.553.894L15 17V4z",
// map icon removed
marker: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z",
market: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2 2z M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2",
refresh: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15",
rocket: "M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z",

View File

@@ -1,8 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { parkingService, routingService, healthService } from '@/services/api';
import { parkingService, healthService } from '@/services/api';
import {
FindNearbyParkingRequest,
RouteRequest,
UpdateAvailabilityRequest
} from '@/types';
@@ -14,10 +13,6 @@ export const QUERY_KEYS = {
byId: (id: number) => ['parking', id],
popular: (limit?: number) => ['parking', 'popular', limit],
},
routing: {
route: (params: RouteRequest) => ['routing', 'route', params],
status: ['routing', 'status'],
},
health: ['health'],
} as const;
@@ -83,26 +78,6 @@ export function useUpdateParkingAvailability() {
});
}
// Routing hooks
export function useRoute(request: RouteRequest, enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.routing.route(request),
queryFn: () => routingService.calculateRoute(request),
enabled: enabled && !!request.originLat && !!request.originLng && !!request.destinationLat && !!request.destinationLng,
staleTime: 15 * 60 * 1000, // 15 minutes
refetchOnWindowFocus: false,
});
}
export function useRoutingStatus() {
return useQuery({
queryKey: QUERY_KEYS.routing.status,
queryFn: routingService.getStatus,
staleTime: 30 * 1000, // 30 seconds
refetchInterval: 60 * 1000, // Refresh every minute
});
}
// Health hooks
export function useHealth() {
return useQuery({

View File

@@ -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);
}

View File

@@ -3,9 +3,7 @@ import {
FindNearbyParkingRequest,
FindNearbyParkingResponse,
ParkingLot,
UpdateAvailabilityRequest,
RouteRequest,
RouteResponse
UpdateAvailabilityRequest
} from '@/types';
class APIClient {
@@ -77,17 +75,6 @@ class APIClient {
return response.data;
}
// Routing endpoints
async calculateRoute(request: RouteRequest): Promise<RouteResponse> {
const response = await this.client.post('/routing/calculate', request);
return response.data;
}
async getRoutingServiceStatus(): Promise<{ status: string; version?: string }> {
const response = await this.client.get('/routing/status');
return response.data;
}
// Health endpoint
async getHealth(): Promise<{ status: string; timestamp: string }> {
const response = await this.client.get('/health');
@@ -108,11 +95,6 @@ export const parkingService = {
getPopular: (limit?: number) => apiClient.getPopularParkingLots(limit),
};
export const routingService = {
calculateRoute: (request: RouteRequest) => apiClient.calculateRoute(request),
getStatus: () => apiClient.getRoutingServiceStatus(),
};
export const healthService = {
getHealth: () => apiClient.getHealth(),
};

View File

@@ -54,37 +54,6 @@ export interface ParkingLot {
isOpen?: boolean;
}
export interface RoutePoint {
lat: number;
lng: number;
}
export interface RouteStep {
instruction: string;
distance: number; // meters
time: number; // seconds
type: string;
geometry: RoutePoint[];
}
export interface Route {
summary: {
distance: number; // km
time: number; // minutes
cost?: number; // estimated cost
};
geometry: RoutePoint[];
steps: RouteStep[];
confidence: number;
}
export interface RouteResponse {
routes: Route[];
origin: RoutePoint;
destination: RoutePoint;
requestId: string;
}
// API Request/Response Types
export interface FindNearbyParkingRequest {
lat: number;
@@ -102,17 +71,6 @@ export interface FindNearbyParkingResponse {
searchRadius: number;
}
export interface RouteRequest {
originLat: number;
originLng: number;
destinationLat: number;
destinationLng: number;
costing?: TransportationMode;
alternatives?: number;
avoidHighways?: boolean;
avoidTolls?: boolean;
}
export interface UpdateAvailabilityRequest {
availableSlots: number;
source?: string;
@@ -174,16 +132,8 @@ export interface ParkingState {
error: string | null;
}
export interface RouteState {
currentRoute: Route | null;
isCalculating: boolean;
error: string | null;
history: Route[];
}
export interface AppState {
parking: ParkingState;
routing: RouteState;
userPreferences: UserPreferences;
ui: {
sidebarOpen: boolean;
@@ -214,11 +164,6 @@ export interface ParkingLotSelectEvent {
source: 'map' | 'list' | 'search';
}
export interface RouteCalculatedEvent {
route: Route;
duration: number; // calculation time in ms
}
export interface LocationUpdateEvent {
location: UserLocation;
accuracy: number;
@@ -269,15 +214,6 @@ export interface SearchAnalytics {
timeToSelection?: number;
}
export interface RouteAnalytics {
origin: RoutePoint;
destination: RoutePoint;
mode: TransportationMode;
distance: number;
duration: number;
completed: boolean;
}
// Configuration Types
export interface AppConfig {
api: {
@@ -320,19 +256,9 @@ export interface UseParkingSearchReturn {
loadMore: () => void;
}
export interface UseRoutingReturn {
route: Route | null;
isLoading: boolean;
error: APIError | null;
calculateRoute: (request: RouteRequest) => Promise<void>;
clearRoute: () => void;
alternatives: Route[];
}
// Component Props Types
export interface HeaderProps {
onRefresh?: () => void;
onClearRoute?: () => void;
isLoading?: boolean;
}
@@ -340,7 +266,6 @@ export interface MapViewProps {
userLocation: UserLocation | null;
parkingLots: ParkingLot[];
selectedParkingLot: ParkingLot | null;
route: Route | null;
onParkingLotSelect: (lot: ParkingLot) => void;
isLoading?: boolean;
}

View File

@@ -1,194 +0,0 @@
import L from 'leaflet';
// Fix for default markers in React Leaflet
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
});
export interface MapBounds {
north: number;
south: number;
east: number;
west: number;
}
export interface MapUtils {
createIcon: (type: 'user' | 'parking' | 'selected') => L.Icon;
createBounds: (coordinates: Array<{ lat: number; lng: number }>) => L.LatLngBounds;
formatDistance: (distanceKm: number) => string;
formatDuration: (durationSeconds: number) => string;
getBoundsFromCoordinates: (coords: Array<[number, number]>) => MapBounds;
}
// Custom icons for different marker types
export const mapIcons = {
user: new L.Icon({
iconUrl: '/icons/location.svg',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
className: 'user-location-icon',
}),
parking: new L.Icon({
iconUrl: '/icons/car.svg',
iconSize: [28, 28],
iconAnchor: [14, 28],
popupAnchor: [0, -28],
className: 'parking-icon',
}),
selected: new L.Icon({
iconUrl: '/icons/target.svg',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
className: 'selected-parking-icon',
}),
unavailable: new L.Icon({
iconUrl: '/icons/warning.svg',
iconSize: [28, 28],
iconAnchor: [14, 28],
popupAnchor: [0, -28],
className: 'unavailable-parking-icon',
}),
};
// Map configuration constants
export const MAP_CONFIG = {
defaultCenter: { lat: 1.3521, lng: 103.8198 }, // Singapore
defaultZoom: 12,
maxZoom: 18,
minZoom: 10,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
tileLayerUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
searchRadius: 5000, // 5km in meters
};
// Utility functions
export const mapUtils: MapUtils = {
createIcon: (type: 'user' | 'parking' | 'selected') => {
return mapIcons[type];
},
createBounds: (coordinates: Array<{ lat: number; lng: number }>) => {
if (coordinates.length === 0) {
return new L.LatLngBounds(
[MAP_CONFIG.defaultCenter.lat, MAP_CONFIG.defaultCenter.lng],
[MAP_CONFIG.defaultCenter.lat, MAP_CONFIG.defaultCenter.lng]
);
}
const latLngs = coordinates.map(coord => new L.LatLng(coord.lat, coord.lng));
return new L.LatLngBounds(latLngs);
},
formatDistance: (distanceKm: number): string => {
if (distanceKm < 1) {
return `${Math.round(distanceKm * 1000)}m`;
}
return `${distanceKm.toFixed(1)}km`;
},
formatDuration: (durationSeconds: number): string => {
const minutes = Math.round(durationSeconds / 60);
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
},
getBoundsFromCoordinates: (coords: Array<[number, number]>): MapBounds => {
if (coords.length === 0) {
return {
north: MAP_CONFIG.defaultCenter.lat + 0.01,
south: MAP_CONFIG.defaultCenter.lat - 0.01,
east: MAP_CONFIG.defaultCenter.lng + 0.01,
west: MAP_CONFIG.defaultCenter.lng - 0.01,
};
}
const lats = coords.map(coord => coord[0]);
const lngs = coords.map(coord => coord[1]);
return {
north: Math.max(...lats),
south: Math.min(...lats),
east: Math.max(...lngs),
west: Math.min(...lngs),
};
},
};
// Route styling
export const routeStyle = {
color: '#2563eb', // Blue
weight: 4,
opacity: 0.8,
dashArray: '0',
lineJoin: 'round' as const,
lineCap: 'round' as const,
};
export const alternativeRouteStyle = {
color: '#6b7280', // Gray
weight: 3,
opacity: 0.6,
dashArray: '5, 10',
lineJoin: 'round' as const,
lineCap: 'round' as const,
};
// Parking lot status colors
export const parkingStatusColors = {
available: '#10b981', // Green
limited: '#f59e0b', // Amber
full: '#ef4444', // Red
unknown: '#6b7280', // Gray
};
// Helper function to get parking lot color based on availability
export const getParkingStatusColor = (
availableSpaces: number,
totalSpaces: number
): string => {
if (totalSpaces === 0) return parkingStatusColors.unknown;
const occupancyRate = 1 - (availableSpaces / totalSpaces);
if (occupancyRate < 0.7) return parkingStatusColors.available;
if (occupancyRate < 0.9) return parkingStatusColors.limited;
return parkingStatusColors.full;
};
// Animation utilities
export const animateMarker = (marker: L.Marker, newPosition: L.LatLng, duration = 1000) => {
const startPosition = marker.getLatLng();
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentLat = startPosition.lat + (newPosition.lat - startPosition.lat) * progress;
const currentLng = startPosition.lng + (newPosition.lng - startPosition.lng) * progress;
marker.setLatLng([currentLat, currentLng]);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
animate();
};
// Bounds padding for better map view
export const boundsOptions = {
padding: [20, 20] as [number, number],
maxZoom: 16,
};

View File

View File

@@ -4,7 +4,7 @@ This directory contains all the deployment and development scripts for Smart Par
## 📋 Available Scripts
### 🚀 Main Scripts
### 🚀 Development Scripts
| Script | Purpose | Usage |
|--------|---------|--------|
@@ -14,6 +14,22 @@ This directory contains all the deployment and development scripts for Smart Par
| **docker-dev.sh** | 🐳 Docker development with all services | `./scripts/docker-dev.sh` |
| **setup.sh** | 🛠️ Initial project setup | `./scripts/setup.sh` |
### 🚀 Production Deployment Scripts
| Script | Purpose | Usage |
|--------|---------|--------|
| **deploy-production.sh** | 🌐 Full production deployment to VPS | `./scripts/deploy-production.sh` |
| **deploy-update.sh** | 🔄 Quick updates (frontend/backend/static) | `./scripts/deploy-update.sh` |
| **deploy-docker.sh** | 🐳 Docker-based production deployment | `./scripts/deploy-docker.sh` |
| **setup-vps.sh** | 🛠️ Initial VPS server setup | Run on VPS as root |
### 📖 Documentation
| File | Purpose |
|------|---------|
| **DEPLOYMENT_GUIDE.md** | 📘 Complete deployment guide |
| **.env.example** | 🔧 Environment configuration template |
### 🎯 Quick Access from Root
From the project root directory, you can use:

0
scripts/docker-dev.sh Normal file → Executable file
View File

0
scripts/frontend-only.sh Normal file → Executable file
View File

2
scripts/full-dev.sh Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#!/bin/bash
``#!/bin/bash
# 🔄 Full Development Environment (Frontend + Backend)
echo "🔄 Starting Full Development Environment..."

0
scripts/setup.sh Normal file → Executable file
View File

0
scripts/start.sh Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More