Files
pacnpal c2c26cfd1d Add comprehensive API documentation for ThrillWiki integration and features
- 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.
2025-09-16 11:29:17 -04:00

18 KiB

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
  2. Authentication
  3. Rich Choice Objects
  4. Core Endpoints
  5. Data Models
  6. Error Handling
  7. 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

// 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

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

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

// 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

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

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

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

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

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

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

// 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

// 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

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

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

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

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:


Note: This documentation is automatically updated when the API changes. Always refer to the latest version for accurate information.