// Top bar, trip menu, publish panel, theme tweaks. const { useState, useEffect, useRef, useMemo } = React; const STATUS_ORDER = ["featured", "used", "context", "hidden"]; const STATUS_LABEL = { featured: "Featured", used: "Used", context: "Context", hidden: "Hidden", }; const STATUS_GLYPH = { featured: "★", used: "●", context: "◌", hidden: "⊘", }; const KIND_LABEL = { photo: "IMG", voice: "VOX", video: "VID", audio: "AUD", text: "TXT" }; const toLocalISO = (dt) => { const y = dt.getFullYear(); const m = String(dt.getMonth() + 1).padStart(2, "0"); const d = String(dt.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; }; const addDays = (iso, n) => { const [y, m, d] = iso.split("-").map(Number); return toLocalISO(new Date(y, m - 1, d + n)); }; const humanDate = (iso) => { const [y, m, d] = iso.split("-").map(Number); const dt = new Date(y, m - 1, d); return dt.toLocaleDateString("en", { weekday: "long", day: "numeric", month: "long" }); }; const kicker = (iso, trip) => { const [y, m, d] = iso.split("-").map(Number); const dt = new Date(y, m - 1, d); const mon = dt.toLocaleDateString("en", { month: "short" }).toLowerCase(); return `${(trip.title || trip.slug).toLowerCase()} · ${d} ${mon} ${y}`; }; // ---------- TopBar ---------- function TopBar({ trip, trips, date, status, dirty, wordCount, inPostCount, featuredCount, contextCount, hiddenCount, publishedAt, isDraft, onToggleDraft, onDate, onSwitchTrip, onNewTrip, onPublish, }) { const [tripOpen, setTripOpen] = useState(false); const [publishOpen, setPublishOpen] = useState(false); const brandRef = useRef(); const pubRef = useRef(); useEffect(() => { const close = (e) => { if (brandRef.current && !brandRef.current.contains(e.target)) setTripOpen(false); if (pubRef.current && !pubRef.current.contains(e.target)) setPublishOpen(false); }; document.addEventListener("click", close); return () => document.removeEventListener("click", close); }, []); const publishLabel = (() => { if (status.kind === "saving") return "Saving…"; if (isDraft) return "Unpublish"; if (status.kind === "edited") return "Publish update"; if (status.kind === "published") return "Published"; return "Publish"; })(); const publishClass = (dirty || isDraft) && status.kind !== "saving" ? "btn btn-primary update" : "btn btn-primary"; // dt day-index in trip (visual hint, "Day NN / MM") const tripDays = useMemo(() => { if (!trip || !trip.created_at) return { n: null, total: null }; return { n: null, total: null }; }, [trip]); return (
{ e.stopPropagation(); setTripOpen((v) => !v); }}>
p
Postcard
{(trip.title || trip.slug).toLowerCase()}
{tripOpen && (
e.stopPropagation()}>
trips · active is where telegram uploads land
{(trips || []).map((t) => (
{ setTripOpen(false); onSwitchTrip(t, { activate: false }); }}> {t.title || t.slug} {t.is_active ? ( active ) : ( )} {t.slug}
))}
{ setTripOpen(false); onNewTrip(); }}> + new trip
)}
{humanDate(date)}
{status.label}
{isDraft ? ( <>

Keep {humanDate(date)} as a private draft.

Pull from site will remove this day's files from git entirely. Local edits and media stay intact — toggle draft off and publish to push it back out.

Body {wordCount} word{wordCount === 1 ? "" : "s"}
Status private
{publishedAt &&
Last published {publishedAt}
}
) : ( <>

Publish update to {humanDate(date)}?

This rewrites the public day to match your current draft. Unused and hidden items drop from the record.

Body {wordCount} word{wordCount === 1 ? "" : "s"}
In post {inPostCount} item{inPostCount === 1 ? "" : "s"} · {featuredCount} featured · {inPostCount - featuredCount} used
Dropping {contextCount + hiddenCount} (context & hidden)
{publishedAt &&
Last published {publishedAt}
}
)}
); } // ---------- Toast ---------- function Toast({ msg, kind }) { if (!msg) return null; return
{msg}
; } // ---------- Tweaks (theme switcher) ---------- function Tweaks({ show, theme, setTheme }) { const themes = [ { k: "paper", l: "Paper", chips: ["#F4EFE5", "#1E1B16", "#B85C3A"] }, { k: "bone", l: "Bone", chips: ["#ECECE8", "#15181C", "#3E6A8C"] }, { k: "dusk", l: "Dusk", chips: ["#161412", "#EDE6D6", "#D98A4A"] }, ]; return (
Tweakstheme
{themes.map((t) => (
setTheme(t.k)}>
{t.chips.map((c, i) =>
)}
{t.l}
))}
); } Object.assign(window, { TopBar, Toast, Tweaks, STATUS_ORDER, STATUS_LABEL, STATUS_GLYPH, KIND_LABEL, toLocalISO, addDays, humanDate, kicker, });