astro-theme-popular 0.3.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/LICENSE +21 -0
- package/README.md +40 -0
- package/index.mjs +81 -0
- package/package.json +47 -0
- package/schemas.ts +137 -0
- package/scripts/blog-filter.js +24 -0
- package/scripts/checklist.js +42 -0
- package/scripts/copy-code.js +25 -0
- package/scripts/nav.js +12 -0
- package/scripts/toc.js +25 -0
- package/src/components/AuthorBox.astro +51 -0
- package/src/components/Callout.astro +22 -0
- package/src/components/Checklist.astro +29 -0
- package/src/components/DemoBar.astro +50 -0
- package/src/components/EventRow.astro +59 -0
- package/src/components/Footer.astro +42 -0
- package/src/components/Header.astro +49 -0
- package/src/components/OrganizerCard.astro +21 -0
- package/src/components/PageHero.astro +19 -0
- package/src/components/Pagination.astro +35 -0
- package/src/components/PostCard.astro +36 -0
- package/src/layouts/BaseLayout.astro +102 -0
- package/src/lib/slugs.ts +10 -0
- package/src/pages/[slug].astro +62 -0
- package/src/pages/authors/[slug].astro +39 -0
- package/src/pages/blog/[...page].astro +52 -0
- package/src/pages/blog/[slug].astro +122 -0
- package/src/pages/events/[...page].astro +86 -0
- package/src/pages/events/[slug].astro +81 -0
- package/src/pages/index.astro +178 -0
- package/src/pages/organizers/[...page].astro +38 -0
- package/src/pages/rss.xml.js +33 -0
- package/src/pages/speakers/[slug].astro +39 -0
- package/src/pages/tags/[tag]/[...page].astro +47 -0
- package/src/pages/venues/[slug].astro +50 -0
- package/src/styles/base.css +113 -0
- package/src/styles/components.css +310 -0
- package/src/styles/tokens/colors.css +82 -0
- package/src/styles/tokens/elevation.css +27 -0
- package/src/styles/tokens/fonts.css +9 -0
- package/src/styles/tokens/spacing.css +39 -0
- package/src/styles/tokens/typography.css +43 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
4
|
+
import PageHero from '../../components/PageHero.astro';
|
|
5
|
+
import EventRow from '../../components/EventRow.astro';
|
|
6
|
+
import Pagination from '../../components/Pagination.astro';
|
|
7
|
+
import { SECTIONS, STRINGS, PAGINATION } from 'popular:config';
|
|
8
|
+
|
|
9
|
+
/* One route serves /events/ and /events/page/N/, matching the Hugo theme's
|
|
10
|
+
paginator URLs. Only past events paginate; upcoming events always show in
|
|
11
|
+
full, mirroring layouts/events/list.html on the Hugo side. */
|
|
12
|
+
export async function getStaticPaths() {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const all = (await getCollection('events')).filter((e) => !e.data.draft);
|
|
15
|
+
const upcoming = all.filter((e) => e.data.date >= now).sort((a, b) => +a.data.date - +b.data.date);
|
|
16
|
+
const past = all.filter((e) => e.data.date < now).sort((a, b) => +b.data.date - +a.data.date);
|
|
17
|
+
const size = PAGINATION.pageSize;
|
|
18
|
+
const total = Math.max(1, Math.ceil(past.length / size));
|
|
19
|
+
return Array.from({ length: total }, (_, i) => ({
|
|
20
|
+
params: { page: i === 0 ? undefined : `page/${i + 1}` },
|
|
21
|
+
props: { current: i + 1, total, upcoming, past: past.slice(i * size, (i + 1) * size) },
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { current, total, upcoming, past } = Astro.props;
|
|
26
|
+
|
|
27
|
+
/* Group consecutive events by year for the .g-year headings; the lists are
|
|
28
|
+
already sorted, so consecutive grouping preserves their order. */
|
|
29
|
+
type EventEntry = (typeof upcoming)[number];
|
|
30
|
+
const groupByYear = (events: EventEntry[]) => {
|
|
31
|
+
const groups: { year: number; events: EventEntry[] }[] = [];
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
const year = event.data.date.getFullYear();
|
|
34
|
+
const last = groups[groups.length - 1];
|
|
35
|
+
if (last && last.year === year) last.events.push(event);
|
|
36
|
+
else groups.push({ year, events: [event] });
|
|
37
|
+
}
|
|
38
|
+
return groups;
|
|
39
|
+
};
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<BaseLayout title="Events">
|
|
43
|
+
<PageHero
|
|
44
|
+
eyebrow={SECTIONS.events.eyebrow}
|
|
45
|
+
title={SECTIONS.events.title}
|
|
46
|
+
lead={SECTIONS.events.lead}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<section class="g-bg-white g-section">
|
|
50
|
+
<div class="g-container">
|
|
51
|
+
<div class="g-section-head">
|
|
52
|
+
<span class="g-eyebrow">{STRINGS.comingUp}</span>
|
|
53
|
+
<h2>{STRINGS.upcomingEvents}</h2>
|
|
54
|
+
<p class="g-lead">{STRINGS.upcomingLead}</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="g-stack" style="margin-top:var(--space-4);gap:var(--space-3)">
|
|
57
|
+
{groupByYear(upcoming).map((group) => (
|
|
58
|
+
<>
|
|
59
|
+
<h3 class="g-year">{group.year}</h3>
|
|
60
|
+
{group.events.map((e) => <EventRow event={e} />)}
|
|
61
|
+
</>
|
|
62
|
+
))}
|
|
63
|
+
{upcoming.length === 0 && (
|
|
64
|
+
<p class="g-lead">{STRINGS.noUpcomingEvents}</p>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
{past.length > 0 && (
|
|
71
|
+
<section class="g-bg-tertiary g-section">
|
|
72
|
+
<div class="g-container">
|
|
73
|
+
<div class="g-section-head"><span class="g-eyebrow">{STRINGS.lookingBack}</span><h2>{STRINGS.pastEvents}</h2></div>
|
|
74
|
+
<div class="g-stack" style="margin-top:var(--space-4);gap:var(--space-3)">
|
|
75
|
+
{groupByYear(past).map((group) => (
|
|
76
|
+
<>
|
|
77
|
+
<h3 class="g-year">{group.year}</h3>
|
|
78
|
+
{group.events.map((e) => <EventRow event={e} />)}
|
|
79
|
+
</>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
<Pagination current={current} total={total} base="/events/" />
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
)}
|
|
86
|
+
</BaseLayout>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SITE, STRINGS } from 'popular:config';
|
|
3
|
+
import { getCollection, render } from 'astro:content';
|
|
4
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
5
|
+
import AuthorBox from '../../components/AuthorBox.astro';
|
|
6
|
+
|
|
7
|
+
export async function getStaticPaths() {
|
|
8
|
+
const events = (await getCollection('events')).filter((e) => !e.data.draft);
|
|
9
|
+
return events.map((event) => ({ params: { slug: event.id }, props: { event } }));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { event } = Astro.props;
|
|
13
|
+
const d = event.data;
|
|
14
|
+
const { Content } = await render(event);
|
|
15
|
+
const dateStr = d.date.toLocaleDateString(SITE.locale, { weekday: 'long', month: 'short', day: 'numeric', year: 'numeric' });
|
|
16
|
+
|
|
17
|
+
/* Venue: a venueRef to the venues collection wins over the flat fields;
|
|
18
|
+
its arrival notes are inherited unless the event sets venueNotes. */
|
|
19
|
+
const venuePage = d.venueRef ? (await getCollection('venues')).find((v) => v.id === d.venueRef) : undefined;
|
|
20
|
+
if (d.venueRef && !venuePage) throw new Error(`events/${event.id}: venueRef "${d.venueRef}" matches no venue page`);
|
|
21
|
+
const venueNotes = d.venueNotes ?? venuePage?.data.notes;
|
|
22
|
+
|
|
23
|
+
/* Speaker profile cards, resolved from the speakers collection. */
|
|
24
|
+
const allSpeakers = await getCollection('speakers');
|
|
25
|
+
const speakerEntries = d.speakers.map((slug) => {
|
|
26
|
+
const entry = allSpeakers.find((s) => s.id === slug);
|
|
27
|
+
if (!entry) throw new Error(`events/${event.id}: speaker "${slug}" matches no speaker page`);
|
|
28
|
+
return entry;
|
|
29
|
+
});
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<BaseLayout title={d.title} description={d.description} image={d.image}>
|
|
33
|
+
<article>
|
|
34
|
+
<header class="g-pagehero">
|
|
35
|
+
<div class="g-container" style="padding-block:var(--space-6) var(--space-5)">
|
|
36
|
+
<div style="margin-bottom:1rem">
|
|
37
|
+
<a href="/events/" style="display:inline-flex;gap:.4rem;align-items:center;font-size:var(--text-sm);font-weight:var(--weight-medium)">
|
|
38
|
+
<i class="fa-solid fa-arrow-left" aria-hidden="true"></i> {STRINGS.allEvents}
|
|
39
|
+
</a>
|
|
40
|
+
</div>
|
|
41
|
+
<span class="g-eyebrow">{dateStr}{d.time && ` · ${d.time}`}</span>
|
|
42
|
+
<h1 style="margin:.5rem 0 0">{d.title}</h1>
|
|
43
|
+
</div>
|
|
44
|
+
</header>
|
|
45
|
+
<div class="g-container g-section--sm">
|
|
46
|
+
<div class="g-article">
|
|
47
|
+
<div class="g-meta-line" style="margin-bottom:var(--space-4)">
|
|
48
|
+
<span><i class="fa-solid fa-location-dot" aria-hidden="true"></i> {venuePage
|
|
49
|
+
? <><a href={`/venues/${venuePage.id}/`}>{venuePage.data.title}</a>{venuePage.data.address && ` · ${venuePage.data.address}`}</>
|
|
50
|
+
: <>{d.venue ?? STRINGS.venueTba}{d.address && ` · ${d.address}`}</>}</span>
|
|
51
|
+
{d.speaker && <span><i class="fa-solid fa-microphone-lines" aria-hidden="true"></i> {d.speaker}</span>}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{(d.checkin || venueNotes) && (
|
|
55
|
+
<div class="g-callout" style="flex-direction:column;gap:.6rem;margin:0 0 var(--space-4)">
|
|
56
|
+
<div class="g-callout__title">{STRINGS.beforeYouArrive}</div>
|
|
57
|
+
{d.checkin && <div class="g-callout__body"><i class="fa-solid fa-ticket" aria-hidden="true"></i> {d.checkin}</div>}
|
|
58
|
+
{venueNotes && <div class="g-callout__body"><i class="fa-solid fa-door-open" aria-hidden="true"></i> {venueNotes}</div>}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<div class="g-prose"><Content /></div>
|
|
63
|
+
|
|
64
|
+
{speakerEntries.length > 0 && (
|
|
65
|
+
<>
|
|
66
|
+
<h2 style="font-size:var(--text-xl);margin:var(--space-5) 0 1rem">{STRINGS.speakers}</h2>
|
|
67
|
+
<div class="g-stack" style="gap:var(--space-3)">
|
|
68
|
+
{speakerEntries.map((s) => <AuthorBox entry={s} base="/speakers/" />)}
|
|
69
|
+
</div>
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{d.rsvp && (
|
|
74
|
+
<div class="g-row" style="margin-top:var(--space-4)">
|
|
75
|
+
<a class="g-btn g-btn--primary g-btn--lg" href={d.rsvp}><i class="fa-solid fa-calendar" aria-hidden="true"></i> {STRINGS.rsvpExternal}</a>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</article>
|
|
81
|
+
</BaseLayout>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
4
|
+
import PostCard from '../components/PostCard.astro';
|
|
5
|
+
import OrganizerCard from '../components/OrganizerCard.astro';
|
|
6
|
+
import { HOME, SITE, STRINGS } from 'popular:config';
|
|
7
|
+
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const events = (await getCollection('events')).filter((e) => !e.data.draft);
|
|
10
|
+
const upcoming = events.filter((e) => e.data.date >= now).sort((a, b) => +a.data.date - +b.data.date);
|
|
11
|
+
const next = upcoming[0];
|
|
12
|
+
|
|
13
|
+
const posts = (await getCollection('blog')).filter((p) => !p.data.draft).sort((a, b) => +b.data.date - +a.data.date).slice(0, 3);
|
|
14
|
+
/* value = "@pastEventCount" renders the live number of past events,
|
|
15
|
+
rounded down to a "70+" style once there are ten or more (Hugo parity). */
|
|
16
|
+
const pastCount = events.filter((e) => e.data.date < now).length;
|
|
17
|
+
const statValue = (v: string) =>
|
|
18
|
+
v === '@pastEventCount' ? (pastCount >= 10 ? `${Math.floor(pastCount / 10) * 10}+` : String(pastCount)) : v;
|
|
19
|
+
const nextVenue = next?.data.venueRef ? (await getCollection('venues')).find((v) => v.id === next.data.venueRef)?.data.title : undefined;
|
|
20
|
+
const organizers = (await getCollection('organizers')).sort((a, b) => a.data.weight - b.data.weight);
|
|
21
|
+
|
|
22
|
+
const h = HOME;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<BaseLayout>
|
|
26
|
+
{h.hero && (
|
|
27
|
+
<section class="g-hero">
|
|
28
|
+
<div class="g-container g-hero__grid">
|
|
29
|
+
<div class="g-hero__content">
|
|
30
|
+
{h.hero.eyebrow && <span class="g-eyebrow">{h.hero.eyebrow}</span>}
|
|
31
|
+
<h1>{h.hero.title}</h1>
|
|
32
|
+
{h.hero.lead && <p class="g-hero__lead">{h.hero.lead}</p>}
|
|
33
|
+
{h.hero.ctas && (
|
|
34
|
+
<div class="g-row">
|
|
35
|
+
{h.hero.ctas.map((c) => (
|
|
36
|
+
<a class={`g-btn g-btn--lg g-btn--${c.variant ?? 'primary'}`} href={c.url}>
|
|
37
|
+
{'icon' in c && c.icon && <i class={c.icon} aria-hidden="true"></i>}{c.label}
|
|
38
|
+
</a>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
{h.hero.image && (
|
|
44
|
+
<div class="g-hero__media"><img src={h.hero.image} alt={SITE.title} /></div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
{h.stats && (
|
|
51
|
+
<section class="g-bg-ink">
|
|
52
|
+
<div class="g-container g-stats">
|
|
53
|
+
{h.stats.map((s) => (
|
|
54
|
+
<div class="g-stat"><div class="g-stat__n">{statValue(s.value)}</div><div class="g-stat__l">{s.label}</div></div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{h.features && (
|
|
61
|
+
<section class="g-bg-white g-section">
|
|
62
|
+
<div class="g-container">
|
|
63
|
+
{h.featuresHead && (
|
|
64
|
+
<div class="g-section-head">
|
|
65
|
+
{h.featuresHead.eyebrow && <span class="g-eyebrow">{h.featuresHead.eyebrow}</span>}
|
|
66
|
+
<h2>{h.featuresHead.title}</h2>
|
|
67
|
+
{h.featuresHead.lead && <p class="g-lead">{h.featuresHead.lead}</p>}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
<div class="g-grid-3" style="margin-top:var(--space-5)">
|
|
71
|
+
{h.features.map((f) => (
|
|
72
|
+
<div class="g-card"><div class="g-card__body">
|
|
73
|
+
<span class="g-medallion"><i class={f.icon} aria-hidden="true"></i></span>
|
|
74
|
+
<h3 class="g-card__title">{f.title}</h3>
|
|
75
|
+
<p class="g-card__text">{f.body}</p>
|
|
76
|
+
</div></div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{next && (
|
|
84
|
+
<section class="g-bg-wash-soft">
|
|
85
|
+
<div class="g-container g-feature">
|
|
86
|
+
{(next.data.image ?? h.hero.image) && <div class="g-feature__media"><img src={next.data.image ?? h.hero.image} alt={next.data.title} /></div>}
|
|
87
|
+
<div class="g-feature__body">
|
|
88
|
+
<span class="g-badge g-badge--soft"><i class="fa-solid fa-calendar" aria-hidden="true"></i> {STRINGS.nextMeetup}</span>
|
|
89
|
+
<h2 style="margin:0">{next.data.title}</h2>
|
|
90
|
+
<div class="g-meta-line">
|
|
91
|
+
<span><i class="fa-solid fa-calendar-day" aria-hidden="true"></i> {next.data.date.toLocaleDateString(SITE.locale, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })}{next.data.time && ` · ${next.data.time}`}</span>
|
|
92
|
+
<span><i class="fa-solid fa-location-dot" aria-hidden="true"></i> {nextVenue ?? next.data.venue ?? STRINGS.venueTba}</span>
|
|
93
|
+
</div>
|
|
94
|
+
{next.data.description && <p style="margin:0;color:var(--text-muted);max-width:34rem">{next.data.description}</p>}
|
|
95
|
+
<div class="g-row">
|
|
96
|
+
{next.data.rsvp && <a class="g-btn g-btn--primary" href={next.data.rsvp}><i class="fa-solid fa-calendar" aria-hidden="true"></i> {STRINGS.rsvpExternal}</a>}
|
|
97
|
+
<a class="g-btn g-btn--outline" href="/events/">{STRINGS.allEvents}</a>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{posts.length > 0 && (
|
|
105
|
+
<section class="g-bg-white g-section">
|
|
106
|
+
<div class="g-container">
|
|
107
|
+
<div class="g-row" style="justify-content:space-between;align-items:flex-end">
|
|
108
|
+
<div class="g-section-head">
|
|
109
|
+
<span class="g-eyebrow">{STRINGS.fromTheBlog}</span>
|
|
110
|
+
<h2>{STRINGS.latestFromCommunity}</h2>
|
|
111
|
+
</div>
|
|
112
|
+
<a class="g-btn g-btn--ghost" href="/blog/">{STRINGS.readTheBlog} <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="g-grid-3" style="margin-top:var(--space-4)">
|
|
115
|
+
{posts.map((p) => <PostCard post={p} />)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</section>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{organizers.length > 0 && (
|
|
122
|
+
<section class="g-bg-tertiary g-section">
|
|
123
|
+
<div class="g-container">
|
|
124
|
+
<div class="g-section-head g-section-head--center">
|
|
125
|
+
<span class="g-eyebrow">The team</span>
|
|
126
|
+
<h2>Meet the organizers</h2>
|
|
127
|
+
<p class="g-lead">The volunteers who make every meetup happen.</p>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="g-grid-3" style="margin-top:var(--space-5)">
|
|
130
|
+
{organizers.slice(0, 3).map((o) => <OrganizerCard organizer={o} />)}
|
|
131
|
+
</div>
|
|
132
|
+
<div class="g-row" style="justify-content:center;margin-top:var(--space-4)">
|
|
133
|
+
<a class="g-btn g-btn--outline" href="/organizers/">All organizers</a>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</section>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{h.testimonials && (
|
|
140
|
+
<section class="g-bg-white g-section">
|
|
141
|
+
<div class="g-container">
|
|
142
|
+
{h.testimonialsHead && (
|
|
143
|
+
<div class="g-section-head g-section-head--center">
|
|
144
|
+
{h.testimonialsHead.eyebrow && <span class="g-eyebrow">{h.testimonialsHead.eyebrow}</span>}
|
|
145
|
+
<h2>{h.testimonialsHead.title}</h2>
|
|
146
|
+
{h.testimonialsHead.lead && <p class="g-lead">{h.testimonialsHead.lead}</p>}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
<div class="g-grid-3" style="margin-top:var(--space-5)">
|
|
150
|
+
{h.testimonials.map((t) => (
|
|
151
|
+
<figure class="g-quote">
|
|
152
|
+
<blockquote>{t.quote}</blockquote>
|
|
153
|
+
<figcaption>
|
|
154
|
+
{t.photo && <img src={t.photo} alt={t.name} />}
|
|
155
|
+
<div><span class="g-quote__name">{t.name}</span>{t.role && <span class="g-quote__role">{t.role}</span>}</div>
|
|
156
|
+
</figcaption>
|
|
157
|
+
</figure>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</section>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{h.getInvolved && (
|
|
165
|
+
<section class="g-bg-wash g-section">
|
|
166
|
+
<div class="g-container" style="text-align:center;display:flex;flex-direction:column;align-items:center;gap:1rem">
|
|
167
|
+
<span class="g-eyebrow">{h.getInvolved.eyebrow ?? 'Community'}</span>
|
|
168
|
+
<h2 style="margin:0;max-width:40rem">{h.getInvolved.title}</h2>
|
|
169
|
+
{h.getInvolved.lead && <p style="margin:0;color:var(--ink-700);max-width:38rem">{h.getInvolved.lead}</p>}
|
|
170
|
+
<div class="g-row" style="justify-content:center">
|
|
171
|
+
{h.getInvolved.ctas.map((c) => (
|
|
172
|
+
<a class={`g-btn g-btn--${c.variant ?? 'primary'}`} href={c.url}>{c.label}</a>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</section>
|
|
177
|
+
)}
|
|
178
|
+
</BaseLayout>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
4
|
+
import PageHero from '../../components/PageHero.astro';
|
|
5
|
+
import OrganizerCard from '../../components/OrganizerCard.astro';
|
|
6
|
+
import Pagination from '../../components/Pagination.astro';
|
|
7
|
+
import { SECTIONS, PAGINATION } from 'popular:config';
|
|
8
|
+
|
|
9
|
+
/* One route serves /organizers/ and /organizers/page/N/, matching the Hugo
|
|
10
|
+
theme's paginator URLs. */
|
|
11
|
+
export async function getStaticPaths() {
|
|
12
|
+
const organizers = (await getCollection('organizers')).sort((a, b) => a.data.weight - b.data.weight);
|
|
13
|
+
const size = PAGINATION.pageSize;
|
|
14
|
+
const total = Math.max(1, Math.ceil(organizers.length / size));
|
|
15
|
+
return Array.from({ length: total }, (_, i) => ({
|
|
16
|
+
params: { page: i === 0 ? undefined : `page/${i + 1}` },
|
|
17
|
+
props: { current: i + 1, total, organizers: organizers.slice(i * size, (i + 1) * size) },
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { current, total, organizers } = Astro.props;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<BaseLayout title="Organizers">
|
|
25
|
+
<PageHero
|
|
26
|
+
eyebrow={SECTIONS.organizers.eyebrow}
|
|
27
|
+
title={SECTIONS.organizers.title}
|
|
28
|
+
lead={SECTIONS.organizers.lead}
|
|
29
|
+
/>
|
|
30
|
+
<section class="g-bg-white g-section">
|
|
31
|
+
<div class="g-container">
|
|
32
|
+
<div class="g-grid-3">
|
|
33
|
+
{organizers.map((o) => <OrganizerCard organizer={o} />)}
|
|
34
|
+
</div>
|
|
35
|
+
<Pagination current={current} total={total} base="/organizers/" />
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
</BaseLayout>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import rss from '@astrojs/rss';
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { SITE } from 'popular:config';
|
|
4
|
+
|
|
5
|
+
const escapeXml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
|
+
|
|
7
|
+
export async function GET(context) {
|
|
8
|
+
const posts = (await getCollection('blog'))
|
|
9
|
+
.filter((p) => !p.data.draft)
|
|
10
|
+
.sort((a, b) => +b.data.date - +a.data.date);
|
|
11
|
+
const allAuthors = await getCollection('authors');
|
|
12
|
+
return rss({
|
|
13
|
+
title: SITE.title,
|
|
14
|
+
description: SITE.description,
|
|
15
|
+
site: context.site,
|
|
16
|
+
xmlns: { dc: 'http://purl.org/dc/elements/1.1/' },
|
|
17
|
+
items: posts.map((post) => {
|
|
18
|
+
const names = [
|
|
19
|
+
...post.data.authors.map((slug) => allAuthors.find((a) => a.id === slug)?.data.title ?? slug),
|
|
20
|
+
...post.data.guestAuthors.map((g) => g.name),
|
|
21
|
+
];
|
|
22
|
+
if (!names.length && post.data.author) names.push(post.data.author);
|
|
23
|
+
return {
|
|
24
|
+
title: post.data.title,
|
|
25
|
+
pubDate: post.data.date,
|
|
26
|
+
description: post.data.description,
|
|
27
|
+
link: `/blog/${post.id}/`,
|
|
28
|
+
categories: post.data.tags,
|
|
29
|
+
customData: names.length ? `<dc:creator>${escapeXml(names.join(', '))}</dc:creator>` : undefined,
|
|
30
|
+
};
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { STRINGS } from 'popular:config';
|
|
3
|
+
import { getCollection, render } from 'astro:content';
|
|
4
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
5
|
+
import PageHero from '../../components/PageHero.astro';
|
|
6
|
+
import AuthorBox from '../../components/AuthorBox.astro';
|
|
7
|
+
import EventRow from '../../components/EventRow.astro';
|
|
8
|
+
|
|
9
|
+
export async function getStaticPaths() {
|
|
10
|
+
const speakers = await getCollection('speakers');
|
|
11
|
+
return speakers.map((entry) => ({ params: { slug: entry.id }, props: { entry } }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { entry } = Astro.props;
|
|
15
|
+
const { Content } = await render(entry);
|
|
16
|
+
const events = (await getCollection('events'))
|
|
17
|
+
.filter((e) => !e.data.draft && e.data.speakers.includes(entry.id))
|
|
18
|
+
.sort((a, b) => +b.data.date - +a.data.date);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<BaseLayout title={entry.data.title} description={entry.data.bio}>
|
|
22
|
+
<PageHero eyebrow={STRINGS.eyebrowSpeaker} title={entry.data.title} lead={entry.data.bio} />
|
|
23
|
+
<section class="g-bg-white g-section">
|
|
24
|
+
<div class="g-container">
|
|
25
|
+
<div class="g-article" style="margin-bottom:var(--space-5)">
|
|
26
|
+
<AuthorBox entry={entry} base="/speakers/" />
|
|
27
|
+
<div class="g-prose" style="margin-top:var(--space-4)"><Content /></div>
|
|
28
|
+
</div>
|
|
29
|
+
{events.length > 0 && (
|
|
30
|
+
<>
|
|
31
|
+
<div class="g-section-head"><span class="g-eyebrow">{STRINGS.eyebrowEvents}</span><h2>{STRINGS.sessionsBy.replace('{name}', entry.data.title)}</h2></div>
|
|
32
|
+
<div class="g-stack" style="margin-top:var(--space-4);gap:var(--space-3)">
|
|
33
|
+
{events.map((e) => <EventRow event={e} />)}
|
|
34
|
+
</div>
|
|
35
|
+
</>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
</BaseLayout>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { STRINGS, PAGINATION } from 'popular:config';
|
|
3
|
+
import { tagSlug } from '../../../lib/slugs';
|
|
4
|
+
import { getCollection } from 'astro:content';
|
|
5
|
+
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
|
6
|
+
import PageHero from '../../../components/PageHero.astro';
|
|
7
|
+
import PostCard from '../../../components/PostCard.astro';
|
|
8
|
+
import Pagination from '../../../components/Pagination.astro';
|
|
9
|
+
|
|
10
|
+
/* One route serves /tags/<tag>/ and /tags/<tag>/page/N/, matching the Hugo
|
|
11
|
+
theme's paginated term pages. URLs use the Hugo-compatible slug; the page
|
|
12
|
+
displays the tag's first-seen spelling. */
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const posts = (await getCollection('blog')).filter((p) => !p.data.draft);
|
|
15
|
+
const labels = new Map(); // slug -> first-seen tag spelling
|
|
16
|
+
for (const p of posts) for (const t of p.data.tags) {
|
|
17
|
+
const slug = tagSlug(t);
|
|
18
|
+
if (!labels.has(slug)) labels.set(slug, t);
|
|
19
|
+
}
|
|
20
|
+
const size = PAGINATION.pageSize;
|
|
21
|
+
return [...labels.entries()].flatMap(([slug, label]) => {
|
|
22
|
+
const tagged = posts
|
|
23
|
+
.filter((p) => p.data.tags.some((t) => tagSlug(t) === slug))
|
|
24
|
+
.sort((a, b) => +b.data.date - +a.data.date);
|
|
25
|
+
const total = Math.max(1, Math.ceil(tagged.length / size));
|
|
26
|
+
return Array.from({ length: total }, (_, i) => ({
|
|
27
|
+
params: { tag: slug, page: i === 0 ? undefined : `page/${i + 1}` },
|
|
28
|
+
props: { label, current: i + 1, total, count: tagged.length, posts: tagged.slice(i * size, (i + 1) * size) },
|
|
29
|
+
}));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { tag } = Astro.params;
|
|
34
|
+
const { label, current, total, count, posts } = Astro.props;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<BaseLayout title={`#${label}`}>
|
|
38
|
+
<PageHero eyebrow={STRINGS.eyebrowTag} title={`#${label}`} lead={STRINGS.taggedCount.replace('{n}', String(count)).replace('{tag}', label)} />
|
|
39
|
+
<section class="g-bg-white g-section">
|
|
40
|
+
<div class="g-container">
|
|
41
|
+
<div class="g-grid-3">
|
|
42
|
+
{posts.map((p) => <PostCard post={p} />)}
|
|
43
|
+
</div>
|
|
44
|
+
<Pagination current={current} total={total} base={`/tags/${tag}/`} />
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
</BaseLayout>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { STRINGS } from 'popular:config';
|
|
3
|
+
import { getCollection, render } from 'astro:content';
|
|
4
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
5
|
+
import PageHero from '../../components/PageHero.astro';
|
|
6
|
+
import EventRow from '../../components/EventRow.astro';
|
|
7
|
+
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
const venues = await getCollection('venues');
|
|
10
|
+
return venues.map((entry) => ({ params: { slug: entry.id }, props: { entry } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { entry } = Astro.props;
|
|
14
|
+
const d = entry.data;
|
|
15
|
+
const { Content } = await render(entry);
|
|
16
|
+
const events = (await getCollection('events'))
|
|
17
|
+
.filter((e) => !e.data.draft && e.data.venueRef === entry.id)
|
|
18
|
+
.sort((a, b) => +b.data.date - +a.data.date);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<BaseLayout title={d.title} description={d.address}>
|
|
22
|
+
<PageHero eyebrow={STRINGS.eyebrowVenue} title={d.title} lead={d.address} />
|
|
23
|
+
<section class="g-bg-white g-section">
|
|
24
|
+
<div class="g-container">
|
|
25
|
+
<div class="g-article" style="margin-bottom:var(--space-5)">
|
|
26
|
+
{d.photo && <img src={d.photo} alt={d.title} style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius-xl);box-shadow:var(--shadow-md);margin-bottom:var(--space-4)" />}
|
|
27
|
+
<div class="g-meta-line" style="margin-bottom:var(--space-3)">
|
|
28
|
+
{d.address && <span><i class="fa-solid fa-location-dot" aria-hidden="true"></i> {d.address}</span>}
|
|
29
|
+
{d.website && <span><i class="fa-solid fa-globe" aria-hidden="true"></i> <a href={d.website} rel="noopener" target="_blank">{STRINGS.website}</a></span>}
|
|
30
|
+
</div>
|
|
31
|
+
{(d.notes || d.accessibility) && (
|
|
32
|
+
<div class="g-callout" style="flex-direction:column;gap:.6rem">
|
|
33
|
+
<div class="g-callout__title">{STRINGS.beforeYouArrive}</div>
|
|
34
|
+
{d.notes && <div class="g-callout__body"><i class="fa-solid fa-door-open" aria-hidden="true"></i> {d.notes}</div>}
|
|
35
|
+
{d.accessibility && <div class="g-callout__body"><i class="fa-solid fa-universal-access" aria-hidden="true"></i> {d.accessibility}</div>}
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
<div class="g-prose" style="margin-top:var(--space-4)"><Content /></div>
|
|
39
|
+
</div>
|
|
40
|
+
{events.length > 0 && (
|
|
41
|
+
<>
|
|
42
|
+
<div class="g-section-head"><span class="g-eyebrow">{STRINGS.eyebrowEvents}</span><h2>{STRINGS.venueEvents}</h2></div>
|
|
43
|
+
<div class="g-stack" style="margin-top:var(--space-4);gap:var(--space-3)">
|
|
44
|
+
{events.map((e) => <EventRow event={e} />)}
|
|
45
|
+
</div>
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
</BaseLayout>
|