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 +1 -1
- package/schemas.ts +1 -0
- package/src/components/CalendarSubscribe.astro +24 -0
- package/src/components/EventRow.astro +7 -1
- package/src/components/EventsEmpty.astro +11 -13
- package/src/lib/ical.ts +4 -0
- package/src/lib/inline-md.ts +1 -1
- package/src/pages/events/[...page].astro +2 -2
- package/src/pages/events/[slug].astro +3 -2
- package/src/pages/index.astro +1 -1
package/package.json
CHANGED
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
|
|
5
|
-
* The calendar
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
{chat
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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}`));
|
package/src/lib/inline-md.ts
CHANGED
|
@@ -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)
|
|
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
|
|
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)"><
|
|
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
|
|
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
|
-
{
|
|
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>
|
package/src/pages/index.astro
CHANGED
|
@@ -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>
|