- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
186 lines
7.5 KiB
HTML
186 lines
7.5 KiB
HTML
{% load static %}
|
|
{% load lyric_extras %}
|
|
{# ─────────────────────────────────────────────────────────────────────── #}
|
|
{# _bud_panel.html — bottom-left handshake btn + slide-out recipient #}
|
|
{# field for the share-post async flow. Mirror of #id_kit_btn (bottom- #}
|
|
{# right). Included by post.html only. #}
|
|
{# #}
|
|
{# Spec lives in functional_tests/test_bud_btn.py — write it red-first. #}
|
|
{# Run: #}
|
|
{# python src/manage.py test functional_tests.test_bud_btn #}
|
|
{# ─────────────────────────────────────────────────────────────────────── #}
|
|
|
|
<button id="id_bud_btn" type="button" aria-label="Share with a bud">
|
|
<i class="fa-solid fa-handshake"></i>
|
|
</button>
|
|
|
|
<div id="id_bud_panel"
|
|
data-share-url="{% url 'billboard:share_post' post.id %}"
|
|
data-sharer-name="{% if request.user.is_authenticated %}{{ request.user|display_name }}{% endif %}">
|
|
<input id="id_recipient"
|
|
name="recipient"
|
|
type="email"
|
|
placeholder="friend@example.com"
|
|
autocomplete="off">
|
|
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
var btn = document.getElementById('id_bud_btn');
|
|
var panel = document.getElementById('id_bud_panel');
|
|
var input = document.getElementById('id_recipient');
|
|
var ok = document.getElementById('id_bud_ok');
|
|
var html = document.documentElement;
|
|
if (!btn || !panel || !input || !ok) return;
|
|
|
|
function _csrf() {
|
|
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
|
return m ? m[1] : '';
|
|
}
|
|
|
|
function _open() {
|
|
html.classList.add('bud-open');
|
|
btn.classList.add('active');
|
|
// small delay before focus so the slide-out animation can play
|
|
setTimeout(function () { input.focus(); }, 60);
|
|
}
|
|
|
|
function _close(opts) {
|
|
opts = opts || {};
|
|
html.classList.remove('bud-open');
|
|
btn.classList.remove('active');
|
|
if (opts.clear !== false) input.value = '';
|
|
}
|
|
|
|
btn.addEventListener('click', function () {
|
|
if (html.classList.contains('bud-open')) {
|
|
_close();
|
|
} else {
|
|
_open();
|
|
}
|
|
});
|
|
|
|
// Escape closes the panel, clears the field
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape' && html.classList.contains('bud-open')) _close();
|
|
});
|
|
|
|
// Click-outside dismiss — same pattern as game-kit.js
|
|
document.addEventListener('click', function (e) {
|
|
if (!html.classList.contains('bud-open')) return;
|
|
if (panel.contains(e.target)) return;
|
|
if (e.target === btn || btn.contains(e.target)) return;
|
|
_close();
|
|
});
|
|
|
|
// OK → POST share-post async; reuses the C3.b response handling so the
|
|
// recipient chip + brief banner + post-line append all light up.
|
|
// Post-May08b layout: #id_post_table is a <ul> of <li class="post-line">
|
|
// rows. Author = the sharer (rendered server-side as data-sharer-name on
|
|
// the panel) so the appended Line matches the post-refresh state, where
|
|
// the persisted Line.author is request.user.
|
|
function _appendLine(text) {
|
|
var list = document.getElementById('id_post_table');
|
|
if (!list) return;
|
|
var li = document.createElement('li');
|
|
li.className = 'post-line';
|
|
var author = document.createElement('span');
|
|
author.className = 'post-line-author';
|
|
author.textContent = panel.dataset.sharerName || '';
|
|
var body = document.createElement('span');
|
|
body.className = 'post-line-text';
|
|
body.textContent = text;
|
|
var time = document.createElement('time');
|
|
time.className = 'post-line-time';
|
|
var now = new Date();
|
|
time.dateTime = now.toISOString();
|
|
time.textContent = now.toLocaleTimeString([], {hour: 'numeric', minute: '2-digit'});
|
|
li.appendChild(author);
|
|
li.appendChild(body);
|
|
li.appendChild(time);
|
|
// Insert before the trailing buffer if present
|
|
var buffer = list.querySelector('.post-line-buffer');
|
|
if (buffer) list.insertBefore(li, buffer);
|
|
else list.appendChild(li);
|
|
}
|
|
|
|
// The shared-with header lives outside #id_bud_panel — it's two <p>
|
|
// siblings under .post-header. State transitions:
|
|
// 0 → 1+ recipients : "just me, X" turns into
|
|
// "shared between {chip}" + "& me, X"
|
|
// ≥1 → +1 recipients: append chip + ", " separator before existing
|
|
// recipient(s).
|
|
function _appendRecipientChip(displayName) {
|
|
if (!displayName) return;
|
|
var header = document.querySelector('.post-page .post-header');
|
|
if (!header) return;
|
|
var existingRecipients = header.querySelector('.post-shared-recipients');
|
|
var selfLine = header.querySelector('.post-shared-self');
|
|
|
|
var chip = document.createElement('span');
|
|
chip.className = 'post-recipient';
|
|
chip.textContent = displayName;
|
|
|
|
if (existingRecipients) {
|
|
existingRecipients.appendChild(document.createTextNode(', '));
|
|
existingRecipients.appendChild(chip);
|
|
return;
|
|
}
|
|
|
|
// 0 → 1+ transition: build the recipients line, rewrite the self
|
|
// line from "just me, …" to "& me, …".
|
|
var recipientsLine = document.createElement('p');
|
|
recipientsLine.className = 'post-shared-recipients';
|
|
recipientsLine.appendChild(document.createTextNode('shared between '));
|
|
recipientsLine.appendChild(chip);
|
|
if (selfLine) {
|
|
header.insertBefore(recipientsLine, selfLine);
|
|
// Replace "just me," prefix with "& me,"
|
|
selfLine.textContent = selfLine.textContent.replace(/^just me,/, '& me,');
|
|
} else {
|
|
header.appendChild(recipientsLine);
|
|
}
|
|
}
|
|
|
|
ok.addEventListener('click', function () {
|
|
var email = input.value.trim();
|
|
if (!email) return;
|
|
|
|
var fd = new FormData();
|
|
fd.set('recipient', email);
|
|
|
|
fetch(panel.dataset.shareUrl, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-CSRFToken': _csrf(),
|
|
},
|
|
body: fd,
|
|
})
|
|
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
|
|
.then(function (data) {
|
|
if (data.line_text) _appendLine(data.line_text);
|
|
if (window.Brief && data.brief) Brief.showBanner(data.brief);
|
|
if (data.recipient_display) _appendRecipientChip(data.recipient_display);
|
|
_close({ clear: true });
|
|
})
|
|
.catch(function () {
|
|
// swallow — privacy-safe response shape means even an
|
|
// unregistered recipient is a 200; only network/5xx land here.
|
|
});
|
|
});
|
|
|
|
// Submit-on-Enter inside the input mirrors clicking OK
|
|
input.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
ok.click();
|
|
}
|
|
});
|
|
}());
|
|
</script>
|