✨ MAJOR FEATURES: • Auto-zoom intelligence với smart bounds fitting • Enhanced 3D GPS markers với pulsing effects • Professional route display với 6-layer rendering • Status-based parking icons với availability indicators • Production-ready build optimizations 🗺️ AUTO-ZOOM FEATURES: • Smart bounds fitting cho GPS + selected parking • Adaptive padding (50px) cho visual balance • Max zoom control (level 16) để tránh quá gần • Dynamic centering khi không có selection 🎨 ENHANCED VISUALS: • 3D GPS marker với multi-layer pulse effects • Advanced parking icons với status colors • Selection highlighting với animation • Dimming system cho non-selected items 🛣️ ROUTE SYSTEM: • OpenRouteService API integration • Multi-layer route rendering (glow, shadow, main, animated) • Real-time distance & duration calculation • Visual route info trong popup 📱 PRODUCTION READY: • SSR safe với dynamic imports • Build errors resolved • Global deployment via Vercel • Optimized performance 🌍 DEPLOYMENT: • Vercel: https://whatever-ctk2auuxr-phong12hexdockworks-projects.vercel.app • Bundle size: 22.8 kB optimized • Global CDN distribution • HTTPS enabled 💾 VERSION CONTROL: • MapView-v2.0.tsx backup created • MAPVIEW_VERSIONS.md documentation • Full version history tracking
18 KiB
18 KiB
🛠️ Development Guide
This guide covers the development workflow, coding standards, and best practices for the Smart Parking Finder application.
📋 Table of Contents
- Development Setup
- Project Structure
- Development Workflow
- Coding Standards
- Testing Strategy
- Debugging
- Performance Guidelines
- Contributing
🚀 Development Setup
Prerequisites
- Node.js 18+ and npm
- Docker and Docker Compose
- Git
- VS Code (recommended)
Initial Setup
# 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
# .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
# 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
{
"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
// 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
// .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
// ✅ 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
// ✅ 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
// 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
// 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
// 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
// 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
// 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
// .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
// 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
// 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
-
Fork and Clone
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 -
Create Feature Branch
git checkout -b feature/your-feature-name -
Development
- Follow coding standards
- Write tests for new features
- Update documentation
-
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
## 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 and Technical Specification.