here we go

This commit is contained in:
pacnpal
2024-10-31 22:32:01 +00:00
parent c52f14e700
commit 80a9d61ca2
68 changed files with 3114 additions and 1485 deletions

View File

@@ -30,8 +30,15 @@
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<!-- Alpine.js -->
<script defer src="{% static 'js/alpine.min.js' %}"></script>
<!-- Location Autocomplete -->
<script src="{% static 'js/location-autocomplete.js' %}"></script>
<!-- Tailwind CSS -->
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet" />
<link href="{% static 'css/alerts.css' %}" rel="stylesheet" />
<!-- Font Awesome -->
<link
@@ -39,6 +46,23 @@
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
/>
<style>
[x-cloak] {
display: none !important;
}
.dropdown-menu {
position: absolute;
right: 0;
margin-top: 0.5rem;
width: 12rem;
border-radius: 0.375rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
z-index: 50;
overflow: hidden;
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body
@@ -46,7 +70,7 @@
>
<!-- Header -->
<header
class="border-b shadow-lg bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50"
class="sticky top-0 z-40 border-b shadow-lg bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50"
>
<nav class="container mx-auto nav-container">
<div class="flex items-center justify-between">
@@ -107,32 +131,39 @@
<span>Moderation</span>
</a>
{% endif %}
<div class="relative" x-data="{ open: false }">
<button
<div
class="relative"
x-data="{ open: false }"
@click.outside="open = false"
>
<!-- Profile Picture Button -->
{% if user.profile.avatar %}
<img
@click="open = !open"
class="flex items-center space-x-2 transition-transform hover:scale-105"
src="{{ user.profile.avatar.url }}"
alt="{{ user.username }}"
class="w-8 h-8 transition-transform rounded-full cursor-pointer ring-2 ring-primary/20 hover:scale-105"
/>
{% else %}
<div
@click="open = !open"
class="flex items-center justify-center w-8 h-8 text-white transition-transform rounded-full cursor-pointer bg-gradient-to-br from-primary to-secondary hover:scale-105"
>
{% if user.profile.avatar %}
<img
src="{{ user.profile.avatar.url }}"
alt="{{ user.username }}"
class="w-8 h-8 rounded-full ring-2 ring-primary/20"
/>
{% else %}
<div
class="flex items-center justify-center w-8 h-8 text-white rounded-full bg-gradient-to-br from-primary to-secondary"
>
{{ user.username.0|upper }}
</div>
{% endif %}
<span>{{ user.username }}</span>
</button>
{{ user.username.0|upper }}
</div>
{% endif %}
<!-- Dropdown Menu -->
<div
x-cloak
x-show="open"
@click.away="open = false"
class="absolute right-0 w-48 py-1 mt-2 bg-white rounded-md shadow-lg dark:bg-gray-800"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="bg-white dropdown-menu dark:bg-gray-800"
>
<a href="{% url 'profile' user.username %}" class="menu-item">
<i class="w-5 fas fa-user"></i>
@@ -158,16 +189,40 @@
</div>
</div>
{% else %}
<!-- Login/Register (Desktop) -->
<div class="hidden space-x-3 lg:flex">
<a href="{% url 'account_login' %}" class="btn-secondary">
<i class="mr-2 fas fa-sign-in-alt"></i>
Login
</a>
<a href="{% url 'account_signup' %}" class="btn-primary">
<i class="mr-2 fas fa-user-plus"></i>
Register
</a>
<!-- Generic Profile Icon for Unauthenticated Users -->
<div
class="relative"
x-data="{ open: false }"
@click.outside="open = false"
>
<div
@click="open = !open"
class="flex items-center justify-center w-8 h-8 text-gray-500 transition-transform rounded-full cursor-pointer hover:text-primary dark:text-gray-400 dark:hover:text-primary hover:scale-105"
>
<i class="text-xl fas fa-user"></i>
</div>
<!-- Auth Menu -->
<div
x-cloak
x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="bg-white dropdown-menu dark:bg-gray-800"
>
<a href="{% url 'account_login' %}" class="menu-item">
<i class="w-5 fas fa-sign-in-alt"></i>
<span>Login</span>
</a>
<a href="{% url 'account_signup' %}" class="menu-item">
<i class="w-5 fas fa-user-plus"></i>
<span>Register</span>
</a>
</div>
</div>
{% endif %}
@@ -194,26 +249,6 @@
class="form-input"
/>
</form>
{% if not user.is_authenticated %}
<!-- Login/Register (Mobile) -->
<div class="flex items-center space-x-3">
<a
href="{% url 'account_login' %}"
class="flex-1 mobile-nav-link secondary"
>
<i class="fas fa-sign-in-alt"></i>
<span>Login</span>
</a>
<a
href="{% url 'account_signup' %}"
class="flex-1 mobile-nav-link primary"
>
<i class="fas fa-user-plus"></i>
<span>Register</span>
</a>
</div>
{% endif %}
</div>
</div>
</nav>
@@ -221,7 +256,7 @@
<!-- Flash Messages -->
{% if messages %}
<div class="container px-6 mx-auto mt-4">
<div class="fixed top-0 right-0 z-50 p-4 space-y-4">
{% for message in messages %}
<div
class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}"
@@ -264,6 +299,7 @@
<!-- Custom JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/alerts.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -4,24 +4,31 @@
{% block title %}{{ company.name }} - ThrillWiki{% endblock %}
{% block content %}
<div class="container mx-auto px-4">
<div class="container px-4 mx-auto">
<!-- Company Header -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center">
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex flex-col items-start justify-between md:flex-row md:items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">{{ company.name }}</h1>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ company.name }}</h1>
{% if company.headquarters %}
<p class="text-gray-600 dark:text-gray-400">
<i class="fas fa-map-marker-alt mr-2"></i>{{ company.headquarters }}
<i class="mr-2 fas fa-map-marker-alt"></i>{{ company.headquarters }}
</p>
{% endif %}
</div>
{% if company.website %}
<a href="{{ company.website }}" target="_blank" rel="noopener noreferrer"
class="btn-secondary mt-4 md:mt-0">
<i class="fas fa-external-link-alt mr-2"></i>Visit Website
</a>
{% endif %}
<div class="flex gap-2 mt-4 md:mt-0">
{% if company.website %}
<a href="{{ company.website }}" target="_blank" rel="noopener noreferrer"
class="btn-secondary">
<i class="mr-2 fas fa-external-link-alt"></i>Visit Website
</a>
{% endif %}
{% if user.is_authenticated %}
<a href="{% url 'companies:company_edit' slug=company.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-edit"></i>Edit
</a>
{% endif %}
</div>
</div>
{% if company.description %}
@@ -32,22 +39,22 @@
</div>
<!-- Company Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 text-center">
<div class="grid grid-cols-1 gap-6 mb-6 md:grid-cols-3">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ parks.count }}
</div>
<div class="text-gray-600 dark:text-gray-400 mt-1">Theme Parks</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Theme Parks</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 text-center">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ parks|length }}
</div>
<div class="text-gray-600 dark:text-gray-400 mt-1">Active Parks</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Active Parks</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 text-center">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{% with total_rides=0 %}
{% for park in parks %}
@@ -56,42 +63,42 @@
{{ total_rides }}
{% endwith %}
</div>
<div class="text-gray-600 dark:text-gray-400 mt-1">Total Attractions</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Total Attractions</div>
</div>
</div>
<!-- Parks List -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Theme Parks</h2>
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Theme Parks</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for park in parks %}
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg overflow-hidden">
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700">
{% if park.photos.exists %}
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="w-full h-48 object-cover">
class="object-cover w-full h-48">
{% else %}
<div class="w-full h-48 bg-gray-200 dark:bg-gray-600 flex items-center justify-center">
<div class="flex items-center justify-center w-full h-48 bg-gray-200 dark:bg-gray-600">
<span class="text-gray-400">No image available</span>
</div>
{% endif %}
<div class="p-4">
<h3 class="text-lg font-semibold mb-2">
<h3 class="mb-2 text-lg font-semibold">
<a href="{% url 'parks:park_detail' park.slug %}"
class="text-blue-600 dark:text-blue-400 hover:underline">
{{ park.name }}
</a>
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-2">{{ park.location }}</p>
<div class="flex justify-between items-center">
<p class="mb-2 text-gray-600 dark:text-gray-400">{{ park.location }}</p>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ park.rides.count }} attractions
</span>
{% if park.average_rating %}
<div class="flex items-center">
<span class="text-yellow-400 mr-1"></span>
<span class="mr-1 text-yellow-400"></span>
<span class="text-gray-600 dark:text-gray-400">
{{ park.average_rating|floatformat:1 }}/10
</span>
@@ -101,7 +108,7 @@
</div>
</div>
{% empty %}
<div class="col-span-full text-center py-8">
<div class="py-8 text-center col-span-full">
<p class="text-gray-500 dark:text-gray-400">No parks found for this company.</p>
</div>
{% endfor %}

