note palette: swatch previews body palette, NVM reverts, OK saves sitewide; note_set_palette also saves user.palette — TDD
- note-page.js: body class swap on swatch click; 10s auto-revert timer; NVM reverts; .note-item--active persists border/glow while modal open; .previewing on swatch - billboard/views.py: note_set_palette also saves user.palette via _unlocked_palettes_for_user - _note.scss: .note-swatch-body gradient (palette vars cascade from parent palette-* class); .previewing state; .note-item--active; note-palette-modal tooltip glass; note-palette-confirm floats below modal (position:absolute, out of flow) - my_notes.html: note-item__body wrapper; image-box right; swatch row OK buttons removed - FTs: T2a URL fix (/recognition → /my-notes); T2b split into preview+persist & NVM tests; NoteSetPaletteViewTest.test_also_saves_user_palette IT 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:
@@ -1,9 +1,10 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var _state = 'closed'; // 'closed' | 'open' | 'previewing'
|
||||
var _selectedPalette = null;
|
||||
var _activeItem = null;
|
||||
var _originalPalette = null;
|
||||
var _dismissTimer = null;
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -15,6 +16,22 @@
|
||||
return Array.from(el.classList).find(function (c) { return c.startsWith('palette-'); }) || '';
|
||||
}
|
||||
|
||||
function _currentBodyPalette() {
|
||||
return Array.from(document.body.classList).find(function (c) { return c.startsWith('palette-'); }) || null;
|
||||
}
|
||||
|
||||
function _swapBodyPalette(paletteName) {
|
||||
var old = _currentBodyPalette();
|
||||
if (old) document.body.classList.remove(old);
|
||||
document.body.classList.add(paletteName);
|
||||
}
|
||||
|
||||
function _revertBodyPalette() {
|
||||
var current = _currentBodyPalette();
|
||||
if (current) document.body.classList.remove(current);
|
||||
if (_originalPalette) document.body.classList.add(_originalPalette);
|
||||
}
|
||||
|
||||
function _getCsrf() {
|
||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
return m ? m[1] : '';
|
||||
@@ -23,7 +40,6 @@
|
||||
// ── modal lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
function _openModal() {
|
||||
// Clone from <template> if the modal is not in the DOM yet.
|
||||
var existing = _activeModal();
|
||||
if (!existing) {
|
||||
var tpl = _activeItem.querySelector('.note-palette-modal-tpl');
|
||||
@@ -32,15 +48,37 @@
|
||||
_activeItem.appendChild(clone);
|
||||
_wireModal();
|
||||
}
|
||||
_state = 'open';
|
||||
_activeItem.classList.add('note-item--active');
|
||||
var confirmEl = _activeModal().querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = true;
|
||||
}
|
||||
|
||||
function _closeModal() {
|
||||
_state = 'closed';
|
||||
var modal = _activeModal();
|
||||
clearTimeout(_dismissTimer);
|
||||
_dismissTimer = null;
|
||||
var modal = _activeModal();
|
||||
if (modal) modal.remove();
|
||||
if (_activeItem) _activeItem.classList.remove('note-item--active');
|
||||
_activeItem = null;
|
||||
_selectedPalette = null;
|
||||
_originalPalette = null;
|
||||
}
|
||||
|
||||
function _revertPreview() {
|
||||
clearTimeout(_dismissTimer);
|
||||
_dismissTimer = null;
|
||||
_revertBodyPalette();
|
||||
// Remove .previewing from any swatch in the active modal
|
||||
var modal = _activeModal();
|
||||
if (modal) {
|
||||
modal.querySelectorAll('.note-swatch-body.previewing').forEach(function (s) {
|
||||
s.classList.remove('previewing');
|
||||
});
|
||||
var confirmEl = modal.querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = true;
|
||||
}
|
||||
_selectedPalette = null;
|
||||
_originalPalette = null;
|
||||
}
|
||||
|
||||
// Wire event listeners onto the freshly-cloned modal DOM.
|
||||
@@ -48,28 +86,30 @@
|
||||
var modal = _activeModal();
|
||||
if (!modal) return;
|
||||
|
||||
// Swatch body → preview (remove modal from DOM)
|
||||
// Swatch body click → preview palette sitewide + show confirm
|
||||
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
||||
body.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_selectedPalette = _paletteClass(body.parentElement);
|
||||
_state = 'previewing';
|
||||
modal.remove();
|
||||
});
|
||||
});
|
||||
// Clear any in-progress preview first
|
||||
if (_selectedPalette) _revertPreview();
|
||||
|
||||
_selectedPalette = _paletteClass(body.parentElement);
|
||||
_originalPalette = _currentBodyPalette();
|
||||
|
||||
body.classList.add('previewing');
|
||||
_swapBodyPalette(_selectedPalette);
|
||||
|
||||
// OK button (not inside confirm submenu) → show confirm submenu
|
||||
modal.querySelectorAll('.btn.btn-confirm').forEach(function (btn) {
|
||||
if (btn.closest('.note-palette-confirm')) return;
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_selectedPalette = _paletteClass(btn.parentElement);
|
||||
var confirmEl = modal.querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = false;
|
||||
|
||||
// Auto-revert after 10s
|
||||
_dismissTimer = setTimeout(function () {
|
||||
_revertPreview();
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm YES → POST and update DOM
|
||||
// Confirm OK → commit palette sitewide
|
||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -77,15 +117,15 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm NVM → hide confirm submenu
|
||||
// Confirm NVM → revert preview, hide confirm
|
||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
btn.closest('.note-palette-confirm').hidden = true;
|
||||
_revertPreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Stop all other modal clicks from reaching body handler.
|
||||
// Stop modal clicks from reaching the body dismiss handler.
|
||||
modal.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||
}
|
||||
|
||||
@@ -94,6 +134,8 @@
|
||||
function _doSetPalette() {
|
||||
var url = _activeItem.dataset.setPaletteUrl;
|
||||
var palette = _selectedPalette;
|
||||
var item = _activeItem;
|
||||
// Body already shows new palette from preview — keep it by not reverting.
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
@@ -106,7 +148,7 @@
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function () {
|
||||
_closeModal();
|
||||
var imageBox = _activeItem.querySelector('.note-item__image-box');
|
||||
var imageBox = item.querySelector('.note-item__image-box');
|
||||
if (imageBox) {
|
||||
var swatch = document.createElement('div');
|
||||
swatch.className = 'note-item__palette ' + palette;
|
||||
@@ -127,11 +169,10 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Body click → restore modal when previewing
|
||||
// Body click → dismiss modal (and revert any preview)
|
||||
document.body.addEventListener('click', function () {
|
||||
if (_state === 'previewing') {
|
||||
_openModal();
|
||||
}
|
||||
if (_selectedPalette) _revertPreview();
|
||||
_closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -259,6 +259,17 @@ class NoteSetPaletteViewTest(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_also_saves_user_palette(self):
|
||||
"""note_set_palette must persist the choice to user.palette so the
|
||||
palette survives page navigation (sitewide commitment)."""
|
||||
self.client.post(
|
||||
self.url,
|
||||
data=_json.dumps({"palette": "palette-bardo"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.palette, "palette-bardo")
|
||||
|
||||
|
||||
class SaveScrollPositionTest(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -110,14 +110,20 @@ _NOTE_META = {
|
||||
@login_required(login_url="/")
|
||||
def note_set_palette(request, slug):
|
||||
from django.http import Http404
|
||||
from apps.dashboard.views import _unlocked_palettes_for_user
|
||||
try:
|
||||
note = Note.objects.get(user=request.user, slug=slug)
|
||||
except Note.DoesNotExist:
|
||||
raise Http404
|
||||
if request.method == "POST":
|
||||
body = json.loads(request.body)
|
||||
note.palette = body.get("palette", "")
|
||||
palette = body.get("palette", "")
|
||||
note.palette = palette
|
||||
note.save(update_fields=["palette"])
|
||||
# Commit as the user's active sitewide palette now that the Note unlocks it.
|
||||
if palette in _unlocked_palettes_for_user(request.user):
|
||||
request.user.palette = palette
|
||||
request.user.save(update_fields=["palette"])
|
||||
return JsonResponse({"ok": True})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user