mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:11:09 -05:00
- Introduced Next.js integration guide for ThrillWiki API, detailing authentication, core domain APIs, data structures, and implementation patterns. - Documented the migration to Rich Choice Objects, highlighting changes for frontend developers and enhanced metadata availability. - Fixed the missing `get_by_slug` method in the Ride model, ensuring proper functionality of ride detail endpoints. - Created a test script to verify manufacturer syncing with ride models, ensuring data integrity across related models.
776 lines
18 KiB
Markdown
776 lines
18 KiB
Markdown
# ThrillWiki Frontend Integration Guide
|
|
|
|
**Last Updated:** 2025-01-15
|
|
|
|
**IMPORTANT:** All tuple-based choice patterns have been migrated to Rich Choice Objects system as of 2025-01-15.
|
|
|
|
This document provides comprehensive information for frontend developers integrating with the ThrillWiki Django backend API. It covers all endpoints, data structures, authentication, and integration patterns.
|
|
|
|
## Table of Contents
|
|
|
|
1. [API Overview](#api-overview)
|
|
2. [Authentication](#authentication)
|
|
3. [Rich Choice Objects](#rich-choice-objects)
|
|
4. [Core Endpoints](#core-endpoints)
|
|
5. [Data Models](#data-models)
|
|
6. [Error Handling](#error-handling)
|
|
7. [Integration Examples](#integration-examples)
|
|
|
|
## API Overview
|
|
|
|
The ThrillWiki API is built using Django REST Framework with comprehensive OpenAPI documentation. All endpoints follow RESTful conventions and return JSON responses.
|
|
|
|
**Base URL:** `https://api.thrillwiki.com/api/v1/`
|
|
**Documentation:** `https://api.thrillwiki.com/api/schema/swagger-ui/`
|
|
|
|
### Key Features
|
|
|
|
- **Rich Choice Objects**: Enhanced choice fields with metadata, colors, icons, and descriptions
|
|
- **Comprehensive Filtering**: Advanced filtering and search capabilities
|
|
- **Real-time Updates**: WebSocket support for live data updates
|
|
- **Media Integration**: Cloudflare Images integration for optimized photo delivery
|
|
- **Geographic Data**: PostGIS integration for location-based features
|
|
|
|
## Authentication
|
|
|
|
The API uses token-based authentication with support for both session and JWT tokens.
|
|
|
|
### Authentication Headers
|
|
|
|
```javascript
|
|
// For API requests
|
|
headers: {
|
|
'Authorization': 'Token your-api-token-here',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
// For JWT (if using)
|
|
headers: {
|
|
'Authorization': 'Bearer your-jwt-token-here',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
```
|
|
|
|
### Login Endpoint
|
|
|
|
```javascript
|
|
POST /api/v1/auth/login/
|
|
{
|
|
"username": "your-username",
|
|
"password": "your-password"
|
|
}
|
|
|
|
// Response
|
|
{
|
|
"key": "your-api-token",
|
|
"user": {
|
|
"user_id": "1234",
|
|
"username": "thrillseeker",
|
|
"email": "user@example.com",
|
|
"role": "USER",
|
|
"theme_preference": "dark"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Rich Choice Objects
|
|
|
|
**CRITICAL:** ThrillWiki uses a Rich Choice Objects system instead of simple string choices. All choice fields return enhanced objects with metadata.
|
|
|
|
### Choice Field Structure
|
|
|
|
```typescript
|
|
interface RichChoice {
|
|
value: string; // The actual value stored in database
|
|
label: string; // Human-readable label
|
|
description: string; // Detailed description
|
|
deprecated: boolean; // Whether this choice is deprecated
|
|
sort_order: number; // Display order
|
|
category: string; // Choice category
|
|
metadata: {
|
|
color: string; // UI color (e.g., "blue", "red")
|
|
icon: string; // Icon name (e.g., "user", "shield-check")
|
|
css_class: string; // CSS classes for styling
|
|
[key: string]: any; // Additional metadata
|
|
};
|
|
}
|
|
```
|
|
|
|
### Using Rich Choices in Frontend
|
|
|
|
```javascript
|
|
// When displaying choice values, use the rich choice data
|
|
const userRole = user.get_role_rich_choice();
|
|
console.log(userRole.label); // "Administrator"
|
|
console.log(userRole.metadata.color); // "purple"
|
|
console.log(userRole.metadata.icon); // "cog"
|
|
|
|
// For forms, you can get all available choices
|
|
const roleChoices = await fetch('/api/v1/choices/accounts/user_roles/');
|
|
// Returns array of RichChoice objects
|
|
```
|
|
|
|
### Available Choice Groups
|
|
|
|
#### Accounts Domain
|
|
- `user_roles`: USER, MODERATOR, ADMIN, SUPERUSER
|
|
- `theme_preferences`: light, dark
|
|
- `privacy_levels`: public, friends, private
|
|
- `top_list_categories`: RC, DR, FR, WR, PK
|
|
- `notification_types`: submission_approved, review_helpful, etc.
|
|
- `notification_priorities`: low, normal, high, urgent
|
|
|
|
#### Parks Domain
|
|
- `statuses`: OPERATING, CLOSED_TEMP, CLOSED_PERM, etc.
|
|
- `types`: THEME_PARK, AMUSEMENT_PARK, WATER_PARK, etc.
|
|
- `company_roles`: OPERATOR, PROPERTY_OWNER
|
|
|
|
#### Rides Domain
|
|
- `statuses`: OPERATING, CLOSED_TEMP, SBNO, etc.
|
|
- `categories`: RC, DR, FR, WR, TR, OT
|
|
- `company_roles`: MANUFACTURER, DESIGNER
|
|
- `track_materials`: STEEL, WOOD, HYBRID
|
|
|
|
## Core Endpoints
|
|
|
|
### Parks API
|
|
|
|
#### List Parks
|
|
```javascript
|
|
GET /api/v1/parks/
|
|
GET /api/v1/parks/?search=cedar&status=OPERATING&park_type=THEME_PARK
|
|
|
|
// Response
|
|
{
|
|
"count": 150,
|
|
"next": "https://api.thrillwiki.com/api/v1/parks/?page=2",
|
|
"previous": null,
|
|
"results": [
|
|
{
|
|
"id": 1,
|
|
"name": "Cedar Point",
|
|
"slug": "cedar-point",
|
|
"description": "Roller coaster capital of the world",
|
|
"park_type": {
|
|
"value": "THEME_PARK",
|
|
"label": "Theme Park",
|
|
"metadata": {
|
|
"color": "blue",
|
|
"icon": "castle"
|
|
}
|
|
},
|
|
"status": {
|
|
"value": "OPERATING",
|
|
"label": "Operating",
|
|
"metadata": {
|
|
"color": "green",
|
|
"icon": "check-circle"
|
|
}
|
|
},
|
|
"location": {
|
|
"city": "Sandusky",
|
|
"state": "OH",
|
|
"country": "USA",
|
|
"coordinates": {
|
|
"latitude": 41.4814,
|
|
"longitude": -82.6838
|
|
}
|
|
},
|
|
"operator": {
|
|
"name": "Cedar Fair",
|
|
"slug": "cedar-fair"
|
|
},
|
|
"stats": {
|
|
"ride_count": 72,
|
|
"coaster_count": 17,
|
|
"average_rating": 8.7
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Park Detail
|
|
```javascript
|
|
GET /api/v1/parks/cedar-point/
|
|
|
|
// Response includes full park details, rides, areas, photos, reviews
|
|
{
|
|
"id": 1,
|
|
"name": "Cedar Point",
|
|
"slug": "cedar-point",
|
|
"description": "America's Roller Coast...",
|
|
"park_type": { /* Rich Choice Object */ },
|
|
"status": { /* Rich Choice Object */ },
|
|
"opening_date": "1870-05-30",
|
|
"size_acres": "364.00",
|
|
"location": { /* Full location data */ },
|
|
"operator": { /* Company details */ },
|
|
"areas": [
|
|
{
|
|
"id": 1,
|
|
"name": "Main Midway",
|
|
"description": "The heart of Cedar Point"
|
|
}
|
|
],
|
|
"rides": [
|
|
{
|
|
"id": 1,
|
|
"name": "Steel Vengeance",
|
|
"category": { /* Rich Choice Object */ },
|
|
"status": { /* Rich Choice Object */ }
|
|
}
|
|
],
|
|
"photos": [
|
|
{
|
|
"id": 1,
|
|
"photo_type": "banner",
|
|
"image_url": "https://imagedelivery.net/...",
|
|
"variants": {
|
|
"thumbnail": "https://imagedelivery.net/.../thumbnail",
|
|
"medium": "https://imagedelivery.net/.../medium",
|
|
"large": "https://imagedelivery.net/.../large"
|
|
}
|
|
}
|
|
],
|
|
"recent_reviews": [ /* Latest reviews */ ],
|
|
"stats": {
|
|
"total_reviews": 1247,
|
|
"average_rating": 8.7,
|
|
"ride_count": 72,
|
|
"coaster_count": 17
|
|
}
|
|
}
|
|
```
|
|
|
|
### Rides API
|
|
|
|
#### List Rides
|
|
```javascript
|
|
GET /api/v1/rides/
|
|
GET /api/v1/rides/?category=RC&status=OPERATING&manufacturer=bolliger-mabillard
|
|
|
|
// Response
|
|
{
|
|
"count": 500,
|
|
"results": [
|
|
{
|
|
"id": 1,
|
|
"name": "Steel Vengeance",
|
|
"slug": "steel-vengeance",
|
|
"park": {
|
|
"name": "Cedar Point",
|
|
"slug": "cedar-point"
|
|
},
|
|
"category": {
|
|
"value": "RC",
|
|
"label": "Roller Coaster",
|
|
"metadata": {
|
|
"color": "red",
|
|
"icon": "roller-coaster"
|
|
}
|
|
},
|
|
"status": {
|
|
"value": "OPERATING",
|
|
"label": "Operating",
|
|
"metadata": {
|
|
"color": "green",
|
|
"icon": "check-circle"
|
|
}
|
|
},
|
|
"manufacturer": {
|
|
"name": "Rocky Mountain Construction",
|
|
"slug": "rocky-mountain-construction"
|
|
},
|
|
"stats": {
|
|
"height_ft": "205.00",
|
|
"speed_mph": "74.00",
|
|
"length_ft": "5740.00"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Ride Detail
|
|
```javascript
|
|
GET /api/v1/rides/steel-vengeance/
|
|
|
|
// Response includes comprehensive ride data
|
|
{
|
|
"id": 1,
|
|
"name": "Steel Vengeance",
|
|
"slug": "steel-vengeance",
|
|
"description": "World's first hyper-hybrid coaster...",
|
|
"park": { /* Park details */ },
|
|
"category": { /* Rich Choice Object */ },
|
|
"status": { /* Rich Choice Object */ },
|
|
"manufacturer": { /* Company details */ },
|
|
"designer": { /* Company details */ },
|
|
"ride_model": {
|
|
"name": "I-Box Track",
|
|
"description": "Steel track on wooden structure"
|
|
},
|
|
"opening_date": "2018-05-05",
|
|
"stats": {
|
|
"height_ft": "205.00",
|
|
"speed_mph": "74.00",
|
|
"length_ft": "5740.00",
|
|
"inversions": 4,
|
|
"ride_time_seconds": 150,
|
|
"track_type": "Hybrid",
|
|
"track_material": {
|
|
"value": "HYBRID",
|
|
"label": "Hybrid",
|
|
"metadata": {
|
|
"color": "orange",
|
|
"icon": "layers"
|
|
}
|
|
}
|
|
},
|
|
"photos": [ /* Photo array */ ],
|
|
"reviews": [ /* Recent reviews */ ],
|
|
"rankings": {
|
|
"global_rank": 1,
|
|
"category_rank": 1,
|
|
"park_rank": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
### User Accounts API
|
|
|
|
#### User Profile
|
|
```javascript
|
|
GET /api/v1/accounts/profile/
|
|
|
|
// Response
|
|
{
|
|
"user_id": "1234",
|
|
"username": "thrillseeker",
|
|
"email": "user@example.com",
|
|
"role": {
|
|
"value": "USER",
|
|
"label": "User",
|
|
"metadata": {
|
|
"color": "blue",
|
|
"icon": "user",
|
|
"permissions": ["create_content", "create_reviews"]
|
|
}
|
|
},
|
|
"theme_preference": {
|
|
"value": "dark",
|
|
"label": "Dark",
|
|
"metadata": {
|
|
"color": "gray",
|
|
"icon": "moon",
|
|
"preview_colors": {
|
|
"background": "#1f2937",
|
|
"text": "#f9fafb"
|
|
}
|
|
}
|
|
},
|
|
"profile": {
|
|
"display_name": "Thrill Seeker",
|
|
"avatar_url": "https://imagedelivery.net/...",
|
|
"bio": "Love roller coasters!",
|
|
"coaster_credits": 150,
|
|
"stats": { /* User statistics */ }
|
|
}
|
|
}
|
|
```
|
|
|
|
#### User Notifications
|
|
```javascript
|
|
GET /api/v1/accounts/notifications/
|
|
|
|
// Response
|
|
{
|
|
"count": 25,
|
|
"unread_count": 5,
|
|
"results": [
|
|
{
|
|
"id": 1,
|
|
"notification_type": {
|
|
"value": "submission_approved",
|
|
"label": "Submission Approved",
|
|
"metadata": {
|
|
"color": "green",
|
|
"icon": "check-circle",
|
|
"category": "submission"
|
|
}
|
|
},
|
|
"title": "Your submission has been approved!",
|
|
"message": "Your photo submission for Cedar Point has been approved.",
|
|
"priority": {
|
|
"value": "normal",
|
|
"label": "Normal",
|
|
"metadata": {
|
|
"urgency_level": 2
|
|
}
|
|
},
|
|
"is_read": false,
|
|
"created_at": "2024-01-15T10:30:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Data Models
|
|
|
|
### TypeScript Interfaces
|
|
|
|
```typescript
|
|
// Core Models
|
|
interface Park {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
park_type: RichChoice;
|
|
status: RichChoice;
|
|
opening_date: string;
|
|
size_acres: string;
|
|
location: ParkLocation;
|
|
operator: Company;
|
|
property_owner?: Company;
|
|
stats: ParkStats;
|
|
photos: Photo[];
|
|
areas: ParkArea[];
|
|
}
|
|
|
|
interface Ride {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
park: Park;
|
|
category: RichChoice;
|
|
status: RichChoice;
|
|
manufacturer?: Company;
|
|
designer?: Company;
|
|
ride_model?: RideModel;
|
|
opening_date: string;
|
|
stats?: RideStats;
|
|
photos: Photo[];
|
|
}
|
|
|
|
interface User {
|
|
user_id: string;
|
|
username: string;
|
|
email: string;
|
|
role: RichChoice;
|
|
theme_preference: RichChoice;
|
|
privacy_level: RichChoice;
|
|
profile: UserProfile;
|
|
}
|
|
|
|
interface UserProfile {
|
|
profile_id: string;
|
|
display_name: string;
|
|
avatar_url: string;
|
|
avatar_variants: {
|
|
thumbnail: string;
|
|
avatar: string;
|
|
large: string;
|
|
};
|
|
bio: string;
|
|
coaster_credits: number;
|
|
dark_ride_credits: number;
|
|
flat_ride_credits: number;
|
|
water_ride_credits: number;
|
|
}
|
|
|
|
// Location Models
|
|
interface ParkLocation {
|
|
street_address: string;
|
|
city: string;
|
|
state: string;
|
|
country: string;
|
|
postal_code: string;
|
|
coordinates: {
|
|
latitude: number;
|
|
longitude: number;
|
|
};
|
|
timezone: string;
|
|
}
|
|
|
|
// Media Models
|
|
interface Photo {
|
|
id: number;
|
|
photo_type: string;
|
|
image_url: string;
|
|
variants: {
|
|
thumbnail: string;
|
|
medium: string;
|
|
large: string;
|
|
[key: string]: string;
|
|
};
|
|
attribution: string;
|
|
caption: string;
|
|
is_primary: boolean;
|
|
}
|
|
|
|
// Company Models
|
|
interface Company {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
roles: RichChoice[];
|
|
description: string;
|
|
website: string;
|
|
founded_year?: number;
|
|
headquarters?: CompanyHeadquarters;
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Standard Error Response
|
|
|
|
```javascript
|
|
// 400 Bad Request
|
|
{
|
|
"error": "validation_error",
|
|
"message": "Invalid input data",
|
|
"details": {
|
|
"field_name": ["This field is required."]
|
|
}
|
|
}
|
|
|
|
// 401 Unauthorized
|
|
{
|
|
"error": "authentication_required",
|
|
"message": "Authentication credentials were not provided."
|
|
}
|
|
|
|
// 403 Forbidden
|
|
{
|
|
"error": "permission_denied",
|
|
"message": "You do not have permission to perform this action."
|
|
}
|
|
|
|
// 404 Not Found
|
|
{
|
|
"error": "not_found",
|
|
"message": "The requested resource was not found."
|
|
}
|
|
|
|
// 500 Internal Server Error
|
|
{
|
|
"error": "server_error",
|
|
"message": "An unexpected error occurred. Please try again later."
|
|
}
|
|
```
|
|
|
|
### Error Handling in Frontend
|
|
|
|
```javascript
|
|
async function fetchPark(slug) {
|
|
try {
|
|
const response = await fetch(`/api/v1/parks/${slug}/`);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.message || 'Request failed');
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Failed to fetch park:', error);
|
|
// Handle error appropriately in UI
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Integration Examples
|
|
|
|
### React Hook for Rich Choices
|
|
|
|
```javascript
|
|
import { useState, useEffect } from 'react';
|
|
|
|
function useRichChoices(domain, choiceGroup) {
|
|
const [choices, setChoices] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function fetchChoices() {
|
|
try {
|
|
const response = await fetch(`/api/v1/choices/${domain}/${choiceGroup}/`);
|
|
const data = await response.json();
|
|
setChoices(data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch choices:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
fetchChoices();
|
|
}, [domain, choiceGroup]);
|
|
|
|
return { choices, loading };
|
|
}
|
|
|
|
// Usage
|
|
function UserRoleSelect({ value, onChange }) {
|
|
const { choices, loading } = useRichChoices('accounts', 'user_roles');
|
|
|
|
if (loading) return <div>Loading...</div>;
|
|
|
|
return (
|
|
<select value={value} onChange={onChange}>
|
|
{choices.map(choice => (
|
|
<option key={choice.value} value={choice.value}>
|
|
{choice.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Choice Display Component
|
|
|
|
```javascript
|
|
function ChoiceDisplay({ choice, showIcon = true, showDescription = false }) {
|
|
const { value, label, description, metadata } = choice;
|
|
|
|
return (
|
|
<span className={`choice-display ${metadata.css_class}`}>
|
|
{showIcon && metadata.icon && (
|
|
<Icon name={metadata.icon} color={metadata.color} />
|
|
)}
|
|
<span className="choice-label">{label}</span>
|
|
{showDescription && description && (
|
|
<span className="choice-description">{description}</span>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|
|
```
|
|
|
|
### API Client with Rich Choice Support
|
|
|
|
```javascript
|
|
class ThrillWikiAPI {
|
|
constructor(baseURL, token) {
|
|
this.baseURL = baseURL;
|
|
this.token = token;
|
|
}
|
|
|
|
async request(endpoint, options = {}) {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Token ${this.token}`,
|
|
...options.headers
|
|
};
|
|
|
|
const response = await fetch(url, { ...options, headers });
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || 'Request failed');
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// Parks
|
|
async getParks(filters = {}) {
|
|
const params = new URLSearchParams(filters);
|
|
return this.request(`/parks/?${params}`);
|
|
}
|
|
|
|
async getPark(slug) {
|
|
return this.request(`/parks/${slug}/`);
|
|
}
|
|
|
|
// Rides
|
|
async getRides(filters = {}) {
|
|
const params = new URLSearchParams(filters);
|
|
return this.request(`/rides/?${params}`);
|
|
}
|
|
|
|
async getRide(slug) {
|
|
return this.request(`/rides/${slug}/`);
|
|
}
|
|
|
|
// Choices
|
|
async getChoices(domain, choiceGroup) {
|
|
return this.request(`/choices/${domain}/${choiceGroup}/`);
|
|
}
|
|
|
|
// User
|
|
async getCurrentUser() {
|
|
return this.request('/accounts/profile/');
|
|
}
|
|
|
|
async updateUserPreferences(preferences) {
|
|
return this.request('/accounts/preferences/', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(preferences)
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Always Use Rich Choice Objects
|
|
- Never hardcode choice values or labels
|
|
- Always fetch choice metadata for proper UI rendering
|
|
- Use choice metadata for styling and icons
|
|
|
|
### 2. Handle Loading States
|
|
- Show loading indicators while fetching data
|
|
- Implement proper error boundaries
|
|
- Cache choice data to avoid repeated requests
|
|
|
|
### 3. Responsive Design
|
|
- Use photo variants for different screen sizes
|
|
- Implement proper image lazy loading
|
|
- Consider mobile-first design patterns
|
|
|
|
### 4. Performance Optimization
|
|
- Use pagination for large datasets
|
|
- Implement proper caching strategies
|
|
- Minimize API requests with efficient data fetching
|
|
|
|
### 5. Error Handling
|
|
- Always handle API errors gracefully
|
|
- Provide meaningful error messages to users
|
|
- Implement retry logic for transient failures
|
|
|
|
## API Versioning
|
|
|
|
The API uses URL-based versioning (`/api/v1/`). When breaking changes are introduced, a new version will be created (`/api/v2/`). The current version (v1) will be maintained for backward compatibility.
|
|
|
|
## Rate Limiting
|
|
|
|
API requests are rate-limited to prevent abuse:
|
|
- **Authenticated users**: 1000 requests per hour
|
|
- **Anonymous users**: 100 requests per hour
|
|
- **Bulk operations**: Special limits apply
|
|
|
|
Rate limit headers are included in responses:
|
|
```
|
|
X-RateLimit-Limit: 1000
|
|
X-RateLimit-Remaining: 999
|
|
X-RateLimit-Reset: 1642694400
|
|
```
|
|
|
|
## Support
|
|
|
|
For API support and questions:
|
|
- **Documentation**: https://api.thrillwiki.com/api/schema/swagger-ui/
|
|
- **GitHub Issues**: https://github.com/thrillwiki/api/issues
|
|
- **Email**: api-support@thrillwiki.com
|
|
|
|
---
|
|
|
|
**Note**: This documentation is automatically updated when the API changes. Always refer to the latest version for accurate information.
|