diff --git a/src/components/pages/EventsPage.astro b/src/components/pages/EventsPage.astro index dc7ee90..a3a5077 100644 --- a/src/components/pages/EventsPage.astro +++ b/src/components/pages/EventsPage.astro @@ -15,13 +15,15 @@ import type { LinkedEvent } from '../../data/linked-events'; const { lang } = Astro.props as { lang: Lang }; const t = useTranslations(lang); -// Events + past meetups are NL-sourced (the @atproto.nl calendar and NL meetup photos), so only -// .nl and the .eu aggregate show them. Other national sites keep the generic organizer guides but -// show no events until they run their own. +// Calendar events are country-tagged (see events-account.ts): a national site shows events tagged +// with its country, the .eu aggregate shows all. Past meetups are NL photos, so they stay on .nl and +// the aggregate only. Other national sites keep the generic organizer guides regardless. const scope = getScope(import.meta.env.PUBLIC_SITE_SCOPE); -const showEvents = scope.aggregate || (Array.isArray(scope.countries) && scope.countries.includes('nl')); -const events = showEvents ? await getCalendarEvents() : []; -const meetups = showEvents ? await getPosts(pastMeetupPosts) : []; +const cc = scope.aggregate || scope.countries === 'all' ? null : scope.countries[0]; +const allEvents = await getCalendarEvents(); +const events = scope.aggregate ? allEvents : allEvents.filter((e) => cc != null && e.countries.includes(cc)); +const showMeetups = scope.aggregate || cc === 'nl'; +const meetups = showMeetups ? await getPosts(pastMeetupPosts) : []; // Merge our own calendar events (full cards) with ecosystem events (stripped link cards) // into single Upcoming / Past lists, sorted chronologically. diff --git a/src/data/events-account.ts b/src/data/events-account.ts index 9a99068..34b39e7 100644 --- a/src/data/events-account.ts +++ b/src/data/events-account.ts @@ -1,26 +1,55 @@ // Accounts whose community.lexicon.calendar.event records we auto-publish on the site (created -// via atmo.rsvp). atproto.nl plus sibling local chapters (e.g. atproto.amsterdam) — any event -// added to any of these appears on the site on the next build. The handle is shown as the event's -// source badge and used for the atmo.rsvp link. -export const EVENTS_ACCOUNTS = [ - { did: 'did:plc:rldh4m4qtswi7tq4rp23r27q', handle: 'atproto.nl' }, - { did: 'did:plc:bs2glhnim6sqt3f4ccllwxej', handle: 'atproto.amsterdam' }, +// via atmo.rsvp). atproto.nl plus sibling local chapters — any event added to any of these appears +// on the site on the next build. The handle is shown as the event's source badge and used for the +// atmo.rsvp link. +// +// `countries` (ISO 3166-1 alpha-2, lowercase) place the account's events: a national site shows +// events tagged with its country; the .eu aggregate shows all. A chapter in a country we don't run +// a site for (e.g. atproto.wien) still surfaces on .eu. +export interface EventsAccount { + did: string; + handle: string; + countries: readonly string[]; +} + +export const EVENTS_ACCOUNTS: EventsAccount[] = [ + { did: 'did:plc:rldh4m4qtswi7tq4rp23r27q', handle: 'atproto.nl', countries: ['nl'] }, + { did: 'did:plc:bs2glhnim6sqt3f4ccllwxej', handle: 'atproto.amsterdam', countries: ['nl'] }, + // Vienna's atproto meetup. No .at site, so these show only on the .eu aggregate. + { did: 'did:plc:vao7qd2l2hy3tpolrqrkk4cj', handle: 'atproto.wien', countries: ['at'] }, ]; // Individual events we have chosen to carry, from accounts we do NOT auto-publish. // -// Adding an account to EVENTS_ACCOUNTS above pulls in everything it ever posts, which is right -// for sibling chapters and wrong for an organisation whose calendar is mostly about something -// else. 40verse is a cultural agenda platform: most of its events are not ours to list, but its -// pre-launch is squarely on topic. Curating one record keeps the atproto record, the RSVP link -// and the thumbnail, none of which a plain link card would have. +// Adding an account to EVENTS_ACCOUNTS above pulls in everything it ever posts, which is right for +// sibling chapters and wrong for an account whose calendar is mostly about something else. We curate +// single records instead, keeping the atproto record, the RSVP link and the thumbnail a plain link +// card would lose. // // `handle` is used for the atmo.rsvp URL and the source badge, so it must be the handle the -// record's DID actually resolves to. -export const CURATED_EVENTS = [ +// record's DID actually resolves to. `countries` places the event, same as above. +export interface CuratedEvent { + did: string; + handle: string; + rkey: string; + countries: readonly string[]; +} + +export const CURATED_EVENTS: CuratedEvent[] = [ + // 40verse: a cultural agenda platform whose pre-launch is on topic; the rest of its calendar is not. { did: 'did:plc:or4aocitcm5ssxwece57m7lu', handle: '40verse.social', rkey: '3muenpsewo25x', + countries: ['nl'], + }, + // FOSDEM 2027 in Brussels, via the atmosphere.community calendar (which also posts non-EU events + // like IETF, so we take only this one). The big yearly gathering point for the European atproto + // and wider open-source crowd — shows on .be and the .eu aggregate. + { + did: 'did:plc:lehcqqkwzcwvjvw66uthu5oq', + handle: 'atmosphere.community', + rkey: '3mucnmuevl2pq', + countries: ['be'], }, ]; diff --git a/src/lib/atproto/events.ts b/src/lib/atproto/events.ts index 53bd5af..2d8ae7c 100644 --- a/src/lib/atproto/events.ts +++ b/src/lib/atproto/events.ts @@ -19,6 +19,7 @@ export interface CalendarEvent { url: string; // atmo.rsvp event page thumb?: string; upcoming: boolean; + countries: readonly string[]; // where this event belongs (from its source account/curation) } const NL_COUNTRY = /^(nl|nederland|netherlands|the netherlands)$/i; @@ -41,6 +42,7 @@ function toEvent( handle: string, pds: string | null, now: number, + countries: readonly string[], ): CalendarEvent { const rkey = uri.split('/').pop() as string; const startsAt = v.startsAt as string; @@ -65,27 +67,35 @@ function toEvent( url: `https://atmo.rsvp/p/${handle}/e/${rkey}`, thumb, upcoming: Number.isFinite(endMs) ? endMs >= now : true, + countries, }; } -async function accountEvents(did: string, handle: string, now: number): Promise { +async function accountEvents( + did: string, + handle: string, + now: number, + countries: readonly string[], +): Promise { const [records, pds] = await Promise.all([ listRecords(did, EVENT_COLLECTION, 100), resolvePds(did), ]); - return records.map((r) => toEvent(r, did, handle, pds, now)).filter((e) => Boolean(e.startsAt)); + return records + .map((r) => toEvent(r, did, handle, pds, now, countries)) + .filter((e) => Boolean(e.startsAt)); } /** Individually curated events from accounts we do not auto-publish (see CURATED_EVENTS). * A record that has gone missing is skipped rather than failing the build. */ async function curatedEvents(now: number): Promise { const out = await Promise.all( - CURATED_EVENTS.map(async ({ did, handle, rkey }) => { + CURATED_EVENTS.map(async ({ did, handle, rkey, countries }) => { const [record, pds] = await Promise.all([ getRecord(did, EVENT_COLLECTION, rkey), resolvePds(did), ]); - return record ? toEvent(record, did, handle, pds, now) : null; + return record ? toEvent(record, did, handle, pds, now, countries) : null; }), ); return out.filter((e): e is CalendarEvent => Boolean(e) && Boolean(e!.startsAt)); @@ -96,7 +106,7 @@ async function curatedEvents(now: number): Promise { export async function getCalendarEvents(): Promise { const now = Date.now(); const [perAccount, curated] = await Promise.all([ - Promise.all(EVENTS_ACCOUNTS.map((a) => accountEvents(a.did, a.handle, now))), + Promise.all(EVENTS_ACCOUNTS.map((a) => accountEvents(a.did, a.handle, now, a.countries))), curatedEvents(now), ]); // Dedupe by URI: a curated event whose account is later auto-published must not appear twice.