mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 05:11:10 -05:00
354 lines
15 KiB
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>© {% 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>
|