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"
|
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 {
|
function parseUtc(s: string): Date {
|
||||||
if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s);
|
if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s);
|
||||||
|
|
@ -49,7 +51,6 @@ const startTime = event ? fmt({ hour: '2-digit', minute: '2-digit', hour12: fa
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{event ? (
|
{event ? (
|
||||||
<>
|
|
||||||
<div class="hero-event">
|
<div class="hero-event">
|
||||||
<!-- Label sits above the date + title so it's clear they describe
|
<!-- Label sits above the date + title so it's clear they describe
|
||||||
the next event. -->
|
the next event. -->
|
||||||
|
|
@ -68,7 +69,12 @@ const startTime = event ? fmt({ hour: '2-digit', minute: '2-digit', hour12: fa
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<footer class="hero-foot">
|
||||||
<p class="hero-status">
|
<p class="hero-status">
|
||||||
{event.capacity ? `${event.capacity} SEATS · ` : ''}{confirmedCount} CONFIRMED
|
{event.capacity ? `${event.capacity} SEATS · ` : ''}{confirmedCount} CONFIRMED
|
||||||
|
|
@ -90,9 +96,6 @@ const startTime = event ? fmt({ hour: '2-digit', minute: '2-digit', hour12: fa
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p class="hero-empty"><em>Nothing scheduled yet — when we have something, you'll be the first to know.</em></p>
|
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|
@ -262,10 +265,11 @@ const startTime = event ? fmt({ hour: '2-digit', minute: '2-digit', hour12: fa
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Bottom strip ────────────────────────────────────────────── */
|
/* ── Bottom strip ────────────────────────────────────────────── */
|
||||||
|
/* RSVP row pinned to the base of the card. */
|
||||||
.hero-foot {
|
.hero-foot {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
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);
|
border-top: 0.5px solid var(--ink-divider);
|
||||||
padding-top: 22px;
|
padding-top: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import AppLayout from '../layouts/AppLayout.astro';
|
||||||
import Avatar from '../components/Avatar.astro';
|
import Avatar from '../components/Avatar.astro';
|
||||||
import EventHeroCard from '../components/EventHeroCard.astro';
|
import EventHeroCard from '../components/EventHeroCard.astro';
|
||||||
import RoadmapCarousel from '../components/RoadmapCarousel.astro';
|
import RoadmapCarousel from '../components/RoadmapCarousel.astro';
|
||||||
|
import type { Event } from '../lib/db';
|
||||||
import {
|
import {
|
||||||
getUpcomingEvents, getPastEvents, getEventBySlug, getEventAttendees,
|
getUpcomingEvents, getPastEvents, getEventBySlug, getEventAttendees,
|
||||||
getUserRsvp, setEventRsvp, recordActivity,
|
getUserRsvp, setEventRsvp, recordActivity,
|
||||||
|
|
@ -10,8 +11,7 @@ import {
|
||||||
getAllCabMembers, getPulseById, castOrChangeVote,
|
getAllCabMembers, getPulseById, castOrChangeVote,
|
||||||
} from '../lib/db';
|
} from '../lib/db';
|
||||||
import {
|
import {
|
||||||
timeOfDay, relativeTime,
|
timeOfDay, relativeTime, eventKindLabel,
|
||||||
eventKindLabel,
|
|
||||||
dispatchSlug, dispatchKindLabel, dispatchKindPigment, dispatchLongPreview,
|
dispatchSlug, dispatchKindLabel, dispatchKindPigment, dispatchLongPreview,
|
||||||
} from '../lib/format';
|
} 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);
|
if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s);
|
||||||
return new Date(s.replace(' ', 'T') + 'Z');
|
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);
|
function alsoParts(ev: Event) {
|
||||||
const weekday = (iso: string) => fmt({ weekday: 'short' }, iso).toUpperCase();
|
return {
|
||||||
const monthShort = (iso: string) => fmt({ month: 'short' }, iso).toUpperCase();
|
day: fmtPart({ day: 'numeric' }, ev.starts_at),
|
||||||
const timeStr = (iso: string) => fmt({ hour: '2-digit', minute: '2-digit', hour12: false }, iso);
|
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 heroAttendees = hero ? getEventAttendees(hero.slug, 'yes') : [];
|
||||||
const heroConfirmedCount = heroAttendees.length;
|
const heroConfirmedCount = heroAttendees.length;
|
||||||
|
|
@ -121,36 +128,36 @@ const members = getAllCabMembers();
|
||||||
firstName={firstName}
|
firstName={firstName}
|
||||||
memberLabel={memberNumberLabel}
|
memberLabel={memberNumberLabel}
|
||||||
/>
|
/>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Previous + Upcoming strip (plain text on cream) ─────── -->
|
<!-- Previous + upcoming gatherings, sitting just below the box. -->
|
||||||
<section class="cascade also-coming-up" aria-label="Surrounding events">
|
{(prevAlso || upcAlso) && (
|
||||||
|
<div class="also-strip">
|
||||||
<div class="also-list">
|
<div class="also-list">
|
||||||
{previousEvent && (
|
{prevAlso && (
|
||||||
<div class="also-item">
|
<a href="/events" class="also-item">
|
||||||
<span class="also-day">{dayNum(previousEvent.starts_at)}</span>
|
<span class="also-day">{prevAlso.day}</span>
|
||||||
<div class="also-meta-col">
|
<span class="also-meta">
|
||||||
<span class="also-eyebrow">Previous</span>
|
<span class="also-eyebrow">Previous</span>
|
||||||
<span class="also-month-kind">{monthShort(previousEvent.starts_at)} · {eventKindLabel(previousEvent.kind).toUpperCase()}</span>
|
<span class="also-sub">{prevAlso.sub}</span>
|
||||||
<span class="also-title">{previousEvent.title}</span>
|
<span class="also-title">{prevAlso.title}</span>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
)}
|
)}
|
||||||
{previousEvent && upcomingAfterHero && (
|
{prevAlso && upcAlso && <span class="also-divider" aria-hidden="true"></span>}
|
||||||
<span class="also-divider" aria-hidden="true"></span>
|
{upcAlso && (
|
||||||
)}
|
<a href="/events" class="also-item">
|
||||||
{upcomingAfterHero && (
|
<span class="also-day">{upcAlso.day}</span>
|
||||||
<div class="also-item">
|
<span class="also-meta">
|
||||||
<span class="also-day">{dayNum(upcomingAfterHero.starts_at)}</span>
|
|
||||||
<div class="also-meta-col">
|
|
||||||
<span class="also-eyebrow">Upcoming</span>
|
<span class="also-eyebrow">Upcoming</span>
|
||||||
<span class="also-month-kind">{monthShort(upcomingAfterHero.starts_at)} · {eventKindLabel(upcomingAfterHero.kind).toUpperCase()}</span>
|
<span class="also-sub">{upcAlso.sub}</span>
|
||||||
<span class="also-title">{upcomingAfterHero.title}</span>
|
<span class="also-title">{upcAlso.title}</span>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<a href="/events" class="also-link">All gatherings →</a>
|
<a href="/events" class="also-all">All gatherings →</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Editorial row: dispatch (1.7fr) + pulse (1fr) ─────────── -->
|
<!-- ── Editorial row: dispatch (1.7fr) + pulse (1fr) ─────────── -->
|
||||||
|
|
@ -282,11 +289,91 @@ const members = getAllCabMembers();
|
||||||
dispatch → 'Earlier' gap stays tight at the original 48px because
|
dispatch → 'Earlier' gap stays tight at the original 48px because
|
||||||
they're the same story. */
|
they're the same story. */
|
||||||
.hero-slot { margin-top: 24px; } /* first section, below nav */
|
.hero-slot { margin-top: 24px; } /* first section, below nav */
|
||||||
.also-coming-up { margin-top: 18px; } /* hero → also (tight; pair) */
|
.editorial-row { margin-top: 96px; } /* hero → editorial */
|
||||||
.editorial-row { margin-top: 96px; } /* also → editorial */
|
|
||||||
.roadmap-wrap { margin-top: 96px; } /* editorial → roadmap */
|
.roadmap-wrap { margin-top: 96px; } /* editorial → roadmap */
|
||||||
.council-section{ margin-top: 96px; } /* roadmap → council */
|
.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 entry (first paint only) ─────────────────────────── */
|
||||||
.cascade {
|
.cascade {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -350,85 +437,8 @@ const members = getAllCabMembers();
|
||||||
.page { padding: 32px 20px 64px; }
|
.page { padding: 32px 20px 64px; }
|
||||||
.greeting { grid-template-columns: 1fr; align-items: start; }
|
.greeting { grid-template-columns: 1fr; align-items: start; }
|
||||||
.greeting-right { align-items: flex-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: dispatch (1.7fr) + pulse (1fr) ────────────── */
|
||||||
.editorial-row {
|
.editorial-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -750,6 +760,5 @@ const members = getAllCabMembers();
|
||||||
/* ── Responsive ───────────────────────────────────────────────── */
|
/* ── Responsive ───────────────────────────────────────────────── */
|
||||||
@media (max-width: 880px) {
|
@media (max-width: 880px) {
|
||||||
.editorial-row { grid-template-columns: 1fr; gap: var(--space-8); }
|
.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>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue