style(pulse): previous/upcoming gatherings strip below the hero card
Move the previous + upcoming dates out of the dark hero box into an editorial strip beneath it (large serif day, terracotta eyebrow, "All gatherings" link top-aligned on the right). Hero card keeps just the RSVP footer.
This commit is contained in:
parent
c0d4a6fdff
commit
436e19170e
2 changed files with 170 additions and 157 deletions
|
|
@ -12,7 +12,9 @@ interface Props {
|
|||
memberLabel?: string | null; // e.g. "MEMBER · 001"
|
||||
}
|
||||
|
||||
const { event, confirmedCount, myRsvp, greetingPrefix, firstName, memberLabel = null } = Astro.props;
|
||||
const {
|
||||
event, confirmedCount, myRsvp, greetingPrefix, firstName, memberLabel = null,
|
||||
} = Astro.props;
|
||||
|
||||
function parseUtc(s: string): Date {
|
||||
if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s);
|
||||
|
|
@ -49,51 +51,52 @@ const startTime = event ? fmt({ hour: '2-digit', minute: '2-digit', hour12: fa
|
|||
</div>
|
||||
|
||||
{event ? (
|
||||
<>
|
||||
<div class="hero-event">
|
||||
<!-- Label sits above the date + title so it's clear they describe
|
||||
the next event. -->
|
||||
<p class="hero-eyebrow">Next gathering · {eventKindLabel(event.kind).toUpperCase()}</p>
|
||||
<div class="hero-top">
|
||||
<div class="hero-date">
|
||||
<span class="hero-weekday">{weekday}</span>
|
||||
<span class="hero-day">{dayPadded}</span>
|
||||
<span class="hero-month-time">{monthShort} · {startTime}</span>
|
||||
</div>
|
||||
<div class="hero-event">
|
||||
<!-- Label sits above the date + title so it's clear they describe
|
||||
the next event. -->
|
||||
<p class="hero-eyebrow">Next gathering · {eventKindLabel(event.kind).toUpperCase()}</p>
|
||||
<div class="hero-top">
|
||||
<div class="hero-date">
|
||||
<span class="hero-weekday">{weekday}</span>
|
||||
<span class="hero-day">{dayPadded}</span>
|
||||
<span class="hero-month-time">{monthShort} · {startTime}</span>
|
||||
</div>
|
||||
|
||||
<div class="hero-mid">
|
||||
<h2 class="hero-title">{event.title}</h2>
|
||||
<p class="hero-desc">{event.description}</p>
|
||||
<p class="hero-location">{event.location}</p>
|
||||
</div>
|
||||
<div class="hero-mid">
|
||||
<h2 class="hero-title">{event.title}</h2>
|
||||
<p class="hero-desc">{event.description}</p>
|
||||
<p class="hero-location">{event.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="hero-foot">
|
||||
<p class="hero-status">
|
||||
{event.capacity ? `${event.capacity} SEATS · ` : ''}{confirmedCount} CONFIRMED
|
||||
</p>
|
||||
|
||||
<form method="POST" class="hero-actions">
|
||||
<input type="hidden" name="action" value="rsvp" />
|
||||
<input type="hidden" name="event_slug" value={event.slug} />
|
||||
{myRsvp === 'yes' ? (
|
||||
<>
|
||||
<span class="hero-confirmed">You're confirmed ✓</span>
|
||||
<button type="submit" name="status" value="no" class="hero-change">Change</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button type="submit" name="status" value="no" class="hero-decline">Can't make it</button>
|
||||
<button type="submit" name="status" value="yes" class="hero-cta">Save your seat →</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</footer>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<p class="hero-empty"><em>Nothing scheduled yet — when we have something, you'll be the first to know.</em></p>
|
||||
)}
|
||||
|
||||
<!-- RSVP row, pinned to the base of the card. -->
|
||||
{event && (
|
||||
<footer class="hero-foot">
|
||||
<p class="hero-status">
|
||||
{event.capacity ? `${event.capacity} SEATS · ` : ''}{confirmedCount} CONFIRMED
|
||||
</p>
|
||||
|
||||
<form method="POST" class="hero-actions">
|
||||
<input type="hidden" name="action" value="rsvp" />
|
||||
<input type="hidden" name="event_slug" value={event.slug} />
|
||||
{myRsvp === 'yes' ? (
|
||||
<>
|
||||
<span class="hero-confirmed">You're confirmed ✓</span>
|
||||
<button type="submit" name="status" value="no" class="hero-change">Change</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button type="submit" name="status" value="no" class="hero-decline">Can't make it</button>
|
||||
<button type="submit" name="status" value="yes" class="hero-cta">Save your seat →</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</footer>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
|
|
@ -262,10 +265,11 @@ const startTime = event ? fmt({ hour: '2-digit', minute: '2-digit', hour12: fa
|
|||
}
|
||||
|
||||
/* ── Bottom strip ────────────────────────────────────────────── */
|
||||
/* RSVP row pinned to the base of the card. */
|
||||
.hero-foot {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: auto; /* pin to the bottom of the taller card */
|
||||
margin-top: auto;
|
||||
border-top: 0.5px solid var(--ink-divider);
|
||||
padding-top: 22px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import AppLayout from '../layouts/AppLayout.astro';
|
|||
import Avatar from '../components/Avatar.astro';
|
||||
import EventHeroCard from '../components/EventHeroCard.astro';
|
||||
import RoadmapCarousel from '../components/RoadmapCarousel.astro';
|
||||
import type { Event } from '../lib/db';
|
||||
import {
|
||||
getUpcomingEvents, getPastEvents, getEventBySlug, getEventAttendees,
|
||||
getUserRsvp, setEventRsvp, recordActivity,
|
||||
|
|
@ -10,8 +11,7 @@ import {
|
|||
getAllCabMembers, getPulseById, castOrChangeVote,
|
||||
} from '../lib/db';
|
||||
import {
|
||||
timeOfDay, relativeTime,
|
||||
eventKindLabel,
|
||||
timeOfDay, relativeTime, eventKindLabel,
|
||||
dispatchSlug, dispatchKindLabel, dispatchKindPigment, dispatchLongPreview,
|
||||
} from '../lib/format';
|
||||
|
||||
|
|
@ -75,13 +75,20 @@ function parseUtc(s: string): Date {
|
|||
if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s);
|
||||
return new Date(s.replace(' ', 'T') + 'Z');
|
||||
}
|
||||
function fmt(part: Intl.DateTimeFormatOptions, iso: string): string {
|
||||
return new Intl.DateTimeFormat('en-GB', { ...part, timeZone: 'Europe/Copenhagen' }).format(parseUtc(iso));
|
||||
|
||||
// Day / month·kind for the previous + upcoming strip below the hero card.
|
||||
function fmtPart(opts: Intl.DateTimeFormatOptions, iso: string): string {
|
||||
return new Intl.DateTimeFormat('en-GB', { ...opts, timeZone: 'Europe/Copenhagen' }).format(parseUtc(iso));
|
||||
}
|
||||
const dayNum = (iso: string) => fmt({ day: 'numeric' }, iso);
|
||||
const weekday = (iso: string) => fmt({ weekday: 'short' }, iso).toUpperCase();
|
||||
const monthShort = (iso: string) => fmt({ month: 'short' }, iso).toUpperCase();
|
||||
const timeStr = (iso: string) => fmt({ hour: '2-digit', minute: '2-digit', hour12: false }, iso);
|
||||
function alsoParts(ev: Event) {
|
||||
return {
|
||||
day: fmtPart({ day: 'numeric' }, ev.starts_at),
|
||||
sub: `${fmtPart({ month: 'short' }, ev.starts_at).toUpperCase()} · ${eventKindLabel(ev.kind).toUpperCase()}`,
|
||||
title: ev.title,
|
||||
};
|
||||
}
|
||||
const prevAlso = previousEvent ? alsoParts(previousEvent) : null;
|
||||
const upcAlso = upcomingAfterHero ? alsoParts(upcomingAfterHero) : null;
|
||||
|
||||
const heroAttendees = hero ? getEventAttendees(hero.slug, 'yes') : [];
|
||||
const heroConfirmedCount = heroAttendees.length;
|
||||
|
|
@ -121,36 +128,36 @@ const members = getAllCabMembers();
|
|||
firstName={firstName}
|
||||
memberLabel={memberNumberLabel}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- ── Previous + Upcoming strip (plain text on cream) ─────── -->
|
||||
<section class="cascade also-coming-up" aria-label="Surrounding events">
|
||||
<div class="also-list">
|
||||
{previousEvent && (
|
||||
<div class="also-item">
|
||||
<span class="also-day">{dayNum(previousEvent.starts_at)}</span>
|
||||
<div class="also-meta-col">
|
||||
<span class="also-eyebrow">Previous</span>
|
||||
<span class="also-month-kind">{monthShort(previousEvent.starts_at)} · {eventKindLabel(previousEvent.kind).toUpperCase()}</span>
|
||||
<span class="also-title">{previousEvent.title}</span>
|
||||
</div>
|
||||
<!-- Previous + upcoming gatherings, sitting just below the box. -->
|
||||
{(prevAlso || upcAlso) && (
|
||||
<div class="also-strip">
|
||||
<div class="also-list">
|
||||
{prevAlso && (
|
||||
<a href="/events" class="also-item">
|
||||
<span class="also-day">{prevAlso.day}</span>
|
||||
<span class="also-meta">
|
||||
<span class="also-eyebrow">Previous</span>
|
||||
<span class="also-sub">{prevAlso.sub}</span>
|
||||
<span class="also-title">{prevAlso.title}</span>
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
{prevAlso && upcAlso && <span class="also-divider" aria-hidden="true"></span>}
|
||||
{upcAlso && (
|
||||
<a href="/events" class="also-item">
|
||||
<span class="also-day">{upcAlso.day}</span>
|
||||
<span class="also-meta">
|
||||
<span class="also-eyebrow">Upcoming</span>
|
||||
<span class="also-sub">{upcAlso.sub}</span>
|
||||
<span class="also-title">{upcAlso.title}</span>
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{previousEvent && upcomingAfterHero && (
|
||||
<span class="also-divider" aria-hidden="true"></span>
|
||||
)}
|
||||
{upcomingAfterHero && (
|
||||
<div class="also-item">
|
||||
<span class="also-day">{dayNum(upcomingAfterHero.starts_at)}</span>
|
||||
<div class="also-meta-col">
|
||||
<span class="also-eyebrow">Upcoming</span>
|
||||
<span class="also-month-kind">{monthShort(upcomingAfterHero.starts_at)} · {eventKindLabel(upcomingAfterHero.kind).toUpperCase()}</span>
|
||||
<span class="also-title">{upcomingAfterHero.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a href="/events" class="also-link">All gatherings →</a>
|
||||
<a href="/events" class="also-all">All gatherings →</a>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<!-- ── Editorial row: dispatch (1.7fr) + pulse (1fr) ─────────── -->
|
||||
|
|
@ -282,11 +289,91 @@ const members = getAllCabMembers();
|
|||
dispatch → 'Earlier' gap stays tight at the original 48px because
|
||||
they're the same story. */
|
||||
.hero-slot { margin-top: 24px; } /* first section, below nav */
|
||||
.also-coming-up { margin-top: 18px; } /* hero → also (tight; pair) */
|
||||
.editorial-row { margin-top: 96px; } /* also → editorial */
|
||||
.editorial-row { margin-top: 96px; } /* hero → editorial */
|
||||
.roadmap-wrap { margin-top: 96px; } /* editorial → roadmap */
|
||||
.council-section{ margin-top: 96px; } /* roadmap → council */
|
||||
|
||||
/* ── Previous + upcoming strip (below the hero box) ───────────── */
|
||||
.also-strip {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 16px 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.also-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.also-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
color: var(--on-surface);
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
transition: opacity var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
.also-item:hover { opacity: 0.7; border-bottom: none; }
|
||||
.also-day {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
color: var(--on-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.also-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.also-eyebrow {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
color: var(--pigment-terracotta);
|
||||
font-weight: 600;
|
||||
}
|
||||
.also-sub {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
.also-title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 15px;
|
||||
line-height: 1.25;
|
||||
color: var(--on-surface);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 20rem;
|
||||
}
|
||||
.also-divider {
|
||||
flex-shrink: 0;
|
||||
width: 1px;
|
||||
height: 34px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.also-all {
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start; /* sit at the top of the strip, not the baseline */
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
color: var(--pigment-terracotta);
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
transition: opacity var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
.also-all:hover { opacity: 0.7; border-bottom: none; }
|
||||
|
||||
/* ── Cascade entry (first paint only) ─────────────────────────── */
|
||||
.cascade {
|
||||
opacity: 0;
|
||||
|
|
@ -350,85 +437,8 @@ const members = getAllCabMembers();
|
|||
.page { padding: 32px 20px 64px; }
|
||||
.greeting { grid-template-columns: 1fr; align-items: start; }
|
||||
.greeting-right { align-items: flex-start; }
|
||||
/* "Also coming up" strip stacks and wraps instead of overflowing. */
|
||||
.also-coming-up { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
|
||||
.also-list { flex-wrap: wrap; gap: 12px 18px; }
|
||||
.also-title { max-width: 70vw; }
|
||||
}
|
||||
|
||||
/* ── 'Also coming up' strip (plain text on cream) ─────────────── */
|
||||
.also-coming-up {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.also-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
.also-divider {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 28px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
transform: scaleX(1);
|
||||
}
|
||||
.also-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.also-day {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
.also-meta-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.also-eyebrow {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
color: var(--pigment-terracotta);
|
||||
font-weight: 600;
|
||||
}
|
||||
.also-month-kind {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
.also-title {
|
||||
font-size: 14px;
|
||||
color: var(--on-surface);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 16rem;
|
||||
}
|
||||
.also-link {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
color: var(--pigment-terracotta);
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
.also-link:hover { opacity: 0.8; border-bottom: none; }
|
||||
|
||||
/* ── Editorial row: dispatch (1.7fr) + pulse (1fr) ────────────── */
|
||||
.editorial-row {
|
||||
display: grid;
|
||||
|
|
@ -750,6 +760,5 @@ const members = getAllCabMembers();
|
|||
/* ── Responsive ───────────────────────────────────────────────── */
|
||||
@media (max-width: 880px) {
|
||||
.editorial-row { grid-template-columns: 1fr; gap: var(--space-8); }
|
||||
.also-coming-up { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue