✨ 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
751 lines
18 KiB
Markdown
751 lines
18 KiB
Markdown
# 🛠️ 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).
|