284 lines
13 KiB
HTML
284 lines
13 KiB
HTML
{% load static %}
|
|
<!DOCTYPE html>
|
|
<html lang="en" data-theme="{{ request.session.theme|default:'classic' }}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{% block title %}Illustrations{% endblock %}</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="stylesheet" href="{% static 'app.css' %}">
|
|
|
|
{# === THEME SYSTEM (BEGIN) === #}
|
|
<link rel="stylesheet" href="{% static 'css/theme-base.css' %}">
|
|
<!-- default to classic; JS below will swap based on storage/session -->
|
|
<link id="theme-css" rel="stylesheet" href="{% static 'themes/classic.css' %}?v={{ APP_VERSION }}">
|
|
<script>
|
|
// Pre-resolve every theme to its exact static URL (works with hashed filenames).
|
|
window.THEME_URLS = {
|
|
{% for t in available_themes %}
|
|
"{{ t }}": "{% static 'themes/'|add:t|add:'.css' %}?v={{ APP_VERSION }}"{% if not forloop.last %},{% endif %}
|
|
{% endfor %}
|
|
};
|
|
|
|
// Pick theme (localStorage > session fallback), set data-theme, and point the CSS href.
|
|
(function () {
|
|
try {
|
|
var server = "{{ request.session.theme|default:'classic' }}";
|
|
var stored = localStorage.getItem('theme');
|
|
var theme = (stored && window.THEME_URLS[stored]) ? stored : server;
|
|
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
|
|
var href = window.THEME_URLS[theme] || window.THEME_URLS['classic'];
|
|
var link = document.getElementById('theme-css');
|
|
if (link && href) link.href = href;
|
|
|
|
try { localStorage.setItem('theme', theme); } catch(e) {}
|
|
window.__resolvedTheme = theme; // used later to toggle gradient body class
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
{# === THEME SYSTEM (END) === #}
|
|
|
|
<!-- === PWA additions (BEGIN) === -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#f3f6f7">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'pwa/apple-touch-icon-180x180.png' %}">
|
|
<link rel="apple-touch-icon" sizes="167x167" href="{% static 'pwa/apple-touch-icon-167x167.png' %}">
|
|
<link rel="apple-touch-icon" sizes="152x152" href="{% static 'pwa/apple-touch-icon-152x152.png' %}">
|
|
<link rel="apple-touch-icon" sizes="120x120" href="{% static 'pwa/apple-touch-icon-120x120.png' %}">
|
|
<link rel="apple-touch-icon" href="{% static 'pwa/apple-touch-icon-180x180.png' %}">
|
|
<meta name="apple-mobile-web-app-title" content="Illustrations">
|
|
<!-- === PWA additions (END) === -->
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
<style>
|
|
:root{
|
|
--nav-bg:#f8fafc; --nav-border:#e5e7eb; --nav-ink:#1f2937; --nav-ink-muted:#6b7280;
|
|
--nav-brand:#2f6cab; --nav-brand-hover:#1f4c7a; --btn-bg:#fff; --btn-border:#d1d5db; --btn-hover:#eef2f7;
|
|
}
|
|
.topbar-wrap {
|
|
border-bottom: 1px solid var(--nav-border);
|
|
background: var(--nav-bg);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1000;
|
|
}
|
|
.topbar-wrap.is-scrolled { box-shadow: 0 4px 16px rgba(0,0,0,.08); border-bottom-color: transparent; }
|
|
.topbar{max-width:1100px; margin:0 auto; padding:14px 16px; display:flex; align-items:center; gap:14px; justify-content:space-between; font-size:17px;}
|
|
.brand{display:flex; align-items:center; gap:10px; text-decoration:none; color:var(--nav-ink); font-weight:600; letter-spacing:.2px;}
|
|
.brand .tagline{color:var(--nav-ink-muted); font-weight:500; font-size:15px}
|
|
.nav-right{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
|
|
.nav-btn{display:inline-flex; align-items:center; justify-content:center; padding:8px 12px; border-radius:10px; background:var(--btn-bg); border:1px solid var(--btn-border); color:var(--nav-ink); text-decoration:none; cursor:pointer; line-height:1; transition:background .12s, border-color .12s, box-shadow .12s;}
|
|
.nav-btn:hover{background:var(--btn-hover)}
|
|
.nav-btn.primary{background:var(--nav-brand); border-color:var(--nav-brand); color:#fff}
|
|
.nav-btn.primary:hover{background:var(--nav-brand-hover); border-color:var(--nav-brand-hover)}
|
|
.nav-btn.danger{background:#b91c1c; border-color:#b91c1c; color:#fff}
|
|
.nav-btn.danger:hover{filter:brightness(.95)}
|
|
.user-chip{padding:6px 10px; border-radius:999px; border:1px solid var(--nav-border); background:#fff; color:var(--nav-ink-muted); font-size:14px}
|
|
|
|
.page{max-width:1100px; margin:18px auto; padding:0 16px}
|
|
.messages{margin:12px 0; display:grid; gap:8px}
|
|
.msg{padding:10px 12px; border-radius:10px; border:1px solid #e5e7eb; background:#fff}
|
|
.msg.info{border-color:#dbeafe; background:#eff6ff}
|
|
.msg.success{border-color:#bbf7d0; background:#ecfdf5}
|
|
.msg.warning{border-color:#fde68a; background:#fffbeb}
|
|
.msg.error{border-color:#fecaca; background:#fef2f2}
|
|
@media (max-width:780px){ .topbar{gap:10px; padding:12px 12px} .nav-right{gap:8px} .nav-btn{padding:8px 10px} }
|
|
|
|
/* --- additions for mobile hamburger --- */
|
|
.hamburger { display:none; }
|
|
.mobile-menu[hidden]{ display:none; }
|
|
@media (max-width:700px){
|
|
.desktop-nav { display:none; }
|
|
.hamburger{
|
|
display:inline-flex; width:38px; height:34px; align-items:center; justify-content:center;
|
|
border:1px solid var(--nav-border); border-radius:10px; background:var(--btn-bg); cursor:pointer;
|
|
flex-direction: column; gap: 4px;
|
|
}
|
|
.hamburger span{ display:block; width:20px; height:2px; margin:0; background:var(--nav-ink); border-radius:2px; }
|
|
.mobile-menu{ position:absolute; top:56px; right:16px; left:16px; z-index:50; }
|
|
.mobile-menu-inner{ background:#fff; border:1px solid var(--nav-border); border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,.08); padding:8px; display:grid; gap:2px; }
|
|
.mobile-link{ display:block; padding:12px 14px; border-radius:10px; text-decoration:none; color:var(--nav-ink); }
|
|
.mobile-link:hover{ background:var(--btn-hover); }
|
|
.mobile-link.primary{ background:var(--nav-brand); color:#fff; }
|
|
.mobile-link.danger{ color:#b91c1c; }
|
|
.mobile-user{ padding:10px 14px; color:var(--nav-ink-muted); font-size:14px; }
|
|
}
|
|
|
|
/* --- user menu (desktop dropdown) --- */
|
|
.user-chip{ cursor:pointer; text-decoration:none; }
|
|
.user-chip:hover{ background:var(--btn-hover); }
|
|
.user-dropdown{ position:relative; }
|
|
.user-menu{
|
|
position:absolute; right:0; top:calc(100% + 8px);
|
|
background:#fff; border:1px solid var(--nav-border);
|
|
border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,.08);
|
|
min-width: 160px; padding:6px; display:grid; gap:4px; z-index:60;
|
|
}
|
|
.user-menu[hidden]{ display:none !important; }
|
|
.user-menu .menu-item{
|
|
display:block; width:100%; text-align:left; padding:10px 12px; border-radius:10px;
|
|
text-decoration:none; color:var(--nav-ink); background:transparent; border:none; cursor:pointer;
|
|
font-size:16px; font-family:inherit;
|
|
}
|
|
.user-menu .menu-item:hover{ background:var(--btn-hover); }
|
|
.user-menu .danger-btn{ color:#b91c1c; }
|
|
</style>
|
|
</head>
|
|
|
|
<body class="{% block body_class %}{% endblock %}">
|
|
<script>
|
|
// Apply/Remove the gradient class based on the final resolved theme.
|
|
(function () {
|
|
try {
|
|
var theme = window.__resolvedTheme || document.documentElement.getAttribute('data-theme') || 'classic';
|
|
if (theme === 'classic') {
|
|
document.body.classList.add('themed-bg'); // your original gradient class from app.css
|
|
} else {
|
|
document.body.classList.remove('themed-bg');
|
|
}
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
|
|
<div class="topbar-wrap">
|
|
<div class="topbar">
|
|
<div class="brand">
|
|
<a class="brand-title" href="{% url 'search' %}">Illustrations Database</a>
|
|
<a class="version-link" href="https://git.lan/joshlaymon/Illustrations/wiki" title="Release notes">{{ APP_VERSION }}</a>
|
|
</div>
|
|
|
|
<div class="nav-right desktop-nav">
|
|
<a class="nav-btn" href="{% url 'search' %}">Find</a>
|
|
<a class="btn btn-success" href="{% url 'entry_add' %}">Create</a>
|
|
<a class="nav-btn" href="{% url 'stats' %}">Insights</a>
|
|
|
|
{% if user.is_authenticated %}
|
|
<!-- USER MENU -->
|
|
<div class="user-dropdown">
|
|
<button id="userMenuBtn" class="user-chip" aria-haspopup="true" aria-expanded="false">
|
|
{{ user.username }}
|
|
</button>
|
|
<div id="userMenu" class="user-menu" hidden>
|
|
{% if user.is_superuser %}<a class="menu-item" href="/admin/">Admin</a>{% endif %}
|
|
{% if user.is_staff %}<a class="menu-item" href="{% url 'export_csv' %}">Backup</a>{% endif %}
|
|
<a class="menu-item" href="{% url 'settings_home' %}">Settings</a>
|
|
<form method="post" action="{% url 'logout' %}">{% csrf_token %}
|
|
<button class="menu-item danger-btn" type="submit">Logout</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<a class="nav-btn primary" href="{% url 'login' %}">Login</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<button id="hamburger" class="hamburger" aria-label="Open menu" aria-controls="mobileMenu" aria-expanded="false">
|
|
<span></span><span></span><span></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div id="mobileMenu" class="mobile-menu" hidden>
|
|
<nav class="mobile-menu-inner" role="menu">
|
|
<a class="mobile-link" href="{% url 'search' %}" role="menuitem">Find</a>
|
|
<a class="mobile-link" href="{% url 'entry_add' %}" role="menuitem">Create</a>
|
|
<a class="mobile-link" href="{% url 'stats' %}" role="menuitem">Insights</a>
|
|
|
|
{% if user.is_authenticated and user.is_superuser %}
|
|
<a class="mobile-link" href="/admin/" role="menuitem">Admin</a>
|
|
{% endif %}
|
|
{% if user.is_authenticated and user.is_staff %}
|
|
<a class="mobile-link" href="{% url 'export_csv' %}" role="menuitem">Backup</a>
|
|
{% endif %}
|
|
|
|
{% if user.is_authenticated %}
|
|
<div class="mobile-user">Signed in: {{ user.username }}</div>
|
|
<a class="mobile-link" href="{% url 'settings_home' %}" role="menuitem">Settings</a>
|
|
<form method="post" action="{% url 'logout' %}" role="menuitem">{% csrf_token %}
|
|
<button class="mobile-link danger" style="width:100%; text-align:left;">Logout</button>
|
|
</form>
|
|
{% else %}
|
|
<a class="mobile-link primary" href="{% url 'login' %}" role="menuitem">Login</a>
|
|
{% endif %}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page">
|
|
{% if messages %}
|
|
<div class="messages">
|
|
{% for message in messages %}
|
|
<div class="msg {{ message.tags|default:'info' }}">{{ message }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<main class="page">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
{% block extra_body %}{% endblock %}
|
|
|
|
<script>
|
|
(function () {
|
|
// mobile hamburger
|
|
const btn = document.getElementById('hamburger');
|
|
const menu = document.getElementById('mobileMenu');
|
|
if (btn && menu) {
|
|
const open = () => { menu.hidden = false; btn.setAttribute('aria-expanded','true'); };
|
|
const close = () => { menu.hidden = true; btn.setAttribute('aria-expanded','false'); };
|
|
btn.addEventListener('click', () => (menu.hidden ? open() : close()));
|
|
document.addEventListener('click', (e) => {
|
|
if (menu.hidden) return;
|
|
if (!menu.contains(e.target) && !btn.contains(e.target)) close();
|
|
});
|
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
|
|
window.matchMedia('(min-width: 701px)').addEventListener('change', () => close());
|
|
}
|
|
|
|
// USER MENU: desktop dropdown
|
|
const userBtn = document.getElementById('userMenuBtn');
|
|
const userMenu = document.getElementById('userMenu');
|
|
if (userBtn && userMenu) {
|
|
const open = () => { userMenu.hidden = false; userBtn.setAttribute('aria-expanded','true'); };
|
|
const close = () => { userMenu.hidden = true; userBtn.setAttribute('aria-expanded','false'); };
|
|
userBtn.addEventListener('click', (e) => { e.stopPropagation(); userMenu.hidden ? open() : close(); });
|
|
document.addEventListener('click', (e) => {
|
|
if (userMenu.hidden) return;
|
|
if (!userMenu.contains(e.target) && !userBtn.contains(e.target)) close();
|
|
});
|
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
|
|
userMenu.addEventListener('click', (e) => {
|
|
if (e.target.closest('a') || e.target.closest('button')) close();
|
|
});
|
|
window.addEventListener('blur', close);
|
|
}
|
|
})();
|
|
</script>
|
|
<script>
|
|
// Sticky header: add shadow when page is scrolled
|
|
(function () {
|
|
const bar = document.querySelector('.topbar-wrap');
|
|
if (!bar) return;
|
|
const onScroll = () => {
|
|
const y = window.scrollY || document.documentElement.scrollTop || 0;
|
|
bar.classList.toggle('is-scrolled', y > 2);
|
|
};
|
|
document.addEventListener('scroll', onScroll, { passive: true });
|
|
onScroll(); // initialize
|
|
})();
|
|
</script>
|
|
<!-- === PWA service worker registration (BEGIN) === -->
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', function () {
|
|
navigator.serviceWorker.register('/service-worker.js').catch(console.error);
|
|
});
|
|
}
|
|
</script>
|
|
<!-- === PWA service worker registration (END) === -->
|
|
</body>
|
|
</html> |