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 _originalPalette = null;
|
||||
var _dismissTimer = null;
|
||||
var _lockedItem = null; // click-locked note (glow + DON/DOFF pinned)
|
||||
var _donnedItem = null; // currently DONned note (persistent glow)
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -47,6 +49,23 @@
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
||||
function _openModal() {
|
||||
@@ -88,47 +107,31 @@
|
||||
_originalPalette = null;
|
||||
}
|
||||
|
||||
// Wire event listeners onto the freshly-cloned modal DOM.
|
||||
function _wireModal() {
|
||||
var modal = _activeModal();
|
||||
if (!modal) return;
|
||||
|
||||
// Swatch body click → preview palette sitewide + show confirm
|
||||
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
||||
body.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (_selectedPalette) _revertPreview();
|
||||
|
||||
_selectedPalette = _paletteClass(body.parentElement);
|
||||
_originalPalette = _currentBodyPalette();
|
||||
|
||||
body.classList.add('previewing');
|
||||
_swapBodyPalette(_selectedPalette);
|
||||
_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) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_doSetPalette();
|
||||
});
|
||||
btn.addEventListener('click', function (e) { e.stopPropagation(); _doSetPalette(); });
|
||||
});
|
||||
|
||||
// Confirm NVM → revert preview only; main swatch modal stays open
|
||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_revertPreview();
|
||||
});
|
||||
btn.addEventListener('click', function (e) { e.stopPropagation(); _revertPreview(); });
|
||||
});
|
||||
|
||||
// Stop modal clicks from reaching the body dismiss handler.
|
||||
modal.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||
}
|
||||
|
||||
@@ -138,18 +141,13 @@
|
||||
var url = _activeItem.dataset.setPaletteUrl;
|
||||
var palette = _selectedPalette;
|
||||
var item = _activeItem;
|
||||
// Read label from swatch row while modal is still in DOM
|
||||
var swatchRow = _activeModal() && _activeModal().querySelector('.' + palette + '[data-palette-label]');
|
||||
var paletteLabel = swatchRow
|
||||
? swatchRow.dataset.paletteLabel
|
||||
: palette.slice(8).replace(/^\w/, function (c) { return c.toUpperCase(); });
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': _getCsrf(),
|
||||
},
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': _getCsrf() },
|
||||
body: JSON.stringify({ palette: palette }),
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
@@ -171,12 +169,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function _setGreeting(name) {
|
||||
var el = document.getElementById('id_greeting_name');
|
||||
if (el) el.textContent = name;
|
||||
}
|
||||
// ── DON/DOFF ──────────────────────────────────────────────────────────────
|
||||
|
||||
function _bindDonDoff(item) {
|
||||
var donBtn = item.querySelector('.note-don-btn');
|
||||
@@ -192,11 +185,21 @@
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
_setGreeting(data.title);
|
||||
donBtn.classList.add('btn-disabled');
|
||||
donBtn.textContent = '×';
|
||||
doffBtn.classList.remove('btn-disabled');
|
||||
doffBtn.textContent = 'DOFF';
|
||||
// Auto-DOFF any previously DONned note (UI only — backend replaces active_title)
|
||||
if (_donnedItem && _donnedItem !== item) {
|
||||
_donnedItem.classList.remove('note-item--donned');
|
||||
var prevDon = _donnedItem.querySelector('.note-don-btn');
|
||||
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',
|
||||
headers: { 'X-CSRFToken': _getCsrf() },
|
||||
})
|
||||
.then(function () {
|
||||
_setGreeting('Earthman');
|
||||
doffBtn.classList.add('btn-disabled');
|
||||
doffBtn.textContent = '×';
|
||||
donBtn.classList.remove('btn-disabled');
|
||||
donBtn.textContent = 'DON';
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
_donnedItem = null;
|
||||
item.classList.remove('note-item--donned');
|
||||
_clearLock();
|
||||
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() {
|
||||
document.querySelectorAll('.note-item__image-box').forEach(function (box) {
|
||||
box.addEventListener('click', function (e) {
|
||||
document.querySelectorAll('.note-item').forEach(function (item) {
|
||||
// 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();
|
||||
_activeItem = box.closest('.note-item');
|
||||
_openModal();
|
||||
if (_lockedItem === item) {
|
||||
_clearLock();
|
||||
} else {
|
||||
_clearLock();
|
||||
_lockedItem = item;
|
||||
item.classList.add('note-item--locked');
|
||||
document.body.classList.add('notes-locked');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.note-item').forEach(function (item) {
|
||||
_bindDonDoff(item);
|
||||
});
|
||||
|
||||
// Body click → dismiss modal and revert any preview
|
||||
// Body click → dismiss modal and clear lock
|
||||
document.body.addEventListener('click', function () {
|
||||
if (_selectedPalette) _revertPreview();
|
||||
_closeModal();
|
||||
_clearLock();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,4 +272,20 @@
|
||||
} else {
|
||||
_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,
|
||||
},
|
||||
"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'),
|
||||
"palette_options": [],
|
||||
"swatch_label": "I",
|
||||
},
|
||||
"super-nomad": {
|
||||
"title": "Stranger",
|
||||
"title": "Super-Nomad",
|
||||
"description": mark_safe('Admin access granted to <span class="card-ref">0. The Nomad</span> as Significator'),
|
||||
"palette_options": [],
|
||||
"swatch_label": "0",
|
||||
@@ -156,13 +156,14 @@ def my_notes(request):
|
||||
active_title = request.user.active_title
|
||||
note_items = [
|
||||
{
|
||||
"obj": n,
|
||||
"title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
|
||||
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
||||
"palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
|
||||
"swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"),
|
||||
"palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "",
|
||||
"is_equipped": active_title is not None and active_title.pk == n.pk,
|
||||
"obj": n,
|
||||
"title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
|
||||
"recognition_title": n.display_title,
|
||||
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
||||
"palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
|
||||
"swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"),
|
||||
"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
|
||||
]
|
||||
@@ -183,8 +184,7 @@ def don_title(request, slug):
|
||||
if request.method == "POST":
|
||||
request.user.active_title = note
|
||||
request.user.save(update_fields=["active_title"])
|
||||
title = _NOTE_META.get(slug, {}).get("title", slug.capitalize())
|
||||
return JsonResponse({"title": title})
|
||||
return JsonResponse({"title": note.display_title, "greeting": note.display_greeting})
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
@@ -192,7 +192,7 @@ def doff_title(request, slug):
|
||||
if request.method == "POST":
|
||||
request.user.active_title = None
|
||||
request.user.save(update_fields=["active_title"])
|
||||
return JsonResponse({"ok": True})
|
||||
return JsonResponse({"ok": True, "greeting": "Welcome,", "title": "Earthman"})
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
|
||||
Reference in New Issue
Block a user