View File

@@ -0,0 +1,128 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{% if is_edit %}Edit{% else %}Add{% endif %} Company - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="max-w-3xl mx-auto">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">{% if is_edit %}Edit{% else %}Add{% endif %} Company</h1>
<form method="post" class="space-y-6">
{% csrf_token %}
<!-- Name field -->
<div>
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name
</label>
<div>
{{ form.name }}
</div>
{% if form.name.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.name.errors }}
</div>
{% endif %}
</div>
<!-- Headquarters field -->
<div x-data="locationAutocomplete('country', false)" class="relative">
<label for="{{ form.headquarters.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Headquarters
</label>
<input type="text"
id="{{ form.headquarters.id_for_label }}"
name="headquarters"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="e.g., Orlando, Florida, United States"
value="{{ form.headquarters.value|default:'' }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<!-- Website field -->
<div>
<label for="{{ form.website.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Website
</label>
<div>
{{ form.website }}
</div>
{% if form.website.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.website.errors }}
</div>
{% endif %}
</div>
<!-- Description field -->
<div>
<label for="{{ form.description.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
<div>
{{ form.description }}
</div>
{% if form.description.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.description.errors }}
</div>
{% endif %}
</div>
{% if not user.role == 'MODERATOR' and not user.role == 'ADMIN' and not user.role == 'SUPERUSER' %}
<div class="space-y-4">
<div>
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for {% if is_edit %}Edit{% else %}Addition{% endif %}
</label>
<textarea name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're {% if is_edit %}editing{% else %}adding{% endif %} this company and provide any relevant details."></textarea>
</div>
<div>
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Source (Optional)
</label>
<input type="text"
name="source"
id="source"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Link to official website, news article, or other source">
</div>
</div>
{% endif %}
<div class="flex justify-end space-x-4">
<a href="{% if is_edit %}{% url 'companies:company_detail' slug=object.slug %}{% else %}{% url 'companies:company_list' %}{% endif %}"
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500">
Cancel
</a>
<button type="submit" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
{% if is_edit %}Save Changes{% else %}Submit{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -16,12 +16,19 @@
</p>
{% endif %}
</div>
{% if manufacturer.website %}
<a href="{{ manufacturer.website }}" target="_blank" rel="noopener noreferrer"
class="mt-4 btn-secondary md:mt-0">
<i class="mr-2 fas fa-external-link-alt"></i>Visit Website
</a>
{% endif %}
<div class="flex gap-2 mt-4 md:mt-0">
{% if manufacturer.website %}
<a href="{{ manufacturer.website }}" target="_blank" rel="noopener noreferrer"
class="btn-secondary">
<i class="mr-2 fas fa-external-link-alt"></i>Visit Website
</a>
{% endif %}
{% if user.is_authenticated %}
<a href="{% url 'companies:manufacturer_edit' slug=manufacturer.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-edit"></i>Edit
</a>
{% endif %}
</div>
</div>
{% if manufacturer.description %}

View File

@@ -0,0 +1,128 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{% if is_edit %}Edit{% else %}Add{% endif %} Manufacturer - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="max-w-3xl mx-auto">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">{% if is_edit %}Edit{% else %}Add{% endif %} Manufacturer</h1>
<form method="post" class="space-y-6">
{% csrf_token %}
<!-- Name field -->
<div>
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name
</label>
<div>
{{ form.name }}
</div>
{% if form.name.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.name.errors }}
</div>
{% endif %}
</div>
<!-- Headquarters field -->
<div x-data="locationAutocomplete('country', false)" class="relative">
<label for="{{ form.headquarters.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Headquarters
</label>
<input type="text"
id="{{ form.headquarters.id_for_label }}"
name="headquarters"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="e.g., Altoona, Pennsylvania, United States"
value="{{ form.headquarters.value|default:'' }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<!-- Website field -->
<div>
<label for="{{ form.website.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Website
</label>
<div>
{{ form.website }}
</div>
{% if form.website.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.website.errors }}
</div>
{% endif %}
</div>
<!-- Description field -->
<div>
<label for="{{ form.description.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
<div>
{{ form.description }}
</div>
{% if form.description.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.description.errors }}
</div>
{% endif %}
</div>
{% if not user.role == 'MODERATOR' and not user.role == 'ADMIN' and not user.role == 'SUPERUSER' %}
<div class="space-y-4">
<div>
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for {% if is_edit %}Edit{% else %}Addition{% endif %}
</label>
<textarea name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're {% if is_edit %}editing{% else %}adding{% endif %} this manufacturer and provide any relevant details."></textarea>
</div>
<div>
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Source (Optional)
</label>
<input type="text"
name="source"
id="source"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Link to official website, news article, or other source">
</div>
</div>
{% endif %}
<div class="flex justify-end space-x-4">
<a href="{% if is_edit %}{% url 'companies:manufacturer_detail' slug=object.slug %}{% else %}{% url 'companies:manufacturer_list' %}{% endif %}"
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500">
Cancel
</a>
<button type="submit" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
{% if is_edit %}Save Changes{% else %}Submit{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,127 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Moderation - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">Moderation Queue</h1>
<!-- Tabs -->
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px" role="tablist">
<li class="mr-2">
<button class="tab-button {% if active_tab == 'new' %}active{% endif %}"
data-tab="new"
hx-get="{% url 'moderation:edit_submissions' %}?tab=new"
hx-target="#submissions-content"
hx-push-url="true">
New
{% if new_count %}<span class="ml-2 badge">{{ new_count }}</span>{% endif %}
</button>
</li>
<li class="mr-2">
<button class="tab-button {% if active_tab == 'approved' %}active{% endif %}"
data-tab="approved"
hx-get="{% url 'moderation:edit_submissions' %}?tab=approved"
hx-target="#submissions-content"
hx-push-url="true">
Approved
</button>
</li>
<li class="mr-2">
<button class="tab-button {% if active_tab == 'rejected' %}active{% endif %}"
data-tab="rejected"
hx-get="{% url 'moderation:edit_submissions' %}?tab=rejected"
hx-target="#submissions-content"
hx-push-url="true">
Rejected
</button>
</li>
{% if user.role == 'ADMIN' or user.role == 'SUPERUSER' %}
<li>
<button class="tab-button {% if active_tab == 'escalated' %}active{% endif %}"
data-tab="escalated"
hx-get="{% url 'moderation:edit_submissions' %}?tab=escalated"
hx-target="#submissions-content"
hx-push-url="true">
Escalated
{% if escalated_count %}<span class="ml-2 badge">{{ escalated_count }}</span>{% endif %}
</button>
</li>
{% endif %}
</ul>
</div>
<!-- Tab Content -->
<div id="submissions-content">
{% include 'moderation/partials/submission_list.html' %}
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.tab-button {
@apply inline-flex items-center px-4 py-2 text-sm font-medium border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300;
}
.tab-button.active {
@apply text-blue-600 border-blue-600 dark:text-blue-500 dark:border-blue-500;
}
.badge {
@apply px-2 py-1 text-xs font-semibold text-white bg-blue-500 rounded-full;
}
.submission-card {
@apply p-4 mb-4 bg-white border rounded-lg shadow dark:bg-gray-700 dark:border-gray-600;
}
.submission-header {
@apply flex items-center justify-between mb-2;
}
.submission-title {
@apply text-lg font-semibold text-gray-900 dark:text-white;
}
.submission-meta {
@apply text-sm text-gray-500 dark:text-gray-400;
}
.submission-changes {
@apply mt-4 space-y-2;
}
.change-item {
@apply flex items-start;
}
.change-label {
@apply w-32 font-medium text-gray-700 dark:text-gray-300;
}
.change-value {
@apply flex-1 text-gray-900 dark:text-white;
}
.action-buttons {
@apply flex gap-2 mt-4;
}
.btn-approve {
@apply px-4 py-2 text-white bg-green-500 rounded-lg hover:bg-green-600;
}
.btn-reject {
@apply px-4 py-2 text-white bg-red-500 rounded-lg hover:bg-red-600;
}
.btn-escalate {
@apply px-4 py-2 text-white bg-yellow-500 rounded-lg hover:bg-yellow-600;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,88 @@
{% for submission in submissions %}
<div class="submission-card">
<div class="submission-header">
<div>
<h3 class="submission-title">
{{ submission.get_content_type_display }} -
{% if submission.submission_type == 'CREATE' %}New{% else %}Edit{% endif %}
</h3>
<div class="submission-meta">
Submitted by {{ submission.user.username }} on {{ submission.created_at|date:"M d, Y H:i" }}
</div>
</div>
{% if submission.status == 'APPROVED' %}
<span class="px-2 py-1 text-sm text-white bg-green-500 rounded-full">Approved</span>
{% elif submission.status == 'REJECTED' %}
<span class="px-2 py-1 text-sm text-white bg-red-500 rounded-full">Rejected</span>
{% elif submission.status == 'ESCALATED' %}
<span class="px-2 py-1 text-sm text-white bg-yellow-500 rounded-full">Escalated</span>
{% endif %}
</div>
{% if submission.reason %}
<div class="p-3 mt-2 rounded bg-gray-50 dark:bg-gray-600">
<div class="text-sm font-medium text-gray-700 dark:text-gray-300">Reason:</div>
<div class="text-gray-600 dark:text-gray-400">{{ submission.reason }}</div>
</div>
{% endif %}
{% if submission.source %}
<div class="p-3 mt-2 rounded bg-gray-50 dark:bg-gray-600">
<div class="text-sm font-medium text-gray-700 dark:text-gray-300">Source:</div>
<div class="text-gray-600 dark:text-gray-400">
<a href="{{ submission.source }}" target="_blank" class="text-blue-500 hover:underline">
{{ submission.source }}
</a>
</div>
</div>
{% endif %}
<div class="submission-changes">
{% for field, value in submission.changes.items %}
<div class="change-item">
<div class="change-label">{{ field|title }}:</div>
<div class="change-value">{{ value }}</div>
</div>
{% endfor %}
</div>
{% if submission.status == 'NEW' %}
<div class="action-buttons">
<button class="btn-approve"
hx-post="{% url 'moderation:approve_submission' submission.id %}"
hx-target="#submissions-content">
<i class="mr-2 fas fa-check"></i>Approve
</button>
<button class="btn-reject"
hx-post="{% url 'moderation:reject_submission' submission.id %}"
hx-target="#submissions-content">
<i class="mr-2 fas fa-times"></i>Reject
</button>
{% if user.role == 'MODERATOR' %}
<button class="btn-escalate"
hx-post="{% url 'moderation:escalate_submission' submission.id %}"
hx-target="#submissions-content">
<i class="mr-2 fas fa-arrow-up"></i>Escalate
</button>
{% endif %}
</div>
{% elif submission.status == 'ESCALATED' and user.role in 'ADMIN,SUPERUSER' %}
<div class="action-buttons">
<button class="btn-approve"
hx-post="{% url 'moderation:approve_submission' submission.id %}"
hx-target="#submissions-content">
<i class="mr-2 fas fa-check"></i>Approve
</button>
<button class="btn-reject"
hx-post="{% url 'moderation:reject_submission' submission.id %}"
hx-target="#submissions-content">
<i class="mr-2 fas fa-times"></i>Reject
</button>
</div>
{% endif %}
</div>
{% empty %}
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
No submissions found in this category.
</div>
{% endfor %}

View File

@@ -3,19 +3,13 @@
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/inline-edit.css' %}">
{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<!-- Park Header -->
<div class="p-6 mb-6 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700 editable-container">
<div class="p-6 mb-6 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
<div class="flex flex-col items-start justify-between md:flex-row md:items-center">
<div>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white"
data-editable data-content-id="{{ park.id }}"
data-field-name="name">{{ park.name }}</h1>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1>
<p class="text-gray-600 dark:text-gray-300">
<i class="mr-2 fas fa-map-marker-alt"></i>
<span>{{ park.get_formatted_location }}</span>
@@ -29,12 +23,9 @@
</a>
{% endif %}
{% if user.is_authenticated %}
<button class="btn-secondary" data-edit-button
data-content-id="{{ park.id }}"
data-content-type="park"
{% if not can_auto_approve %}data-require-reason="true"{% endif %}>
<a href="{% url 'parks:park_edit' slug=park.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-edit"></i>Edit
</button>
</a>
{% endif %}
</div>
</div>
@@ -44,17 +35,7 @@
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}"
data-editable data-content-id="{{ park.id }}"
data-field-name="status" data-field-type="select"
data-options='[
{"value": "OPERATING", "label": "Operating"},
{"value": "CLOSED_TEMP", "label": "Temporarily Closed"},
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
{"value": "UNDER_CONSTRUCTION", "label": "Under Construction"},
{"value": "DEMOLISHED", "label": "Demolished"},
{"value": "RELOCATED", "label": "Relocated"}
]'>
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.average_rating %}
@@ -97,11 +78,9 @@
<!-- Left Column - Description and Areas -->
<div class="lg:col-span-2">
{% if park.description %}
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800 editable-container">
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">About</h2>
<div class="text-gray-700 dark:text-gray-300"
data-editable data-content-id="{{ park.id }}"
data-field-name="description" data-field-type="textarea">
<div class="text-gray-700 dark:text-gray-300">
{{ park.description|linebreaks }}
</div>
</div>
@@ -180,7 +159,7 @@
<!-- Right Column - Quick Facts and History -->
<div>
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800 editable-container">
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">Quick Facts</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-1">
{% if park.owner %}
@@ -212,9 +191,7 @@
<i class="w-5 text-blue-500 fas fa-calendar-alt dark:text-blue-400"></i>
<span class="ml-2">Opening Date</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ park.id }}"
data-field-name="opening_date" data-field-type="date">
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.opening_date }}
</dd>
</div>
@@ -225,9 +202,7 @@
<i class="w-5 text-blue-500 fas fa-calendar-times dark:text-blue-400"></i>
<span class="ml-2">Closing Date</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ park.id }}"
data-field-name="closing_date" data-field-type="date">
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.closing_date }}
</dd>
</div>
@@ -238,9 +213,7 @@
<i class="w-5 text-blue-500 fas fa-clock dark:text-blue-400"></i>
<span class="ml-2">Operating Season</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ park.id }}"
data-field-name="operating_season">
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.operating_season }}
</dd>
</div>
@@ -251,9 +224,7 @@
<i class="w-5 text-blue-500 fas fa-ruler-combined dark:text-blue-400"></i>
<span class="ml-2">Size</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ park.id }}"
data-field-name="size_acres" data-field-type="number">
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.size_acres }} acres
</dd>
</div>
@@ -307,7 +278,3 @@
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/inline-edit.js' %}"></script>
{% endblock %}

