@the-smithy/create-hearth-site 0.20.0 → 0.22.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/bin/create-hearth-site.js +5 -0
- package/package.json +2 -2
- package/template/AGENTS.md +10 -4
- package/template/CLAUDE.md +77 -19
- package/template/README.md +55 -25
- package/template/SITE.md +17 -0
- package/template/package.json +2 -1
- package/template/scripts/hearth.mjs +157 -13
- package/template/src/app/[[...slug]]/page.tsx +2 -54
- package/template/src/app/api/preview/exit/route.ts +1 -1
- package/template/src/app/api/preview/route.ts +1 -1
- package/template/src/app/api/revalidate/route.ts +1 -1
- package/template/src/app/apple-icon.tsx +1 -1
- package/template/src/app/donate/page.tsx +2 -35
- package/template/src/app/donate/thank-you/page.tsx +2 -22
- package/template/src/app/events/[slug]/page.tsx +2 -26
- package/template/src/app/events/page.tsx +2 -29
- package/template/src/app/fundraisers/[slug]/page.tsx +2 -55
- package/template/src/app/fundraisers/page.tsx +2 -37
- package/template/src/app/globals.css +3 -77
- package/template/src/app/icon.tsx +1 -1
- package/template/src/app/layout.tsx +2 -36
- package/template/src/app/llms.txt/route.ts +7 -0
- package/template/src/app/not-found.tsx +2 -14
- package/template/src/app/registered/page.tsx +2 -22
- package/template/src/app/robots.ts +1 -1
- package/template/src/app/sitemap.ts +2 -2
- package/template/src/{app → hearth}/actions.ts +1 -1
- package/template/src/{components → hearth}/blocks/OpenMap.tsx +11 -9
- package/template/src/hearth/blocks/collections.tsx +74 -0
- package/template/src/hearth/blocks/content.tsx +252 -0
- package/template/src/hearth/blocks/custom.tsx +12 -0
- package/template/src/hearth/blocks/events.tsx +130 -0
- package/template/src/hearth/blocks/giving.tsx +123 -0
- package/template/src/{components → hearth}/blocks/index.tsx +14 -10
- package/template/src/hearth/blocks/info.tsx +84 -0
- package/template/src/{components → hearth}/blocks/media.tsx +30 -27
- package/template/src/hearth/components/EventView.tsx +120 -0
- package/template/src/{components → hearth/components}/Registration.tsx +14 -14
- package/template/src/hearth/components/Share.tsx +102 -0
- package/template/src/hearth/format.ts +31 -0
- package/template/src/hearth/forms/ContactForm.tsx +27 -0
- package/template/src/hearth/forms/DonateForm.tsx +69 -0
- package/template/src/{components/blocks → hearth/forms}/EmberFormBlock.tsx +5 -4
- package/template/src/hearth/forms/RegistrationForm.tsx +68 -0
- package/template/src/hearth/forms/VolunteerForm.tsx +58 -0
- package/template/src/hearth/forms/fields.tsx +137 -0
- package/template/src/hearth/hearth.css +52 -0
- package/template/src/hearth/layout/MobileNav.tsx +49 -0
- package/template/src/hearth/layout/NavLink.tsx +16 -0
- package/template/src/hearth/layout/RootLayout.tsx +40 -0
- package/template/src/{components → hearth/layout}/SiteFooter.tsx +19 -14
- package/template/src/hearth/layout/SiteHeader.tsx +53 -0
- package/template/src/hearth/manifest.json +37 -0
- package/template/src/hearth/pages/donate.tsx +36 -0
- package/template/src/hearth/pages/event.tsx +26 -0
- package/template/src/hearth/pages/events.tsx +30 -0
- package/template/src/hearth/pages/fundraiser.tsx +77 -0
- package/template/src/hearth/pages/fundraisers.tsx +38 -0
- package/template/src/hearth/pages/not-found.tsx +15 -0
- package/template/src/hearth/pages/page.tsx +54 -0
- package/template/src/hearth/pages/registered.tsx +19 -0
- package/template/src/hearth/pages/thank-you.tsx +19 -0
- package/template/src/hearth/theme/defaults.ts +510 -0
- package/template/src/hearth/theme/index.ts +31 -0
- package/template/src/hearth/ui.tsx +144 -0
- package/template/src/theme/custom-blocks.tsx +11 -0
- package/template/src/theme/fonts.ts +9 -0
- package/template/src/theme/index.ts +13 -0
- package/template/src/theme/overrides.tsx +16 -0
- package/template/src/theme/theme.css +29 -0
- package/template/src/components/DonateForm.tsx +0 -104
- package/template/src/components/EventView.tsx +0 -107
- package/template/src/components/RegistrationForm.tsx +0 -103
- package/template/src/components/SiteHeader.tsx +0 -37
- package/template/src/components/VolunteerForm.tsx +0 -143
- package/template/src/components/blocks/Form.tsx +0 -92
- package/template/src/components/blocks/collections.tsx +0 -75
- package/template/src/components/blocks/content.tsx +0 -258
- package/template/src/components/blocks/custom.tsx +0 -20
- package/template/src/components/blocks/events.tsx +0 -138
- package/template/src/components/blocks/giving.tsx +0 -120
- package/template/src/components/blocks/info.tsx +0 -83
- package/template/src/components/ui.tsx +0 -137
- /package/template/src/{lib/hearth.ts → hearth/client.ts} +0 -0
- /package/template/src/{lib → hearth}/site.ts +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { BlockOf } from "@the-smithy/hearth";
|
|
2
|
+
import { theme } from "@/theme";
|
|
3
|
+
import { Button, Emphasis, fill, ground, Img, Markdown, Section, SectionHeading, SmartLink, toTone } from "../ui";
|
|
4
|
+
|
|
5
|
+
export function Hero({ block }: { block: BlockOf<"hero"> }) {
|
|
6
|
+
const d = block.data;
|
|
7
|
+
const t = theme.hero;
|
|
8
|
+
return (
|
|
9
|
+
<section data-block="hero" {...ground("inverse")} className={t.root}>
|
|
10
|
+
{d.image ? <Img image={d.image} crop priority className={t.image} /> : null}
|
|
11
|
+
<div className={t.inner} data-size={d.size}>
|
|
12
|
+
{d.logo ? <Img image={d.logo} priority className={t.logo} /> : null}
|
|
13
|
+
{d.eyebrow ? <p className={t.eyebrow}>{d.eyebrow}</p> : null}
|
|
14
|
+
<h1 className={t.title}>
|
|
15
|
+
<Emphasis text={d.heading} className={t.emphasis} />
|
|
16
|
+
</h1>
|
|
17
|
+
{d.body ? <p className={t.body}>{d.body}</p> : null}
|
|
18
|
+
{d.primaryCta || d.secondaryCta ? (
|
|
19
|
+
<div className={t.actions}>
|
|
20
|
+
<Button link={d.primaryCta} />
|
|
21
|
+
<Button link={d.secondaryCta} variant="secondary" />
|
|
22
|
+
</div>
|
|
23
|
+
) : null}
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function RichText({ block }: { block: BlockOf<"richText"> }) {
|
|
30
|
+
return (
|
|
31
|
+
<Section block="richText">
|
|
32
|
+
<Markdown className={theme.richText.body} data-width={block.data.width}>
|
|
33
|
+
{block.data.markdown}
|
|
34
|
+
</Markdown>
|
|
35
|
+
</Section>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function MediaText({ block }: { block: BlockOf<"mediaText"> }) {
|
|
40
|
+
const d = block.data;
|
|
41
|
+
const t = theme.mediaText;
|
|
42
|
+
return (
|
|
43
|
+
<Section block="mediaText">
|
|
44
|
+
<div className={t.grid} data-image={d.imagePosition}>
|
|
45
|
+
<div className={t.text}>
|
|
46
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
|
|
47
|
+
<Markdown className={t.body}>{d.markdown}</Markdown>
|
|
48
|
+
{d.cta ? (
|
|
49
|
+
<div className={t.actions}>
|
|
50
|
+
<Button link={d.cta} />
|
|
51
|
+
</div>
|
|
52
|
+
) : null}
|
|
53
|
+
</div>
|
|
54
|
+
{d.image ? <Img image={d.image} crop={d.imageFit === "contain" ? undefined : 4 / 3} className={t.image} data-fit={d.imageFit} /> : null}
|
|
55
|
+
</div>
|
|
56
|
+
</Section>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function Stats({ block }: { block: BlockOf<"stats"> }) {
|
|
61
|
+
const t = theme.stats;
|
|
62
|
+
return (
|
|
63
|
+
<Section block="stats" tone={toTone(t.tone)}>
|
|
64
|
+
<SectionHeading heading={block.data.heading} />
|
|
65
|
+
<dl className={t.list}>
|
|
66
|
+
{block.data.items.map((s, i) => (
|
|
67
|
+
<div key={i} className={t.item}>
|
|
68
|
+
<dt className={t.label}>{s.label}</dt>
|
|
69
|
+
<dd className={t.value}>{s.value}</dd>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</dl>
|
|
73
|
+
</Section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function CardGrid({ block }: { block: BlockOf<"cardGrid"> }) {
|
|
78
|
+
const d = block.data;
|
|
79
|
+
const t = theme.cardGrid;
|
|
80
|
+
return (
|
|
81
|
+
<Section block="cardGrid" tone={d.tone}>
|
|
82
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
83
|
+
<ul className={t.list}>
|
|
84
|
+
{d.items.map((c, i) => (
|
|
85
|
+
<li key={i} className={t.card}>
|
|
86
|
+
{c.image ? <Img image={c.image} crop={16 / 10} className={t.image} /> : null}
|
|
87
|
+
<h3 className={t.title}>{c.title}</h3>
|
|
88
|
+
{c.body ? <p className={t.body}>{c.body}</p> : null}
|
|
89
|
+
{c.link ? (
|
|
90
|
+
<SmartLink href={c.link.href} className={t.link}>
|
|
91
|
+
{c.link.label}
|
|
92
|
+
</SmartLink>
|
|
93
|
+
) : null}
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
</Section>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function Projects({ block }: { block: BlockOf<"projects"> }) {
|
|
102
|
+
const d = block.data;
|
|
103
|
+
const t = theme.projects;
|
|
104
|
+
const grid = d.layout === "grid";
|
|
105
|
+
return (
|
|
106
|
+
<Section block="projects" tone={d.tone}>
|
|
107
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
108
|
+
<ul className={grid ? t.gridList : t.showcaseList} data-layout={d.layout}>
|
|
109
|
+
{d.items.map((p, i) => (
|
|
110
|
+
<li key={i} className={grid ? t.gridItem : t.showcaseItem} data-flip={!grid && i % 2 ? "" : undefined}>
|
|
111
|
+
{p.image ? <Img image={p.image} crop={16 / 10} className={t.image} /> : null}
|
|
112
|
+
<div className={t.text}>
|
|
113
|
+
<div className={t.top}>
|
|
114
|
+
<h3 className={t.name}>{p.name}</h3>
|
|
115
|
+
{p.status ? <span className={t.status}>{p.status}</span> : null}
|
|
116
|
+
</div>
|
|
117
|
+
{p.role ? <p className={t.role}>{p.role}</p> : null}
|
|
118
|
+
{p.description ? <Markdown className={t.description}>{p.description}</Markdown> : null}
|
|
119
|
+
{p.stack.length ? (
|
|
120
|
+
<ul className={t.stack}>
|
|
121
|
+
{p.stack.map((s, j) => (
|
|
122
|
+
<li key={j} className={t.stackItem}>
|
|
123
|
+
{s}
|
|
124
|
+
</li>
|
|
125
|
+
))}
|
|
126
|
+
</ul>
|
|
127
|
+
) : null}
|
|
128
|
+
{p.url ? (
|
|
129
|
+
<SmartLink href={p.url} className={t.link}>
|
|
130
|
+
{fill(theme.text.visitProject, { name: p.name })}
|
|
131
|
+
</SmartLink>
|
|
132
|
+
) : null}
|
|
133
|
+
</div>
|
|
134
|
+
</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
</Section>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function Pricing({ block }: { block: BlockOf<"pricing"> }) {
|
|
142
|
+
const d = block.data;
|
|
143
|
+
const t = theme.pricing;
|
|
144
|
+
return (
|
|
145
|
+
<Section block="pricing" tone={d.tone}>
|
|
146
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
147
|
+
<ul className={t.list} data-count={d.plans.length >= 4 ? "many" : "few"}>
|
|
148
|
+
{d.plans.map((p, i) => (
|
|
149
|
+
<li key={i} className={t.plan} data-highlight={p.highlight ? "" : undefined}>
|
|
150
|
+
{p.highlight ? <p className={t.highlight}>{p.highlight}</p> : null}
|
|
151
|
+
<h3 className={t.name}>{p.name}</h3>
|
|
152
|
+
<p className={t.price}>
|
|
153
|
+
<span className={t.amount}>{p.price}</span>
|
|
154
|
+
{p.period ? <span className={t.period}>{p.period}</span> : null}
|
|
155
|
+
</p>
|
|
156
|
+
{p.description ? <p className={t.description}>{p.description}</p> : null}
|
|
157
|
+
{p.features.length ? (
|
|
158
|
+
<ul className={t.features}>
|
|
159
|
+
{p.features.map((f, j) => (
|
|
160
|
+
<li key={j} className={t.feature}>
|
|
161
|
+
<span aria-hidden className={t.check} />
|
|
162
|
+
{f}
|
|
163
|
+
</li>
|
|
164
|
+
))}
|
|
165
|
+
</ul>
|
|
166
|
+
) : null}
|
|
167
|
+
{p.cta ? (
|
|
168
|
+
<div className={t.actions}>
|
|
169
|
+
<Button link={p.cta} variant={p.highlight ? "primary" : "secondary"} />
|
|
170
|
+
</div>
|
|
171
|
+
) : null}
|
|
172
|
+
</li>
|
|
173
|
+
))}
|
|
174
|
+
</ul>
|
|
175
|
+
{d.footnote ? <Markdown className={t.footnote}>{d.footnote}</Markdown> : null}
|
|
176
|
+
</Section>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function CallToAction({ block }: { block: BlockOf<"cta"> }) {
|
|
181
|
+
const d = block.data;
|
|
182
|
+
const t = theme.cta;
|
|
183
|
+
const content = (
|
|
184
|
+
<div className={t.row}>
|
|
185
|
+
<div className={t.text}>
|
|
186
|
+
<h2 className={t.title}>
|
|
187
|
+
<Emphasis text={d.heading} />
|
|
188
|
+
</h2>
|
|
189
|
+
{d.body ? <p className={t.body}>{d.body}</p> : null}
|
|
190
|
+
</div>
|
|
191
|
+
<div className={t.actions}>
|
|
192
|
+
<Button link={d.primary} />
|
|
193
|
+
<Button link={d.secondary} variant="secondary" />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
if (!d.image)
|
|
198
|
+
return (
|
|
199
|
+
<Section block="cta" tone={d.tone}>
|
|
200
|
+
{content}
|
|
201
|
+
</Section>
|
|
202
|
+
);
|
|
203
|
+
return (
|
|
204
|
+
<section data-block="cta" {...ground("inverse")} className={t.imageRoot}>
|
|
205
|
+
<Img image={d.image} crop className={t.image} />
|
|
206
|
+
<div className={t.overlay}>
|
|
207
|
+
<div className={t.overlayInner}>{content}</div>
|
|
208
|
+
</div>
|
|
209
|
+
</section>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function Faq({ block }: { block: BlockOf<"faq"> }) {
|
|
214
|
+
const t = theme.faq;
|
|
215
|
+
return (
|
|
216
|
+
<Section block="faq">
|
|
217
|
+
<SectionHeading heading={block.data.heading} />
|
|
218
|
+
<div className={t.list}>
|
|
219
|
+
{block.data.items.map((q, i) => (
|
|
220
|
+
<details key={i} className={t.item}>
|
|
221
|
+
<summary className={t.question}>
|
|
222
|
+
{q.question}
|
|
223
|
+
<span aria-hidden className={t.icon} />
|
|
224
|
+
</summary>
|
|
225
|
+
<Markdown className={t.answer}>{q.answer}</Markdown>
|
|
226
|
+
</details>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</Section>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function Quote({ block }: { block: BlockOf<"quote"> }) {
|
|
234
|
+
const d = block.data;
|
|
235
|
+
const t = theme.quote;
|
|
236
|
+
return (
|
|
237
|
+
<Section block="quote" tone={d.tone}>
|
|
238
|
+
<figure className={t.figure}>
|
|
239
|
+
<blockquote className={t.text}>{d.quote}</blockquote>
|
|
240
|
+
{d.name ? (
|
|
241
|
+
<figcaption className={t.caption}>
|
|
242
|
+
{d.image ? <Img image={d.image} crop={1} className={t.image} /> : null}
|
|
243
|
+
<span>
|
|
244
|
+
<span className={t.name}>{d.name}</span>
|
|
245
|
+
{d.role ? <span className={t.role}>{d.role}</span> : null}
|
|
246
|
+
</span>
|
|
247
|
+
</figcaption>
|
|
248
|
+
) : null}
|
|
249
|
+
</figure>
|
|
250
|
+
</Section>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BlockOf } from "@the-smithy/hearth";
|
|
2
|
+
import { customBlocks } from "@/theme/custom-blocks";
|
|
3
|
+
|
|
4
|
+
/** A block type of this site's own (Settings → Custom blocks in Hearth), drawn by its component in src/theme/custom-blocks.tsx. */
|
|
5
|
+
export function CustomBlock({ block }: { block: BlockOf<"custom"> }) {
|
|
6
|
+
const Component = customBlocks[block.data.block];
|
|
7
|
+
if (!Component) {
|
|
8
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[hearth] No component for custom block "${block.data.block}"; add one in src/theme/custom-blocks.tsx.`);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return <Component values={block.data.values} />;
|
|
12
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { BlockOf, HearthEvent } from "@the-smithy/hearth";
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { Registration } from "../components/Registration";
|
|
5
|
+
import { VolunteerForm } from "../forms/VolunteerForm";
|
|
6
|
+
import { formatEventDate } from "../format";
|
|
7
|
+
import { fill, Img, Section, SectionHeading, toTone } from "../ui";
|
|
8
|
+
|
|
9
|
+
export { formatEventDate };
|
|
10
|
+
|
|
11
|
+
export function EventCard({ event }: { event: HearthEvent }) {
|
|
12
|
+
const t = theme.eventCard;
|
|
13
|
+
const cancelled = event.status === "cancelled";
|
|
14
|
+
return (
|
|
15
|
+
<li className={t.card} data-cancelled={cancelled ? "" : undefined}>
|
|
16
|
+
{event.image ? <Img image={event.image} crop={16 / 9} className={t.image} /> : null}
|
|
17
|
+
<div className={t.body}>
|
|
18
|
+
<p className={t.date}>
|
|
19
|
+
{cancelled ? <span className={t.cancelled}>{theme.text.cancelled}</span> : null}
|
|
20
|
+
<time dateTime={event.startsAt}>{formatEventDate(event)}</time>
|
|
21
|
+
</p>
|
|
22
|
+
<h3 className={t.title}>
|
|
23
|
+
<Link href={event.path} className={t.link}>
|
|
24
|
+
{event.title}
|
|
25
|
+
</Link>
|
|
26
|
+
</h3>
|
|
27
|
+
{event.location.name ? <p className={t.place}>{event.location.name}</p> : null}
|
|
28
|
+
{event.summary ? <p className={t.summary}>{event.summary}</p> : null}
|
|
29
|
+
<div className={t.actions}>
|
|
30
|
+
<Registration event={event} size="small" />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</li>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function EventList({ events }: { events: HearthEvent[] }) {
|
|
38
|
+
if (!events.length) return <p className={theme.eventCard.empty}>{theme.text.noEvents}</p>;
|
|
39
|
+
return (
|
|
40
|
+
<ul className={theme.eventCard.list}>
|
|
41
|
+
{events.map((e) => (
|
|
42
|
+
<EventCard key={e.slug} event={e} />
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function UpcomingEvents({ block }: { block: BlockOf<"upcomingEvents"> }) {
|
|
49
|
+
const d = block.data;
|
|
50
|
+
return (
|
|
51
|
+
<Section block="upcomingEvents">
|
|
52
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
53
|
+
<EventList events={d.events} />
|
|
54
|
+
{d.events.length ? (
|
|
55
|
+
<Link href="/events" className={theme.upcomingEvents.more}>
|
|
56
|
+
{theme.text.allEvents}
|
|
57
|
+
</Link>
|
|
58
|
+
) : null}
|
|
59
|
+
</Section>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Events that have ended, most recent first (a talk archive). They keep their pages, so each still links to it. */
|
|
64
|
+
export function PastEvents({ block }: { block: BlockOf<"pastEvents"> }) {
|
|
65
|
+
const d = block.data;
|
|
66
|
+
if (!d.events.length) return null;
|
|
67
|
+
return (
|
|
68
|
+
<Section block="pastEvents" tone={toTone(theme.pastEvents.tone)}>
|
|
69
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
70
|
+
<EventList events={d.events} />
|
|
71
|
+
</Section>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function EventSpotlight({ block }: { block: BlockOf<"eventSpotlight"> }) {
|
|
76
|
+
const e = block.data.event;
|
|
77
|
+
const t = theme.eventSpotlight;
|
|
78
|
+
if (!e) return null;
|
|
79
|
+
return (
|
|
80
|
+
<Section block="eventSpotlight" tone={toTone(t.tone)}>
|
|
81
|
+
<div className={t.grid}>
|
|
82
|
+
<div>
|
|
83
|
+
{block.data.heading ? <p className={t.label}>{block.data.heading}</p> : null}
|
|
84
|
+
<h2 className={t.title}>{e.title}</h2>
|
|
85
|
+
<p className={t.date}>{formatEventDate(e)}</p>
|
|
86
|
+
{e.location.name ? <p className={t.place}>{e.location.name}</p> : null}
|
|
87
|
+
{e.summary ? <p className={t.summary}>{e.summary}</p> : null}
|
|
88
|
+
<div className={t.actions}>
|
|
89
|
+
<Link href={e.path} className={theme.button.primary}>
|
|
90
|
+
{theme.text.eventDetails}
|
|
91
|
+
</Link>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
{e.image ? <Img image={e.image} crop={4 / 3} className={t.image} /> : null}
|
|
95
|
+
</div>
|
|
96
|
+
</Section>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Where an event's Volunteer sign-up block sits, for the event header's Volunteer button. */
|
|
101
|
+
export const volunteerAnchor = (slug: string) => `volunteer-${slug}`;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Volunteer sign-up for an event, wherever the block is placed. On another page it names the event and links to it;
|
|
105
|
+
* once sign-up closes it says so. The event's own page leaves out its built-in form when this block is on it.
|
|
106
|
+
*/
|
|
107
|
+
export function VolunteerSignup({ block }: { block: BlockOf<"volunteerSignup"> }) {
|
|
108
|
+
const e = block.data.event;
|
|
109
|
+
const v = e.volunteering;
|
|
110
|
+
const t = theme.volunteerSignup;
|
|
111
|
+
const intro = block.data.intro || v.intro;
|
|
112
|
+
return (
|
|
113
|
+
<Section block="volunteerSignup" id={volunteerAnchor(e.slug)}>
|
|
114
|
+
<div className={t.wrap}>
|
|
115
|
+
<h2 className={t.title}>{block.data.heading || theme.text.volunteer}</h2>
|
|
116
|
+
{block.data.eventSlug ? (
|
|
117
|
+
<p className={t.event}>
|
|
118
|
+
<Link href={e.path} className={t.eventLink}>
|
|
119
|
+
{e.title}
|
|
120
|
+
</Link>
|
|
121
|
+
<span className={t.eventDate}> · {formatEventDate(e)}</span>
|
|
122
|
+
</p>
|
|
123
|
+
) : null}
|
|
124
|
+
{intro ? <p className={t.intro}>{intro}</p> : null}
|
|
125
|
+
{v.fundraiser ? <p className={t.note}>{fill(theme.text.supports, { fundraiser: v.fundraiser.title })}</p> : null}
|
|
126
|
+
<div className={t.form}>{v.open ? <VolunteerForm slug={e.slug} timezone={e.timezone} volunteering={v} /> : <p className={t.closed}>{theme.text.volunteerClosed}</p>}</div>
|
|
127
|
+
</div>
|
|
128
|
+
</Section>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { BlockOf, Fundraiser } from "@the-smithy/hearth";
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { DonateForm } from "../forms/DonateForm";
|
|
5
|
+
import { money } from "../format";
|
|
6
|
+
import { fill, Img, Section, SectionHeading, toTone } from "../ui";
|
|
7
|
+
|
|
8
|
+
export { money };
|
|
9
|
+
|
|
10
|
+
/** Raised so far, against the goal when there is one. */
|
|
11
|
+
export function Progress({ fundraiser, large }: { fundraiser: Fundraiser; large?: boolean }) {
|
|
12
|
+
const { raisedCents, goalCents, donorCount } = fundraiser;
|
|
13
|
+
const t = theme.progress;
|
|
14
|
+
const pct = goalCents ? Math.min(100, Math.round((raisedCents / goalCents) * 100)) : null;
|
|
15
|
+
return (
|
|
16
|
+
<div className={t.root}>
|
|
17
|
+
<p className={t.amounts} data-size={large ? "large" : undefined}>
|
|
18
|
+
<span className={t.raised}>{money(raisedCents)}</span>
|
|
19
|
+
<span className={t.goal}>{goalCents ? ` raised of ${money(goalCents)}` : " raised"}</span>
|
|
20
|
+
</p>
|
|
21
|
+
{pct !== null ? (
|
|
22
|
+
<div className={t.bar} role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} aria-label="Progress toward the goal">
|
|
23
|
+
<div className={t.fill} style={{ width: `${pct}%` }} />
|
|
24
|
+
</div>
|
|
25
|
+
) : null}
|
|
26
|
+
{donorCount ? (
|
|
27
|
+
<p className={t.donors}>
|
|
28
|
+
{donorCount} online donation{donorCount === 1 ? "" : "s"}
|
|
29
|
+
</p>
|
|
30
|
+
) : null}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function FundraiserCard({ fundraiser }: { fundraiser: Fundraiser }) {
|
|
36
|
+
const t = theme.fundraiserCard;
|
|
37
|
+
return (
|
|
38
|
+
<li className={t.card} data-status={fundraiser.status}>
|
|
39
|
+
{fundraiser.image ? <Img image={fundraiser.image} crop={16 / 9} className={t.image} /> : null}
|
|
40
|
+
<div className={t.body}>
|
|
41
|
+
{fundraiser.status === "completed" ? <p className={t.completed}>{theme.text.completed}</p> : null}
|
|
42
|
+
<h3 className={t.title}>
|
|
43
|
+
<Link href={fundraiser.path} className={t.link}>
|
|
44
|
+
{fundraiser.title}
|
|
45
|
+
</Link>
|
|
46
|
+
</h3>
|
|
47
|
+
{fundraiser.summary ? <p className={t.summary}>{fundraiser.summary}</p> : null}
|
|
48
|
+
<div className={t.progress}>
|
|
49
|
+
<Progress fundraiser={fundraiser} />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</li>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function FundraiserList({ fundraisers }: { fundraisers: Fundraiser[] }) {
|
|
57
|
+
if (!fundraisers.length) return <p className={theme.fundraiserCard.empty}>{theme.text.noFundraisers}</p>;
|
|
58
|
+
return (
|
|
59
|
+
<ul className={theme.fundraiserCard.list}>
|
|
60
|
+
{fundraisers.map((f) => (
|
|
61
|
+
<FundraiserCard key={f.slug} fundraiser={f} />
|
|
62
|
+
))}
|
|
63
|
+
</ul>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function FundraiserGrid({ block }: { block: BlockOf<"fundraiserGrid"> }) {
|
|
68
|
+
const d = block.data;
|
|
69
|
+
return (
|
|
70
|
+
<Section block="fundraiserGrid">
|
|
71
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
72
|
+
<FundraiserList fundraisers={d.fundraisers} />
|
|
73
|
+
{d.fundraisers.length ? (
|
|
74
|
+
<Link href="/fundraisers" className={theme.fundraiserGrid.more}>
|
|
75
|
+
{theme.text.allFundraisers}
|
|
76
|
+
</Link>
|
|
77
|
+
) : null}
|
|
78
|
+
</Section>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function FundraiserSpotlight({ block }: { block: BlockOf<"fundraiserSpotlight"> }) {
|
|
83
|
+
const f = block.data.fundraiser;
|
|
84
|
+
const t = theme.fundraiserSpotlight;
|
|
85
|
+
if (!f) return null;
|
|
86
|
+
return (
|
|
87
|
+
<Section block="fundraiserSpotlight" tone={toTone(t.tone)}>
|
|
88
|
+
<div className={t.grid}>
|
|
89
|
+
<div className={t.details}>
|
|
90
|
+
{block.data.heading ? <p className={t.label}>{block.data.heading}</p> : null}
|
|
91
|
+
<h2 className={t.title}>{f.title}</h2>
|
|
92
|
+
{f.summary ? <p className={t.summary}>{f.summary}</p> : null}
|
|
93
|
+
<Progress fundraiser={f} large />
|
|
94
|
+
<p className={t.actions}>
|
|
95
|
+
{f.status === "active" ? (
|
|
96
|
+
<Link href={`${f.path}#donate`} className={theme.button.primary}>
|
|
97
|
+
{theme.text.donate}
|
|
98
|
+
</Link>
|
|
99
|
+
) : null}
|
|
100
|
+
<Link href={f.path} className={t.learnMore}>
|
|
101
|
+
{theme.text.learnMore}
|
|
102
|
+
</Link>
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
{f.image ? <Img image={f.image} crop={4 / 3} className={t.image} /> : null}
|
|
106
|
+
</div>
|
|
107
|
+
</Section>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function Donate({ block }: { block: BlockOf<"donate"> }) {
|
|
112
|
+
const d = block.data;
|
|
113
|
+
const t = theme.donate;
|
|
114
|
+
return (
|
|
115
|
+
<Section block="donate">
|
|
116
|
+
<div className={t.wrap}>
|
|
117
|
+
<SectionHeading heading={d.heading} intro={d.intro} />
|
|
118
|
+
{d.fundraiser ? <p className={t.supporting}>{fill(theme.text.supporting, { fundraiser: d.fundraiser.title })}</p> : null}
|
|
119
|
+
<DonateForm presetsCents={d.presetsCents} minCents={d.minCents} maxCents={d.maxCents} fundraiser={d.fundraiser?.slug} submitLabel={d.submitLabel} />
|
|
120
|
+
</div>
|
|
121
|
+
</Section>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import type { BlockOf, ResolvedBlock } from "@the-smithy/hearth";
|
|
2
2
|
import { HearthBlocks, type BlockComponents } from "@the-smithy/hearth/react";
|
|
3
|
-
import {
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { blocks as overrides } from "@/theme/overrides";
|
|
5
|
+
import { EmberFormBlock } from "../forms/EmberFormBlock";
|
|
6
|
+
import { HearthForm } from "../forms/ContactForm";
|
|
7
|
+
import { Section, SectionHeading } from "../ui";
|
|
4
8
|
import { CollectionList } from "./collections";
|
|
5
9
|
import { CallToAction, CardGrid, Faq, Hero, MediaText, Pricing, Projects, Quote, RichText, Stats } from "./content";
|
|
6
|
-
import { EventSpotlight, PastEvents, UpcomingEvents, VolunteerSignup } from "./events";
|
|
7
10
|
import { CustomBlock } from "./custom";
|
|
8
|
-
import {
|
|
9
|
-
import { HearthForm } from "./Form";
|
|
11
|
+
import { EventSpotlight, PastEvents, UpcomingEvents, VolunteerSignup } from "./events";
|
|
10
12
|
import { Donate, FundraiserGrid, FundraiserSpotlight } from "./giving";
|
|
11
13
|
import { Location, Locations, Schedule } from "./info";
|
|
12
14
|
import { Embed, Gallery, Logos, People, Video } from "./media";
|
|
13
15
|
|
|
14
16
|
function FormBlock({ block }: { block: BlockOf<"form"> }) {
|
|
15
17
|
return (
|
|
16
|
-
<Section>
|
|
17
|
-
<div className=
|
|
18
|
+
<Section block="form">
|
|
19
|
+
<div className={theme.formBlock.wrap}>
|
|
18
20
|
<SectionHeading heading={block.data.heading} intro={block.data.intro} />
|
|
19
21
|
{/* Set to an Ember form in Hearth: Ember renders it and keeps the leads; the site's form is the fallback. */}
|
|
20
22
|
{block.data.emberFormId ? (
|
|
@@ -28,9 +30,9 @@ function FormBlock({ block }: { block: BlockOf<"form"> }) {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
* One component per Hearth block type.
|
|
32
|
-
*
|
|
33
|
-
* render nothing.
|
|
33
|
+
* One component per Hearth block type. Sites style them through the theme (src/theme/index.ts); a site that has to
|
|
34
|
+
* replace one entirely does it in src/theme/overrides.tsx. Types without a component (including ones Hearth adds
|
|
35
|
+
* later) render nothing.
|
|
34
36
|
*/
|
|
35
37
|
export const blocks: BlockComponents = {
|
|
36
38
|
hero: Hero,
|
|
@@ -63,6 +65,8 @@ export const blocks: BlockComponents = {
|
|
|
63
65
|
custom: CustomBlock,
|
|
64
66
|
};
|
|
65
67
|
|
|
68
|
+
const components: BlockComponents = { ...blocks, ...overrides };
|
|
69
|
+
|
|
66
70
|
export function Blocks({ blocks: list }: { blocks: ResolvedBlock[] }) {
|
|
67
|
-
return <HearthBlocks blocks={list} components={
|
|
71
|
+
return <HearthBlocks blocks={list} components={components} fallback={() => null} />;
|
|
68
72
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { directionsUrl, pinned, type BlockOf } from "@the-smithy/hearth";
|
|
2
|
+
import { theme } from "@/theme";
|
|
3
|
+
import { Markdown, Section, SectionHeading, SmartLink } from "../ui";
|
|
4
|
+
import { OpenMap } from "./OpenMap";
|
|
5
|
+
|
|
6
|
+
export function Location({ block }: { block: BlockOf<"location"> }) {
|
|
7
|
+
const d = block.data;
|
|
8
|
+
const t = theme.location;
|
|
9
|
+
return (
|
|
10
|
+
<Section block="location">
|
|
11
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
|
|
12
|
+
<div className={t.grid}>
|
|
13
|
+
<div className={t.details}>
|
|
14
|
+
<h3 className={t.name}>{d.name}</h3>
|
|
15
|
+
<address className={t.address}>{d.address}</address>
|
|
16
|
+
{d.hours ? <p className={t.hours}>{d.hours}</p> : null}
|
|
17
|
+
{d.notes ? <Markdown className={t.notes}>{d.notes}</Markdown> : null}
|
|
18
|
+
<a href={directionsUrl(d)} rel="noopener" className={t.directions}>
|
|
19
|
+
{theme.text.directions}
|
|
20
|
+
</a>
|
|
21
|
+
</div>
|
|
22
|
+
{/* Content from before map pins has none: it shows no map until the pin is set in Hearth. */}
|
|
23
|
+
{d.showMap && d.position ? <OpenMap places={[{ name: d.name, address: d.address, position: d.position }]} label={`Map of ${d.name}`} className={t.map} /> : null}
|
|
24
|
+
</div>
|
|
25
|
+
</Section>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Several places: one map with a pin for each (places without a pin are only listed), and the list beside it. */
|
|
30
|
+
export function Locations({ block }: { block: BlockOf<"locations"> }) {
|
|
31
|
+
const d = block.data;
|
|
32
|
+
const t = theme.locations;
|
|
33
|
+
const onMap = d.showMap ? pinned(d.items) : [];
|
|
34
|
+
return (
|
|
35
|
+
<Section block="locations">
|
|
36
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
37
|
+
<div className={t.grid} data-map={onMap.length ? "" : undefined}>
|
|
38
|
+
{onMap.length ? <OpenMap places={onMap} label={d.heading ? `Map: ${d.heading}` : "Map of our locations"} className={t.map} /> : null}
|
|
39
|
+
<ul className={onMap.length ? t.list : t.listNoMap}>
|
|
40
|
+
{d.items.map((place, i) => (
|
|
41
|
+
<li key={i} className={t.item}>
|
|
42
|
+
<h3 className={t.name}>{place.name}</h3>
|
|
43
|
+
<address className={t.address}>{place.address}</address>
|
|
44
|
+
{place.hours ? <p className={t.hours}>{place.hours}</p> : null}
|
|
45
|
+
{place.notes ? <p className={t.notes}>{place.notes}</p> : null}
|
|
46
|
+
<p className={t.links}>
|
|
47
|
+
<a href={directionsUrl(place)} rel="noopener" className={t.link}>
|
|
48
|
+
{theme.text.directions}
|
|
49
|
+
<span className="sr-only"> to {place.name}</span>
|
|
50
|
+
</a>
|
|
51
|
+
{place.link ? (
|
|
52
|
+
<SmartLink href={place.link.href} className={t.link}>
|
|
53
|
+
{place.link.label}
|
|
54
|
+
</SmartLink>
|
|
55
|
+
) : null}
|
|
56
|
+
</p>
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
</div>
|
|
61
|
+
</Section>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function Schedule({ block }: { block: BlockOf<"schedule"> }) {
|
|
66
|
+
const d = block.data;
|
|
67
|
+
const t = theme.schedule;
|
|
68
|
+
return (
|
|
69
|
+
<Section block="schedule">
|
|
70
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
71
|
+
<ol className={t.list}>
|
|
72
|
+
{d.items.map((item, i) => (
|
|
73
|
+
<li key={i} className={t.item}>
|
|
74
|
+
<p className={t.time}>{item.time}</p>
|
|
75
|
+
<div>
|
|
76
|
+
<p className={t.title}>{item.title}</p>
|
|
77
|
+
{item.details ? <p className={t.details}>{item.details}</p> : null}
|
|
78
|
+
</div>
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ol>
|
|
82
|
+
</Section>
|
|
83
|
+
);
|
|
84
|
+
}
|