Illustrations/web/templates/base.html

297 lines
12 KiB
HTML

{% load static %}
<!DOCTYPE html>
<html lang="en">
<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' %}">
<!-- === PWA additions (BEGIN) === -->
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#f3f6f7">
<!--<meta name="theme-color" content="#2f6cab"> -->
<!-- iOS standalone when added to Home Screen -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<!-- iOS app icons -->
<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' %}">
<!-- Optional fallback some iOS versions fetch if sizes are missing -->
<link rel="apple-touch-icon" href="{% static 'pwa/apple-touch-icon-180x180.png' %}">
<!-- Optional: control the name under the icon -->
<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);
/* make it sticky */
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; /* ensure uniform size */
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 %}">
<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);
}
})();
// Safe to include site-wide; it only adds a class.
(function(){
if (localStorage.getItem("darkMode") === "true") {
document.body.classList.add("dark-mode");
}
})();
</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>