mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
feat(dashboard): breadcrumbs that follow how you reached a page
Detail pages carry a breadcrumb bar built from a per-tab trail in sessionStorage: following a link from a page on the trail extends it, going back to one cuts it there, and opening a page directly shows its natural parents. The property 3D view's back button goes to the previous crumb, and the property page renames its crumb once its name loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -353,6 +353,83 @@
|
||||
document.addEventListener('shown.bs.collapse', function (e) { if (e.target.dataset.prefOpen) Prefs.set(e.target.dataset.prefOpen, true); });
|
||||
document.addEventListener('hidden.bs.collapse', function (e) { if (e.target.dataset.prefOpen) Prefs.set(e.target.dataset.prefOpen, false); });
|
||||
|
||||
/**
|
||||
* Breadcrumbs that follow how you actually got to a page, kept per browser tab (sessionStorage).
|
||||
* A detail page has <nav data-crumbs data-crumb-label="..."> holding its natural parents as links (what shows when
|
||||
* the page is opened directly); every other page starts a new trail. Arriving from a page on the trail (by link
|
||||
* or script, seen through the same-origin referrer) continues it from there; returning to a page on the trail
|
||||
* (back, a crumb, reload) cuts it back to that page. Crumbs.label(text) renames the current page once its name
|
||||
* has loaded. A link marked data-crumb-back points at the previous page on the trail.
|
||||
*/
|
||||
window.Crumbs = (function () {
|
||||
var KEY = 'dash.trail', MAX = 7;
|
||||
var here = window.location.pathname + window.location.search;
|
||||
var nav = document.querySelector('[data-crumbs]');
|
||||
function load() {
|
||||
try { var t = JSON.parse(sessionStorage.getItem(KEY) || '[]'); return Array.isArray(t) ? t : []; } catch (e) { return []; }
|
||||
}
|
||||
function save(t) { try { sessionStorage.setItem(KEY, JSON.stringify(t)); } catch (e) { /* storage unavailable */ } }
|
||||
function indexOf(t, href) { for (var i = 0; i < t.length; i++) if (t[i] && t[i].href === href) return i; return -1; }
|
||||
function titleLabel() {
|
||||
var title = document.title.replace(/\s*-\s*DarkflameServer\s*$/, '').trim();
|
||||
return !title || title === 'DarkflameServer' ? 'Home' : title;
|
||||
}
|
||||
var from = '';
|
||||
try {
|
||||
var ref = document.referrer ? new URL(document.referrer) : null;
|
||||
if (ref && ref.origin === window.location.origin) from = ref.pathname + ref.search;
|
||||
} catch (e) {}
|
||||
|
||||
var trail = load(), label = nav ? (nav.dataset.crumbLabel || titleLabel()) : titleLabel();
|
||||
if (!nav) {
|
||||
// Lists and other top-level pages start the trail
|
||||
trail = [{ href: here, label: label }];
|
||||
} else {
|
||||
var at = indexOf(trail, here), came = from && from !== here ? indexOf(trail, from) : -1;
|
||||
if (at >= 0) trail = trail.slice(0, at + 1);
|
||||
else if (came >= 0) trail = trail.slice(0, came + 1).concat([{ href: here, label: label }]);
|
||||
else {
|
||||
// Opened directly or from outside the trail: the page's natural parents
|
||||
trail = Array.prototype.map.call(nav.querySelectorAll('a[href]'), function (a) {
|
||||
return { href: a.getAttribute('href'), label: a.textContent.trim() };
|
||||
}).concat([{ href: here, label: label }]);
|
||||
}
|
||||
if (trail.length > MAX) trail = [trail[0]].concat(trail.slice(trail.length - MAX + 1));
|
||||
}
|
||||
save(trail);
|
||||
|
||||
function render() {
|
||||
if (!nav) return;
|
||||
nav.hidden = trail.length < 2;
|
||||
var ol = document.createElement('ol');
|
||||
ol.className = 'breadcrumb mb-3';
|
||||
trail.forEach(function (c, i) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'breadcrumb-item' + (i === trail.length - 1 ? ' active' : '');
|
||||
if (i === trail.length - 1) { li.setAttribute('aria-current', 'page'); li.textContent = c.label; }
|
||||
else { var a = document.createElement('a'); a.href = c.href; a.textContent = c.label; li.appendChild(a); }
|
||||
ol.appendChild(li);
|
||||
});
|
||||
nav.replaceChildren(ol);
|
||||
var prev = trail.length > 1 ? trail[trail.length - 2] : null;
|
||||
document.querySelectorAll('a[data-crumb-back]').forEach(function (a) {
|
||||
if (!prev) return;
|
||||
a.href = prev.href;
|
||||
a.textContent = '← ' + prev.label;
|
||||
});
|
||||
}
|
||||
render();
|
||||
return {
|
||||
label: function (text) {
|
||||
if (!nav || !text) return;
|
||||
trail[trail.length - 1].label = String(text);
|
||||
save(trail);
|
||||
render();
|
||||
},
|
||||
previous: function () { return trail.length > 1 ? trail[trail.length - 2] : null; }
|
||||
};
|
||||
})();
|
||||
|
||||
if (window.jQuery && $.fn.dataTable) {
|
||||
// Columns without a custom renderer are inserted as HTML by default; render them as text instead
|
||||
$.extend(true, $.fn.dataTable.defaults, {
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
{% block title %}Account #{{ account.id }} - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if can.accounts_view %}
|
||||
<a href="/accounts" class="text-decoration-none mb-3 d-inline-block">← Back to Accounts</a>
|
||||
{% endif %}
|
||||
<nav data-crumbs data-crumb-label="{{ account.name }}" aria-label="Breadcrumb"><ol class="breadcrumb mb-3">{% if can.accounts_view %}<li class="breadcrumb-item"><a href="/accounts">Accounts</a></li>{% endif %}<li class="breadcrumb-item active" aria-current="page">{{ account.name }}</li></ol></nav>
|
||||
|
||||
<div class="card mb-4" id="account" data-account-id="{{ account.id }}" data-gm-level="{{ account.gm_level }}" data-email="{{ account.email }}" data-account-name="{{ account.name }}" data-self="{% if is_self %}1{% endif %}" data-can-moderate="{% if manage.moderation %}1{% endif %}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Bug Report #{{ report.id }} - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a href="/bug_reports" class="text-decoration-none mb-3 d-inline-block">← Back to Bug Reports</a>
|
||||
<nav data-crumbs data-crumb-label="Bug Report #{{ report.id }}" aria-label="Breadcrumb"><ol class="breadcrumb mb-3"><li class="breadcrumb-item"><a href="/bug_reports">Bug Reports</a></li><li class="breadcrumb-item active" aria-current="page">Bug Report #{{ report.id }}</li></ol></nav>
|
||||
|
||||
<div class="card mb-4" id="report" data-report-id="{{ report.id }}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
|
||||
@@ -42,11 +42,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if can.characters_view %}
|
||||
<a href="/characters" class="text-decoration-none mb-3 d-inline-block">← Back to Characters</a>
|
||||
{% else %}
|
||||
<a href="/" class="text-decoration-none mb-3 d-inline-block">← Back to your characters</a>
|
||||
{% endif %}
|
||||
<nav data-crumbs data-crumb-label="{{ character.name }}" aria-label="Breadcrumb"><ol class="breadcrumb mb-3">{% if can.characters_view %}<li class="breadcrumb-item"><a href="/characters">Characters</a></li>{% else %}<li class="breadcrumb-item"><a href="/">Your characters</a></li>{% endif %}<li class="breadcrumb-item active" aria-current="page">{{ character.name }}</li></ol></nav>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Play Key #{{ key.id }} - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a href="/play_keys" class="text-decoration-none mb-3 d-inline-block">← Back to Play Keys</a>
|
||||
<nav data-crumbs data-crumb-label="Play Key #{{ key.id }}" aria-label="Breadcrumb"><ol class="breadcrumb mb-3"><li class="breadcrumb-item"><a href="/play_keys">Play Keys</a></li><li class="breadcrumb-item active" aria-current="page">Play Key #{{ key.id }}</li></ol></nav>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-5">
|
||||
|
||||
@@ -23,9 +23,10 @@
|
||||
|
||||
{% block content %}
|
||||
{# Also the showcase's read-only view of approved public properties (Showcase.cpp passes showcase) #}
|
||||
{% if not exists("showcase") %}<nav data-crumbs data-crumb-label="3D view" aria-label="Breadcrumb"><ol class="breadcrumb mb-2">{% if can.properties_view %}<li class="breadcrumb-item"><a href="/properties">Properties</a></li>{% endif %}<li class="breadcrumb-item"><a href="/properties/{{ property_id }}">Property {{ property_id }}</a></li><li class="breadcrumb-item active" aria-current="page">3D view</li></ol></nav>{% endif %}
|
||||
<div id="property" data-property-id="{{ property_id }}" data-behavior-rules="{{ behavior_rules }}" data-showcase="{% if exists("showcase") %}1{% endif %}">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
|
||||
{% if exists("showcase") %}<a href="/showcase" class="btn btn-sm btn-outline-secondary">← Showcase</a>{% else %}<a href="/properties/{{ property_id }}" class="btn btn-sm btn-outline-secondary">← Property</a>{% endif %}
|
||||
{% if exists("showcase") %}<a href="/showcase" class="btn btn-sm btn-outline-secondary">← Showcase</a>{% else %}<a href="/properties/{{ property_id }}" class="btn btn-sm btn-outline-secondary" data-crumb-back>← Property</a>{% endif %}
|
||||
<div class="me-auto">
|
||||
<h5 class="mb-0 d-inline" id="propName">Loading…</h5>
|
||||
<span class="small text-body-secondary ms-2" id="propMeta"></span>
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
{% block title %}Property - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if can.properties_view %}
|
||||
<a href="/properties" class="text-decoration-none mb-3 d-inline-block">← Back to Properties</a>
|
||||
{% endif %}
|
||||
<nav data-crumbs data-crumb-label="Property {{ property_id }}" aria-label="Breadcrumb"><ol class="breadcrumb mb-3">{% if can.properties_view %}<li class="breadcrumb-item"><a href="/properties">Properties</a></li>{% endif %}<li class="breadcrumb-item active" aria-current="page">Property {{ property_id }}</li></ol></nav>
|
||||
|
||||
<div id="property" data-property-id="{{ property_id }}">
|
||||
<div class="card mb-4">
|
||||
@@ -189,6 +187,7 @@ function load() {
|
||||
if (p.success === false) { setText('propName', p.error); return; }
|
||||
loaded = p;
|
||||
setText('propName', p.name || 'Unnamed property');
|
||||
Crumbs.label(p.name || 'Unnamed property');
|
||||
// This property's own events and statistics (each property is its own instance of the zone, told apart by the clone)
|
||||
const economy = document.getElementById('propEconomy');
|
||||
if (economy && p.clone_id) {
|
||||
|
||||
@@ -32,6 +32,10 @@ Every table remembers how you last sorted it and how many rows it shows per page
|
||||
(per dashboard user), so it doesn't follow you to another browser; clearing site data resets it. Searches and the page
|
||||
you were on aren't kept.
|
||||
|
||||
Detail pages (accounts, characters, properties and their 3D view, bug reports, play keys) show breadcrumbs for the way
|
||||
you actually got there in this tab, for example Accounts > an account > a character > a property, so you can step
|
||||
back to where you came from. Opened directly, a page shows its usual parent (Properties > a property).
|
||||
|
||||
## Files to back up
|
||||
|
||||
Next to the server binaries, the dashboard creates:
|
||||
|
||||
Reference in New Issue
Block a user