Files
thrillwiki_django_no_react/backend/templates/base/base.html

354 lines
15 KiB
HTML

{% load static %}
{% load cache %}
{# =============================================================================
ThrillWiki Base Template
This is the root template that all pages extend. It provides:
- HTML5 document structure with accessibility features
- SEO meta tags and Open Graph/Twitter cards
- CSS and JavaScript asset loading
- Navigation header and footer
- Django messages and toast notifications
- HTMX and Alpine.js configuration
Available Blocks:
----------------
Content Blocks:
- title: Page title (appears in <title> tag and meta)
- content: Main page content
- navigation: Navigation header (defaults to enhanced_header.html)
- footer: Page footer
Meta Blocks:
- meta_description: Page meta description for SEO
- meta_keywords: Page meta keywords
- og_type: Open Graph type (default: website)
- og_title: Open Graph title (defaults to title block)
- og_description: Open Graph description
- og_image: Open Graph image URL
- twitter_title: Twitter card title
- twitter_description: Twitter card description
Customization Blocks:
- extra_head: Additional CSS/meta tags in <head>
- extra_js: Additional JavaScript before </body>
- body_class: Additional classes for <body> tag
- main_class: Additional classes for <main> tag
Usage Example:
{% extends "base/base.html" %}
{% block title %}My Page - ThrillWiki{% endblock %}
{% block content %}
<h1>My Page Content</h1>
{% endblock %}
============================================================================= #}
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">
<meta name="description" content="{% block meta_description %}ThrillWiki - Your comprehensive guide to theme parks and roller coasters{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}theme parks, roller coasters, rides, amusement parks{% endblock %}">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="{% block og_type %}website{% endblock %}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:title" content="{% block og_title %}{% block title %}ThrillWiki{% endblock %}{% endblock %}">
<meta property="og:description" content="{% block og_description %}ThrillWiki - Your comprehensive guide to theme parks and roller coasters{% endblock %}">
<meta property="og:image" content="{% block og_image %}{% static 'images/og-default.jpg' %}{% endblock %}">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:title" content="{% block twitter_title %}ThrillWiki{% endblock %}">
<meta property="twitter:description" content="{% block twitter_description %}ThrillWiki - Your comprehensive guide to theme parks and roller coasters{% endblock %}">
{# Use title block directly #}
<title>{% block page_title %}{% block title %}ThrillWiki{% endblock %}{% endblock %}</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="{% static 'favicon.ico' %}">
<!-- Fonts - Preconnect for performance -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Playfair+Display:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Prevent flash of wrong theme -->
<script>
(function() {
let theme = localStorage.getItem('theme');
if (!theme) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
})();
</script>
<!-- Design System CSS - Load in correct order -->
<link href="{% static 'css/design-tokens.css' %}" rel="stylesheet">
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
<link href="{% static 'css/components.css' %}" rel="stylesheet">
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- Alpine.js Plugins -->
<script defer src="https://unpkg.com/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://unpkg.com/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine.js Core -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine.js Stores (must load before alpine:init) -->
<script src="{% static 'js/stores/index.js' %}"></script>
<!-- Alpine.js Components -->
<script src="{% static 'js/alpine-components.js' %}"></script>
<!-- Location Autocomplete -->
<script src="{% static 'js/location-autocomplete.js' %}"></script>
<style>
/* Hide elements until Alpine.js is ready */
[x-cloak] { display: none !important; }
/* HTMX loading indicator styles */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }
</style>
{% block extra_head %}{% endblock %}
</head>
<body class="flex flex-col min-h-screen font-sans antialiased bg-background text-foreground {% block body_class %}{% endblock %}"
x-data
x-init="$store.theme.init(); $store.auth.init()"
:class="{ 'dark': $store.theme.isDark }">
<!-- Skip to main content link for accessibility -->
<a href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 z-50 px-4 py-2 rounded-md bg-primary text-primary-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
Skip to main content
</a>
<!-- HTMX CSRF Configuration -->
<div hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' style="display: none;"></div>
<!-- Navigation Header -->
{% block navigation %}
{% include 'components/layout/enhanced_header.html' %}
{% endblock navigation %}
<!-- Breadcrumb Navigation -->
{% block breadcrumbs %}
{% if breadcrumbs %}
<div class="container px-4 mx-auto md:px-6 lg:px-8">
{% include 'components/navigation/breadcrumbs.html' %}
</div>
{% endif %}
{% endblock breadcrumbs %}
<!-- Flash Messages -->
{% if messages %}
<div class="fixed top-4 right-4 z-50 space-y-2" role="alert" aria-live="polite">
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %} animate-slide-in"
x-data="{ show: true }"
x-show="show"
x-init="setTimeout(() => show = false, 5000)"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform translate-x-full">
<div class="flex items-center gap-2">
<span>{{ message }}</span>
<button type="button"
@click="show = false"
class="ml-auto opacity-70 hover:opacity-100 focus:outline-none"
aria-label="Dismiss">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Main Content -->
<main id="main-content" class="container flex-grow px-4 py-8 mx-auto md:px-6 lg:px-8 {% block main_class %}{% endblock %}" role="main" aria-label="Main content">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
{% block footer %}
{# Cache footer for 1 hour - static content #}
{% cache 3600 footer_content %}
<footer class="mt-auto border-t bg-card/50 backdrop-blur-sm border-border" role="contentinfo">
<div class="container px-4 py-6 mx-auto md:px-6 lg:px-8">
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row">
<div class="text-sm text-muted-foreground">
<p>&copy; {% now "Y" %} ThrillWiki. All rights reserved.</p>
</div>
<nav class="flex items-center gap-4 text-sm" aria-label="Footer navigation">
<a href="{% url 'terms' %}"
class="text-muted-foreground hover:text-foreground transition-colors">
Terms
</a>
<a href="{% url 'privacy' %}"
class="text-muted-foreground hover:text-foreground transition-colors">
Privacy
</a>
</nav>
</div>
</div>
</footer>
{% endcache %}
{% endblock footer %}
<!-- Global Auth Modal -->
{% include 'components/auth/auth-modal.html' %}
<!-- Global Toast Container -->
{% include 'components/ui/toast-container.html' %}
<!-- Core JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/alerts.js' %}"></script>
<script src="{% static 'js/fsm-transitions.js' %}"></script>
<!-- HTMX Configuration and Error Handling -->
<script>
/**
* HTMX Configuration
* ==================
* This section configures HTMX behavior and error handling.
*
* Swap Strategies Used:
* - innerHTML: Replace content inside container (default for lists)
* - outerHTML: Replace entire element (status badges, rows)
* - beforeend: Append items (infinite scroll)
* - afterbegin: Prepend items (new items at top)
*
* Target Naming Conventions:
* - #object-type-id: For specific objects (e.g., #park-123)
* - #section-name: For page sections (e.g., #results, #filters)
* - #modal-container: For modals
* - this: For self-replacement
*
* Custom Event Naming:
* - {model}-status-changed: Status updates (park-status-changed)
* - auth-changed: Authentication state changes
* - {model}-created: New item created
* - {model}-updated: Item updated
* - {model}-deleted: Item deleted
*/
document.addEventListener('DOMContentLoaded', function() {
// Configure HTMX defaults
htmx.config.globalViewTransitions = true;
htmx.config.useTemplateFragments = true;
htmx.config.timeout = 30000; // 30 second timeout
htmx.config.historyCacheSize = 10;
htmx.config.refreshOnHistoryMiss = true;
// Add loading states
document.body.addEventListener('htmx:beforeRequest', function(evt) {
evt.target.classList.add('htmx-request');
});
document.body.addEventListener('htmx:afterRequest', function(evt) {
evt.target.classList.remove('htmx-request');
});
// Comprehensive HTMX error handling
document.body.addEventListener('htmx:responseError', function(evt) {
const xhr = evt.detail.xhr;
const showToast = (type, message) => {
if (Alpine && Alpine.store('toast')) {
Alpine.store('toast')[type](message);
}
};
// Handle different HTTP status codes
if (xhr.status >= 500) {
showToast('error', 'Server error. Please try again later.');
console.error('HTMX Server Error:', xhr.status, xhr.statusText);
} else if (xhr.status === 429) {
showToast('warning', 'Too many requests. Please wait a moment.');
} else if (xhr.status === 403) {
showToast('error', 'You do not have permission to perform this action.');
} else if (xhr.status === 401) {
showToast('warning', 'Please log in to continue.');
// Optionally trigger auth modal
document.body.dispatchEvent(new CustomEvent('show-login'));
} else if (xhr.status === 404) {
showToast('error', 'Resource not found.');
} else if (xhr.status === 422) {
showToast('error', 'Validation error. Please check your input.');
} else if (xhr.status === 0) {
showToast('error', 'Network error. Please check your connection.');
} else if (xhr.status >= 400) {
showToast('error', 'Request failed. Please try again.');
}
});
// Handle HTMX timeout
document.body.addEventListener('htmx:timeout', function(evt) {
if (Alpine && Alpine.store('toast')) {
Alpine.store('toast').error('Request timed out. Please try again.');
}
});
// Handle network errors (sendError)
document.body.addEventListener('htmx:sendError', function(evt) {
if (Alpine && Alpine.store('toast')) {
Alpine.store('toast').error('Network error. Please check your connection and try again.');
}
});
});
// Handle HX-Trigger headers for toast notifications
// Expected format: {"showToast": {"type": "success|error|warning|info", "message": "...", "duration": 5000}}
document.body.addEventListener('htmx:afterOnLoad', function(evt) {
const triggerHeader = evt.detail.xhr.getResponseHeader('HX-Trigger');
if (triggerHeader) {
try {
const triggers = JSON.parse(triggerHeader);
if (triggers.showToast && Alpine && Alpine.store('toast')) {
const { type = 'info', message, duration } = triggers.showToast;
Alpine.store('toast')[type](message, duration);
}
} catch (e) {
// Ignore parsing errors for non-JSON triggers (e.g., simple event names)
}
}
});
</script>
<!-- Auth Context for Alpine.js -->
<script>
window.__AUTH_USER__ = {% if user.is_authenticated %}{
id: {{ user.id }},
username: "{{ user.username|escapejs }}",
email: "{{ user.email|escapejs }}",
avatar: "{{ user.profile.avatar.url|default:''|escapejs }}"
}{% else %}null{% endif %};
window.__AUTH_PERMISSIONS__ = [
{% for perm in perms %}
"{{ perm }}",
{% endfor %}
];
</script>
{% block extra_js %}{% endblock %}
</body>
</html>