View File

@@ -1,15 +1,31 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Add Park - ThrillWiki{% endblock %}
{% block title %}{% if is_edit %}Edit{% else %}Add{% endif %} Park - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="max-w-3xl mx-auto">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">Add Park</h1>
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">{% if is_edit %}Edit{% else %}Add{% endif %} Park</h1>
<form method="post" class="space-y-6">
{% if form.errors %}
<div class="p-4 mb-6 text-red-700 bg-red-100 border border-red-400 rounded-lg dark:bg-red-900 dark:text-red-100 dark:border-red-700">
<p class="font-medium">Please correct the following errors:</p>
<ul class="ml-4 list-disc">
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post" class="space-y-6" id="park-form">
{% csrf_token %}
<!-- Hidden fields -->
@@ -20,7 +36,7 @@
<!-- Name field -->
<div>
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name
Name *
</label>
<div>
{{ form.name }}
@@ -33,46 +49,88 @@
</div>
<!-- Location fields -->
<div>
<div x-data="locationAutocomplete('country', false)" class="relative">
<label for="{{ form.country_name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Country
Country *
</label>
<div>
{{ form.country_name }}
</div>
{% if form.country_name.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.country_name.errors }}
</div>
{% endif %}
<input type="text"
id="id_country_name"
name="country_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select country..."
value="{{ form.country_name.value|default:'' }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<div>
<div x-data="locationAutocomplete('region', false)" class="relative">
<label for="{{ form.region_name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Region/State
</label>
<div>
{{ form.region_name }}
</div>
{% if form.region_name.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.region_name.errors }}
</div>
{% endif %}
<input type="text"
id="id_region_name"
name="region_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select region/state..."
value="{{ form.region_name.value|default:'' }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<div>
<div x-data="locationAutocomplete('city', false)" class="relative">
<label for="{{ form.city_name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<div>
{{ form.city_name }}
</div>
{% if form.city_name.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.city_name.errors }}
</div>
{% endif %}
<input type="text"
id="id_city_name"
name="city_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select city..."
value="{{ form.city_name.value|default:'' }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<!-- Other fields -->
@@ -80,7 +138,7 @@
{% if field.name not in 'name,country,region,city,country_name,region_name,city_name' %}
<div>
<label for="{{ field.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ field.label }}
{{ field.label }}{% if field.field.required %} *{% endif %}
</label>
<div>
{{ field }}
@@ -98,17 +156,20 @@
{% endfor %}
{% if not user.role == 'MODERATOR' and not user.role == 'ADMIN' and not user.role == 'SUPERUSER' %}
<div class="p-4 mb-4 text-blue-700 bg-blue-100 border border-blue-400 rounded-lg dark:bg-blue-900 dark:text-blue-100 dark:border-blue-700">
<p>Your submission will be reviewed by a moderator before being published.</p>
</div>
<div class="space-y-4">
<div>
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for Addition
Reason for {% if is_edit %}Edit{% else %}Addition{% endif %} *
</label>
<textarea name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're adding this park and provide any relevant details."></textarea>
placeholder="Please explain why you're {% if is_edit %}editing{% else %}adding{% endif %} this park and provide any relevant details."></textarea>
</div>
<div>
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -124,11 +185,12 @@
{% endif %}
<div class="flex justify-end space-x-4">
<a href="{% url 'parks:park_list' %}" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500">
<a href="{% if is_edit %}{% url 'parks:park_detail' slug=object.slug %}{% else %}{% url 'parks:park_list' %}{% endif %}"
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500">
Cancel
</a>
<button type="submit" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
Submit
{% if is_edit %}Save Changes{% else %}Submit{% endif %}
</button>
</div>
</form>
@@ -136,100 +198,3 @@
</div>
</div>
{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css" />
<style>
.awesomplete {
width: 100%;
}
.awesomplete > ul {
@apply bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg;
}
.awesomplete > ul > li {
@apply px-4 py-2 cursor-pointer text-gray-700 dark:text-gray-300;
}
.awesomplete > ul > li:hover,
.awesomplete > ul > li[aria-selected="true"] {
@apply bg-gray-100 dark:bg-gray-600;
}
.awesomplete mark {
@apply bg-blue-100 dark:bg-blue-900;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Helper function to initialize Awesomplete
function initAwesomplete(input, url, params = {}) {
if (!input) return null;
var awesomplete = new Awesomplete(input, {
minChars: 1,
maxItems: 10,
autoFirst: true
});
input.addEventListener('input', function() {
// Build query parameters
const queryParams = new URLSearchParams({
q: this.value,
...Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, typeof value === 'function' ? value() : value])
)
});
fetch(`${url}?${queryParams}`)
.then(response => response.json())
.then(data => {
awesomplete.list = data;
});
});
return awesomplete;
}
// Initialize Awesomplete for each location field
var countryInput = document.getElementById('id_country_name');
var regionInput = document.getElementById('id_region_name');
var cityInput = document.getElementById('id_city_name');
var countryHidden = document.getElementById('id_country');
var regionHidden = document.getElementById('id_region');
var cityHidden = document.getElementById('id_city');
var countryAwesomplete = initAwesomplete(countryInput, '/parks/ajax/countries/');
if (regionInput) {
var regionAwesomplete = initAwesomplete(regionInput, '/parks/ajax/regions/', {
country: () => countryInput ? countryInput.value : ''
});
// Clear dependent fields when country changes
countryInput.addEventListener('awesomplete-select', function(event) {
regionInput.value = '';
regionHidden.value = '';
if (cityInput) {
cityInput.value = '';
cityHidden.value = '';
}
});
}
if (cityInput) {
var cityAwesomplete = initAwesomplete(cityInput, '/parks/ajax/cities/', {
country: () => countryInput ? countryInput.value : '',
region: () => regionInput ? regionInput.value : ''
});
// Clear city when region changes
regionInput.addEventListener('awesomplete-select', function(event) {
cityInput.value = '';
cityHidden.value = '';
});
}
});
</script>
{% endblock %}

