feat(trips): owner publish/unpublish toggle on /trips listing
Add an owner-only publish switch to each /trips card. It POSTs to a new
entry-actions route that mutates trip.md `published` and invalidates the
page-tree cache so the listing, nav and home reflect the change on the
next load. Owner sees drafts (Draft badge); anon/non-owner unchanged.
- U1 EntryScopeGuard::resolveTripChild — resolve a slug to a direct child
of /trips (drafts included, for republish)
- U2 POST /api/v1/trip/{slug}/publish (setTripPublished) — owner-gated
write, strict is_bool body, header-mutation save, audit log
- U3 trip-publish-toggle partial + CSS (role=switch, Draft badge, visible
failure toast, ≥44px target)
- U4 owner-aware /trips listing + card restructure (toggle overlays cover
as a non-anchor sibling; works for coverless drafts)
- U5 home active-trip branch falls back when the active trip is unpublished
- U6 trip-publish.js (confirm/pending/optimistic/revert) + esbuild wiring
Cache note: an in-place frontmatter edit keeps the folder-check cache id,
and driver:auto uses APCu in web memory, so deleteAll()+invalidateCache()
is insufficient — the endpoint also calls apcu_clear_cache().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
This commit is contained in:
@@ -9,7 +9,10 @@
|
||||
{% set trip_route = config.site.active_trip %}
|
||||
{% set trip = grav.pages.find(trip_route) %}
|
||||
|
||||
{% if config.site.travelling %}
|
||||
{# An unpublished active trip falls through to the between-trips state (R16,
|
||||
KTD7): trip is resolved above and `.published` reads the trip.md flag. This is
|
||||
the whole home fallback — site.active_trip is not touched. #}
|
||||
{% if config.site.travelling and trip and trip.published %}
|
||||
{# ══════════════════════════════════════════════════════════ ACTIVE TRIP MODE #}
|
||||
|
||||
{% set dailies_page = grav.pages.find(trip_route ~ '/dailies') %}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{#
|
||||
Owner-only publish/unpublish switch for a /trips listing card (U3, R10/R11).
|
||||
|
||||
Rendered by trips.html.twig as a SIBLING of the navigating card <a> (never a
|
||||
child of it), overlaid top-right on the cover, so a click on it toggles publish
|
||||
state rather than following the card link (KTD6). trip-publish.js binds the
|
||||
button by `.trip-publish-toggle` and drives it from the data-* below.
|
||||
|
||||
Params:
|
||||
trip Page — the trip whose published state this controls
|
||||
is_active bool — true when this trip is site.active_trip; drives the
|
||||
"home page loses it" confirm in the JS (R12)
|
||||
#}
|
||||
{% set is_active = is_active ?? false %}
|
||||
<div class="trip-publish-overlay">
|
||||
<span class="trip-draft-badge"{% if trip.published %} hidden{% endif %}>Draft</span>
|
||||
<button type="button"
|
||||
class="trip-publish-toggle"
|
||||
role="switch"
|
||||
aria-checked="{{ trip.published ? 'true' : 'false' }}"
|
||||
aria-label="Published — {{ trip.title }}"
|
||||
data-trip-slug="{{ trip.slug }}"
|
||||
data-published="{{ trip.published ? 'true' : 'false' }}"
|
||||
data-active="{{ is_active ? 'true' : 'false' }}">
|
||||
<span class="trip-publish-track" aria-hidden="true"><span class="trip-publish-knob"></span></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -3,7 +3,13 @@
|
||||
{% block content %}
|
||||
{% import 'macros/cover.html.twig' as cover %}
|
||||
<h1 class="trips-heading">Past Trips</h1>
|
||||
{% set trips = page.children.published()|sort((a, b) => a.date < b.date ? 1 : -1) %}
|
||||
{# Owner gate (R1): broader than trip.html.twig's owner_can_edit — publishing
|
||||
works on ANY trip, not just the active one, so it's just owner identity. #}
|
||||
{% set is_owner = grav.user.authenticated
|
||||
and grav.user.username == grav.config.site.owner_username %}
|
||||
{# Owner sees drafts (R9); everyone else published only. #}
|
||||
{% set trips = (is_owner ? page.children : page.children.published())|sort((a, b) => a.date < b.date ? 1 : -1) %}
|
||||
{% if is_owner %}{% do assets.addJs('theme://js/trip-publish.js', {group: 'bottom'}) %}{% endif %}
|
||||
{% if trips|length == 0 %}
|
||||
<p class="feed-empty">No trips yet.</p>
|
||||
{% else %}
|
||||
@@ -13,6 +19,18 @@
|
||||
{% set stories_page = grav.pages.find(trip.route ~ '/stories') %}
|
||||
{% set journal_count = dailies_page ? dailies_page.children.published()|length : 0 %}
|
||||
{% set story_count = stories_page ? stories_page.children.published()|length : 0 %}
|
||||
{# Robust active-trip match: site.active_trip may be a full route
|
||||
(/trips/x) or a bare slug — normalise both sides before comparing so the
|
||||
R12 confirm never silently drops (both forms exist in helpers.js /
|
||||
cache-on-save). #}
|
||||
{% set active = (config.site.active_trip ?? '')|trim('/') %}
|
||||
{% set trip_route = trip.route|trim('/') %}
|
||||
{% set is_active = active != '' and (active == trip_route or active == ('trips/' ~ trip.slug) or active == trip.slug) %}
|
||||
{# The wrapper is a positioned container so the owner toggle can overlay the
|
||||
cover as a SIBLING of the navigating <a> (KTD6) — and it exists even for a
|
||||
coverless draft, giving the toggle an anchor whether or not the cover macro
|
||||
emits an image. #}
|
||||
<div class="trip-card-wrap">
|
||||
<a class="trip-card" href="{{ trip.url }}">
|
||||
{{ cover.render(trip, trip.title, 720, 240, 'trip-card-cover', '(max-width: 700px) 100vw, 360px') }}
|
||||
<div class="trip-card-title">{{ trip.title }}</div>
|
||||
@@ -32,6 +50,8 @@
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{% if is_owner %}{% include 'partials/trip-publish-toggle.html.twig' with { trip: trip, is_active: is_active } only %}{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user