buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- 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>
This commit is contained in:
23
src/templates/apps/applets/_partials/_applet-list-shell.html
Normal file
23
src/templates/apps/applets/_partials/_applet-list-shell.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{# Shared applet-scroll-style list section — vertical-title <h2> on the #}
|
||||
{# left + scrollable <ul> aperture. Inclusion shell (NOT a base template) #}
|
||||
{# so a single page can invoke it more than once (e.g. my_posts.html #}
|
||||
{# stacks "My Posts" + "Posts shared with me"). #}
|
||||
{# #}
|
||||
{# Parameters: #}
|
||||
{# shell_title — vertical-rotated heading text (string) #}
|
||||
{# shell_items — iterable rendered into the list #}
|
||||
{# shell_item_template — partial rendering each <li>; receives `item` #}
|
||||
{# shell_list_id — optional `id=` for the <ul> (e.g. "id_buds_list" #}
|
||||
{# so buddy-panel JS can target it) #}
|
||||
{# shell_empty — text for the {% empty %} fallback row #}
|
||||
<section class="applet-scroll">
|
||||
<h2>{{ shell_title }}</h2>
|
||||
<ul {% if shell_list_id %}id="{{ shell_list_id }}"{% endif %} class="applet-list">
|
||||
{% for item in shell_items %}
|
||||
{% include shell_item_template %}
|
||||
{% empty %}
|
||||
<li class="applet-list-entry applet-list-entry--empty">{{ shell_empty|default:"Nothing here yet." }}</li>
|
||||
{% endfor %}
|
||||
<li class="applet-list-buffer" aria-hidden="true"></li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -1,7 +1,7 @@
|
||||
<section
|
||||
id="id_applet_my_buddies"
|
||||
id="id_applet_my_buds"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2><a href="{% url 'billboard:my_buddies' %}" class="my-buddies-main">My Buddies</a></h2>
|
||||
<h2><a href="{% url 'billboard:my_buds' %}" class="my-buds-main">My Buds</a></h2>
|
||||
{% include "core/_partials/_forthcoming.html" %}
|
||||
</section>
|
||||
@@ -1,32 +1,32 @@
|
||||
{% load static %}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
{# _buddy_add_panel.html — bottom-left handshake btn + slide-out add- #}
|
||||
{# buddy field. Mirrors _buddy_panel.html (post-share) but POSTs to #}
|
||||
{# add_buddy and appends to #id_buddies_list instead of #id_post_table. #}
|
||||
{# Included by my_buddies.html only. #}
|
||||
{# _bud_add_panel.html — bottom-left handshake btn + slide-out add- #}
|
||||
{# bud field. Mirrors _bud_panel.html (post-share) but POSTs to #}
|
||||
{# add_bud and appends to #id_buds_list instead of #id_post_table. #}
|
||||
{# Included by my_buds.html only. #}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
|
||||
<button id="id_buddy_btn" type="button" aria-label="Add a buddy">
|
||||
<button id="id_bud_btn" type="button" aria-label="Add a bud">
|
||||
<i class="fa-solid fa-handshake"></i>
|
||||
</button>
|
||||
|
||||
<div id="id_buddy_panel" data-add-url="{% url 'billboard:add_buddy' %}">
|
||||
<div id="id_bud_panel" data-add-url="{% url 'billboard:add_bud' %}">
|
||||
<input id="id_recipient"
|
||||
name="recipient"
|
||||
type="email"
|
||||
placeholder="friend@example.com"
|
||||
autocomplete="off">
|
||||
<button id="id_buddy_ok" type="button" class="btn btn-confirm">OK</button>
|
||||
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var btn = document.getElementById('id_buddy_btn');
|
||||
var panel = document.getElementById('id_buddy_panel');
|
||||
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_buddy_ok');
|
||||
var ok = document.getElementById('id_bud_ok');
|
||||
var html = document.documentElement;
|
||||
if (!btn || !panel || !input || !ok) return;
|
||||
|
||||
@@ -36,20 +36,20 @@
|
||||
}
|
||||
|
||||
function _open() {
|
||||
html.classList.add('buddy-open');
|
||||
html.classList.add('bud-open');
|
||||
btn.classList.add('active');
|
||||
setTimeout(function () { input.focus(); }, 60);
|
||||
}
|
||||
|
||||
function _close(opts) {
|
||||
opts = opts || {};
|
||||
html.classList.remove('buddy-open');
|
||||
html.classList.remove('bud-open');
|
||||
btn.classList.remove('active');
|
||||
if (opts.clear !== false) input.value = '';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (html.classList.contains('buddy-open')) {
|
||||
if (html.classList.contains('bud-open')) {
|
||||
_close();
|
||||
} else {
|
||||
_open();
|
||||
@@ -57,34 +57,34 @@
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && html.classList.contains('buddy-open')) _close();
|
||||
if (e.key === 'Escape' && html.classList.contains('bud-open')) _close();
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!html.classList.contains('buddy-open')) return;
|
||||
if (!html.classList.contains('bud-open')) return;
|
||||
if (panel.contains(e.target)) return;
|
||||
if (e.target === btn || btn.contains(e.target)) return;
|
||||
_close();
|
||||
});
|
||||
|
||||
function _appendBuddyEntry(buddy) {
|
||||
var list = document.getElementById('id_buddies_list');
|
||||
if (!list || !buddy) return;
|
||||
function _appendBudEntry(bud) {
|
||||
var list = document.getElementById('id_buds_list');
|
||||
if (!list || !bud) return;
|
||||
// Skip if already in DOM (server-side dedup ensures M2M idempotence;
|
||||
// this guards a fast double-click that races the post-add refresh).
|
||||
if (list.querySelector('[data-buddy-id="' + buddy.id + '"]')) return;
|
||||
if (list.querySelector('[data-bud-id="' + bud.id + '"]')) return;
|
||||
// Drop the empty-state row if present
|
||||
var empty = list.querySelector('.buddy-entry--empty');
|
||||
var empty = list.querySelector('.bud-entry--empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
var li = document.createElement('li');
|
||||
li.className = 'buddy-entry';
|
||||
li.dataset.buddyId = buddy.id;
|
||||
li.className = 'bud-entry';
|
||||
li.dataset.budId = bud.id;
|
||||
var name = document.createElement('span');
|
||||
name.className = 'buddy-name';
|
||||
name.textContent = buddy.username;
|
||||
name.className = 'bud-name';
|
||||
name.textContent = bud.username;
|
||||
li.appendChild(name);
|
||||
var buffer = list.querySelector('.buddy-entry-buffer');
|
||||
var buffer = list.querySelector('.bud-entry-buffer');
|
||||
if (buffer) list.insertBefore(li, buffer);
|
||||
else list.appendChild(li);
|
||||
}
|
||||
@@ -107,12 +107,12 @@
|
||||
})
|
||||
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
|
||||
.then(function (data) {
|
||||
if (data.buddy) _appendBuddyEntry(data.buddy);
|
||||
if (data.bud) _appendBudEntry(data.bud);
|
||||
_close({ clear: true });
|
||||
})
|
||||
.catch(function () {
|
||||
// Privacy-safe response shape — even an unregistered email is
|
||||
// a 200 w. {buddy: null}. Network/5xx land here; just close.
|
||||
// a 200 w. {bud: null}. Network/5xx land here; just close.
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{% load static %}
|
||||
{% load lyric_extras %}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
{# _buddy_panel.html — bottom-left handshake btn + slide-out recipient #}
|
||||
{# _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_buddy_btn.py — write it red-first. #}
|
||||
{# Spec lives in functional_tests/test_bud_btn.py — write it red-first. #}
|
||||
{# Run: #}
|
||||
{# python src/manage.py test functional_tests.test_buddy_btn #}
|
||||
{# python src/manage.py test functional_tests.test_bud_btn #}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
|
||||
<button id="id_buddy_btn" type="button" aria-label="Share with a buddy">
|
||||
<button id="id_bud_btn" type="button" aria-label="Share with a bud">
|
||||
<i class="fa-solid fa-handshake"></i>
|
||||
</button>
|
||||
|
||||
<div id="id_buddy_panel"
|
||||
<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"
|
||||
@@ -22,17 +22,17 @@
|
||||
type="email"
|
||||
placeholder="friend@example.com"
|
||||
autocomplete="off">
|
||||
<button id="id_buddy_ok" type="button" class="btn btn-confirm">OK</button>
|
||||
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var btn = document.getElementById('id_buddy_btn');
|
||||
var panel = document.getElementById('id_buddy_panel');
|
||||
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_buddy_ok');
|
||||
var ok = document.getElementById('id_bud_ok');
|
||||
var html = document.documentElement;
|
||||
if (!btn || !panel || !input || !ok) return;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
function _open() {
|
||||
html.classList.add('buddy-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);
|
||||
@@ -50,13 +50,13 @@
|
||||
|
||||
function _close(opts) {
|
||||
opts = opts || {};
|
||||
html.classList.remove('buddy-open');
|
||||
html.classList.remove('bud-open');
|
||||
btn.classList.remove('active');
|
||||
if (opts.clear !== false) input.value = '';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (html.classList.contains('buddy-open')) {
|
||||
if (html.classList.contains('bud-open')) {
|
||||
_close();
|
||||
} else {
|
||||
_open();
|
||||
@@ -65,12 +65,12 @@
|
||||
|
||||
// Escape closes the panel, clears the field
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && html.classList.contains('buddy-open')) _close();
|
||||
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('buddy-open')) return;
|
||||
if (!html.classList.contains('bud-open')) return;
|
||||
if (panel.contains(e.target)) return;
|
||||
if (e.target === btn || btn.contains(e.target)) return;
|
||||
_close();
|
||||
@@ -107,7 +107,7 @@
|
||||
else list.appendChild(li);
|
||||
}
|
||||
|
||||
// The shared-with header lives outside #id_buddy_panel — it's two <p>
|
||||
// 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"
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load lyric_extras %}
|
||||
<li class="applet-list-entry bud-entry" data-bud-id="{{ item.id }}">
|
||||
<span class="bud-name">{{ item|display_name }}</span>
|
||||
</li>
|
||||
@@ -0,0 +1,3 @@
|
||||
<li class="applet-list-entry post-entry">
|
||||
<a href="{{ item.get_absolute_url }}">{{ item.title }}</a>
|
||||
</li>
|
||||
@@ -1,30 +0,0 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load lyric_extras %}
|
||||
|
||||
{% block title_text %}Dashbuddies{% endblock title_text %}
|
||||
{% block header_text %}<span>Dash</span>buddies{% endblock header_text %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="buddies-page">
|
||||
<header class="buddies-header">
|
||||
<h3 class="buddies-title">My Buddies</h3>
|
||||
</header>
|
||||
|
||||
<ul id="id_buddies_list" class="buddies-list">
|
||||
{% for buddy in buddies %}
|
||||
<li class="buddy-entry" data-buddy-id="{{ buddy.id }}">
|
||||
<span class="buddy-name">{{ buddy|display_name }}</span>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="buddy-entry buddy-entry--empty">No buddies yet.</li>
|
||||
{% endfor %}
|
||||
<li class="buddy-entry-buffer" aria-hidden="true"></li>
|
||||
</ul>
|
||||
|
||||
{# Buddy btn (bottom-left) + slide-out add-buddy panel — async POST #}
|
||||
{# to add_buddy. Mirror of the post.html share buddy btn but distinct #}
|
||||
{# action (adds to User.buddies M2M) and DOM hooks (#id_buddies_list).#}
|
||||
{% include "apps/billboard/_partials/_buddy_add_panel.html" %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
15
src/templates/apps/billboard/my_buds.html
Normal file
15
src/templates/apps/billboard/my_buds.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load lyric_extras %}
|
||||
|
||||
{% block title_text %}Billbuds{% endblock title_text %}
|
||||
{% block header_text %}<span>Bill</span>buds{% endblock header_text %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="applet-list-page applet-list-page--single">
|
||||
{% include "apps/applets/_partials/_applet-list-shell.html" with shell_title="My Buds" shell_list_id="id_buds_list" shell_items=buds shell_item_template="apps/billboard/_partials/_my_buds_item.html" shell_empty="No buds yet." %}
|
||||
</div>
|
||||
|
||||
{# Bud btn (bottom-left) + slide-out add-bud panel — async POST to add_bud. #}
|
||||
{% include "apps/billboard/_partials/_bud_add_panel.html" %}
|
||||
{% endblock content %}
|
||||
@@ -1,20 +1,15 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load lyric_extras %}
|
||||
|
||||
{% block title_text %}Dashposts{% endblock title_text %}
|
||||
{% block header_text %}<span>Dash</span>posts{% endblock header_text %}
|
||||
{% block title_text %}Billposts{% endblock title_text %}
|
||||
{% block header_text %}<span>Bill</span>posts{% endblock header_text %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h3>{{ owner|display_name }}'s posts</h3>
|
||||
<ul>
|
||||
{% for post in owner.posts.all %}
|
||||
<li><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h3>Posts shared with me</h3>
|
||||
<ul>
|
||||
{% for post in owner.shared_posts.all %}
|
||||
<li><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{# Two applet-scroll sections — own posts + posts shared with me. #}
|
||||
{# Stack vertically in portrait, sit side-by-side in landscape (.--two-up).#}
|
||||
<div class="applet-list-page applet-list-page--two-up">
|
||||
{% include "apps/applets/_partials/_applet-list-shell.html" with shell_title=owner|display_name|add:" posts" shell_items=owner.posts.all shell_item_template="apps/billboard/_partials/_my_posts_item.html" shell_empty="No posts yet." %}
|
||||
{% include "apps/applets/_partials/_applet-list-shell.html" with shell_title="Shared with me" shell_items=owner.shared_posts.all shell_item_template="apps/billboard/_partials/_my_posts_item.html" shell_empty="Nothing shared yet." %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load lyric_extras %}
|
||||
|
||||
{% block title_text %}Dashpost{% endblock title_text %}
|
||||
{% block header_text %}<span>Dash</span>post{% endblock header_text %}
|
||||
{% block title_text %}Billpost{% endblock title_text %}
|
||||
{% block header_text %}<span>Bill</span>post{% endblock header_text %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
@@ -69,11 +69,11 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{# Buddy btn (bottom-left) + slide-out recipient field — async share. #}
|
||||
{# Bud btn (bottom-left) + slide-out recipient field — async share. #}
|
||||
{# Suppressed on admin Posts (note unlock thread) since friend-invites #}
|
||||
{# don't apply to system-authored threads. #}
|
||||
{% if post.kind != 'note_unlock' %}
|
||||
{% include "apps/billboard/_partials/_buddy_panel.html" %}
|
||||
{% include "apps/billboard/_partials/_bud_panel.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
Reference in New Issue
Block a user