View File

@@ -16,9 +16,9 @@
<!-- Filters -->
<div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<form id="park-filters" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-5"
<form id="park-filters" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
hx-get="{% url 'parks:park_list' %}"
hx-trigger="change from:select, input from:input[type='text'] delay:500ms"
hx-trigger="change from:select, input from:input[type='text'] delay:500ms, click from:.status-filter"
hx-target="#parks-grid"
hx-push-url="true">
<div>
@@ -28,41 +28,134 @@
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Search parks...">
</div>
<div>
<!-- Country Field -->
<div x-data="locationAutocomplete('country', true)" class="relative">
<label for="country" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
<input type="text" name="country" id="country"
<input type="text"
name="country"
id="country"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select country..."
value="{{ current_filters.country }}"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select country...">
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<div>
<!-- Region Field -->
<div x-data="locationAutocomplete('region', true)" class="relative">
<label for="region" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">State/Region</label>
<input type="text" name="region" id="region"
<input type="text"
name="region"
id="region"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select state/region..."
value="{{ current_filters.region }}"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select state/region...">
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<div>
<!-- City Field -->
<div x-data="locationAutocomplete('city', true)" class="relative">
<label for="city" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">City</label>
<input type="text" name="city" id="city"
value="{{ current_filters.city }}"
<input type="text"
name="city"
id="city"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select city...">
</div>
<div>
<label for="status" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Status</label>
<select name="status" id="status"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">All Statuses</option>
<option value="OPERATING" {% if current_filters.status == 'OPERATING' %}selected{% endif %}>Operating</option>
<option value="CLOSED_TEMP" {% if current_filters.status == 'CLOSED_TEMP' %}selected{% endif %}>Temporarily Closed</option>
<option value="CLOSED_PERM" {% if current_filters.status == 'CLOSED_PERM' %}selected{% endif %}>Permanently Closed</option>
<option value="UNDER_CONSTRUCTION" {% if current_filters.status == 'UNDER_CONSTRUCTION' %}selected{% endif %}>Under Construction</option>
<option value="DEMOLISHED" {% if current_filters.status == 'DEMOLISHED' %}selected{% endif %}>Demolished</option>
<option value="RELOCATED" {% if current_filters.status == 'RELOCATED' %}selected{% endif %}>Relocated</option>
</select>
placeholder="Select city..."
value="{{ current_filters.city }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
</ul>
</div>
<!-- Hidden inputs for selected statuses -->
{% for status in current_filters.statuses %}
<input type="hidden" name="status" value="{{ status }}">
{% endfor %}
</form>
<!-- Status Filter Icons -->
<div class="flex flex-wrap gap-2 mt-4">
<label class="block w-full mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Status Filter</label>
<button type="button"
class="status-filter status-badge status-operating {% if 'OPERATING' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
data-status="OPERATING"
onclick="toggleStatus(this, 'OPERATING')">
<i class="mr-1 fas fa-check-circle"></i>Operating
</button>
<button type="button"
class="status-filter status-badge status-closed {% if 'CLOSED_TEMP' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
data-status="CLOSED_TEMP"
onclick="toggleStatus(this, 'CLOSED_TEMP')">
<i class="mr-1 fas fa-clock"></i>Temporarily Closed
</button>
<button type="button"
class="status-filter status-badge status-closed {% if 'CLOSED_PERM' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
data-status="CLOSED_PERM"
onclick="toggleStatus(this, 'CLOSED_PERM')">
<i class="mr-1 fas fa-times-circle"></i>Permanently Closed
</button>
<button type="button"
class="status-filter status-badge status-construction {% if 'UNDER_CONSTRUCTION' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
data-status="UNDER_CONSTRUCTION"
onclick="toggleStatus(this, 'UNDER_CONSTRUCTION')">
<i class="mr-1 fas fa-hard-hat"></i>Under Construction
</button>
<button type="button"
class="status-filter status-badge status-demolished {% if 'DEMOLISHED' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
data-status="DEMOLISHED"
onclick="toggleStatus(this, 'DEMOLISHED')">
<i class="mr-1 fas fa-ban"></i>Demolished
</button>
<button type="button"
class="status-filter status-badge status-relocated {% if 'RELOCATED' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
data-status="RELOCATED"
onclick="toggleStatus(this, 'RELOCATED')">
<i class="mr-1 fas fa-truck-moving"></i>Relocated
</button>
</div>
</div>
<!-- Parks Grid -->
@@ -70,107 +163,28 @@
{% include "parks/partials/park_list.html" %}
</div>
</div>
{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css" />
<style>
.awesomplete {
width: 100%;
}
.awesomplete > ul {
@apply bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg;
}
.awesomplete > ul > li {
@apply px-4 py-2 cursor-pointer text-gray-700 dark:text-gray-300;
}
.awesomplete > ul > li:hover,
.awesomplete > ul > li[aria-selected="true"] {
@apply bg-gray-100 dark:bg-gray-600;
}
.awesomplete mark {
@apply bg-blue-100 dark:bg-blue-900;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const countryInput = document.getElementById('country');
const regionInput = document.getElementById('region');
const cityInput = document.getElementById('city');
// Initialize Awesomplete for country
if (countryInput) {
const countryList = new Awesomplete(countryInput, {
minChars: 1,
maxItems: 10,
autoFirst: true
});
countryInput.addEventListener('input', function() {
fetch(`/parks/ajax/countries/?q=${encodeURIComponent(this.value)}`)
.then(response => response.json())
.then(data => {
countryList.list = data;
});
});
function toggleStatus(button, status) {
const form = document.getElementById('park-filters');
const existingInputs = form.querySelectorAll(`input[name="status"][value="${status}"]`);
if (existingInputs.length > 0) {
// Status is already selected, remove it
existingInputs.forEach(input => input.remove());
button.classList.remove('ring-2', 'ring-blue-500');
} else {
// Status is not selected, add it
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'status';
input.value = status;
form.appendChild(input);
button.classList.add('ring-2', 'ring-blue-500');
}
// Initialize Awesomplete for region
if (regionInput) {
const regionList = new Awesomplete(regionInput, {
minChars: 1,
maxItems: 10,
autoFirst: true
});
regionInput.addEventListener('input', function() {
const country = countryInput.value;
fetch(`/parks/ajax/regions/?q=${encodeURIComponent(this.value)}&country=${encodeURIComponent(country)}`)
.then(response => response.json())
.then(data => {
regionList.list = data;
});
});
}
// Initialize Awesomplete for city
if (cityInput) {
const cityList = new Awesomplete(cityInput, {
minChars: 1,
maxItems: 10,
autoFirst: true
});
cityInput.addEventListener('input', function() {
const country = countryInput.value;
const region = regionInput.value;
fetch(`/parks/ajax/cities/?q=${encodeURIComponent(this.value)}&country=${encodeURIComponent(country)}&region=${encodeURIComponent(region)}`)
.then(response => response.json())
.then(data => {
cityList.list = data;
});
});
}
// Handle location link clicks
document.querySelectorAll('.location-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const params = new URLSearchParams(this.getAttribute('href').split('?')[1]);
// Update form inputs
countryInput.value = params.get('country') || '';
regionInput.value = params.get('region') || '';
cityInput.value = params.get('city') || '';
// Trigger form submission
htmx.trigger('#park-filters', 'change');
});
});
});
// Trigger form submission
form.dispatchEvent(new Event('change'));
}
</script>
{% endblock %}

