Billnotes note-page interaction: hover glow, click-lock, DON/DOFF; note titles + card-ref styling — TDD
- note-page.js: click-lock (_lockedItem + notes-locked body class); DON auto-DOFFs prev donned; _setGreeting updates navbar; _donnedItem exposed for test API - NotePageSpec.js: 18 Jasmine specs covering lock/unlock, DON/DOFF state, auto-DOFF, greeting update, initial load; flushPromises helper for chained fetch .then() - _note.scss: DON/DOFF opacity:0 by default; hover + locked + donned states show them; body:not(.notes-locked) hover suppression - views.py: Super-Schizo/Super-Nomad card titles; recognition_title field (display_title) separate from card title; mark_safe descriptions w. card-ref spans - my_notes.html: |safe on description; recognition_title for Recognitions block - _navbar.html: id_greeting_prefix/id_greeting_name spans for JS greeting update - _base.scss: global .card-ref rule (--terUser, font-weight 600, !important) Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
var _activeItem = null;
|
var _activeItem = null;
|
||||||
var _originalPalette = null;
|
var _originalPalette = null;
|
||||||
var _dismissTimer = null;
|
var _dismissTimer = null;
|
||||||
|
var _lockedItem = null; // click-locked note (glow + DON/DOFF pinned)
|
||||||
|
var _donnedItem = null; // currently DONned note (persistent glow)
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -47,6 +49,23 @@
|
|||||||
if (el) el.style.display = 'none';
|
if (el) el.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── lock helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _clearLock() {
|
||||||
|
if (_lockedItem) {
|
||||||
|
_lockedItem.classList.remove('note-item--locked');
|
||||||
|
_lockedItem = null;
|
||||||
|
}
|
||||||
|
document.body.classList.remove('notes-locked');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setGreeting(greeting, name) {
|
||||||
|
var prefix = document.getElementById('id_greeting_prefix');
|
||||||
|
var nameEl = document.getElementById('id_greeting_name');
|
||||||
|
if (prefix) prefix.innerHTML = greeting;
|
||||||
|
if (nameEl) nameEl.textContent = name;
|
||||||
|
}
|
||||||
|
|
||||||
// ── modal lifecycle ───────────────────────────────────────────────────────
|
// ── modal lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _openModal() {
|
function _openModal() {
|
||||||
@@ -88,47 +107,31 @@
|
|||||||
_originalPalette = null;
|
_originalPalette = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire event listeners onto the freshly-cloned modal DOM.
|
|
||||||
function _wireModal() {
|
function _wireModal() {
|
||||||
var modal = _activeModal();
|
var modal = _activeModal();
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
|
|
||||||
// Swatch body click → preview palette sitewide + show confirm
|
|
||||||
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
||||||
body.addEventListener('click', function (e) {
|
body.addEventListener('click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (_selectedPalette) _revertPreview();
|
if (_selectedPalette) _revertPreview();
|
||||||
|
|
||||||
_selectedPalette = _paletteClass(body.parentElement);
|
_selectedPalette = _paletteClass(body.parentElement);
|
||||||
_originalPalette = _currentBodyPalette();
|
_originalPalette = _currentBodyPalette();
|
||||||
|
|
||||||
body.classList.add('previewing');
|
body.classList.add('previewing');
|
||||||
_swapBodyPalette(_selectedPalette);
|
_swapBodyPalette(_selectedPalette);
|
||||||
_showConfirm(modal);
|
_showConfirm(modal);
|
||||||
|
_dismissTimer = setTimeout(function () { _revertPreview(); }, 10000);
|
||||||
_dismissTimer = setTimeout(function () {
|
|
||||||
_revertPreview();
|
|
||||||
}, 10000);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm OK → commit palette sitewide
|
|
||||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) {
|
modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function (e) {
|
btn.addEventListener('click', function (e) { e.stopPropagation(); _doSetPalette(); });
|
||||||
e.stopPropagation();
|
|
||||||
_doSetPalette();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm NVM → revert preview only; main swatch modal stays open
|
|
||||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function (e) {
|
btn.addEventListener('click', function (e) { e.stopPropagation(); _revertPreview(); });
|
||||||
e.stopPropagation();
|
|
||||||
_revertPreview();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop modal clicks from reaching the body dismiss handler.
|
|
||||||
modal.addEventListener('click', function (e) { e.stopPropagation(); });
|
modal.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,18 +141,13 @@
|
|||||||
var url = _activeItem.dataset.setPaletteUrl;
|
var url = _activeItem.dataset.setPaletteUrl;
|
||||||
var palette = _selectedPalette;
|
var palette = _selectedPalette;
|
||||||
var item = _activeItem;
|
var item = _activeItem;
|
||||||
// Read label from swatch row while modal is still in DOM
|
|
||||||
var swatchRow = _activeModal() && _activeModal().querySelector('.' + palette + '[data-palette-label]');
|
var swatchRow = _activeModal() && _activeModal().querySelector('.' + palette + '[data-palette-label]');
|
||||||
var paletteLabel = swatchRow
|
var paletteLabel = swatchRow
|
||||||
? swatchRow.dataset.paletteLabel
|
? swatchRow.dataset.paletteLabel
|
||||||
: palette.slice(8).replace(/^\w/, function (c) { return c.toUpperCase(); });
|
: palette.slice(8).replace(/^\w/, function (c) { return c.toUpperCase(); });
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST', credentials: 'same-origin',
|
||||||
credentials: 'same-origin',
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': _getCsrf() },
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': _getCsrf(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ palette: palette }),
|
body: JSON.stringify({ palette: palette }),
|
||||||
})
|
})
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
@@ -171,12 +169,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── init ──────────────────────────────────────────────────────────────────
|
// ── DON/DOFF ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _setGreeting(name) {
|
|
||||||
var el = document.getElementById('id_greeting_name');
|
|
||||||
if (el) el.textContent = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _bindDonDoff(item) {
|
function _bindDonDoff(item) {
|
||||||
var donBtn = item.querySelector('.note-don-btn');
|
var donBtn = item.querySelector('.note-don-btn');
|
||||||
@@ -192,11 +185,21 @@
|
|||||||
})
|
})
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
_setGreeting(data.title);
|
// Auto-DOFF any previously DONned note (UI only — backend replaces active_title)
|
||||||
donBtn.classList.add('btn-disabled');
|
if (_donnedItem && _donnedItem !== item) {
|
||||||
donBtn.textContent = '×';
|
_donnedItem.classList.remove('note-item--donned');
|
||||||
doffBtn.classList.remove('btn-disabled');
|
var prevDon = _donnedItem.querySelector('.note-don-btn');
|
||||||
doffBtn.textContent = 'DOFF';
|
var prevDoff = _donnedItem.querySelector('.note-doff-btn');
|
||||||
|
if (prevDon) { prevDon.classList.remove('btn-disabled'); prevDon.textContent = 'DON'; }
|
||||||
|
if (prevDoff) { prevDoff.classList.add('btn-disabled'); prevDoff.textContent = '×'; }
|
||||||
|
}
|
||||||
|
_donnedItem = item;
|
||||||
|
item.classList.add('note-item--donned');
|
||||||
|
// Clear lock so hover is restored for other notes
|
||||||
|
_clearLock();
|
||||||
|
donBtn.classList.add('btn-disabled'); donBtn.textContent = '×';
|
||||||
|
doffBtn.classList.remove('btn-disabled'); doffBtn.textContent = 'DOFF';
|
||||||
|
_setGreeting(data.greeting || 'Welcome,', data.title || 'Earthman');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,33 +210,60 @@
|
|||||||
method: 'POST', credentials: 'same-origin',
|
method: 'POST', credentials: 'same-origin',
|
||||||
headers: { 'X-CSRFToken': _getCsrf() },
|
headers: { 'X-CSRFToken': _getCsrf() },
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function (r) { return r.json(); })
|
||||||
_setGreeting('Earthman');
|
.then(function (data) {
|
||||||
doffBtn.classList.add('btn-disabled');
|
_donnedItem = null;
|
||||||
doffBtn.textContent = '×';
|
item.classList.remove('note-item--donned');
|
||||||
donBtn.classList.remove('btn-disabled');
|
_clearLock();
|
||||||
donBtn.textContent = 'DON';
|
doffBtn.classList.add('btn-disabled'); doffBtn.textContent = '×';
|
||||||
|
donBtn.classList.remove('btn-disabled'); donBtn.textContent = 'DON';
|
||||||
|
_setGreeting(data.greeting || 'Welcome,', data.title || 'Earthman');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── init ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _init() {
|
function _init() {
|
||||||
document.querySelectorAll('.note-item__image-box').forEach(function (box) {
|
document.querySelectorAll('.note-item').forEach(function (item) {
|
||||||
box.addEventListener('click', function (e) {
|
// Detect already-DONned note on load (DON btn is disabled = currently equipped)
|
||||||
|
var don = item.querySelector('.note-don-btn');
|
||||||
|
if (don && don.classList.contains('btn-disabled')) {
|
||||||
|
item.classList.add('note-item--donned');
|
||||||
|
_donnedItem = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindDonDoff(item);
|
||||||
|
|
||||||
|
// Image box click → palette modal (for notes that have one)
|
||||||
|
var box = item.querySelector('.note-item__image-box:not(.note-item__image-box--label)');
|
||||||
|
if (box) {
|
||||||
|
box.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
_activeItem = item;
|
||||||
|
_openModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note click → toggle lock
|
||||||
|
item.addEventListener('click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
_activeItem = box.closest('.note-item');
|
if (_lockedItem === item) {
|
||||||
_openModal();
|
_clearLock();
|
||||||
|
} else {
|
||||||
|
_clearLock();
|
||||||
|
_lockedItem = item;
|
||||||
|
item.classList.add('note-item--locked');
|
||||||
|
document.body.classList.add('notes-locked');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.note-item').forEach(function (item) {
|
// Body click → dismiss modal and clear lock
|
||||||
_bindDonDoff(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Body click → dismiss modal and revert any preview
|
|
||||||
document.body.addEventListener('click', function () {
|
document.body.addEventListener('click', function () {
|
||||||
if (_selectedPalette) _revertPreview();
|
if (_selectedPalette) _revertPreview();
|
||||||
_closeModal();
|
_closeModal();
|
||||||
|
_clearLock();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,4 +272,20 @@
|
|||||||
} else {
|
} else {
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose test API
|
||||||
|
window.NotePage = {
|
||||||
|
_init: _init,
|
||||||
|
_testReset: function () {
|
||||||
|
_selectedPalette = null;
|
||||||
|
_activeItem = null;
|
||||||
|
_originalPalette = null;
|
||||||
|
_dismissTimer = null;
|
||||||
|
_lockedItem = null;
|
||||||
|
_donnedItem = null;
|
||||||
|
document.body.classList.remove('notes-locked');
|
||||||
|
},
|
||||||
|
get _donnedItem() { return _donnedItem; },
|
||||||
|
set _donnedItem(v) { _donnedItem = v; },
|
||||||
|
};
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -116,13 +116,13 @@ _NOTE_META = {
|
|||||||
"swatch_label": None,
|
"swatch_label": None,
|
||||||
},
|
},
|
||||||
"super-schizo": {
|
"super-schizo": {
|
||||||
"title": "Schizoid Man",
|
"title": "Super-Schizo",
|
||||||
"description": mark_safe('Admin access granted to <span class="card-ref">I. The Schizo</span> as Significator'),
|
"description": mark_safe('Admin access granted to <span class="card-ref">I. The Schizo</span> as Significator'),
|
||||||
"palette_options": [],
|
"palette_options": [],
|
||||||
"swatch_label": "I",
|
"swatch_label": "I",
|
||||||
},
|
},
|
||||||
"super-nomad": {
|
"super-nomad": {
|
||||||
"title": "Stranger",
|
"title": "Super-Nomad",
|
||||||
"description": mark_safe('Admin access granted to <span class="card-ref">0. The Nomad</span> as Significator'),
|
"description": mark_safe('Admin access granted to <span class="card-ref">0. The Nomad</span> as Significator'),
|
||||||
"palette_options": [],
|
"palette_options": [],
|
||||||
"swatch_label": "0",
|
"swatch_label": "0",
|
||||||
@@ -156,13 +156,14 @@ def my_notes(request):
|
|||||||
active_title = request.user.active_title
|
active_title = request.user.active_title
|
||||||
note_items = [
|
note_items = [
|
||||||
{
|
{
|
||||||
"obj": n,
|
"obj": n,
|
||||||
"title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
|
"title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
|
||||||
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
"recognition_title": n.display_title,
|
||||||
"palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
|
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
||||||
"swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"),
|
"palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
|
||||||
"palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "",
|
"swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"),
|
||||||
"is_equipped": active_title is not None and active_title.pk == n.pk,
|
"palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "",
|
||||||
|
"is_equipped": active_title is not None and active_title.pk == n.pk,
|
||||||
}
|
}
|
||||||
for n in qs
|
for n in qs
|
||||||
]
|
]
|
||||||
@@ -183,8 +184,7 @@ def don_title(request, slug):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
request.user.active_title = note
|
request.user.active_title = note
|
||||||
request.user.save(update_fields=["active_title"])
|
request.user.save(update_fields=["active_title"])
|
||||||
title = _NOTE_META.get(slug, {}).get("title", slug.capitalize())
|
return JsonResponse({"title": note.display_title, "greeting": note.display_greeting})
|
||||||
return JsonResponse({"title": title})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
@@ -192,7 +192,7 @@ def doff_title(request, slug):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
request.user.active_title = None
|
request.user.active_title = None
|
||||||
request.user.save(update_fields=["active_title"])
|
request.user.save(update_fields=["active_title"])
|
||||||
return JsonResponse({"ok": True})
|
return JsonResponse({"ok": True, "greeting": "Welcome,", "title": "Earthman"})
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
|
|||||||
204
src/static/tests/NotePageSpec.js
Normal file
204
src/static/tests/NotePageSpec.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
function flushPromises() {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("NotePage", () => {
|
||||||
|
let testDiv, item1, item2, don1, doff1, don2, doff2;
|
||||||
|
|
||||||
|
function makeItem(slug, isEquipped) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "note-item";
|
||||||
|
li.dataset.slug = slug;
|
||||||
|
li.dataset.donUrl = `/billboard/note/${slug}/don`;
|
||||||
|
li.dataset.doffUrl = `/billboard/note/${slug}/doff`;
|
||||||
|
li.innerHTML = `
|
||||||
|
<div class="note-don-doff">
|
||||||
|
<button class="btn btn-equip note-don-btn${isEquipped ? " btn-disabled" : ""}">${isEquipped ? "×" : "DON"}</button>
|
||||||
|
<button class="btn btn-unequip note-doff-btn${isEquipped ? "" : " btn-disabled"}">${isEquipped ? "DOFF" : "×"}</button>
|
||||||
|
</div>
|
||||||
|
<div class="note-item__body"><p class="note-item__title">${slug}</p></div>
|
||||||
|
`;
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
NotePage._testReset();
|
||||||
|
testDiv = document.createElement("div");
|
||||||
|
testDiv.innerHTML = `<span id="id_greeting_prefix">Welcome,</span><span id="id_greeting_name">Earthman</span>`;
|
||||||
|
item1 = makeItem("super-schizo", false);
|
||||||
|
item2 = makeItem("super-nomad", false);
|
||||||
|
testDiv.appendChild(item1);
|
||||||
|
testDiv.appendChild(item2);
|
||||||
|
document.body.appendChild(testDiv);
|
||||||
|
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ title: "Schizoid Man", greeting: "21st Century" }) })
|
||||||
|
);
|
||||||
|
|
||||||
|
don1 = item1.querySelector(".note-don-btn");
|
||||||
|
doff1 = item1.querySelector(".note-doff-btn");
|
||||||
|
don2 = item2.querySelector(".note-don-btn");
|
||||||
|
doff2 = item2.querySelector(".note-doff-btn");
|
||||||
|
|
||||||
|
NotePage._init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDiv.remove();
|
||||||
|
document.body.classList.remove("notes-locked");
|
||||||
|
delete window.fetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Lock / unlock ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("click-lock behaviour", () => {
|
||||||
|
it("clicking a note adds note-item--locked", () => {
|
||||||
|
item1.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking a note adds notes-locked to body", () => {
|
||||||
|
item1.click();
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking the same note again removes lock", () => {
|
||||||
|
item1.click();
|
||||||
|
item1.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking a different note moves lock to that note", () => {
|
||||||
|
item1.click();
|
||||||
|
item2.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(item2.classList.contains("note-item--locked")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("body click clears all locks", () => {
|
||||||
|
item1.click();
|
||||||
|
document.body.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── DON/DOFF state ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("DON button", () => {
|
||||||
|
it("clicking DON sends POST to don URL", async () => {
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
|
"/billboard/note/super-schizo/don",
|
||||||
|
jasmine.objectContaining({ method: "POST" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DON, note gains note-item--donned", async () => {
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(item1.classList.contains("note-item--donned")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DON, lock is cleared", async () => {
|
||||||
|
item1.click(); // lock first
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DON, greeting prefix and name are updated", async () => {
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(document.getElementById("id_greeting_prefix").innerHTML).toContain("21st Century");
|
||||||
|
expect(document.getElementById("id_greeting_name").textContent).toBe("Schizoid Man");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DONning item1 auto-removes donned state from previously DONned item2", async () => {
|
||||||
|
item2.classList.add("note-item--donned");
|
||||||
|
don2.classList.add("btn-disabled"); don2.textContent = "×";
|
||||||
|
doff2.classList.remove("btn-disabled"); doff2.textContent = "DOFF";
|
||||||
|
NotePage._donnedItem = item2;
|
||||||
|
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(item2.classList.contains("note-item--donned")).toBe(false);
|
||||||
|
expect(don2.classList.contains("btn-disabled")).toBe(false);
|
||||||
|
expect(don2.textContent).toBe("DON");
|
||||||
|
expect(doff2.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DONning one does not POST a doff for the previous", async () => {
|
||||||
|
item2.classList.add("note-item--donned");
|
||||||
|
NotePage._donnedItem = item2;
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
// Only one fetch call (the DON), no DOFF call for item2
|
||||||
|
expect(window.fetch.calls.count()).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DOFF button", () => {
|
||||||
|
it("clicking DOFF sends POST to doff URL", async () => {
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
|
||||||
|
);
|
||||||
|
item1.classList.add("note-item--donned");
|
||||||
|
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
|
||||||
|
don1.classList.add("btn-disabled"); don1.textContent = "×";
|
||||||
|
NotePage._donnedItem = item1;
|
||||||
|
|
||||||
|
doff1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
|
"/billboard/note/super-schizo/doff",
|
||||||
|
jasmine.objectContaining({ method: "POST" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DOFF, note loses note-item--donned", async () => {
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
|
||||||
|
);
|
||||||
|
item1.classList.add("note-item--donned");
|
||||||
|
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
|
||||||
|
NotePage._donnedItem = item1;
|
||||||
|
|
||||||
|
doff1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(item1.classList.contains("note-item--donned")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DOFF, greeting reverts to Welcome, / Earthman", async () => {
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
|
||||||
|
);
|
||||||
|
document.getElementById("id_greeting_prefix").textContent = "21st Century";
|
||||||
|
document.getElementById("id_greeting_name").textContent = "Schizoid Man";
|
||||||
|
item1.classList.add("note-item--donned");
|
||||||
|
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
|
||||||
|
NotePage._donnedItem = item1;
|
||||||
|
|
||||||
|
doff1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(document.getElementById("id_greeting_prefix").textContent).toBe("Welcome,");
|
||||||
|
expect(document.getElementById("id_greeting_name").textContent).toBe("Earthman");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Initial donned state ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("initial load with an already-donned note", () => {
|
||||||
|
it("note whose DON is btn-disabled gets note-item--donned on init", () => {
|
||||||
|
const equippedItem = makeItem("stargazer", true);
|
||||||
|
testDiv.appendChild(equippedItem);
|
||||||
|
NotePage._testReset();
|
||||||
|
NotePage._init();
|
||||||
|
expect(equippedItem.classList.contains("note-item--donned")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,9 +24,11 @@
|
|||||||
<script src="SigSelectSpec.js"></script>
|
<script src="SigSelectSpec.js"></script>
|
||||||
<script src="NatusWheelSpec.js"></script>
|
<script src="NatusWheelSpec.js"></script>
|
||||||
<script src="NoteSpec.js"></script>
|
<script src="NoteSpec.js"></script>
|
||||||
|
<script src="NotePageSpec.js"></script>
|
||||||
<!-- src files -->
|
<!-- src files -->
|
||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/dashboard/note.js"></script>
|
<script src="/static/apps/dashboard/note.js"></script>
|
||||||
|
<script src="/static/apps/billboard/note-page.js"></script>
|
||||||
<script src="/static/apps/epic/role-select.js"></script>
|
<script src="/static/apps/epic/role-select.js"></script>
|
||||||
<script src="/static/apps/epic/tray.js"></script>
|
<script src="/static/apps/epic/tray.js"></script>
|
||||||
<script src="/static/apps/epic/sig-select.js"></script>
|
<script src="/static/apps/epic/sig-select.js"></script>
|
||||||
|
|||||||
@@ -596,3 +596,8 @@ body {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-ref {
|
||||||
|
color: rgba(var(--terUser), 1) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
|
||||||
.btn { margin: 0; }
|
.btn { margin: 0; }
|
||||||
}
|
}
|
||||||
@@ -85,19 +87,33 @@
|
|||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
cursor: help;
|
cursor: pointer;
|
||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
|
||||||
&:hover,
|
// Hover glow — only when no note is click-locked on the page
|
||||||
&:active,
|
body:not(.notes-locked) &:hover {
|
||||||
|
border-color: rgba(var(--terUser), 1);
|
||||||
|
box-shadow: 0 0 10px rgba(var(--ninUser), 0.35);
|
||||||
|
.note-item__title { color: rgba(var(--terUser), 1); }
|
||||||
|
.note-don-doff { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette modal open (existing)
|
||||||
&.note-item--active {
|
&.note-item--active {
|
||||||
border-color: rgba(var(--terUser), 1);
|
border-color: rgba(var(--terUser), 1);
|
||||||
opacity: 1;
|
|
||||||
box-shadow: 0 0 10px rgba(var(--ninUser), 0.35);
|
box-shadow: 0 0 10px rgba(var(--ninUser), 0.35);
|
||||||
|
|
||||||
.note-item__title { color: rgba(var(--terUser), 1); }
|
.note-item__title { color: rgba(var(--terUser), 1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Click-locked: glow + DON/DOFF always visible
|
||||||
|
&.note-item--locked,
|
||||||
|
&.note-item--donned {
|
||||||
|
border-color: rgba(var(--terUser), 1);
|
||||||
|
box-shadow: 0 0 10px rgba(var(--ninUser), 0.35);
|
||||||
|
.note-item__title { color: rgba(var(--terUser), 1); }
|
||||||
|
.note-don-doff { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
.note-item__body {
|
.note-item__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -168,11 +184,11 @@
|
|||||||
&--label {
|
&--label {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: italic;
|
color: rgba(var(--secUser), 0.6);
|
||||||
color: rgba(var(--terUser), 1);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
border-color: rgba(var(--secUser), 0.6);
|
||||||
&:hover { opacity: 1; }
|
&:hover { opacity: 1; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
204
src/static_src/tests/NotePageSpec.js
Normal file
204
src/static_src/tests/NotePageSpec.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
function flushPromises() {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("NotePage", () => {
|
||||||
|
let testDiv, item1, item2, don1, doff1, don2, doff2;
|
||||||
|
|
||||||
|
function makeItem(slug, isEquipped) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "note-item";
|
||||||
|
li.dataset.slug = slug;
|
||||||
|
li.dataset.donUrl = `/billboard/note/${slug}/don`;
|
||||||
|
li.dataset.doffUrl = `/billboard/note/${slug}/doff`;
|
||||||
|
li.innerHTML = `
|
||||||
|
<div class="note-don-doff">
|
||||||
|
<button class="btn btn-equip note-don-btn${isEquipped ? " btn-disabled" : ""}">${isEquipped ? "×" : "DON"}</button>
|
||||||
|
<button class="btn btn-unequip note-doff-btn${isEquipped ? "" : " btn-disabled"}">${isEquipped ? "DOFF" : "×"}</button>
|
||||||
|
</div>
|
||||||
|
<div class="note-item__body"><p class="note-item__title">${slug}</p></div>
|
||||||
|
`;
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
NotePage._testReset();
|
||||||
|
testDiv = document.createElement("div");
|
||||||
|
testDiv.innerHTML = `<span id="id_greeting_prefix">Welcome,</span><span id="id_greeting_name">Earthman</span>`;
|
||||||
|
item1 = makeItem("super-schizo", false);
|
||||||
|
item2 = makeItem("super-nomad", false);
|
||||||
|
testDiv.appendChild(item1);
|
||||||
|
testDiv.appendChild(item2);
|
||||||
|
document.body.appendChild(testDiv);
|
||||||
|
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ title: "Schizoid Man", greeting: "21st Century" }) })
|
||||||
|
);
|
||||||
|
|
||||||
|
don1 = item1.querySelector(".note-don-btn");
|
||||||
|
doff1 = item1.querySelector(".note-doff-btn");
|
||||||
|
don2 = item2.querySelector(".note-don-btn");
|
||||||
|
doff2 = item2.querySelector(".note-doff-btn");
|
||||||
|
|
||||||
|
NotePage._init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDiv.remove();
|
||||||
|
document.body.classList.remove("notes-locked");
|
||||||
|
delete window.fetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Lock / unlock ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("click-lock behaviour", () => {
|
||||||
|
it("clicking a note adds note-item--locked", () => {
|
||||||
|
item1.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking a note adds notes-locked to body", () => {
|
||||||
|
item1.click();
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking the same note again removes lock", () => {
|
||||||
|
item1.click();
|
||||||
|
item1.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking a different note moves lock to that note", () => {
|
||||||
|
item1.click();
|
||||||
|
item2.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(item2.classList.contains("note-item--locked")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("body click clears all locks", () => {
|
||||||
|
item1.click();
|
||||||
|
document.body.click();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── DON/DOFF state ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("DON button", () => {
|
||||||
|
it("clicking DON sends POST to don URL", async () => {
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
|
"/billboard/note/super-schizo/don",
|
||||||
|
jasmine.objectContaining({ method: "POST" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DON, note gains note-item--donned", async () => {
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(item1.classList.contains("note-item--donned")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DON, lock is cleared", async () => {
|
||||||
|
item1.click(); // lock first
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(item1.classList.contains("note-item--locked")).toBe(false);
|
||||||
|
expect(document.body.classList.contains("notes-locked")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DON, greeting prefix and name are updated", async () => {
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(document.getElementById("id_greeting_prefix").innerHTML).toContain("21st Century");
|
||||||
|
expect(document.getElementById("id_greeting_name").textContent).toBe("Schizoid Man");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DONning item1 auto-removes donned state from previously DONned item2", async () => {
|
||||||
|
item2.classList.add("note-item--donned");
|
||||||
|
don2.classList.add("btn-disabled"); don2.textContent = "×";
|
||||||
|
doff2.classList.remove("btn-disabled"); doff2.textContent = "DOFF";
|
||||||
|
NotePage._donnedItem = item2;
|
||||||
|
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(item2.classList.contains("note-item--donned")).toBe(false);
|
||||||
|
expect(don2.classList.contains("btn-disabled")).toBe(false);
|
||||||
|
expect(don2.textContent).toBe("DON");
|
||||||
|
expect(doff2.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DONning one does not POST a doff for the previous", async () => {
|
||||||
|
item2.classList.add("note-item--donned");
|
||||||
|
NotePage._donnedItem = item2;
|
||||||
|
don1.click();
|
||||||
|
await flushPromises();
|
||||||
|
// Only one fetch call (the DON), no DOFF call for item2
|
||||||
|
expect(window.fetch.calls.count()).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DOFF button", () => {
|
||||||
|
it("clicking DOFF sends POST to doff URL", async () => {
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
|
||||||
|
);
|
||||||
|
item1.classList.add("note-item--donned");
|
||||||
|
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
|
||||||
|
don1.classList.add("btn-disabled"); don1.textContent = "×";
|
||||||
|
NotePage._donnedItem = item1;
|
||||||
|
|
||||||
|
doff1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
|
"/billboard/note/super-schizo/doff",
|
||||||
|
jasmine.objectContaining({ method: "POST" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DOFF, note loses note-item--donned", async () => {
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
|
||||||
|
);
|
||||||
|
item1.classList.add("note-item--donned");
|
||||||
|
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
|
||||||
|
NotePage._donnedItem = item1;
|
||||||
|
|
||||||
|
doff1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(item1.classList.contains("note-item--donned")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after DOFF, greeting reverts to Welcome, / Earthman", async () => {
|
||||||
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
|
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
|
||||||
|
);
|
||||||
|
document.getElementById("id_greeting_prefix").textContent = "21st Century";
|
||||||
|
document.getElementById("id_greeting_name").textContent = "Schizoid Man";
|
||||||
|
item1.classList.add("note-item--donned");
|
||||||
|
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
|
||||||
|
NotePage._donnedItem = item1;
|
||||||
|
|
||||||
|
doff1.click();
|
||||||
|
await flushPromises();
|
||||||
|
expect(document.getElementById("id_greeting_prefix").textContent).toBe("Welcome,");
|
||||||
|
expect(document.getElementById("id_greeting_name").textContent).toBe("Earthman");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Initial donned state ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("initial load with an already-donned note", () => {
|
||||||
|
it("note whose DON is btn-disabled gets note-item--donned on init", () => {
|
||||||
|
const equippedItem = makeItem("stargazer", true);
|
||||||
|
testDiv.appendChild(equippedItem);
|
||||||
|
NotePage._testReset();
|
||||||
|
NotePage._init();
|
||||||
|
expect(equippedItem.classList.contains("note-item--donned")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,9 +24,11 @@
|
|||||||
<script src="SigSelectSpec.js"></script>
|
<script src="SigSelectSpec.js"></script>
|
||||||
<script src="NatusWheelSpec.js"></script>
|
<script src="NatusWheelSpec.js"></script>
|
||||||
<script src="NoteSpec.js"></script>
|
<script src="NoteSpec.js"></script>
|
||||||
|
<script src="NotePageSpec.js"></script>
|
||||||
<!-- src files -->
|
<!-- src files -->
|
||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/dashboard/note.js"></script>
|
<script src="/static/apps/dashboard/note.js"></script>
|
||||||
|
<script src="/static/apps/billboard/note-page.js"></script>
|
||||||
<script src="/static/apps/epic/role-select.js"></script>
|
<script src="/static/apps/epic/role-select.js"></script>
|
||||||
<script src="/static/apps/epic/tray.js"></script>
|
<script src="/static/apps/epic/tray.js"></script>
|
||||||
<script src="/static/apps/epic/sig-select.js"></script>
|
<script src="/static/apps/epic/sig-select.js"></script>
|
||||||
|
|||||||
@@ -25,11 +25,11 @@
|
|||||||
|
|
||||||
<div class="note-item__body">
|
<div class="note-item__body">
|
||||||
<p class="note-item__title">{{ item.title }}</p>
|
<p class="note-item__title">{{ item.title }}</p>
|
||||||
<p class="note-item__description">{{ item.description }}</p>
|
<p class="note-item__description">{{ item.description|safe }}</p>
|
||||||
<div class="note-recognitions">
|
<div class="note-recognitions">
|
||||||
<div class="note-recognitions__header">Recognitions</div>
|
<div class="note-recognitions__header">Recognitions</div>
|
||||||
<ul class="note-recognitions__list">
|
<ul class="note-recognitions__list">
|
||||||
<li><span class="note-recognitions__dim">Title:</span> <strong>{{ item.title }}</strong></li>
|
<li><span class="note-recognitions__dim">Title:</span> <strong>{{ item.recognition_title }}</strong></li>
|
||||||
{% if item.obj.palette %}
|
{% if item.obj.palette %}
|
||||||
<li class="note-recognitions__palette-line"><span class="note-recognitions__dim">Palette:</span> <strong>{{ item.palette_label }}</strong></li>
|
<li class="note-recognitions__palette-line"><span class="note-recognitions__dim">Palette:</span> <strong>{{ item.palette_label }}</strong></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a href="/" class="navbar-brand">
|
<a href="/" class="navbar-brand">
|
||||||
<h1>{% if user.active_title %}{{ user.active_title.display_greeting|safe }}{% else %}Welcome,{% endif %}<br><span id="id_greeting_name">{% if user.active_title %}{{ user.active_title.display_title }}{% else %}Earthman{% endif %}</span></h1>
|
<h1><span id="id_greeting_prefix">{% if user.active_title %}{{ user.active_title.display_greeting|safe }}{% else %}Welcome,{% endif %}</span><br><span id="id_greeting_name">{% if user.active_title %}{{ user.active_title.display_title }}{% else %}Earthman{% endif %}</span></h1>
|
||||||
</a>
|
</a>
|
||||||
{% if user.email %}
|
{% if user.email %}
|
||||||
<div class="navbar-user">
|
<div class="navbar-user">
|
||||||
|
|||||||
Reference in New Issue
Block a user