Files
jobbi-bewerbung/views/partials/header.ejs
T
thomasandClaude Opus 4.8 1aa902bdce Top-Navigation: neu strukturiert, mit Aktiv-Zustand und Dropdowns
Die Leiste war eine flache Reihe aus sieben gleichrangigen Pillen. Zwei
Probleme: sie zeigte nie an, wo man gerade ist (es gab schlicht keinen
Aktiv-Zustand), und auf dem Handy fielen nur die Beschriftungen weg - uebrig
blieben sieben unbeschriftete Icons.

Struktur folgt jetzt dem tatsaechlichen Ablauf (finden -> bewerben ->
antworten) statt der Reihenfolge, in der die Seiten entstanden sind:

  Bewerbungen | Jobangebote | Stellensuche v | Postfach | Vorlagen

"Stellensuche" buendelt als Dropdown, was Angebote erzeugt und filtert:
Suchprofil & Laeufe, Uebernommene Angebote, Blacklist - jeweils mit einer Zeile,
die erklaert, wozu der Punkt gut ist. Einstellungen, Benutzerverwaltung und
Abmelden wandern ins Konto-Menue rechts, wo Systemkram hingehoert. Die
taeglichen Wege (Bewerbungen, Jobangebote, Postfach) bleiben ein Klick weit weg.

Gestaltung: Die Leiste behaelt in beiden Themes ihr dunkles Navy - sie ist der
Rahmen des Werkzeugs und die einzige Flaeche, die sich beim Navigieren nie
aendert. Alles darin ist bewusst still (Slate auf Navy); der einzige Akzent ist
der aktive Punkt, markiert durch eine schmale leuchtende Schiene an der
Unterkante. Beim Hover deutet sich dieselbe Schiene gedaempft an.

Zaehler (ungelesene Post, offene Angebote) haengen jetzt an Klassen statt IDs,
weil sie in Leiste UND Mobilmenue erscheinen. Ein Controller bedient alle
Dropdowns: Klick oeffnet, andere schliessen, Escape und Klick daneben schliessen,
aria-expanded bleibt ehrlich. Fokus ist sichtbar, prefers-reduced-motion wird
respektiert, das Mobilmenue zeigt alle Punkte ausgeschrieben und gruppiert.

Der aktive Pfad kommt aus res.locals.pfad (Auth-Middleware). Die Navigation liegt
weiterhin in genau einem Partial - sie ist damit auf allen Seiten identisch;
nur die Login-Seite hat bewusst keine.

Geprueft mit echtem Browser (Screenshots): hell/dunkel, Dropdowns, Mobilmenue,
und der Aktiv-Zustand je Seite - Unterseiten wie /blacklist oder /jobsuche heben
korrekt "Stellensuche" hervor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:55:58 +02:00

399 lines
24 KiB
Plaintext

