astro-theme-popular 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-theme-popular",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Popular: a warm, community-first Astro theme for meetups, clubs and fan communities. Ships as an Astro integration.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/schemas.ts CHANGED
@@ -49,6 +49,7 @@ const events = defineCollection({
49
49
  time: z.string().optional(),
50
50
  venue: z.string().optional(),
51
51
  venueWanted: z.boolean().default(false),
52
+ planning: z.boolean().default(false), // "Planning" badge instead of "Confirmed", and no RSVP button
52
53
  address: z.string().optional(),
53
54
  speaker: z.string().optional(), // one-liner fallback when no speaker profiles are referenced
54
55
  speakers: z.array(z.string()).default([]), // slugs of entries in the speakers collection
@@ -0,0 +1,24 @@
1
+ ---
2
+ /** Calendar subscribe buttons for the events feed: a webcal link that opens
3
+ * the user's calendar app subscribed to the live feed (Apple Calendar,
4
+ * Outlook, Thunderbird), a Google Calendar deep link (Google registers no
5
+ * webcal handler on the web or Android), and the plain .ics download as the
6
+ * universal fallback for "add calendar from URL" flows. webcal needs an
7
+ * absolute URL, so the feed path goes through absoluteUrl. Mirrors Hugo's
8
+ * partials/calendar-subscribe.html; callers supply the wrapping .g-row. */
9
+ import { STRINGS } from 'popular:config';
10
+ import { withBase, absoluteUrl } from '../lib/url';
11
+
12
+ interface Props {
13
+ small?: boolean;
14
+ }
15
+ const { small = false } = Astro.props;
16
+
17
+ const feedPath = '/events/calendar.ics';
18
+ const webcal = absoluteUrl(feedPath, Astro.site ?? Astro.url).replace(/^https?:/, 'webcal:');
19
+ const google = `https://calendar.google.com/calendar/render?cid=${encodeURIComponent(webcal)}`;
20
+ const size = small ? ' g-btn--sm' : '';
21
+ ---
22
+ <a class={`g-btn g-btn--outline${size}`} href={webcal}><i class="fa-solid fa-calendar-plus" aria-hidden="true"></i> {STRINGS.subscribeCalendar}</a>
23
+ <a class={`g-btn g-btn--outline${size}`} href={google} rel="noopener" target="_blank"><i class="fa-brands fa-google" aria-hidden="true"></i> {STRINGS.subscribeGoogle}</a>
24
+ <a class={`g-btn g-btn--ghost${size}`} href={withBase(feedPath)} download><i class="fa-solid fa-download" aria-hidden="true"></i> {STRINGS.downloadCalendar}</a>
@@ -16,6 +16,10 @@ if (d.venueRef && !venuePage) console.warn(`events/${event.id}: venueRef "${d.ve
16
16
  const venueName = venuePage?.data.title ?? d.venue;
17
17
  const venueAddress = venuePage?.data.address ?? (venuePage ? undefined : d.address);
18
18
  const tbd = d.venueWanted || !venueName;
19
+ /* Planning: the event is on the calendar but its details are not settled.
20
+ Wins over the venue-wanted badge, which is inferred, and hides the RSVP
21
+ button even when `rsvp` is already filled in. */
22
+ const planning = d.planning;
19
23
 
20
24
  /* Speaker line: resolved speaker-profile names, falling back to the string. */
21
25
  const allSpeakers = d.speakers.length ? await safeCollection('speakers') : [];
@@ -57,10 +61,12 @@ const day = d.date.toLocaleDateString(SITE.locale, { month: 'short', day: 'numer
57
61
  <div class="g-event__aside">
58
62
  {d.cancelled
59
63
  ? <span class="g-badge g-badge--danger g-badge--soft">{STRINGS.eventCancelled}</span>
64
+ : planning
65
+ ? <span class="g-badge g-badge--soft">{STRINGS.eventPlanning}</span>
60
66
  : tbd
61
67
  ? <span class="g-badge g-badge--warning g-badge--soft">{STRINGS.venueWanted}</span>
62
68
  : <span class="g-badge g-badge--success g-badge--soft">{STRINGS.confirmed}</span>}
63
- {d.rsvp && (
69
+ {!planning && d.rsvp && (
64
70
  <a class="g-btn g-btn--primary g-btn--sm" href={withBase(d.rsvp)}><i class="fa-solid fa-calendar" aria-hidden="true"></i>{STRINGS.rsvp}</a>
65
71
  )}
66
72
  </div>
@@ -1,26 +1,24 @@
1
1
  ---
2
2
  /** Designed empty state for zero upcoming events (§5). Shown on the home page
3
3
  * and events list instead of a blank section: a message, then the chat CTA
4
- * (if SITE.community.chat is set, §4) and the calendar-subscribe link.
5
- * The calendar link mirrors Hugo's OutputFormats "Calendar" endpoint. */
4
+ * (if SITE.community.chat is set, §4) and the calendar-subscribe buttons.
5
+ * The calendar feed mirrors Hugo's OutputFormats "Calendar" endpoint. */
6
6
  import { SITE, STRINGS } from 'popular:config';
7
7
  import { withBase } from '../lib/url';
8
+ import CalendarSubscribe from './CalendarSubscribe.astro';
8
9
 
9
10
  const chat = (SITE as Record<string, any>).community?.chat;
10
- const calendar = '/events/calendar.ics';
11
11
  const external = (url: string) => !url.startsWith('/');
12
12
  ---
13
13
  <div class="g-empty">
14
14
  <span class="g-medallion g-medallion--lg"><i class="fa-solid fa-calendar-day" aria-hidden="true"></i></span>
15
15
  <p class="g-lead">{STRINGS.noUpcomingEvents}</p>
16
- {(chat?.url || calendar) && (
17
- <div class="g-row" style="justify-content:center">
18
- {chat?.url && (
19
- <a class="g-btn g-btn--primary" href={withBase(chat.url)} {...(external(chat.url) ? { rel: 'noopener', target: '_blank' } : {})}>
20
- <i class="fa-solid fa-comments" aria-hidden="true"></i> {chat.label ?? STRINGS.joinChat}
21
- </a>
22
- )}
23
- <a class="g-btn g-btn--outline" href={withBase(calendar)}><i class="fa-solid fa-calendar-plus" aria-hidden="true"></i> {STRINGS.subscribeCalendar}</a>
24
- </div>
25
- )}
16
+ <div class="g-row" style="justify-content:center">
17
+ {chat?.url && (
18
+ <a class="g-btn g-btn--primary" href={withBase(chat.url)} {...(external(chat.url) ? { rel: 'noopener', target: '_blank' } : {})}>
19
+ <i class="fa-solid fa-comments" aria-hidden="true"></i> {chat.label ?? STRINGS.joinChat}
20
+ </a>
21
+ )}
22
+ <CalendarSubscribe />
23
+ </div>
26
24
  </div>
package/src/lib/ical.ts CHANGED
@@ -61,6 +61,10 @@ export function buildIcs(
61
61
  L.push(fold('CALSCALE:GREGORIAN'));
62
62
  L.push(fold('METHOD:PUBLISH'));
63
63
  L.push(fold(`X-WR-CALNAME:${opts.siteTitle} events`));
64
+ /* Subscription refresh hints (RFC 7986 + the Apple/Outlook legacy twin).
65
+ Google Calendar ignores both and polls on its own schedule. */
66
+ L.push(fold('REFRESH-INTERVAL;VALUE=DURATION:PT12H'));
67
+ L.push(fold('X-PUBLISHED-TTL:PT12H'));
64
68
  for (const e of events) {
65
69
  L.push(fold('BEGIN:VEVENT'));
66
70
  L.push(fold(`UID:${e.permalink}`));
@@ -10,7 +10,7 @@ const escapeHtml = (s: string): string =>
10
10
 
11
11
  export function inlineMarkdown(src: string): string {
12
12
  let s = escapeHtml(src);
13
- // [label](http(s)://… | /path) — only http(s) and root-relative targets
13
+ // [label](http(s)://… | /path): only http(s) and root-relative targets
14
14
  s = s.replace(
15
15
  /\[([^\]]+)\]\((https?:\/\/[^\s)]+|\/[^\s)]*)\)/g,
16
16
  (_m, label, href) =>
@@ -4,7 +4,7 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
4
4
  import PageHero from 'popular:component/PageHero';
5
5
  import EventRow from 'popular:component/EventRow';
6
6
  import Pagination from '../../components/Pagination.astro';
7
- import { withBase } from '../../lib/url';
7
+ import CalendarSubscribe from '../../components/CalendarSubscribe.astro';
8
8
  import EventsEmpty from '../../components/EventsEmpty.astro';
9
9
  import { SECTIONS, STRINGS, PAGINATION } from 'popular:config';
10
10
 
@@ -54,7 +54,7 @@ const groupByYear = (events: EventEntry[]) => {
54
54
  <span class="g-eyebrow">{STRINGS.comingUp}</span>
55
55
  <h2>{STRINGS.upcomingEvents}</h2>
56
56
  <p class="g-lead">{STRINGS.upcomingLead}</p>
57
- <div class="g-row" style="margin-top:var(--space-3)"><a class="g-btn g-btn--outline g-btn--sm" href={withBase('/events/calendar.ics')}><i class="fa-solid fa-calendar-plus" aria-hidden="true"></i> {STRINGS.subscribeCalendar}</a></div>
57
+ <div class="g-row" style="margin-top:var(--space-3)"><CalendarSubscribe small /></div>
58
58
  </div>
59
59
  <div class="g-stack" style="margin-top:var(--space-4);gap:var(--space-3)">
60
60
  {groupByYear(upcoming).map((group) => (
@@ -72,7 +72,7 @@ const eventLd = jsonld(buildEventLd({
72
72
  </a>
73
73
  </div>
74
74
  <span class="g-eyebrow">{dateStr}{d.time && ` · ${d.time}`}</span>
75
- <h1 style="margin:.5rem 0 0">{d.title}{d.cancelled && <> <span class="g-badge g-badge--danger" style="vertical-align:middle;font-size:var(--text-sm)">{STRINGS.eventCancelled}</span></>}</h1>
75
+ <h1 style="margin:.5rem 0 0">{d.title}{d.cancelled ? <> <span class="g-badge g-badge--danger" style="vertical-align:middle;font-size:var(--text-sm)">{STRINGS.eventCancelled}</span></> : d.planning ? <> <span class="g-badge g-badge--soft" style="vertical-align:middle;font-size:var(--text-sm)">{STRINGS.eventPlanning}</span></> : null}</h1>
76
76
  </div>
77
77
  </header>
78
78
  <div class="g-container g-section--sm">
@@ -129,7 +129,8 @@ const eventLd = jsonld(buildEventLd({
129
129
  </>
130
130
  )}
131
131
 
132
- {d.rsvp && (
132
+ {/* An event still being planned takes no RSVPs yet, even with `rsvp` set. */}
133
+ {!d.planning && d.rsvp && (
133
134
  <div class="g-row" style="margin-top:var(--space-4)">
134
135
  <a class="g-btn g-btn--primary g-btn--lg" href={withBase(d.rsvp)}><i class="fa-solid fa-calendar" aria-hidden="true"></i> {STRINGS.rsvpExternal}</a>
135
136
  </div>
@@ -115,7 +115,7 @@ const orgLd = jsonld(buildOrgLd({
115
115
  </div>
116
116
  {next.data.description && <p style="margin:0;color:var(--text-muted);max-width:34rem">{next.data.description}</p>}
117
117
  <div class="g-row">
118
- {next.data.rsvp && <a class="g-btn g-btn--primary" href={withBase(next.data.rsvp)}><i class="fa-solid fa-calendar" aria-hidden="true"></i> {STRINGS.rsvpExternal}</a>}
118
+ {!next.data.planning && next.data.rsvp && <a class="g-btn g-btn--primary" href={withBase(next.data.rsvp)}><i class="fa-solid fa-calendar" aria-hidden="true"></i> {STRINGS.rsvpExternal}</a>}
119
119
  <a class="g-btn g-btn--outline" href={withBase('/events/')}>{STRINGS.allEvents}</a>
120
120
  </div>
121
121
  </div>