Files
python-tdd/src/templates/apps/billboard/bud.html
Disco DeDisco 6cc11924e3
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
bud landing page: /billboard/buds/<id>/ + my_buds tooltip portal + @mailman post-attribution anchor — TDD
Replaces the @mailman invite Line's inline OK/BYE block w. a dedicated per-bud surface. Three new FTs (test_bill_bud_page, test_bill_my_buds_tooltip, test_bill_mailman_invite_post — landed red 2026-05-27 PM) drive: per-bud landing page rendering 4-btn apparatus + shoptalk textarea + invite-cascade glow handoff; my-buds row tooltip portal w. .tt-title/.tt-description/.tt-email/.tt-shoptalk/.tt-milestone slots; mailman Brief surfacing on any authenticated page-load via context processor + base.html JSON-script.

Models: new `BudshipNote(user, bud, shoptalk[CharField max=160], edited_at)` w. unique_together — per-relation personal note about a bud, never visible to the bud. Lazy-created on first shoptalk save so absence of a row reads as 'never edited' (drives .tt-milestone slot presence).

URLs (billboard): `buds/<uuid:bud_id>/` (bud_page), `buds/<uuid:bud_id>/shoptalk` (save_bud_shoptalk), `buds/<uuid:bud_id>/delete` (delete_bud).

Views: bud_page auto-adds the bud on first visit (mirrors share_post implicit-add); resolves `pending_invite` as non-expired PENDING SeaInvite(owner=bud, invitee=request.user) → drives `sea_btn_active` + `sea_first_draw_pending` flags that _burger.html already reads on my_sea + room. my_buds enriches each bud w. `.shoptalk_text` + `.milestone_dt` so the row template can render data-tt-* attrs without an extra template tag.

mail.py: INVITE_TEMPLATE now interpolates `owner_id` into an `<a class="post-attribution" href="/billboard/buds/{owner_id}/">{handle}</a>` wrapper around the owner's handle. post.html's existing safe-filter branch (gated on author username == 'mailman') passes it through unescaped. Removed the {% if line.sea_invite %} include path — _invite_actions.html left in place for archival.

Templates: new bud.html (header + shoptalk form + apparatus + gear + burger fan + sea_btn nav inline JS); new _bud_gear.html (NVM→my_buds, DEL→guard portal "Delete this bud?" → POST delete_bud); new _bud_tooltip.html (portal w. .tt-* slots); _my_buds_item.html wraps `@handle` in an anchor to bud_page + carries data-tt-* attrs + " the {{ active_title_display }}"; my_buds.html includes the tooltip portal + loads my-buds-tooltip.js.

JS: new my-buds-tooltip.js binds row clicks → .row-locked + populates #id_tooltip_portal from data-tt-* attrs; anchor clicks pass through to navigate; .tt-milestone is removed from DOM (not just emptied) when never-edited so the FT can distinguish absent vs cleared-after-edit.

SCSS: extend landscape gear-btn rule + #id_*_menu rule w. `.bud-page` + `#id_bud_menu` (otherwise gear-btn collided w. bud-btn in landscape on bud.html). Bump active burger sub-btn z-index to 1 so click hit-test picks the active sub-btn during the 0.25s fan arc-out animation (otherwise a later-in-DOM inactive btn obscured the active target during transition).

Cross-page Brief surface: new `mail_brief_payload` context processor injects the user's oldest unread MAIL_ACCEPTANCE Brief into every authenticated response; base.html renders the JSON-script + auto-fires Brief.showBanner. Mark-read still rides view_post's existing GET unread-flip — no new endpoint.

Pre-existing MySeaInvitePostRenderTest (test_sea_invite_views.py) inverted to match the new contract: the .invite-actions sweep is unconditional (PENDING / ACCEPTED / DECLINED all carry prose only); pinned the post-attribution anchor + bud-page href in its place.

1518 ITs green (1475 app ITs + 43 sprint ITs), 23 sprint FTs green (5 my_buds tooltip + 13 bud page + 5 mailman invite post). Jasmine specs from sprint plan deferred — FT coverage of burger-glow / row-lock / portal-populate paths suffices and the textarea blur-POST flow isn't implemented in this sprint (form is server-action only, save-on-blur AJAX is a follow-on).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:45:20 -04:00

86 lines
3.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "core/base.html" %}
{% load static %}
{% load lyric_extras %}
{% block title_text %}Billbud{% endblock title_text %}
{% block header_text %}<span>Bill</span><span>bud</span>{% endblock header_text %}
{% block content %}
{# note.js exposes window.Brief — needed by the auto-fired @mailman Brief #}
{# when this page is the destination of an invite cascade. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script>
<div class="bud-page">
<header class="bud-page-header">
<h3>{{ bud|at_handle }} the {{ bud.active_title_display }}</h3>
<p class="bud-page-email">{{ bud.email }}</p>
</header>
<form id="id_bud_shoptalk_form"
method="POST"
action="{% url 'billboard:save_bud_shoptalk' bud.id %}"
class="bud-shoptalk-form">
{% csrf_token %}
<label for="id_shoptalk" class="bud-shoptalk-label">Shoptalk</label>
<textarea id="id_shoptalk"
name="shoptalk"
maxlength="160"
class="form-control bud-shoptalk-input"
placeholder="Your personal note about this bud…">{{ shoptalk_text }}</textarea>
</form>
{# 4-btn apparatus mirrors my_sea.html: kit (from base.html) + bud + #}
{# gear + burger. Apparatus loads bud-btn.js (defines window. #}
{# bindBudBtn — does NOT auto-bind); the inline script below opens #}
{# the panel + stubs OK to disabled per the sprint plan. #}
{% include "apps/billboard/_partials/_bud_apparatus.html" with aria_label="Bud" include_suggestions=False %}
<script>
(function () {
var btn = document.getElementById('id_bud_btn');
var panel = document.getElementById('id_bud_panel');
var ok = document.getElementById('id_bud_ok');
if (!btn || !panel || !ok) return;
// bud-of-bud flow isn't wired yet — stub OK as disabled w. × text.
ok.classList.add('btn-disabled');
ok.textContent = '×';
ok.setAttribute('disabled', 'disabled');
btn.addEventListener('click', function () {
var open = document.documentElement.classList.toggle('bud-open');
btn.classList.toggle('active', open);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.documentElement.classList.contains('bud-open')) {
document.documentElement.classList.remove('bud-open');
btn.classList.remove('active');
}
});
}());
</script>
{% include "apps/billboard/_partials/_bud_gear.html" %}
{# Burger fan — sea_btn_active + sea_first_draw_pending drive the #}
{# server-side .active / .glow-handoff classes (same vars my_sea + #}
{# room read). burger-btn.js auto-binds on DOMContentLoaded. #}
{% include "apps/gameboard/_partials/_burger.html" %}
</div>
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
{% if pending_invite %}
<script>
(function () {
var sea = document.getElementById('id_sea_btn');
if (!sea) return;
// Active sub-btn navigation handler — fires BEFORE burger-btn.js's
// delegated fan handler (target-phase before bubble-up) so the click
// routes to the bud's spectator my-sea page. Burger then closes the
// fan as usual.
sea.addEventListener('click', function () {
if (!sea.classList.contains('active')) return;
window.location.href = '/gameboard/my-sea/visit/{{ bud.id }}/';
});
}());
</script>
{% endif %}
{% endblock content %}