<%
// Current path (res.locals.pfad, set in the auth middleware) drives the active
// state. The old header had none — you could never tell which section you were
// in. Copied into a differently named local: re-declaring `pfad` here would
// shadow the EJS local under `with(locals)`.
var aktPfad = (typeof pfad !== 'undefined' && pfad) ? String(pfad) : '/';
// Bewerbungen owns the dashboard and every single application page.
var istBewerbungen = (aktPfad === '/' || aktPfad.indexOf('/bewerbung') === 0);
// Offene Angebote only — "übernommen" lives under Stellensuche, with the rest
// of the search machinery.
var istAngebote = (aktPfad === '/jobangebote');
var istPostfach = (aktPfad.indexOf('/postfach') === 0);
var istVorlagen = (aktPfad.indexOf('/vorlagen') === 0);
// The search machinery: how offers get found, what was taken, what is blocked.
var istSuchprofil = (aktPfad.indexOf('/jobsuche') === 0);
var istUebernommen = (aktPfad.indexOf('/jobangebote/uebernommen') === 0);
var istBlacklist = (aktPfad.indexOf('/blacklist') === 0);
var istStellensuche = istSuchprofil || istUebernommen || istBlacklist;
var istEinstellungen = (aktPfad.indexOf('/einstellungen') === 0);
var istAdmin = (aktPfad.indexOf('/admin') === 0);
var angemeldet = (typeof user !== 'undefined' && user);
var initiale = angemeldet ? String(user.username || '?').charAt(0).toUpperCase() : '?';
%>
<header id="siteHeader"
class="sticky top-0 z-50 bg-[#0b1220] text-slate-300 border-b border-white/5 transition-shadow duration-300">
<!-- A single cool glow anchored at the brand, instead of the old full-width
left-to-right gradient sweep. Gives the bar depth without shouting. -->
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(900px_circle_at_0%_-40%,rgba(56,189,248,0.16),transparent_60%)]"></div>
<div class="relative container mx-auto px-4">
<div class="flex h-16 items-center gap-3">
<!-- Brand -->
<a href="/" class="group flex shrink-0 items-center gap-2.5 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0b1220]">
<span class="grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-sky-400 to-blue-600 shadow-lg shadow-blue-900/40 ring-1 ring-white/10">
<svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</span>
<span class="truncate text-[15px] font-semibold tracking-tight text-white">
Bewerbungs-Tracker
</span>
</a>
<!-- Primary navigation (desktop). Order follows the actual workflow:
finden -> bewerben -> antworten. -->
<nav class="ml-4 hidden items-center gap-1 lg:flex" aria-label="Hauptnavigation">
<a href="/" class="nav-item <%= istBewerbungen ? 'is-active' : '' %>">
<span>Bewerbungen</span>
<span class="nav-rail"></span>
</a>
<a href="/jobangebote" class="nav-item <%= istAngebote ? 'is-active' : '' %>">
<span>Jobangebote</span>
<span class="js-badge-angebote nav-badge nav-badge--green hidden">0</span>
<span class="nav-rail"></span>
</a>
<!-- Stellensuche: everything that produces or filters offers. -->
<div class="relative">
<button type="button" data-dd="ddSuche" aria-expanded="false" aria-haspopup="true"
class="nav-item <%= istStellensuche ? 'is-active' : '' %>">
<span>Stellensuche</span>
<svg class="dd-chevron h-3.5 w-3.5 text-slate-500 transition-transform duration-200" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
</svg>
<span class="nav-rail"></span>
</button>
<div id="ddSuche" class="dd-panel hidden w-80">
<a href="/jobsuche" class="dd-item <%= istSuchprofil ? 'is-active' : '' %>">
<svg class="dd-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<span>
<span class="dd-title">Suchprofil &amp; Läufe</span>
<span class="dd-desc">Wo und wann automatisch gesucht wird</span>
</span>
</a>
<a href="/jobangebote/uebernommen" class="dd-item <%= istUebernommen ? 'is-active' : '' %>">
<svg class="dd-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path>
</svg>
<span>
<span class="dd-title">Übernommene Angebote</span>
<span class="dd-desc">Bereits in eine Bewerbung überführt</span>
</span>
</a>
<a href="/blacklist" class="dd-item <%= istBlacklist ? 'is-active' : '' %>">
<svg class="dd-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
</svg>
<span>
<span class="dd-title">Blacklist</span>
<span class="dd-desc">Taucht in keinem Suchlauf wieder auf</span>
</span>
</a>
</div>
</div>
<a href="/postfach" class="nav-item <%= istPostfach ? 'is-active' : '' %>">
<span>Postfach</span>
<span class="js-badge-postfach nav-badge nav-badge--red hidden">0</span>
<span class="nav-rail"></span>
</a>
<a href="/vorlagen" class="nav-item <%= istVorlagen ? 'is-active' : '' %>">
<span>Vorlagen</span>
<span class="nav-rail"></span>
</a>
</nav>
<!-- Right cluster: notifications + account -->
<div class="ml-auto flex items-center gap-1.5">
<!-- Notifications -->
<div class="relative">
<button id="notifBtn" type="button" data-dd="notifPanel" aria-expanded="false"
class="icon-btn" aria-label="Benachrichtigungen" title="Ungelesene E-Mails">
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
</svg>
<span id="notifBadge" class="dot-badge dot-badge--red hidden">0</span>
</button>
<div id="notifPanel" class="dd-panel hidden w-80 max-w-[calc(100vw-2rem)] p-0">
<div class="sticky top-0 flex items-center justify-between rounded-t-xl border-b border-gray-100 bg-white px-4 py-2.5 dark:border-white/5 dark:bg-slate-800">
<span class="text-sm font-semibold text-gray-800 dark:text-white">Benachrichtigungen</span>
<button id="notifMarkAll" type="button" class="hidden text-xs font-medium text-blue-600 hover:underline dark:text-sky-400">Alle gelesen</button>
</div>
<ul id="notifList" class="max-h-96 divide-y divide-gray-100 overflow-y-auto dark:divide-white/5"></ul>
<div id="notifEmpty" class="px-4 py-10 text-center text-sm text-gray-400 dark:text-slate-500">
Keine ungelesenen Nachrichten
</div>
</div>
</div>
<% if (angemeldet) { %>
<!-- Account -->
<div class="relative">
<button type="button" data-dd="ddUser" aria-expanded="false" aria-haspopup="true"
class="flex items-center gap-2 rounded-lg py-1.5 pl-1.5 pr-2 text-sm text-slate-300 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0b1220]">
<span class="grid h-7 w-7 place-items-center rounded-full bg-white/10 text-[11px] font-semibold text-white ring-1 ring-white/15">
<%= initiale %>
</span>
<span class="hidden max-w-[10rem] truncate font-medium md:inline"><%= user.username %></span>
<svg class="dd-chevron hidden h-3.5 w-3.5 text-slate-500 transition-transform duration-200 md:block" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="ddUser" class="dd-panel hidden w-60">
<div class="border-b border-gray-100 px-3 pb-2.5 pt-1 dark:border-white/5">
<p class="truncate text-sm font-semibold text-gray-800 dark:text-white"><%= user.username %></p>
<p class="text-xs text-gray-500 dark:text-slate-400">
<%= user.is_admin ? 'Administrator' : 'Benutzer' %>
</p>
</div>
<div class="pt-1">
<a href="/einstellungen" class="dd-item <%= istEinstellungen ? 'is-active' : '' %>">
<svg class="dd-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="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.066 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.066c-.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.066-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"></path>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="dd-title">Einstellungen</span>
</a>
<% if (user.is_admin) { %>
<a href="/admin" class="dd-item <%= istAdmin ? 'is-active' : '' %>">
<svg class="dd-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m6-2a4 4 0 100-8 4 4 0 000 8z"></path>
</svg>
<span class="dd-title">Benutzer verwalten</span>
</a>
<% } %>
</div>
<div class="mt-1 border-t border-gray-100 pt-1 dark:border-white/5">
<a href="/logout" class="dd-item dd-item--danger">
<svg class="dd-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" 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>
<span class="dd-title">Abmelden</span>
</a>
</div>
</div>
</div>
<% } %>
<!-- Mobile menu -->
<button type="button" id="burgerBtn" aria-expanded="false" aria-controls="mobileNav"
class="icon-btn lg:hidden" aria-label="Menü">
<svg id="burgerOpen" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<svg id="burgerClose" class="hidden h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile navigation: the same structure, spelled out. The old header just
dropped the labels and left seven unlabelled icons. -->
<div id="mobileNav" class="hidden border-t border-white/5 bg-[#0b1220] lg:hidden">
<nav class="container mx-auto space-y-4 px-4 py-4" aria-label="Hauptnavigation (mobil)">
<div class="space-y-0.5">
<a href="/" class="m-item <%= istBewerbungen ? 'is-active' : '' %>">Bewerbungen</a>
<a href="/jobangebote" class="m-item <%= istAngebote ? 'is-active' : '' %>">
Jobangebote
<span class="js-badge-angebote nav-badge nav-badge--green ml-auto hidden">0</span>
</a>
<a href="/postfach" class="m-item <%= istPostfach ? 'is-active' : '' %>">
Postfach
<span class="js-badge-postfach nav-badge nav-badge--red ml-auto hidden">0</span>
</a>
<a href="/vorlagen" class="m-item <%= istVorlagen ? 'is-active' : '' %>">Vorlagen</a>
</div>
<div class="space-y-0.5">
<p class="m-label">Stellensuche</p>
<a href="/jobsuche" class="m-item <%= istSuchprofil ? 'is-active' : '' %>">Suchprofil &amp; Läufe</a>
<a href="/jobangebote/uebernommen" class="m-item <%= istUebernommen ? 'is-active' : '' %>">Übernommene Angebote</a>
<a href="/blacklist" class="m-item <%= istBlacklist ? 'is-active' : '' %>">Blacklist</a>
</div>
<% if (angemeldet) { %>
<div class="space-y-0.5">
<p class="m-label">Konto</p>
<p class="px-3 pb-1 text-sm text-white">
<%= user.username %>
<span class="text-slate-500">· <%= user.is_admin ? 'Administrator' : 'Benutzer' %></span>
</p>
<a href="/einstellungen" class="m-item <%= istEinstellungen ? 'is-active' : '' %>">Einstellungen</a>
<% if (user.is_admin) { %>
<a href="/admin" class="m-item <%= istAdmin ? 'is-active' : '' %>">Benutzer verwalten</a>
<% } %>
<a href="/logout" class="m-item text-rose-300 hover:bg-rose-500/10 hover:text-rose-200">Abmelden</a>
</div>
<% } %>
</nav>
</div>
<script>
// --- Dropdowns (Stellensuche, Konto, Benachrichtigungen) ---------------
// One controller for all of them: click toggles, opening one closes the
// others, Escape and outside-click close, aria-expanded stays truthful.
(function () {
var trigger = Array.prototype.slice.call(document.querySelectorAll('[data-dd]'));
function panelOf(btn) { return document.getElementById(btn.getAttribute('data-dd')); }
function close(btn) {
var p = panelOf(btn);
if (!p) return;
p.classList.add('hidden');
btn.setAttribute('aria-expanded', 'false');
var chev = btn.querySelector('.dd-chevron');
if (chev) chev.classList.remove('rotate-180');
}
function closeAll(ausser) {
trigger.forEach(function (b) { if (b !== ausser) close(b); });
}
trigger.forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var p = panelOf(btn);
if (!p) return;
var offen = !p.classList.contains('hidden');
closeAll(btn);
if (offen) { close(btn); return; }
p.classList.remove('hidden');
btn.setAttribute('aria-expanded', 'true');
var chev = btn.querySelector('.dd-chevron');
if (chev) chev.classList.add('rotate-180');
btn.dispatchEvent(new CustomEvent('dd:open'));
});
});
document.addEventListener('click', function (e) {
trigger.forEach(function (btn) {
var p = panelOf(btn);
if (p && !p.classList.contains('hidden') && !p.contains(e.target) && !btn.contains(e.target)) {
close(btn);
}
});
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeAll(null);
});
// --- Mobile menu ---
var burger = document.getElementById('burgerBtn');
var mob = document.getElementById('mobileNav');
if (burger && mob) {
burger.addEventListener('click', function (e) {
e.stopPropagation();
var offen = mob.classList.toggle('hidden');
burger.setAttribute('aria-expanded', String(!offen));
document.getElementById('burgerOpen').classList.toggle('hidden', !offen);
document.getElementById('burgerClose').classList.toggle('hidden', offen);
});
}
// Subtle lift once the page is scrolled — the bar reads as floating
// above the content rather than welded to it.
var head = document.getElementById('siteHeader');
function onScroll() {
if (!head) return;
head.classList.toggle('shadow-xl', window.scrollY > 4);
head.classList.toggle('shadow-black/30', window.scrollY > 4);
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
})();
// --- Notification bell: unread received e-mails ------------------------
(function () {
var btn = document.getElementById('notifBtn');
var badge = document.getElementById('notifBadge');
var list = document.getElementById('notifList');
var empty = document.getElementById('notifEmpty');
var markAll = document.getElementById('notifMarkAll');
if (!btn) return;
function esc(s) { var d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
function fmtDate(d) {
if (!d) return '';
try {
var dt = new Date(d), diff = (Date.now() - dt.getTime()) / 1000;
if (diff < 60) return 'gerade eben';
if (diff < 3600) return Math.floor(diff / 60) + ' Min.';
if (diff < 86400) return Math.floor(diff / 3600) + ' Std.';
return dt.toLocaleDateString('de-DE');
} catch (e) { return ''; }
}
function render(data) {
var items = (data && data.items) || [], count = (data && data.count) || 0;
if (count > 0) { badge.textContent = count > 99 ? '99+' : count; badge.classList.remove('hidden'); }
else { badge.classList.add('hidden'); }
list.innerHTML = '';
if (!items.length) { empty.classList.remove('hidden'); markAll.classList.add('hidden'); return; }
empty.classList.add('hidden'); markAll.classList.remove('hidden');
items.forEach(function (it) {
var li = document.createElement('li');
var a = document.createElement('a');
a.href = it.url;
a.className = 'block px-4 py-3 transition-colors hover:bg-gray-50 dark:hover:bg-white/5';
a.innerHTML =
'<div class="flex items-center justify-between gap-2">'
+ '<span class="truncate text-sm font-medium text-gray-800 dark:text-gray-100">' + esc(it.from) + '</span>'
+ '<span class="shrink-0 text-[11px] text-gray-400">' + esc(fmtDate(it.date)) + '</span>'
+ '</div>'
+ '<div class="truncate text-sm text-gray-700 dark:text-gray-200">' + esc(it.subject) + '</div>'
+ (it.snippet ? '<div class="mt-0.5 truncate text-xs text-gray-500 dark:text-slate-400">' + esc(it.snippet) + '</div>' : '')
+ (it.kontext ? '<div class="mt-0.5 truncate text-[11px] text-blue-600 dark:text-sky-400">' + esc(it.kontext) + '</div>' : '');
li.appendChild(a); list.appendChild(li);
});
}
function load() {
fetch('/api/notifications', { cache: 'no-store' })
.then(function (r) { return r.json(); }).then(render).catch(function () { /* ignore */ });
}
btn.addEventListener('dd:open', load); // refresh whenever the panel opens
markAll.addEventListener('click', function (e) {
e.preventDefault(); e.stopPropagation();
fetch('/api/emails/mark-all-read', { method: 'POST' })
.then(function () { render({ count: 0, items: [] }); }).catch(function () { /* ignore */ });
});
load();
setInterval(load, 60000);
})();
// --- Counters: unlinked mail + open job offers -------------------------
// Class-based, because each count now shows in both the desktop bar and
// the mobile menu.
(function () {
function setzen(selektor, count) {
if (!(count > 0)) return;
document.querySelectorAll(selektor).forEach(function (el) {
el.textContent = count > 99 ? '99+' : count;
el.classList.remove('hidden');
});
}
fetch('/api/emails/unassigned-count').then(function (r) { return r.json(); })
.then(function (d) { setzen('.js-badge-postfach', d && d.count); }).catch(function () { /* ignore */ });
fetch('/jobangebote/anzahl-offen').then(function (r) { return r.json(); })
.then(function (d) { setzen('.js-badge-angebote', d && d.count); }).catch(function () { /* ignore */ });
})();
</script>
</header>