View File

@@ -3,19 +3,13 @@
{% block title %}{{ ride.name }} at {{ ride.park.name }} - ThrillWiki{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/inline-edit.css' %}">
{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<!-- Header -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800 editable-container">
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-start justify-between">
<div>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="name">{{ ride.name }}</h1>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ ride.name }}</h1>
<p class="mb-2 text-gray-600 dark:text-gray-400">
at <a href="{% url 'parks:park_detail' ride.park.slug %}" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.park.name }}
@@ -29,30 +23,10 @@
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif ride.status == 'DEMOLISHED' %}status-demolished
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}"
data-editable data-content-id="{{ ride.id }}"
data-field-name="status" data-field-type="select"
data-options='[
{"value": "OPERATING", "label": "Operating"},
{"value": "CLOSED_TEMP", "label": "Temporarily Closed"},
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
{"value": "UNDER_CONSTRUCTION", "label": "Under Construction"},
{"value": "DEMOLISHED", "label": "Demolished"},
{"value": "RELOCATED", "label": "Relocated"}
]'>
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ ride.get_status_display }}
</span>
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-700 dark:text-blue-50"
data-editable data-content-id="{{ ride.id }}"
data-field-name="category" data-field-type="select"
data-options='[
{"value": "RC", "label": "Roller Coaster"},
{"value": "DR", "label": "Dark Ride"},
{"value": "FR", "label": "Flat Ride"},
{"value": "WR", "label": "Water Ride"},
{"value": "TR", "label": "Transport"},
{"value": "OT", "label": "Other"}
]'>
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-700 dark:text-blue-50">
{{ ride.get_category_display }}
</span>
{% if ride.average_rating %}
@@ -65,13 +39,10 @@
</div>
{% if user.is_authenticated %}
<div class="flex gap-2">
<button class="btn-secondary" data-edit-button
data-content-id="{{ ride.id }}"
data-content-type="ride"
{% if not can_auto_approve %}data-require-reason="true"{% endif %}>
<a href="{% url 'parks:rides:ride_edit' park_slug=ride.park.slug ride_slug=ride.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-edit"></i>
Edit
</button>
</a>
</div>
{% endif %}
</div>
@@ -97,11 +68,9 @@
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Column - Description and Details -->
<div class="lg:col-span-2">
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800 editable-container">
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">About</h2>
<div class="prose dark:prose-invert max-w-none"
data-editable data-content-id="{{ ride.id }}"
data-field-name="description" data-field-type="textarea">
<div class="prose dark:prose-invert max-w-none">
{{ ride.description|linebreaks }}
</div>
</div>
@@ -121,15 +90,13 @@
{% endif %}
{% if coaster_stats %}
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800 editable-container">
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Roller Coaster Statistics</h2>
<div class="grid grid-cols-2 gap-4 md:grid-cols-3">
{% if coaster_stats.height_ft %}
<div>
<span class="block text-gray-500">Height</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"
data-editable data-content-id="{{ coaster_stats.id }}"
data-field-name="height_ft" data-field-type="number">
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.height_ft }} ft
</span>
</div>
@@ -137,9 +104,7 @@
{% if coaster_stats.length_ft %}
<div>
<span class="block text-gray-500">Length</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"
data-editable data-content-id="{{ coaster_stats.id }}"
data-field-name="length_ft" data-field-type="number">
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.length_ft }} ft
</span>
</div>
@@ -147,27 +112,21 @@
{% if coaster_stats.speed_mph %}
<div>
<span class="block text-gray-500">Speed</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"
data-editable data-content-id="{{ coaster_stats.id }}"
data-field-name="speed_mph" data-field-type="number">
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.speed_mph }} mph
</span>
</div>
{% endif %}
<div>
<span class="block text-gray-500">Inversions</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"
data-editable data-content-id="{{ coaster_stats.id }}"
data-field-name="inversions" data-field-type="number">
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.inversions }}
</span>
</div>
{% if coaster_stats.ride_time_seconds %}
<div>
<span class="block text-gray-500">Ride Duration</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"
data-editable data-content-id="{{ coaster_stats.id }}"
data-field-name="ride_time_seconds" data-field-type="number">
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.ride_time_seconds }} sec
</span>
</div>
@@ -179,29 +138,23 @@
<!-- Right Column - Quick Facts -->
<div class="lg:col-span-1">
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800 editable-container">
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Quick Facts</h2>
<dl class="space-y-4">
<div>
<dt class="text-gray-500">Manufacturer</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="manufacturer">{{ ride.manufacturer }}</dd>
<dd class="font-medium text-gray-900 dark:text-white">{{ ride.manufacturer }}</dd>
</div>
{% if ride.model_name %}
<div>
<dt class="text-gray-500">Model</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="model_name">{{ ride.model_name }}</dd>
<dd class="font-medium text-gray-900 dark:text-white">{{ ride.model_name }}</dd>
</div>
{% endif %}
{% if ride.opening_date %}
<div>
<dt class="text-gray-500">Opening Date</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="opening_date" data-field-type="date">
<dd class="font-medium text-gray-900 dark:text-white">
{{ ride.opening_date }}
</dd>
</div>
@@ -209,9 +162,7 @@
{% if ride.status_since %}
<div>
<dt class="text-gray-500">Status Since</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="status_since" data-field-type="date">
<dd class="font-medium text-gray-900 dark:text-white">
{{ ride.status_since }}
</dd>
</div>
@@ -219,9 +170,7 @@
{% if ride.closing_date %}
<div>
<dt class="text-gray-500">Closing Date</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="closing_date" data-field-type="date">
<dd class="font-medium text-gray-900 dark:text-white">
{{ ride.closing_date }}
</dd>
</div>
@@ -229,9 +178,7 @@
{% if ride.capacity_per_hour %}
<div>
<dt class="text-gray-500">Capacity</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="capacity_per_hour" data-field-type="number">
<dd class="font-medium text-gray-900 dark:text-white">
{{ ride.capacity_per_hour }} riders/hour
</dd>
</div>
@@ -239,9 +186,7 @@
{% if ride.min_height_in %}
<div>
<dt class="text-gray-500">Minimum Height</dt>
<dd class="font-medium text-gray-900 dark:text-white"
data-editable data-content-id="{{ ride.id }}"
data-field-name="min_height_in" data-field-type="number">
<dd class="font-medium text-gray-900 dark:text-white">
{{ ride.min_height_in }} inches
</dd>
</div>
@@ -332,7 +277,3 @@
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/inline-edit.js' %}"></script>
{% endblock %}

