Files
intotheeast-com-content/themes/intotheeast/js/src/trip-publish.js
T
m038andClaude Opus 4.8 37b669424b fix(trips): remaining publish-toggle review findings
- resolveTripChild now asserts the resolved page uses the trip template, so a
  non-trip direct child of /trips could never be toggled through this endpoint
  (P3 adversarial).
- apiSend gains an optional timeoutMs (AbortController); trip-publish passes 10s
  so a hung toggle can't leave the switch stuck aria-busy. post-form omits it,
  keeping media uploads unbounded (P2 reliability).
- trips.html.twig reuses trip.html.twig's one-line active-trip slug match
  instead of a bespoke 3-branch OR (P2 maintainability).
- Draft-badge amber is now a --color-draft-accent token shared by the trip and
  journal badges instead of a twice-hardcoded #E0A458 literal (P3).

Rebuilt js/trip-publish.js and js/post/post-form.js (shared api-utils change).
PHP lint clean; trip-publish suite 10/10; post suite unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 17:31:40 +02:00

132 lines
4.9 KiB
JavaScript
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.
/*
* trip-publish.js (U6) — owner publish/unpublish toggle for the /trips listing.
*
* Loaded only for the owner (trips.html.twig gates the asset). Each listing card
* carries a switch (partials/trip-publish-toggle.html.twig) overlaid on the cover
* as a sibling of the navigating <a>, so toggling never follows the card link.
*
* Flow: click/Enter/Space on the switch → (if unpublishing the ACTIVE trip)
* window.confirm → lock the switch (aria-busy) → POST /api/v1/trip/<slug>/publish
* {published} (session cookie) → on success flip the switch + Draft badge in place
* (no reload); on failure re-enable and surface a VISIBLE page-level toast.
*
* Markup contract (data-* on button.trip-publish-toggle):
* data-trip-slug, data-published ("true"|"false"),
* data-active ("true" when this is site.active_trip); aria-checked mirrors
* data-published. The sibling .trip-draft-badge is shown iff not published.
*/
import { apiSend, apiErrorMsg } from './api-utils.js';
var TOAST_TIMEOUT_MS = 5000;
var PUBLISH_TIMEOUT_MS = 10000; // abort a hung toggle so the switch never sticks (R13)
var toastTimer = null;
// One visible, page-level polite toast (R15). Modelled on the feed-actions.js
// live region but intentionally NOT sr-only: the trip card has no inline message
// slot, so a sighted owner must actually see the failure. Replaces (never queues)
// the message, auto-dismisses, and carries a manual close control.
function toastEl() {
var el = document.getElementById('trip-publish-live');
if (!el) {
el = document.createElement('div');
el.id = 'trip-publish-live';
el.className = 'trip-publish-toast';
el.setAttribute('role', 'status');
el.setAttribute('aria-live', 'polite');
el.hidden = true;
var msg = document.createElement('span');
msg.className = 'trip-publish-toast__msg';
var close = document.createElement('button');
close.type = 'button';
close.className = 'trip-publish-toast__close';
close.setAttribute('aria-label', 'Dismiss');
close.textContent = '×'; // ×
close.addEventListener('click', hideToast);
el.appendChild(msg);
el.appendChild(close);
document.body.appendChild(el);
}
return el;
}
function showToast(message) {
var el = toastEl();
el.querySelector('.trip-publish-toast__msg').textContent = message;
el.hidden = false;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(hideToast, TOAST_TIMEOUT_MS);
}
function hideToast() {
var el = document.getElementById('trip-publish-live');
if (el) el.hidden = true;
if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; }
}
function setPublishedUI(btn, published) {
btn.setAttribute('aria-checked', published ? 'true' : 'false');
btn.setAttribute('data-published', published ? 'true' : 'false');
var overlay = btn.closest('.trip-publish-overlay');
var badge = overlay ? overlay.querySelector('.trip-draft-badge') : null;
if (badge) badge.hidden = published;
}
function setPending(btn, pending) {
if (pending) {
btn.setAttribute('aria-busy', 'true');
btn.disabled = true;
} else {
btn.removeAttribute('aria-busy');
btn.disabled = false;
}
}
function onToggle(btn) {
if (btn.getAttribute('aria-busy') === 'true') return; // already in flight (R13)
var current = btn.getAttribute('data-published') === 'true';
var next = !current;
// R12: unpublishing the ACTIVE trip removes it from the home page — confirm
// first; cancelling leaves it published (no request, no UI change).
if (!next && btn.getAttribute('data-active') === 'true') {
if (!window.confirm('This is your active trip — unpublishing it also removes it from the home page. Unpublish anyway?')) {
return;
}
}
var slug = btn.getAttribute('data-trip-slug');
setPending(btn, true);
apiSend('/api/v1/trip/' + encodeURIComponent(slug) + '/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ published: next })
}, null, PUBLISH_TIMEOUT_MS).then(function () {
// Success: flip the switch + Draft badge in place, no reload (R14).
setPublishedUI(btn, next);
setPending(btn, false);
}).catch(function (err) {
// Failure: the UI never flipped, so revert is just re-enable (R15).
setPending(btn, false);
showToast(apiErrorMsg(err, "Couldn't update — try again."));
});
}
function initTripPublish() {
document.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('.trip-publish-toggle') : null;
if (!btn) return;
e.preventDefault();
onToggle(btn);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTripPublish);
} else {
initTripPublish();
}