300 lines
8.5 KiB
Text
300 lines
8.5 KiB
Text
---
|
||
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>
|