View File

@@ -1,14 +1,14 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Add Ride at {{ park.name }} - ThrillWiki{% endblock %}
{% block title %}{% if is_edit %}Edit{% else %}Add{% endif %} Ride at {{ park.name }} - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="max-w-3xl mx-auto">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Add Ride at {{ park.name }}</h1>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{% if is_edit %}Edit{% else %}Add{% endif %} Ride at {{ park.name }}</h1>
<a href="{% url 'parks:rides:ride_list' park.slug %}" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
Back to {{ park.name }} Rides
</a>
@@ -62,14 +62,14 @@
<div class="space-y-4">
<div>
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for Addition
Reason for {% if is_edit %}Edit{% else %}Addition{% endif %}
</label>
<textarea name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're adding this ride and provide any relevant details."></textarea>
placeholder="Please explain why you're {% if is_edit %}editing{% else %}adding{% endif %} this ride and provide any relevant details."></textarea>
</div>
<div>
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -85,11 +85,12 @@
{% endif %}
<div class="flex justify-end space-x-4">
<a href="{% url 'parks:rides:ride_list' park.slug %}" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500">
<a href="{% if is_edit %}{% url 'parks:rides:ride_detail' park.slug object.slug %}{% else %}{% url 'parks:rides:ride_list' park.slug %}{% endif %}"
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500">
Cancel
</a>
<button type="submit" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
Submit
{% if is_edit %}Save Changes{% else %}Submit{% endif %}
</button>
</div>
</form>