project-bifrost-platform/src/pages/calendar/[slug].astro
2026-04-18 22:48:27 +02:00

300 lines
8.5 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import { getCollection, getEntry } from 'astro:content';
import AppLayout from '../../layouts/AppLayout.astro';
import { fmtDate, renderMd } from '../../lib/markdown';
import { getAttendanceSummary, getUserAttendance, setAttendance } from '../../lib/db';
const user = Astro.locals.user;
const { slug } = Astro.params;
const meeting = await getEntry('meetings', slug as string);
if (!meeting) return Astro.redirect('/calendar');
const isPast = new Date(meeting.data.date) < new Date();
// Handle attendance POST
if (Astro.request.method === 'POST') {
const data = await Astro.request.formData();
const status = data.get('status') as 'yes' | 'no' | null;
if (status === 'yes' || status === 'no') {
setAttendance(user.id, meeting.slug, status);
}
return Astro.redirect(`/calendar/${slug}`);
}
const { Content } = await meeting.render();
const attendance = getAttendanceSummary(meeting.slug);
const myStatus = getUserAttendance(user.id, meeting.slug);
---
<AppLayout title={meeting.data.title} user={user}>
<div class="page">
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/calendar" class="crumb label-sm">Calendar</a>
<span class="crumb-sep" aria-hidden="true"></span>
<span class="crumb label-sm crumb-current">{meeting.data.title}</span>
</nav>
<div class="layout">
<article class="article">
<header class="meeting-header">
<time class="label-sm meeting-date" datetime={String(meeting.data.date)}>
{fmtDate(String(meeting.data.date))}
</time>
<h1 class="display-md meeting-title">{meeting.data.title}</h1>
<dl class="meta-list">
<div class="meta-row">
<dt class="label-sm meta-label">Time</dt>
<dd class="body-md meta-value">{meeting.data.time}</dd>
</div>
<div class="meta-row">
<dt class="label-sm meta-label">Location</dt>
<dd class="body-md meta-value">{meeting.data.location}</dd>
</div>
{meeting.data.attendees && (
<div class="meta-row">
<dt class="label-sm meta-label">Attendees</dt>
<dd class="body-md meta-value">{meeting.data.attendees}</dd>
</div>
)}
</dl>
</header>
<div class="prose">
<Content />
</div>
</article>
{!isPast && (
<aside class="attendance-sidebar">
<div class="attendance-card">
<h2 class="label-sm attendance-heading">Will you attend?</h2>
<form method="POST" class="attendance-form">
<button
type="submit"
name="status"
value="yes"
class:list={['attend-btn', { active: myStatus === 'yes' }]}
>
Yes, I will be there
</button>
<button
type="submit"
name="status"
value="no"
class:list={['attend-btn attend-btn-no', { active: myStatus === 'no' }]}
>
No, I cannot make it
</button>
</form>
<div class="attendance-tally">
<span class="label-sm tally-yes">{attendance.yes} attending</span>
{attendance.no > 0 && (
<span class="label-sm tally-no">{attendance.no} not attending</span>
)}
</div>
</div>
</aside>
)}
</div>
</div>
</AppLayout>
<style>
.page {
padding: var(--space-10) var(--space-20) var(--space-16);
max-width: var(--content-max);
margin: 0 auto;
}
/* ── Breadcrumb ──────────────────────────────────────────────────── */
.breadcrumb {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-8);
}
.crumb {
color: var(--on-surface-muted);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
text-decoration: none;
border-bottom: none;
transition: color var(--duration-fast) var(--ease-standard);
}
a.crumb:hover {
color: var(--on-surface-variant);
border-bottom: none;
}
.crumb-sep { color: var(--on-surface-muted); }
.crumb-current { color: var(--on-surface-variant); }
/* ── Layout ──────────────────────────────────────────────────────── */
.layout {
display: grid;
grid-template-columns: 1fr 18rem;
gap: var(--space-10);
align-items: start;
}
/* ── Article ─────────────────────────────────────────────────────── */
.meeting-header {
margin-bottom: var(--space-10);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.meeting-date {
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--on-surface-muted);
}
.meeting-title {
margin: 0;
}
.meta-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin: 0;
padding: 0;
}
.meta-row {
display: flex;
gap: var(--space-4);
align-items: baseline;
}
.meta-label {
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--on-surface-muted);
min-width: 5rem;
flex-shrink: 0;
}
.meta-value {
color: var(--on-surface-variant);
margin: 0;
}
/* ── Prose ───────────────────────────────────────────────────────── */
.prose :global(h2) {
font-family: var(--font-serif);
font-size: var(--text-headline-md);
font-weight: 400;
letter-spacing: var(--tracking-snug);
color: var(--on-surface);
margin: var(--space-8) 0 var(--space-4);
line-height: var(--leading-snug);
}
.prose :global(p) {
margin: 0 0 var(--space-4);
color: var(--on-surface-variant);
line-height: var(--leading-relaxed);
font-size: var(--text-body-lg);
}
.prose :global(ol),
.prose :global(ul) {
padding-left: var(--space-6);
margin: 0 0 var(--space-5);
color: var(--on-surface-variant);
line-height: var(--leading-relaxed);
font-size: var(--text-body-md);
}
.prose :global(li) {
margin-bottom: var(--space-2);
}
.prose :global(strong) {
font-weight: 600;
color: var(--on-surface);
}
.prose :global(em) {
font-style: italic;
color: var(--on-surface-muted);
}
/* ── Attendance sidebar ──────────────────────────────────────────── */
.attendance-card {
background: var(--surface-container-lowest);
border-radius: var(--radius-md);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.attendance-heading {
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--on-surface-muted);
margin: 0;
}
.attendance-form {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.attend-btn {
padding: var(--space-3) var(--space-4);
background: var(--surface-container-low);
border: var(--ghost-border);
border-radius: var(--radius-sm);
font-family: var(--font-sans);
font-size: var(--text-body-sm);
font-weight: 500;
color: var(--on-surface-variant);
cursor: pointer;
text-align: left;
transition: background var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard);
}
.attend-btn:hover {
background: var(--surface-container);
color: var(--on-surface);
}
.attend-btn.active {
background: var(--surface-container-high);
color: var(--on-surface);
font-weight: 600;
}
.attend-btn-no.active {
background: rgba(185, 107, 88, 0.08);
color: var(--pigment-terracotta);
}
.attendance-tally {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding-top: var(--space-2);
border-top: var(--ghost-border);
}
.tally-yes {
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--pigment-copper);
}
.tally-no {
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--on-surface-muted);
}
</style>