mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 17:31:09 -05:00
1537 lines
37 KiB
Markdown
1537 lines
37 KiB
Markdown
# ThrillWiki Frontend Connection Guide
|
|
|
|
A comprehensive guide for connecting any frontend framework to the ThrillWiki Django REST API backend.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [API Architecture](#api-architecture)
|
|
3. [Authentication System](#authentication-system)
|
|
4. [Core API Endpoints](#core-api-endpoints)
|
|
5. [Frontend Framework Examples](#frontend-framework-examples)
|
|
6. [Dark/Light Mode Requirements](#darklight-mode-requirements)
|
|
7. [Error Handling](#error-handling)
|
|
8. [Best Practices](#best-practices)
|
|
9. [Troubleshooting](#troubleshooting)
|
|
|
|
## Overview
|
|
|
|
ThrillWiki uses a modern Django REST Framework backend with a centralized API architecture. All API endpoints are organized under `/api/v1/` with domain-specific modules for parks, rides, authentication, and more.
|
|
|
|
### Key Features
|
|
- **Centralized API Structure** - All endpoints under `/api/v1/`
|
|
- **Token-based Authentication** - Using Django REST Framework tokens
|
|
- **Comprehensive Serialization** - Well-structured JSON responses
|
|
- **CORS Support** - Configured for frontend integration
|
|
- **OpenAPI Documentation** - Auto-generated API docs at `/api/docs/`
|
|
|
|
### Base Configuration
|
|
```javascript
|
|
const API_BASE_URL = 'http://localhost:8000/api/v1'
|
|
```
|
|
|
|
## API Architecture
|
|
|
|
### Centralized Structure
|
|
All API endpoints follow this pattern:
|
|
```
|
|
/api/v1/{domain}/{endpoint}/
|
|
```
|
|
|
|
### Domain Organization
|
|
- **Authentication**: `/api/v1/auth/`
|
|
- **Parks**: `/api/v1/parks/`
|
|
- **Rides**: `/api/v1/rides/`
|
|
- **Accounts**: `/api/v1/accounts/`
|
|
- **Health**: `/api/v1/health/`
|
|
|
|
### URL Routing
|
|
The main API router is located at `backend/apps/api/v1/urls.py`:
|
|
|
|
```python
|
|
# Main API v1 URLs
|
|
urlpatterns = [
|
|
path("auth/", include("apps.api.v1.auth.urls")),
|
|
path("health/", include("apps.api.v1.health.urls")),
|
|
path("trending/", include("apps.api.v1.trending.urls")),
|
|
path("parks/", include("apps.api.v1.parks.urls")),
|
|
path("rides/", include("apps.api.v1.rides.urls")),
|
|
path("accounts/", include("apps.api.v1.accounts.urls")),
|
|
]
|
|
```
|
|
|
|
## Authentication System
|
|
|
|
### Token-Based Authentication
|
|
|
|
ThrillWiki uses Django REST Framework's token authentication system.
|
|
|
|
#### Authentication Flow
|
|
|
|
1. **Login/Signup** - Obtain authentication token
|
|
2. **Store Token** - Save token securely (localStorage, sessionStorage, or cookies)
|
|
3. **Include Token** - Add token to all authenticated requests
|
|
4. **Handle Expiration** - Refresh or re-authenticate when token expires
|
|
|
|
#### Authentication Endpoints
|
|
|
|
| Endpoint | Method | Purpose | Authentication Required |
|
|
|----------|--------|---------|------------------------|
|
|
| `/api/v1/auth/login/` | POST | User login | No |
|
|
| `/api/v1/auth/signup/` | POST | User registration | No |
|
|
| `/api/v1/auth/logout/` | POST | User logout | Yes |
|
|
| `/api/v1/auth/current-user/` | GET | Get current user info | Yes |
|
|
| `/api/v1/auth/password-reset/` | POST | Request password reset | No |
|
|
| `/api/v1/auth/password-change/` | POST | Change password | Yes |
|
|
| `/api/v1/auth/status/` | POST | Check auth status | No |
|
|
| `/api/v1/auth/social-providers/` | GET | Get social auth providers | No |
|
|
|
|
#### Request/Response Examples
|
|
|
|
**Login Request:**
|
|
```json
|
|
{
|
|
"username": "thrillseeker",
|
|
"password": "securepassword123"
|
|
}
|
|
```
|
|
|
|
**Login Response:**
|
|
```json
|
|
{
|
|
"token": "abc123def456ghi789",
|
|
"user": {
|
|
"id": 1,
|
|
"username": "thrillseeker",
|
|
"email": "user@example.com",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"is_active": true,
|
|
"date_joined": "2024-01-01T00:00:00Z"
|
|
},
|
|
"message": "Login successful"
|
|
}
|
|
```
|
|
|
|
**Signup Request:**
|
|
```json
|
|
{
|
|
"username": "newuser",
|
|
"email": "newuser@example.com",
|
|
"password": "securepassword123",
|
|
"password_confirm": "securepassword123",
|
|
"first_name": "Jane",
|
|
"last_name": "Smith"
|
|
}
|
|
```
|
|
|
|
## Core API Endpoints
|
|
|
|
### Parks API (`/api/v1/parks/`)
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/api/v1/parks/` | GET | List all parks |
|
|
| `/api/v1/parks/` | POST | Create new park |
|
|
| `/api/v1/parks/{id}/` | GET | Get park details |
|
|
| `/api/v1/parks/{id}/` | PUT/PATCH | Update park |
|
|
| `/api/v1/parks/{id}/` | DELETE | Delete park |
|
|
| `/api/v1/parks/search/` | GET | Search parks |
|
|
| `/api/v1/parks/filter/` | GET | Filter parks |
|
|
| `/api/v1/parks/{id}/photos/` | GET/POST | Park photos |
|
|
|
|
### Rides API (`/api/v1/rides/`)
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/api/v1/rides/` | GET | List all rides |
|
|
| `/api/v1/rides/` | POST | Create new ride |
|
|
| `/api/v1/rides/{id}/` | GET | Get ride details |
|
|
| `/api/v1/rides/{id}/` | PUT/PATCH | Update ride |
|
|
| `/api/v1/rides/{id}/` | DELETE | Delete ride |
|
|
| `/api/v1/rides/search/` | GET | Search rides |
|
|
| `/api/v1/rides/filter/` | GET | Filter rides |
|
|
| `/api/v1/rides/{id}/photos/` | GET/POST | Ride photos |
|
|
|
|
### Health Check (`/api/v1/health/`)
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/api/v1/health/` | GET | Basic health check |
|
|
| `/api/v1/health/detailed/` | GET | Detailed system status |
|
|
|
|
## Frontend Framework Examples
|
|
|
|
### Vanilla JavaScript
|
|
|
|
```javascript
|
|
class ThrillWikiAPI {
|
|
constructor(baseURL = 'http://localhost:8000/api/v1') {
|
|
this.baseURL = baseURL;
|
|
this.token = localStorage.getItem('thrillwiki_token');
|
|
}
|
|
|
|
// Set authentication token
|
|
setToken(token) {
|
|
this.token = token;
|
|
localStorage.setItem('thrillwiki_token', token);
|
|
}
|
|
|
|
// Remove authentication token
|
|
clearToken() {
|
|
this.token = null;
|
|
localStorage.removeItem('thrillwiki_token');
|
|
}
|
|
|
|
// Make authenticated request
|
|
async request(endpoint, options = {}) {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
};
|
|
|
|
if (this.token) {
|
|
headers['Authorization'] = `Token ${this.token}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// Authentication methods
|
|
async login(username, password) {
|
|
const response = await this.request('/auth/login/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
if (response.token) {
|
|
this.setToken(response.token);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async signup(userData) {
|
|
const response = await this.request('/auth/signup/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(userData)
|
|
});
|
|
|
|
if (response.token) {
|
|
this.setToken(response.token);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async logout() {
|
|
try {
|
|
await this.request('/auth/logout/', { method: 'POST' });
|
|
} finally {
|
|
this.clearToken();
|
|
}
|
|
}
|
|
|
|
async getCurrentUser() {
|
|
return this.request('/auth/current-user/');
|
|
}
|
|
|
|
// Parks methods
|
|
async getParks(params = {}) {
|
|
const queryString = new URLSearchParams(params).toString();
|
|
return this.request(`/parks/${queryString ? '?' + queryString : ''}`);
|
|
}
|
|
|
|
async getPark(id) {
|
|
return this.request(`/parks/${id}/`);
|
|
}
|
|
|
|
async createPark(parkData) {
|
|
return this.request('/parks/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(parkData)
|
|
});
|
|
}
|
|
|
|
// Rides methods
|
|
async getRides(params = {}) {
|
|
const queryString = new URLSearchParams(params).toString();
|
|
return this.request(`/rides/${queryString ? '?' + queryString : ''}`);
|
|
}
|
|
|
|
async getRide(id) {
|
|
return this.request(`/rides/${id}/`);
|
|
}
|
|
|
|
async createRide(rideData) {
|
|
return this.request('/rides/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(rideData)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Usage example
|
|
const api = new ThrillWikiAPI();
|
|
|
|
// Login
|
|
api.login('username', 'password')
|
|
.then(response => {
|
|
console.log('Logged in:', response.user);
|
|
return api.getParks();
|
|
})
|
|
.then(parks => {
|
|
console.log('Parks:', parks);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
});
|
|
```
|
|
|
|
### React with Axios
|
|
|
|
```jsx
|
|
import axios from 'axios';
|
|
import { createContext, useContext, useState, useEffect } from 'react';
|
|
|
|
// API Configuration
|
|
const API_BASE_URL = 'http://localhost:8000/api/v1';
|
|
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Request interceptor to add auth token
|
|
api.interceptors.request.use((config) => {
|
|
const token = localStorage.getItem('thrillwiki_token');
|
|
if (token) {
|
|
config.headers.Authorization = `Token ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// Response interceptor for error handling
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
if (error.response?.status === 401) {
|
|
localStorage.removeItem('thrillwiki_token');
|
|
window.location.href = '/login';
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Auth Context
|
|
const AuthContext = createContext();
|
|
|
|
export const AuthProvider = ({ children }) => {
|
|
const [user, setUser] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('thrillwiki_token');
|
|
if (token) {
|
|
getCurrentUser();
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const getCurrentUser = async () => {
|
|
try {
|
|
const response = await api.get('/auth/current-user/');
|
|
setUser(response.data);
|
|
} catch (error) {
|
|
localStorage.removeItem('thrillwiki_token');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const login = async (username, password) => {
|
|
const response = await api.post('/auth/login/', { username, password });
|
|
localStorage.setItem('thrillwiki_token', response.data.token);
|
|
setUser(response.data.user);
|
|
return response.data;
|
|
};
|
|
|
|
const signup = async (userData) => {
|
|
const response = await api.post('/auth/signup/', userData);
|
|
localStorage.setItem('thrillwiki_token', response.data.token);
|
|
setUser(response.data.user);
|
|
return response.data;
|
|
};
|
|
|
|
const logout = async () => {
|
|
try {
|
|
await api.post('/auth/logout/');
|
|
} finally {
|
|
localStorage.removeItem('thrillwiki_token');
|
|
setUser(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, login, signup, logout, loading }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useAuth = () => useContext(AuthContext);
|
|
|
|
// API Service
|
|
export const apiService = {
|
|
// Parks
|
|
getParks: (params) => api.get('/parks/', { params }),
|
|
getPark: (id) => api.get(`/parks/${id}/`),
|
|
createPark: (data) => api.post('/parks/', data),
|
|
updatePark: (id, data) => api.patch(`/parks/${id}/`, data),
|
|
deletePark: (id) => api.delete(`/parks/${id}/`),
|
|
|
|
// Rides
|
|
getRides: (params) => api.get('/rides/', { params }),
|
|
getRide: (id) => api.get(`/rides/${id}/`),
|
|
createRide: (data) => api.post('/rides/', data),
|
|
updateRide: (id, data) => api.patch(`/rides/${id}/`, data),
|
|
deleteRide: (id) => api.delete(`/rides/${id}/`),
|
|
};
|
|
|
|
// Component Example
|
|
const ParksList = () => {
|
|
const [parks, setParks] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const { user } = useAuth();
|
|
|
|
useEffect(() => {
|
|
const fetchParks = async () => {
|
|
try {
|
|
const response = await apiService.getParks();
|
|
setParks(response.data.results || response.data);
|
|
} catch (error) {
|
|
console.error('Error fetching parks:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchParks();
|
|
}, []);
|
|
|
|
if (loading) return <div>Loading parks...</div>;
|
|
|
|
return (
|
|
<div>
|
|
<h2>Theme Parks</h2>
|
|
{parks.map(park => (
|
|
<div key={park.id} className="park-card">
|
|
<h3>{park.name}</h3>
|
|
<p>{park.description}</p>
|
|
<p>Location: {park.location}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### Vue.js 3 with Composition API
|
|
|
|
```vue
|
|
<template>
|
|
<div>
|
|
<h2>Theme Parks</h2>
|
|
<div v-if="loading">Loading parks...</div>
|
|
<div v-else>
|
|
<div v-for="park in parks" :key="park.id" class="park-card">
|
|
<h3>{{ park.name }}</h3>
|
|
<p>{{ park.description }}</p>
|
|
<p>Location: {{ park.location }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useApi } from '@/composables/useApi'
|
|
|
|
const { apiService } = useApi()
|
|
const parks = ref([])
|
|
const loading = ref(true)
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const response = await apiService.getParks()
|
|
parks.value = response.data.results || response.data
|
|
} catch (error) {
|
|
console.error('Error fetching parks:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
</script>
|
|
```
|
|
|
|
**Vue Composable (`composables/useApi.js`):**
|
|
|
|
```javascript
|
|
import axios from 'axios'
|
|
import { ref, computed } from 'vue'
|
|
|
|
const API_BASE_URL = 'http://localhost:8000/api/v1'
|
|
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
|
|
// Global state
|
|
const user = ref(null)
|
|
const token = ref(localStorage.getItem('thrillwiki_token'))
|
|
|
|
// Request interceptor
|
|
api.interceptors.request.use((config) => {
|
|
if (token.value) {
|
|
config.headers.Authorization = `Token ${token.value}`
|
|
}
|
|
return config
|
|
})
|
|
|
|
// Response interceptor
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
if (error.response?.status === 401) {
|
|
token.value = null
|
|
user.value = null
|
|
localStorage.removeItem('thrillwiki_token')
|
|
}
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
export function useApi() {
|
|
const isAuthenticated = computed(() => !!token.value)
|
|
|
|
const setToken = (newToken) => {
|
|
token.value = newToken
|
|
localStorage.setItem('thrillwiki_token', newToken)
|
|
}
|
|
|
|
const clearToken = () => {
|
|
token.value = null
|
|
user.value = null
|
|
localStorage.removeItem('thrillwiki_token')
|
|
}
|
|
|
|
const login = async (username, password) => {
|
|
const response = await api.post('/auth/login/', { username, password })
|
|
setToken(response.data.token)
|
|
user.value = response.data.user
|
|
return response.data
|
|
}
|
|
|
|
const signup = async (userData) => {
|
|
const response = await api.post('/auth/signup/', userData)
|
|
setToken(response.data.token)
|
|
user.value = response.data.user
|
|
return response.data
|
|
}
|
|
|
|
const logout = async () => {
|
|
try {
|
|
await api.post('/auth/logout/')
|
|
} finally {
|
|
clearToken()
|
|
}
|
|
}
|
|
|
|
const getCurrentUser = async () => {
|
|
const response = await api.get('/auth/current-user/')
|
|
user.value = response.data
|
|
return response.data
|
|
}
|
|
|
|
const apiService = {
|
|
// Parks
|
|
getParks: (params) => api.get('/parks/', { params }),
|
|
getPark: (id) => api.get(`/parks/${id}/`),
|
|
createPark: (data) => api.post('/parks/', data),
|
|
updatePark: (id, data) => api.patch(`/parks/${id}/`, data),
|
|
deletePark: (id) => api.delete(`/parks/${id}/`),
|
|
|
|
// Rides
|
|
getRides: (params) => api.get('/rides/', { params }),
|
|
getRide: (id) => api.get(`/rides/${id}/`),
|
|
createRide: (data) => api.post('/rides/', data),
|
|
updateRide: (id, data) => api.patch(`/rides/${id}/`, data),
|
|
deleteRide: (id) => api.delete(`/rides/${id}/`),
|
|
}
|
|
|
|
return {
|
|
user: readonly(user),
|
|
isAuthenticated,
|
|
login,
|
|
signup,
|
|
logout,
|
|
getCurrentUser,
|
|
apiService
|
|
}
|
|
}
|
|
```
|
|
|
|
### Angular with HttpClient
|
|
|
|
```typescript
|
|
// api.service.ts
|
|
import { Injectable } from '@angular/core';
|
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
import { Observable, BehaviorSubject } from 'rxjs';
|
|
import { tap } from 'rxjs/operators';
|
|
|
|
export interface User {
|
|
id: number;
|
|
username: string;
|
|
email: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
is_active: boolean;
|
|
date_joined: string;
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
token: string;
|
|
user: User;
|
|
message: string;
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class ApiService {
|
|
private baseURL = 'http://localhost:8000/api/v1';
|
|
private tokenSubject = new BehaviorSubject<string | null>(
|
|
localStorage.getItem('thrillwiki_token')
|
|
);
|
|
private userSubject = new BehaviorSubject<User | null>(null);
|
|
|
|
public token$ = this.tokenSubject.asObservable();
|
|
public user$ = this.userSubject.asObservable();
|
|
|
|
constructor(private http: HttpClient) {
|
|
// Initialize user if token exists
|
|
if (this.tokenSubject.value) {
|
|
this.getCurrentUser().subscribe();
|
|
}
|
|
}
|
|
|
|
private getHeaders(): HttpHeaders {
|
|
const token = this.tokenSubject.value;
|
|
let headers = new HttpHeaders({
|
|
'Content-Type': 'application/json'
|
|
});
|
|
|
|
if (token) {
|
|
headers = headers.set('Authorization', `Token ${token}`);
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
// Authentication methods
|
|
login(username: string, password: string): Observable<LoginResponse> {
|
|
return this.http.post<LoginResponse>(`${this.baseURL}/auth/login/`, {
|
|
username,
|
|
password
|
|
}).pipe(
|
|
tap(response => {
|
|
localStorage.setItem('thrillwiki_token', response.token);
|
|
this.tokenSubject.next(response.token);
|
|
this.userSubject.next(response.user);
|
|
})
|
|
);
|
|
}
|
|
|
|
signup(userData: any): Observable<LoginResponse> {
|
|
return this.http.post<LoginResponse>(`${this.baseURL}/auth/signup/`, userData)
|
|
.pipe(
|
|
tap(response => {
|
|
localStorage.setItem('thrillwiki_token', response.token);
|
|
this.tokenSubject.next(response.token);
|
|
this.userSubject.next(response.user);
|
|
})
|
|
);
|
|
}
|
|
|
|
logout(): Observable<any> {
|
|
return this.http.post(`${this.baseURL}/auth/logout/`, {}, {
|
|
headers: this.getHeaders()
|
|
}).pipe(
|
|
tap(() => {
|
|
localStorage.removeItem('thrillwiki_token');
|
|
this.tokenSubject.next(null);
|
|
this.userSubject.next(null);
|
|
})
|
|
);
|
|
}
|
|
|
|
getCurrentUser(): Observable<User> {
|
|
return this.http.get<User>(`${this.baseURL}/auth/current-user/`, {
|
|
headers: this.getHeaders()
|
|
}).pipe(
|
|
tap(user => this.userSubject.next(user))
|
|
);
|
|
}
|
|
|
|
// Parks methods
|
|
getParks(params?: any): Observable<any> {
|
|
let httpParams = new HttpParams();
|
|
if (params) {
|
|
Object.keys(params).forEach(key => {
|
|
httpParams = httpParams.set(key, params[key]);
|
|
});
|
|
}
|
|
|
|
return this.http.get(`${this.baseURL}/parks/`, {
|
|
headers: this.getHeaders(),
|
|
params: httpParams
|
|
});
|
|
}
|
|
|
|
getPark(id: number): Observable<any> {
|
|
return this.http.get(`${this.baseURL}/parks/${id}/`, {
|
|
headers: this.getHeaders()
|
|
});
|
|
}
|
|
|
|
createPark(parkData: any): Observable<any> {
|
|
return this.http.post(`${this.baseURL}/parks/`, parkData, {
|
|
headers: this.getHeaders()
|
|
});
|
|
}
|
|
|
|
// Rides methods
|
|
getRides(params?: any): Observable<any> {
|
|
let httpParams = new HttpParams();
|
|
if (params) {
|
|
Object.keys(params).forEach(key => {
|
|
httpParams = httpParams.set(key, params[key]);
|
|
});
|
|
}
|
|
|
|
return this.http.get(`${this.baseURL}/rides/`, {
|
|
headers: this.getHeaders(),
|
|
params: httpParams
|
|
});
|
|
}
|
|
|
|
getRide(id: number): Observable<any> {
|
|
return this.http.get(`${this.baseURL}/rides/${id}/`, {
|
|
headers: this.getHeaders()
|
|
});
|
|
}
|
|
|
|
createRide(rideData: any): Observable<any> {
|
|
return this.http.post(`${this.baseURL}/rides/`, rideData, {
|
|
headers: this.getHeaders()
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Component Example:**
|
|
|
|
```typescript
|
|
// parks-list.component.ts
|
|
import { Component, OnInit } from '@angular/core';
|
|
import { ApiService } from '../services/api.service';
|
|
|
|
@Component({
|
|
selector: 'app-parks-list',
|
|
template: `
|
|
<div>
|
|
<h2>Theme Parks</h2>
|
|
<div *ngIf="loading">Loading parks...</div>
|
|
<div *ngIf="!loading">
|
|
<div *ngFor="let park of parks" class="park-card">
|
|
<h3>{{ park.name }}</h3>
|
|
<p>{{ park.description }}</p>
|
|
<p>Location: {{ park.location }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
})
|
|
export class ParksListComponent implements OnInit {
|
|
parks: any[] = [];
|
|
loading = true;
|
|
|
|
constructor(private apiService: ApiService) {}
|
|
|
|
ngOnInit() {
|
|
this.apiService.getParks().subscribe({
|
|
next: (response) => {
|
|
this.parks = response.results || response;
|
|
this.loading = false;
|
|
},
|
|
error: (error) => {
|
|
console.error('Error fetching parks:', error);
|
|
this.loading = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Dark/Light Mode Requirements
|
|
|
|
ThrillWiki requires comprehensive dark/light mode support across all frontend implementations.
|
|
|
|
### CSS Variables Approach
|
|
|
|
```css
|
|
:root {
|
|
/* Light mode colors */
|
|
--bg-primary: #ffffff;
|
|
--bg-secondary: #f8f9fa;
|
|
--text-primary: #212529;
|
|
--text-secondary: #6c757d;
|
|
--border-color: #dee2e6;
|
|
--accent-color: #007bff;
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
/* Dark mode colors */
|
|
--bg-primary: #1a1a1a;
|
|
--bg-secondary: #2d2d2d;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #b0b0b0;
|
|
--border-color: #404040;
|
|
--accent-color: #4dabf7;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
}
|
|
```
|
|
|
|
### Theme Toggle Implementation
|
|
|
|
**Vanilla JavaScript:**
|
|
```javascript
|
|
class ThemeManager {
|
|
constructor() {
|
|
this.theme = localStorage.getItem('theme') || 'light';
|
|
this.applyTheme();
|
|
}
|
|
|
|
applyTheme() {
|
|
document.documentElement.setAttribute('data-theme', this.theme);
|
|
localStorage.setItem('theme', this.theme);
|
|
}
|
|
|
|
toggle() {
|
|
this.theme = this.theme === 'light' ? 'dark' : 'light';
|
|
this.applyTheme();
|
|
}
|
|
|
|
setTheme(theme) {
|
|
this.theme = theme;
|
|
this.applyTheme();
|
|
}
|
|
}
|
|
|
|
const themeManager = new ThemeManager();
|
|
|
|
// Theme toggle button
|
|
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
themeManager.toggle();
|
|
});
|
|
```
|
|
|
|
**React Hook:**
|
|
```jsx
|
|
import { useState, useEffect } from 'react';
|
|
|
|
export const useTheme = () => {
|
|
const [theme, setTheme] = useState(() => {
|
|
return localStorage.getItem('theme') || 'light';
|
|
});
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
}, [theme]);
|
|
|
|
const toggleTheme = () => {
|
|
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
|
};
|
|
|
|
return { theme, toggleTheme, setTheme };
|
|
};
|
|
|
|
// Component usage
|
|
const ThemeToggle = () => {
|
|
const { theme, toggleTheme } = useTheme();
|
|
|
|
return (
|
|
<button onClick={toggleTheme} className="theme-toggle">
|
|
{theme === 'light' ? '🌙' : '☀️'}
|
|
</button>
|
|
);
|
|
};
|
|
```
|
|
|
|
**Vue Composable:**
|
|
```javascript
|
|
import { ref, watch } from 'vue';
|
|
|
|
export function useTheme() {
|
|
const theme = ref(localStorage.getItem('theme') || 'light');
|
|
|
|
const applyTheme = (newTheme) => {
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('theme', newTheme);
|
|
};
|
|
|
|
const toggleTheme = () => {
|
|
theme.value = theme.value === 'light' ? 'dark' : 'light';
|
|
};
|
|
|
|
const setTheme = (newTheme) => {
|
|
theme.value = newTheme;
|
|
};
|
|
|
|
watch(theme, applyTheme, { immediate: true });
|
|
|
|
return {
|
|
theme: readonly(theme),
|
|
toggleTheme,
|
|
setTheme
|
|
};
|
|
}
|
|
```
|
|
|
|
### Tailwind CSS Integration
|
|
|
|
If using Tailwind CSS, configure dark mode in `tailwind.config.js`:
|
|
|
|
```javascript
|
|
module.exports = {
|
|
darkMode: ['class', '[data-theme="dark"]'],
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
light: '#ffffff',
|
|
dark: '#1a1a1a'
|
|
},
|
|
secondary: {
|
|
light: '#f8f9fa',
|
|
dark: '#2d2d2d'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Usage in components:
|
|
```html
|
|
<div class="bg-primary-light dark:bg-primary-dark text-gray-900 dark:text-white">
|
|
Content that adapts to theme
|
|
</div>
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### HTTP Status Codes
|
|
|
|
The API returns standard HTTP status codes:
|
|
|
|
- **200 OK** - Successful GET, PUT, PATCH
|
|
- **201 Created** - Successful POST
|
|
- **204 No Content** - Successful DELETE
|
|
- **400 Bad Request** - Invalid request data
|
|
- **401 Unauthorized** - Authentication required
|
|
- **403 Forbidden** - Permission denied
|
|
- **404 Not Found** - Resource not found
|
|
- **500 Internal Server Error** - Server error
|
|
|
|
### Error Response Format
|
|
|
|
```json
|
|
{
|
|
"error": "Error message",
|
|
"details": {
|
|
"field_name": ["Field-specific error message"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Handling Examples
|
|
|
|
**JavaScript:**
|
|
```javascript
|
|
async function handleApiCall(apiFunction) {
|
|
try {
|
|
const response = await apiFunction();
|
|
return response;
|
|
} catch (error) {
|
|
if (error.response) {
|
|
// Server responded with error status
|
|
const { status, data } = error.response;
|
|
|
|
switch (status) {
|
|
case 401:
|
|
// Handle authentication error
|
|
redirectToLogin();
|
|
break;
|
|
case 403:
|
|
// Handle permission error
|
|
showErrorMessage('You do not have permission to perform this action');
|
|
break;
|
|
case 404:
|
|
// Handle not found
|
|
showErrorMessage('Resource not found');
|
|
break;
|
|
case 500:
|
|
// Handle server error
|
|
showErrorMessage('Server error. Please try again later.');
|
|
break;
|
|
default:
|
|
// Handle other errors
|
|
showErrorMessage(data.error || 'An error occurred');
|
|
}
|
|
} else if (error.request) {
|
|
// Network error
|
|
showErrorMessage('Network error. Please check your connection.');
|
|
} else {
|
|
// Other error
|
|
showErrorMessage('An unexpected error occurred');
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
**React Error Boundary:**
|
|
```jsx
|
|
class ApiErrorBoundary extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { hasError: false, error: null };
|
|
}
|
|
|
|
static getDerivedStateFromError(error) {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error, errorInfo) {
|
|
console.error('API Error:', error, errorInfo);
|
|
}
|
|
|
|
render() {
|
|
if
|
|
if (this.state.hasError) {
|
|
return (
|
|
<div className="error-boundary">
|
|
<h2>Something went wrong</h2>
|
|
<p>Please try refreshing the page or contact support if the problem persists.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Security Considerations
|
|
|
|
1. **Token Storage**
|
|
- Use `httpOnly` cookies for production when possible
|
|
- Avoid storing tokens in localStorage for sensitive applications
|
|
- Implement token refresh mechanisms
|
|
|
|
2. **HTTPS Only**
|
|
- Always use HTTPS in production
|
|
- Configure secure cookie flags
|
|
|
|
3. **Input Validation**
|
|
- Validate all user inputs on the frontend
|
|
- Sanitize data before sending to API
|
|
|
|
4. **CORS Configuration**
|
|
- Configure CORS properly in Django settings
|
|
- Restrict allowed origins in production
|
|
|
|
### Performance Optimization
|
|
|
|
1. **Request Caching**
|
|
```javascript
|
|
// Simple cache implementation
|
|
class ApiCache {
|
|
constructor(ttl = 300000) { // 5 minutes default
|
|
this.cache = new Map();
|
|
this.ttl = ttl;
|
|
}
|
|
|
|
get(key) {
|
|
const item = this.cache.get(key);
|
|
if (!item) return null;
|
|
|
|
if (Date.now() > item.expiry) {
|
|
this.cache.delete(key);
|
|
return null;
|
|
}
|
|
|
|
return item.data;
|
|
}
|
|
|
|
set(key, data) {
|
|
this.cache.set(key, {
|
|
data,
|
|
expiry: Date.now() + this.ttl
|
|
});
|
|
}
|
|
|
|
clear() {
|
|
this.cache.clear();
|
|
}
|
|
}
|
|
|
|
const apiCache = new ApiCache();
|
|
|
|
// Usage in API calls
|
|
async function getCachedParks() {
|
|
const cacheKey = 'parks_list';
|
|
let parks = apiCache.get(cacheKey);
|
|
|
|
if (!parks) {
|
|
const response = await api.get('/parks/');
|
|
parks = response.data;
|
|
apiCache.set(cacheKey, parks);
|
|
}
|
|
|
|
return parks;
|
|
}
|
|
```
|
|
|
|
2. **Request Debouncing**
|
|
```javascript
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Usage for search
|
|
const debouncedSearch = debounce(async (query) => {
|
|
if (query.length > 2) {
|
|
const results = await api.get('/parks/search/', { params: { q: query } });
|
|
setSearchResults(results.data);
|
|
}
|
|
}, 300);
|
|
```
|
|
|
|
3. **Pagination Handling**
|
|
```javascript
|
|
class PaginatedAPI {
|
|
constructor(baseURL) {
|
|
this.baseURL = baseURL;
|
|
}
|
|
|
|
async getAllPages(endpoint, params = {}) {
|
|
let allResults = [];
|
|
let nextUrl = `${this.baseURL}${endpoint}`;
|
|
|
|
while (nextUrl) {
|
|
const response = await fetch(nextUrl, {
|
|
headers: this.getHeaders(),
|
|
...params
|
|
});
|
|
const data = await response.json();
|
|
|
|
allResults = allResults.concat(data.results || []);
|
|
nextUrl = data.next;
|
|
}
|
|
|
|
return allResults;
|
|
}
|
|
|
|
async getPage(endpoint, page = 1, pageSize = 20) {
|
|
const response = await fetch(`${this.baseURL}${endpoint}?page=${page}&page_size=${pageSize}`, {
|
|
headers: this.getHeaders()
|
|
});
|
|
return response.json();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Code Organization
|
|
|
|
1. **Service Layer Pattern**
|
|
```javascript
|
|
// services/api.js
|
|
export class APIService {
|
|
constructor(baseURL, authService) {
|
|
this.baseURL = baseURL;
|
|
this.authService = authService;
|
|
}
|
|
|
|
async request(endpoint, options = {}) {
|
|
// Common request logic
|
|
}
|
|
}
|
|
|
|
// services/parksService.js
|
|
export class ParksService extends APIService {
|
|
async getParks(filters = {}) {
|
|
return this.request('/parks/', { params: filters });
|
|
}
|
|
|
|
async getPark(id) {
|
|
return this.request(`/parks/${id}/`);
|
|
}
|
|
}
|
|
|
|
// services/index.js
|
|
export { APIService } from './api';
|
|
export { ParksService } from './parksService';
|
|
export { RidesService } from './ridesService';
|
|
export { AuthService } from './authService';
|
|
```
|
|
|
|
2. **Environment Configuration**
|
|
```javascript
|
|
// config/api.js
|
|
const config = {
|
|
development: {
|
|
API_BASE_URL: 'http://localhost:8000/api/v1',
|
|
ENABLE_LOGGING: true,
|
|
CACHE_TTL: 60000 // 1 minute
|
|
},
|
|
production: {
|
|
API_BASE_URL: 'https://api.thrillwiki.com/api/v1',
|
|
ENABLE_LOGGING: false,
|
|
CACHE_TTL: 300000 // 5 minutes
|
|
}
|
|
};
|
|
|
|
export default config[process.env.NODE_ENV || 'development'];
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### 1. CORS Errors
|
|
**Problem:** Browser blocks requests due to CORS policy
|
|
|
|
**Solution:**
|
|
- Ensure Django CORS settings are configured:
|
|
```python
|
|
# settings.py
|
|
CORS_ALLOWED_ORIGINS = [
|
|
"http://localhost:3000",
|
|
"http://127.0.0.1:3000",
|
|
"http://localhost:5173", # Vite default
|
|
]
|
|
|
|
CORS_ALLOW_CREDENTIALS = True
|
|
```
|
|
|
|
#### 2. Authentication Token Issues
|
|
**Problem:** 401 Unauthorized errors
|
|
|
|
**Solutions:**
|
|
- Check token format: `Authorization: Token abc123def456`
|
|
- Verify token is not expired
|
|
- Ensure token is included in requests:
|
|
```javascript
|
|
// Check if token exists
|
|
const token = localStorage.getItem('thrillwiki_token');
|
|
if (!token) {
|
|
// Redirect to login
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
```
|
|
|
|
#### 3. Network Connectivity
|
|
**Problem:** Network errors or timeouts
|
|
|
|
**Solutions:**
|
|
- Implement retry logic:
|
|
```javascript
|
|
async function retryRequest(fn, retries = 3, delay = 1000) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
if (retries > 0 && error.code === 'NETWORK_ERROR') {
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
return retryRequest(fn, retries - 1, delay * 2);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. Data Serialization Issues
|
|
**Problem:** Unexpected data formats or missing fields
|
|
|
|
**Solutions:**
|
|
- Always validate API responses:
|
|
```javascript
|
|
function validateParkData(park) {
|
|
const required = ['id', 'name', 'location'];
|
|
const missing = required.filter(field => !park[field]);
|
|
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing required fields: ${missing.join(', ')}`);
|
|
}
|
|
|
|
return park;
|
|
}
|
|
```
|
|
|
|
#### 5. Proxy Configuration Issues (Vite/Webpack)
|
|
**Problem:** API calls fail in development due to proxy misconfiguration
|
|
|
|
**Vite Solution:**
|
|
```javascript
|
|
// vite.config.js
|
|
export default defineConfig({
|
|
server: {
|
|
proxy: {
|
|
'/api': {
|
|
target: 'http://localhost:8000',
|
|
changeOrigin: true,
|
|
secure: false,
|
|
}
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
**Webpack Solution:**
|
|
```javascript
|
|
// webpack.config.js or next.config.js
|
|
module.exports = {
|
|
async rewrites() {
|
|
return [
|
|
{
|
|
source: '/api/:path*',
|
|
destination: 'http://localhost:8000/api/:path*',
|
|
},
|
|
];
|
|
},
|
|
};
|
|
```
|
|
|
|
### Debugging Tools
|
|
|
|
#### 1. API Request Logging
|
|
```javascript
|
|
class APILogger {
|
|
static log(method, url, data, response) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.group(`🌐 API ${method.toUpperCase()} ${url}`);
|
|
if (data) console.log('📤 Request:', data);
|
|
console.log('📥 Response:', response);
|
|
console.groupEnd();
|
|
}
|
|
}
|
|
|
|
static error(method, url, error) {
|
|
console.group(`❌ API ${method.toUpperCase()} ${url} FAILED`);
|
|
console.error('Error:', error);
|
|
console.groupEnd();
|
|
}
|
|
}
|
|
|
|
// Usage in API service
|
|
async request(endpoint, options = {}) {
|
|
try {
|
|
const response = await fetch(url, options);
|
|
const data = await response.json();
|
|
|
|
APILogger.log(options.method || 'GET', endpoint, options.body, data);
|
|
return data;
|
|
} catch (error) {
|
|
APILogger.error(options.method || 'GET', endpoint, error);
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. Network Monitoring
|
|
```javascript
|
|
// Monitor network status
|
|
class NetworkMonitor {
|
|
constructor() {
|
|
this.isOnline = navigator.onLine;
|
|
this.listeners = [];
|
|
|
|
window.addEventListener('online', () => this.setOnline(true));
|
|
window.addEventListener('offline', () => this.setOnline(false));
|
|
}
|
|
|
|
setOnline(status) {
|
|
this.isOnline = status;
|
|
this.listeners.forEach(listener => listener(status));
|
|
}
|
|
|
|
onStatusChange(callback) {
|
|
this.listeners.push(callback);
|
|
}
|
|
}
|
|
|
|
const networkMonitor = new NetworkMonitor();
|
|
networkMonitor.onStatusChange((isOnline) => {
|
|
if (isOnline) {
|
|
console.log('🟢 Back online - retrying failed requests');
|
|
// Retry failed requests
|
|
} else {
|
|
console.log('🔴 Gone offline - queuing requests');
|
|
// Queue requests for later
|
|
}
|
|
});
|
|
```
|
|
|
|
### Testing API Integration
|
|
|
|
#### Unit Testing Example (Jest)
|
|
```javascript
|
|
// __tests__/api.test.js
|
|
import { APIService } from '../services/api';
|
|
|
|
// Mock fetch
|
|
global.fetch = jest.fn();
|
|
|
|
describe('APIService', () => {
|
|
let apiService;
|
|
|
|
beforeEach(() => {
|
|
apiService = new APIService('http://localhost:8000/api/v1');
|
|
fetch.mockClear();
|
|
});
|
|
|
|
test('should fetch parks successfully', async () => {
|
|
const mockParks = [
|
|
{ id: 1, name: 'Test Park', location: 'Test Location' }
|
|
];
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ results: mockParks })
|
|
});
|
|
|
|
const parks = await apiService.getParks();
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'http://localhost:8000/api/v1/parks/',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json'
|
|
})
|
|
})
|
|
);
|
|
|
|
expect(parks.results).toEqual(mockParks);
|
|
});
|
|
|
|
test('should handle authentication errors', async () => {
|
|
fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
json: async () => ({ error: 'Unauthorized' })
|
|
});
|
|
|
|
await expect(apiService.getParks()).rejects.toThrow('Unauthorized');
|
|
});
|
|
});
|
|
```
|
|
|
|
### Performance Monitoring
|
|
|
|
```javascript
|
|
// Performance monitoring utility
|
|
class PerformanceMonitor {
|
|
static startTimer(label) {
|
|
performance.mark(`${label}-start`);
|
|
}
|
|
|
|
static endTimer(label) {
|
|
performance.mark(`${label}-end`);
|
|
performance.measure(label, `${label}-start`, `${label}-end`);
|
|
|
|
const measure = performance.getEntriesByName(label)[0];
|
|
console.log(`⏱️ ${label}: ${measure.duration.toFixed(2)}ms`);
|
|
|
|
// Clean up
|
|
performance.clearMarks(`${label}-start`);
|
|
performance.clearMarks(`${label}-end`);
|
|
performance.clearMeasures(label);
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
PerformanceMonitor.startTimer('api-parks-fetch');
|
|
const parks = await api.getParks();
|
|
PerformanceMonitor.endTimer('api-parks-fetch');
|
|
```
|
|
|
|
## Additional Resources
|
|
|
|
### API Documentation
|
|
- **Swagger/OpenAPI Docs**: `http://localhost:8000/api/docs/`
|
|
- **ReDoc Documentation**: `http://localhost:8000/api/redoc/`
|
|
- **API Schema**: `http://localhost:8000/api/schema/`
|
|
|
|
### Development Tools
|
|
- **Django Admin**: `http://localhost:8000/admin/`
|
|
- **Health Check**: `http://localhost:8000/api/v1/health/`
|
|
- **Environment Settings**: `http://localhost:8000/env-settings/`
|
|
|
|
### Example Projects
|
|
Check the `frontend/` directory for a complete Vue.js implementation example that demonstrates all the patterns described in this guide.
|
|
|
|
### Community Resources
|
|
- **Django REST Framework Documentation**: https://www.django-rest-framework.org/
|
|
- **Vue.js Documentation**: https://vuejs.org/
|
|
- **React Documentation**: https://react.dev/
|
|
- **Angular Documentation**: https://angular.io/
|
|
|
|
---
|
|
|
|
This guide provides a comprehensive foundation for connecting any frontend framework to the ThrillWiki Django backend. For specific implementation questions or issues not covered here, please refer to the project's issue tracker or documentation. |