project-bifrost-platform/src/components/EventHeroCard.astro
Arlind 436e19170e 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.
2026-06-18 16:05:09 +02:00

374 lines
11 KiB
Text

---
import type { Event, UserPublic } from '../lib/db';
import { eventKindLabel } from '../lib/format';
interface Props {
event: Event | null;
attendees: UserPublic[]; // confirmed (status='yes') — kept for caller compat, not rendered here
confirmedCount: number;
myRsvp: 'yes' | 'no' | 'interested' | null;
greetingPrefix: string; // e.g. "Good afternoon, "
firstName: string;
memberLabel?: string | null; // e.g. "MEMBER · 001"
}
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);
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));
}
const dayPadded = event ? String(parseUtc(event.starts_at).getUTCDate()).padStart(2, '0') : '';
const weekday = event ? fmt({ weekday: 'long' }, event.starts_at).toUpperCase() : '';
const monthShort = event ? fmt({ month: 'short' }, event.starts_at).toUpperCase() : '';
const startTime = event ? fmt({ hour: '2-digit', minute: '2-digit', hour12: false }, event.starts_at) : '';
---
<article
class:list={["hero", { "hero--empty": !event }]}
aria-label={event ? `Next gathering: ${event.title}` : 'Next gathering'}
>
{event?.photo_url && (
<div class="hero-photo" aria-hidden="true">
<img src={event.photo_url} alt="" loading="lazy" />
</div>
)}
<!-- Greeting now lives inside the box, top of the card. -->
<div class="hero-greeting">
<h1 class="hero-greeting-line">{greetingPrefix}<em>{firstName}</em>.</h1>
{memberLabel && (
<div class="hero-greeting-meta">
<span class="hero-greeting-member">{memberLabel}</span>
<span class="hero-greeting-circle">Founding circle</span>
</div>
)}
</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-mid">
<h2 class="hero-title">{event.title}</h2>
<p class="hero-desc">{event.description}</p>
<p class="hero-location">{event.location}</p>
</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">
<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>
.hero {
position: relative;
overflow: hidden;
background: var(--ink);
color: var(--on-ink);
border-radius: 14px;
padding: 32px 36px 30px;
display: flex;
flex-direction: column;
gap: 44px; /* generous space between greeting and the event */
min-height: 480px; /* much taller — gives the photo room to breathe */
}
/* Event block: label stacked above the date + title grid. */
.hero-event {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
/* ── Greeting (inside the box, top) ──────────────────────────────── */
.hero-greeting {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
flex-wrap: wrap;
}
.hero-greeting-line {
font-family: var(--font-serif);
font-weight: 400;
font-size: 34px;
line-height: 1.05;
letter-spacing: var(--tracking-tight);
color: var(--on-ink);
margin: 0;
}
.hero-greeting-line em { font-style: italic; }
.hero-greeting-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
text-align: right;
}
.hero-greeting-member,
.hero-greeting-circle {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-ink-muted);
}
.hero-top {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 140px 1fr;
gap: 32px;
align-items: start;
}
/* ── Date column ─────────────────────────────────────────────── */
.hero-date {
display: flex;
flex-direction: column;
gap: 6px;
}
.hero-weekday {
font-family: var(--font-sans);
font-size: 11px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-ink-muted);
}
.hero-day {
font-family: var(--font-serif);
font-weight: 400;
font-size: 88px;
line-height: 0.85;
color: var(--on-ink);
}
.hero-month-time {
font-family: var(--font-sans);
font-size: 12px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-ink-muted);
}
/* ── Mid column ──────────────────────────────────────────────── */
.hero-mid {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.hero-eyebrow {
font-family: var(--font-sans);
font-size: 11px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-ink-muted);
margin: 0;
}
.hero-title {
font-family: var(--font-serif);
font-weight: 400;
font-size: 26px;
line-height: 1.15;
color: var(--on-ink);
margin: 0;
}
.hero-desc {
font-size: 14px;
line-height: 1.55;
color: var(--on-ink-body);
margin: 0;
max-width: 380px;
}
.hero-location {
font-size: 13px;
color: var(--on-ink-muted);
margin: 0;
}
/* ── Photo as card background ──────────────────────────────────
The image fills the whole card behind the content. A multi-stop
--ink gradient keeps the left (text) side solid and lets the photo
surface on the right, fading back into the box at the bottom so the
footer stays legible. --ink is rgb(44,58,82). */
.hero-photo {
position: absolute;
top: 0;
right: 0;
width: 70%; /* 30% smaller than the full card */
height: 70%;
z-index: 0;
}
.hero-photo img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
.hero-photo::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
/* Blend the inner edges (left + bottom) into the box; a light veil at
the top keeps the greeting meta legible where it overlaps. */
background:
linear-gradient(to left, rgba(44, 58, 82, 0) 38%, var(--ink) 100%),
linear-gradient(to bottom, rgba(44, 58, 82, 0) 48%, var(--ink) 100%),
linear-gradient(to top, rgba(44, 58, 82, 0) 78%, rgba(44, 58, 82, 0.45) 100%);
}
/* ── Bottom strip ────────────────────────────────────────────── */
/* RSVP row pinned to the base of the card. */
.hero-foot {
position: relative;
z-index: 1;
margin-top: auto;
border-top: 0.5px solid var(--ink-divider);
padding-top: 22px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.hero-status {
font-family: var(--font-sans);
font-size: 12px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-ink-muted);
margin: 0;
}
.hero-actions {
display: flex;
align-items: center;
gap: 16px;
}
.hero-decline {
background: none;
border: none;
color: var(--on-ink-muted);
font-family: var(--font-sans);
font-size: 12px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
cursor: pointer;
padding: 0;
}
.hero-decline:hover { color: var(--on-ink); }
.hero-cta {
background: var(--on-ink);
color: var(--ink);
border: none;
padding: 9px 22px;
border-radius: 999px;
font-family: var(--font-sans);
font-size: var(--text-label-md);
font-weight: 500;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
cursor: pointer;
transition: opacity var(--duration-fast) var(--ease-standard);
}
.hero-cta:hover { opacity: 0.88; }
.hero-confirmed {
color: var(--on-ink);
font-family: var(--font-sans);
font-size: var(--text-label-md);
font-weight: 500;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
padding: 9px 22px;
border: 0.5px solid rgba(255, 252, 247, 0.3);
border-radius: 999px;
}
.hero-change {
background: transparent;
border: none;
color: var(--on-ink-muted);
font-family: var(--font-sans);
font-size: 12px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
cursor: pointer;
text-decoration: underline;
}
.hero-change:hover { color: var(--on-ink); }
/* ── Empty state ─────────────────────────────────────────────── */
.hero--empty {
min-height: 320px;
}
.hero-empty {
font-family: var(--font-serif);
font-size: 20px;
color: var(--on-ink-body);
margin: 0;
max-width: 32rem;
}
.hero-empty em { font-style: italic; }
/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 880px) {
.hero-top { grid-template-columns: 1fr; }
}
/* Phone tuning: lighter padding, smaller display date, full-width copy. */
@media (max-width: 480px) {
.hero { padding: 24px 22px 22px; gap: 22px; min-height: 440px; }
.hero-greeting-line { font-size: 27px; }
.hero-day { font-size: 64px; }
.hero-title { font-size: 22px; }
.hero-desc { max-width: none; }
.hero-foot { gap: 12px; }
}
</style>