mirror of
https://github.com/wizarrrr/wizarr.git
synced 2026-07-30 23:07:19 -04:00
Add overflow-hidden to admin layout's h-screen container to prevent body-level scrolling competing with #content's overflow-y-auto. Mark LDAP settings as experimental with a badge.
390 lines
21 KiB
HTML
390 lines
21 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}
|
|
{{ _("Admin") }}
|
|
{% endblock title %}
|
|
{% block main %}
|
|
<div class="flex flex-col h-screen overflow-hidden">
|
|
{% if update_available %}
|
|
<div class="w-full bg-primary text-white text-center py-1">
|
|
<a href="https://docs.wizarr.dev/getting-started/installation">
|
|
<span class="font-medium">A new update is available!</span>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
<!-- Desktop Navigation -->
|
|
<nav hx-ext="preload"
|
|
class="hidden md:block border-gray-200 px-2 sm:px-4 py-2.5 rounded-sm dark:bg-gray-900">
|
|
<div class="container flex flex-wrap items-center justify-between mx-auto">
|
|
<a href="{{ url_for('admin.home') }}" class="flex items-center">
|
|
<img src="{{ url_for('static', filename='wizarr-logo.png') }}"
|
|
alt="Wizarr Logo"
|
|
class="h-8 mr-3">
|
|
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Wizarr</span>
|
|
</a>
|
|
<div class="w-full md:w-auto" id="navbar-default">
|
|
<div class="flex flex-col md:flex-row md:items-center gap-4 md:gap-8 text-sm font-medium">
|
|
<!-- Main Navigation -->
|
|
<ul class="flex flex-col md:flex-row md:items-center gap-2 md:gap-4 md:space-y-0 space-y-2">
|
|
{% set activity_url = url_for('activity.activity_dashboard') %}
|
|
{% set tabs = [('/home', '#', 'Home'), ('/invites', '#invites', 'Invitations'), ('/users', '#users', 'Users'), (activity_url, '#activity', 'Activity'), ('/settings', '#settings', 'Settings')] %}
|
|
{% for href, hash, label in tabs %}
|
|
<li>
|
|
<button hx-get="{{ href }}"
|
|
preload="mouseover"
|
|
hx-trigger="click"
|
|
hx-target="#content"
|
|
hx-swap="innerHTML"
|
|
hx-push-url="{{ hash }}"
|
|
class="nav-btn block px-4 py-2 rounded-lg text-gray-600 dark:text-gray-400 hover:text-primary hover:bg-gray-50 dark:hover:text-white dark:hover:bg-gray-800 transition-all duration-200"
|
|
onmouseenter="this.nextElementSibling.dispatchEvent(new Event('hover'))">
|
|
{{ _(label) }}
|
|
</button>
|
|
<!-- Page-specific prefetches -->
|
|
{% if href == "/home" %}
|
|
<div hx-get="/now-playing-cards"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
{% elif href == "/invites" %}
|
|
<div hx-post="/invite/table"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
{% elif href == "/users" %}
|
|
<div hx-get="/users/table"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
{% elif href == activity_url %}
|
|
<div hx-get="{{ url_for('activity.activity_grid') }}"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
<div hx-get="{{ url_for('activity.activity_stats') }}"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
{% elif href == "/plus" %}
|
|
<div hx-get="/plus/overview"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
{% elif href == "/settings" %}
|
|
<div hx-get="/settings/general"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
<div hx-get="/settings/api-keys"
|
|
hx-trigger="hover once"
|
|
hx-swap="none"
|
|
style="display: none"></div>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<!-- Divider -->
|
|
<div class="hidden md:block w-px h-6 bg-gray-300 dark:bg-gray-600"></div>
|
|
<!-- User Controls -->
|
|
<div class="flex flex-col md:flex-row md:items-center gap-3 md:gap-4 pt-4 md:pt-0 border-t md:border-0 border-gray-200 dark:border-gray-700">
|
|
<!-- User Profile -->
|
|
<div class="relative" x-data="{ open: false }">
|
|
<button @click="open = !open"
|
|
class="flex items-center gap-2 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-white px-3 py-2 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200">
|
|
<div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-white"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z">
|
|
</path>
|
|
</svg>
|
|
</div>
|
|
<span class="text-sm font-medium">{{ current_user.username }}</span>
|
|
<svg class="w-4 h-4 transition-transform"
|
|
:class="open ? 'rotate-180' : ''"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
</button>
|
|
<!-- Dropdown Menu -->
|
|
<div x-show="open"
|
|
@click.away="open = false"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-75"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
|
<div class="py-1">
|
|
<a href="#profile"
|
|
hx-get="/profile"
|
|
hx-target="#content"
|
|
hx-swap="innerHTML"
|
|
hx-push-url="#profile"
|
|
class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
<div class="flex items-center gap-2">
|
|
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z">
|
|
</path>
|
|
</svg>
|
|
{{ _("Profile") }}
|
|
</div>
|
|
</a>
|
|
<div class="border-t border-gray-200 dark:border-gray-600"></div>
|
|
<a href="{{ url_for('auth.logout') }}"
|
|
class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
<div class="flex items-center gap-2">
|
|
<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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1">
|
|
</path>
|
|
</svg>
|
|
{{ _("Logout") }}
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Theme toggle -->
|
|
<button id="theme-toggle"
|
|
type="button"
|
|
class="text-gray-600 dark:text-gray-400 hover:text-primary hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-hidden p-2 rounded-md transition-all duration-200">
|
|
<svg id="theme-toggle-dark-icon"
|
|
class="block dark:hidden w-5 h-5"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
|
</svg>
|
|
<svg id="theme-toggle-light-icon"
|
|
class="hidden dark:block w-5 h-5"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<!-- Mobile Header (logo only) -->
|
|
<header class="md:hidden border-gray-200 px-4 py-2.5 dark:bg-gray-900 border-b dark:border-gray-700">
|
|
<div class="flex items-center justify-between">
|
|
<a href="{{ url_for('admin.home') }}" class="flex items-center">
|
|
<img src="{{ url_for('static', filename='wizarr-logo.png') }}"
|
|
alt="Wizarr Logo"
|
|
class="h-8 mr-3">
|
|
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Wizarr</span>
|
|
</a>
|
|
<div class="flex items-center gap-2">
|
|
<!-- User Profile for mobile -->
|
|
<button hx-get="/profile"
|
|
hx-trigger="click"
|
|
hx-target="#content"
|
|
hx-swap="innerHTML"
|
|
hx-push-url="#profile"
|
|
class="text-gray-600 dark:text-gray-400 hover:text-primary hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-hidden p-2 rounded-md transition-all duration-200">
|
|
<div class="w-6 h-6 bg-primary rounded-full flex items-center justify-center">
|
|
<svg class="w-4 h-4 text-white"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z">
|
|
</path>
|
|
</svg>
|
|
</div>
|
|
</button>
|
|
<!-- Theme toggle for mobile -->
|
|
<button id="mobile-theme-toggle"
|
|
type="button"
|
|
class="text-gray-600 dark:text-gray-400 hover:text-primary hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-hidden p-2 rounded-md transition-all duration-200">
|
|
<svg class="block dark:hidden w-5 h-5"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
|
</svg>
|
|
<svg class="hidden dark:block w-5 h-5"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<div id="content" class="flex-1 z-10 overflow-y-auto pb-16 md:pb-0"></div>
|
|
<!-- Mobile Bottom Navigation -->
|
|
<nav class="md:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 z-50">
|
|
<div class="grid grid-cols-5 h-16">
|
|
{% set activity_url = url_for('activity.activity_dashboard') %}
|
|
{% set mobile_tabs = [
|
|
('/home', '#', 'Home', 'm4 12 8-8 8 8M6 10.5V19a1 1 0 0 0 1 1h3v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h3a1 1 0 0 0 1-1v-8.5'),
|
|
('/invites', '#invites', 'Invitations', 'm3.5 5.5 7.893 6.036a1 1 0 0 0 1.214 0L20.5 5.5M4 19h16a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1Z'),
|
|
('/users', '#users', 'Users', 'M16 19h4a1 1 0 0 0 1-1v-1a3 3 0 0 0-3-3h-2m-2.236-4a3 3 0 1 0 0-4M3 18v-1a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Zm8-10a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z'),
|
|
(activity_url, '#activity', 'Activity', 'M5 3a1 1 0 0 1 1 1v12a1 1 0 1 1-2 0V4a1 1 0 0 1 1-1Zm7 4a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm7 4a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z'),
|
|
('/settings', '#settings', 'Settings', 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 0 1 6 0z')
|
|
] %}
|
|
{% for href, hash, label, icon_path in mobile_tabs %}
|
|
<button hx-get="{{ href }}"
|
|
hx-trigger="click"
|
|
hx-target="#content"
|
|
hx-swap="innerHTML"
|
|
hx-push-url="{{ hash }}"
|
|
class="mobile-nav-btn flex flex-col items-center justify-center py-2 px-1 text-gray-500 dark:text-gray-400 hover:text-primary dark:hover:text-white transition-colors duration-200">
|
|
<svg class="w-6 h-6 mb-1"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ icon_path }}"></path>
|
|
</svg>
|
|
<span class="text-xs font-medium">{{ _(label) }}</span>
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</nav>
|
|
{% set preload_hashes = ["#invites", "#users", "#activity", "#settings", "#profile"] %}
|
|
{% set preload_pages = ["invites", "users", "activity", "settings", "profile"] %}
|
|
<div hx-get="/home"
|
|
hx-target="#content"
|
|
hx-swap="innerHTML"
|
|
hx-trigger="load[!{{ preload_hashes | tojson }}.includes(window.location.hash)]"
|
|
style="display:none"></div>
|
|
{% for page in preload_pages %}
|
|
<div hx-get="/{{ page }}"
|
|
hx-target="#content"
|
|
hx-swap="innerHTML"
|
|
hx-trigger="load[window.location.hash === '#{{ page }}']"
|
|
style="display:none"></div>
|
|
{% endfor %}
|
|
<!-- Support Banner (Flask/Jinja) -->
|
|
<div id="support-banner"
|
|
class="hidden md:flex bg-gray-800 text-white text-center flex-col md:flex-row justify-center items-center gap-4 py-1">
|
|
<!-- Call-to-action -->
|
|
<span class="text-sm sm:text-base">
|
|
If you like Wizarr, please
|
|
<a href="https://github.com/sponsors/mtthidoteu"
|
|
class="underline font-semibold text-indigo-300 hover:text-indigo-200 transition">support development</a>
|
|
♥️
|
|
or
|
|
<a href="https://github.com/mtthidoteu"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="inline-flex items-center gap-1 text-yellow-300 hover:text-yellow-200 transition underline">
|
|
help me find work
|
|
</a>
|
|
💼
|
|
</span>
|
|
<!-- Avatars provided by the Flask template variable `sponsors` -->
|
|
<div id="sponsor-carousel"
|
|
class="flex items-center gap-6 overflow-hidden min-h-[60px]">
|
|
{# sponsors come as [{"sponsorEntity": {...}}, ...] #}
|
|
{% for s in sponsors %}
|
|
{% set p = s.sponsorEntity %}
|
|
<a href="{{ p.url }}"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="flex items-center w-80 justify-center animate__animated
|
|
{% if not loop.first %}
|
|
hidden
|
|
{% else %}
|
|
animate__fadeIn
|
|
{% endif %}">
|
|
<span class="text-xs mr-2 text-gray-300 truncate">Thank you,</span>
|
|
<img src="{{ p.avatarUrl }}"
|
|
alt="{{ p.login }}"
|
|
width="36"
|
|
height="36"
|
|
class="rounded-full shadow-lg shrink-0">
|
|
<span class="text-xs ml-2 text-gray-300 truncate">{{ p.login|capitalize }}</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
const ROTATE_MS = 3000;
|
|
const items = Array.from(document.querySelectorAll('#sponsor-carousel a'));
|
|
if (items.length <= 1) return;
|
|
|
|
let current = 0;
|
|
setInterval(() => {
|
|
const prev = items[current];
|
|
// Fade out the current item
|
|
prev.classList.remove('animate__fadeIn');
|
|
prev.classList.add('animate__fadeOut');
|
|
|
|
// After fade-out animation, hide it
|
|
setTimeout(() => {
|
|
prev.classList.add('hidden');
|
|
prev.classList.remove('animate__animated', 'animate__fadeOut');
|
|
|
|
// Show next item with fade-in
|
|
current = (current + 1) % items.length;
|
|
const next = items[current];
|
|
next.classList.remove('hidden');
|
|
next.classList.add('animate__animated', 'animate__fadeIn');
|
|
}, 500); // Animate.css default fade duration (~500ms)
|
|
}, ROTATE_MS);
|
|
})();
|
|
</script>
|
|
<script>
|
|
// Highlight active tab with pill-style background instead of underline
|
|
function highlightNav() {
|
|
const hash = window.location.hash || "#";
|
|
|
|
// Highlight desktop navigation
|
|
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
const target = btn.getAttribute('hx-push-url');
|
|
if (target === hash) {
|
|
btn.classList.add('bg-primary', 'text-white', 'font-semibold', 'shadow-sm');
|
|
btn.classList.remove('text-gray-600', 'dark:text-gray-400', 'hover:text-primary', 'hover:bg-gray-50', 'dark:hover:text-white', 'dark:hover:bg-gray-800');
|
|
} else {
|
|
btn.classList.remove('bg-primary', 'text-white', 'font-semibold', 'shadow-sm');
|
|
btn.classList.add('text-gray-600', 'dark:text-gray-400', 'hover:text-primary', 'hover:bg-gray-50', 'dark:hover:text-white', 'dark:hover:bg-gray-800');
|
|
}
|
|
});
|
|
|
|
// Highlight mobile navigation
|
|
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
|
|
const target = btn.getAttribute('hx-push-url');
|
|
if (target === hash) {
|
|
btn.classList.add('text-primary', 'dark:text-primary');
|
|
btn.classList.remove('text-gray-500', 'dark:text-gray-400');
|
|
} else {
|
|
btn.classList.remove('text-primary', 'dark:text-primary');
|
|
btn.classList.add('text-gray-500', 'dark:text-gray-400');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Mobile theme toggle functionality
|
|
function setupMobileThemeToggle() {
|
|
const mobileToggle = document.getElementById('mobile-theme-toggle');
|
|
const desktopToggle = document.getElementById('theme-toggle');
|
|
|
|
if (mobileToggle && desktopToggle) {
|
|
mobileToggle.addEventListener('click', function() {
|
|
// Trigger the existing desktop theme toggle functionality
|
|
desktopToggle.click();
|
|
});
|
|
}
|
|
}
|
|
|
|
window.addEventListener('hashchange', highlightNav);
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
highlightNav();
|
|
setupMobileThemeToggle();
|
|
});
|
|
// Also tie into HTMX after content swap to ensure hash may be set programmatically
|
|
document.body.addEventListener('htmx:afterSwap', highlightNav);
|
|
</script>
|
|
{% endblock main %}
|