@the-smithy/create-hearth-site 0.31.0 → 0.33.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 +1 -1
- package/package.json +2 -2
- package/template/AGENTS.md +82 -15
- package/template/README.md +11 -5
- package/template/SITE.md +2 -2
- package/template/_gitattributes +2 -0
- package/template/_gitignore +2 -1
- package/template/package.json +1 -0
- package/template/scripts/hearth.mjs +124 -5
- package/template/src/hearth/blocks/collections.tsx +42 -35
- package/template/src/hearth/blocks/content.tsx +140 -99
- package/template/src/hearth/blocks/events.tsx +25 -10
- package/template/src/hearth/blocks/giving.tsx +29 -14
- package/template/src/hearth/blocks/index.tsx +20 -9
- package/template/src/hearth/blocks/info.tsx +43 -30
- package/template/src/hearth/blocks/media.tsx +36 -21
- package/template/src/hearth/components/EventView.tsx +8 -2
- package/template/src/hearth/components/FundraiserView.tsx +8 -2
- package/template/src/hearth/forms/DonationForm.tsx +9 -2
- package/template/src/hearth/layout/SiteFrame.tsx +8 -2
- package/template/src/hearth/overrides.ts +97 -15
- package/template/src/hearth/parts.tsx +17 -0
- package/template/src/theme/overrides.tsx +29 -14
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BlockOf } from "@the-smithy/hearth";
|
|
2
2
|
import { theme } from "@/theme";
|
|
3
|
+
import { hasPart, Part } from "../parts";
|
|
3
4
|
import { cx } from "../theme";
|
|
4
5
|
import { Button, Emphasis, fill, ground, Img, Markdown, plain, Section, SectionHeading, SmartLink, toTone } from "../ui";
|
|
5
6
|
|
|
@@ -10,26 +11,32 @@ export function Hero({ block }: { block: BlockOf<"hero"> }) {
|
|
|
10
11
|
// Without a tone it's the design's own ground, dark, in `hero.root`. A tone sets the ground, which turns on-dark: on or off.
|
|
11
12
|
return (
|
|
12
13
|
<section id={block.anchor} data-block="hero" data-size={d.size} {...ground(d.tone ?? "inverse")} className={cx(d.tone ? `${theme.tones[d.tone]} ${t.toned}` : t.root, block.anchor && theme.section.anchored)}>
|
|
13
|
-
|
|
14
|
+
<Part name="hero.media" props={{ block }}>
|
|
15
|
+
{d.image ? <Img image={d.image} crop priority className={t.image} /> : null}
|
|
16
|
+
</Part>
|
|
14
17
|
<div className={t.inner} data-size={d.size}>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
paragraphs.
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
<Part name="hero.heading" props={{ block }}>
|
|
19
|
+
{d.logo ? <Img image={d.logo} priority className={t.logo} /> : null}
|
|
20
|
+
{d.eyebrow ? <p className={t.eyebrow}>{d.eyebrow}</p> : null}
|
|
21
|
+
<h1 className={t.title}>
|
|
22
|
+
<Emphasis text={d.heading} className={t.emphasis} />
|
|
23
|
+
</h1>
|
|
24
|
+
{paragraphs.length > 1 ? (
|
|
25
|
+
paragraphs.map((p, i) => (
|
|
26
|
+
<p key={i} className={t.body}>
|
|
27
|
+
{p}
|
|
28
|
+
</p>
|
|
29
|
+
))
|
|
30
|
+
) : d.body ? (
|
|
31
|
+
<p className={t.body}>{d.body}</p>
|
|
32
|
+
) : null}
|
|
33
|
+
</Part>
|
|
34
|
+
{d.primaryCta || d.secondaryCta || hasPart("hero.actions") ? (
|
|
30
35
|
<div className={t.actions}>
|
|
31
|
-
<
|
|
32
|
-
|
|
36
|
+
<Part name="hero.actions" props={{ block }}>
|
|
37
|
+
<Button link={d.primaryCta} />
|
|
38
|
+
<Button link={d.secondaryCta} variant="secondary" />
|
|
39
|
+
</Part>
|
|
33
40
|
</div>
|
|
34
41
|
) : null}
|
|
35
42
|
</div>
|
|
@@ -54,15 +61,21 @@ export function MediaText({ block }: { block: BlockOf<"mediaText"> }) {
|
|
|
54
61
|
<Section block="mediaText" id={block.anchor}>
|
|
55
62
|
<div className={t.grid} data-image={d.imagePosition}>
|
|
56
63
|
<div className={t.text}>
|
|
57
|
-
<
|
|
64
|
+
<Part name="mediaText.heading" props={{ block }}>
|
|
65
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
|
|
66
|
+
</Part>
|
|
58
67
|
<Markdown className={t.body}>{d.markdown}</Markdown>
|
|
59
|
-
{d.cta ? (
|
|
68
|
+
{d.cta || hasPart("mediaText.actions") ? (
|
|
60
69
|
<div className={t.actions}>
|
|
61
|
-
<
|
|
70
|
+
<Part name="mediaText.actions" props={{ block }}>
|
|
71
|
+
<Button link={d.cta} />
|
|
72
|
+
</Part>
|
|
62
73
|
</div>
|
|
63
74
|
) : null}
|
|
64
75
|
</div>
|
|
65
|
-
|
|
76
|
+
<Part name="mediaText.media" props={{ block }}>
|
|
77
|
+
{d.image ? <Img image={d.image} crop={d.imageFit === "contain" ? undefined : 4 / 3} className={t.image} data-fit={d.imageFit} /> : null}
|
|
78
|
+
</Part>
|
|
66
79
|
</div>
|
|
67
80
|
</Section>
|
|
68
81
|
);
|
|
@@ -72,12 +85,16 @@ export function Stats({ block }: { block: BlockOf<"stats"> }) {
|
|
|
72
85
|
const t = theme.stats;
|
|
73
86
|
return (
|
|
74
87
|
<Section block="stats" id={block.anchor} tone={toTone(t.tone)}>
|
|
75
|
-
<
|
|
88
|
+
<Part name="stats.heading" props={{ block }}>
|
|
89
|
+
<SectionHeading heading={block.data.heading} />
|
|
90
|
+
</Part>
|
|
76
91
|
<dl className={t.list}>
|
|
77
92
|
{block.data.items.map((s, i) => (
|
|
78
93
|
<div key={i} className={t.item}>
|
|
79
|
-
<
|
|
80
|
-
|
|
94
|
+
<Part name="stats.item" props={{ block, item: s, index: i }}>
|
|
95
|
+
<dt className={t.label}>{s.label}</dt>
|
|
96
|
+
<dd className={t.value}>{s.value}</dd>
|
|
97
|
+
</Part>
|
|
81
98
|
</div>
|
|
82
99
|
))}
|
|
83
100
|
</dl>
|
|
@@ -98,20 +115,24 @@ export function CardGrid({ block }: { block: BlockOf<"cardGrid"> }) {
|
|
|
98
115
|
const Title = d.heading ? "h3" : "h2";
|
|
99
116
|
return (
|
|
100
117
|
<Section block="cardGrid" id={block.anchor} tone={d.tone}>
|
|
101
|
-
<
|
|
118
|
+
<Part name="cardGrid.heading" props={{ block }}>
|
|
119
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
120
|
+
</Part>
|
|
102
121
|
<ul className={t.list}>
|
|
103
122
|
{d.items.map((c, i) => (
|
|
104
123
|
<li key={i} className={t.card} data-card={cardKey(c.title)} data-icon={c.icon}>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{c.link.
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
<Part name="cardGrid.item" props={{ block, item: c, index: i, heading: Title }}>
|
|
125
|
+
{c.image ? <Img image={c.image} crop={16 / 10} className={t.image} /> : null}
|
|
126
|
+
<Title className={t.title}>
|
|
127
|
+
<Emphasis text={c.title} />
|
|
128
|
+
</Title>
|
|
129
|
+
{c.body ? <p className={t.body}>{c.body}</p> : null}
|
|
130
|
+
{c.link ? (
|
|
131
|
+
<SmartLink href={c.link.href} className={t.link}>
|
|
132
|
+
{c.link.label}
|
|
133
|
+
</SmartLink>
|
|
134
|
+
) : null}
|
|
135
|
+
</Part>
|
|
115
136
|
</li>
|
|
116
137
|
))}
|
|
117
138
|
</ul>
|
|
@@ -126,33 +147,37 @@ export function Projects({ block }: { block: BlockOf<"projects"> }) {
|
|
|
126
147
|
const Title = d.heading ? "h3" : "h2";
|
|
127
148
|
return (
|
|
128
149
|
<Section block="projects" id={block.anchor} tone={d.tone}>
|
|
129
|
-
<
|
|
150
|
+
<Part name="projects.heading" props={{ block }}>
|
|
151
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
152
|
+
</Part>
|
|
130
153
|
<ul className={grid ? t.gridList : t.showcaseList} data-layout={d.layout}>
|
|
131
154
|
{d.items.map((p, i) => (
|
|
132
155
|
<li key={i} className={grid ? t.gridItem : t.showcaseItem} data-flip={!grid && i % 2 ? "" : undefined}>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<div className={t.
|
|
136
|
-
<
|
|
137
|
-
|
|
156
|
+
<Part name="projects.item" props={{ block, item: p, index: i, heading: Title }}>
|
|
157
|
+
{p.image ? <Img image={p.image} crop={16 / 10} className={t.image} /> : null}
|
|
158
|
+
<div className={t.text}>
|
|
159
|
+
<div className={t.top}>
|
|
160
|
+
<Title className={t.name}>{p.name}</Title>
|
|
161
|
+
{p.status ? <span className={t.status}>{p.status}</span> : null}
|
|
162
|
+
</div>
|
|
163
|
+
{p.role ? <p className={t.role}>{p.role}</p> : null}
|
|
164
|
+
{p.description ? <Markdown className={t.description}>{p.description}</Markdown> : null}
|
|
165
|
+
{p.stack.length ? (
|
|
166
|
+
<ul className={t.stack}>
|
|
167
|
+
{p.stack.map((s, j) => (
|
|
168
|
+
<li key={j} className={t.stackItem}>
|
|
169
|
+
{s}
|
|
170
|
+
</li>
|
|
171
|
+
))}
|
|
172
|
+
</ul>
|
|
173
|
+
) : null}
|
|
174
|
+
{p.url ? (
|
|
175
|
+
<SmartLink href={p.url} className={t.link}>
|
|
176
|
+
{fill(theme.text.visitProject, { name: p.name })}
|
|
177
|
+
</SmartLink>
|
|
178
|
+
) : null}
|
|
138
179
|
</div>
|
|
139
|
-
|
|
140
|
-
{p.description ? <Markdown className={t.description}>{p.description}</Markdown> : null}
|
|
141
|
-
{p.stack.length ? (
|
|
142
|
-
<ul className={t.stack}>
|
|
143
|
-
{p.stack.map((s, j) => (
|
|
144
|
-
<li key={j} className={t.stackItem}>
|
|
145
|
-
{s}
|
|
146
|
-
</li>
|
|
147
|
-
))}
|
|
148
|
-
</ul>
|
|
149
|
-
) : null}
|
|
150
|
-
{p.url ? (
|
|
151
|
-
<SmartLink href={p.url} className={t.link}>
|
|
152
|
-
{fill(theme.text.visitProject, { name: p.name })}
|
|
153
|
-
</SmartLink>
|
|
154
|
-
) : null}
|
|
155
|
-
</div>
|
|
180
|
+
</Part>
|
|
156
181
|
</li>
|
|
157
182
|
))}
|
|
158
183
|
</ul>
|
|
@@ -166,32 +191,36 @@ export function Pricing({ block }: { block: BlockOf<"pricing"> }) {
|
|
|
166
191
|
const Title = d.heading ? "h3" : "h2";
|
|
167
192
|
return (
|
|
168
193
|
<Section block="pricing" id={block.anchor} tone={d.tone}>
|
|
169
|
-
<
|
|
194
|
+
<Part name="pricing.heading" props={{ block }}>
|
|
195
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
196
|
+
</Part>
|
|
170
197
|
<ul className={t.list} data-count={d.plans.length >= 4 ? "many" : "few"}>
|
|
171
198
|
{d.plans.map((p, i) => (
|
|
172
199
|
<li key={i} className={t.plan} data-highlight={p.highlight ? "" : undefined}>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
<Part name="pricing.item" props={{ block, item: p, index: i, heading: Title }}>
|
|
201
|
+
{p.highlight ? <p className={t.highlight}>{p.highlight}</p> : null}
|
|
202
|
+
<Title className={t.name}>{p.name}</Title>
|
|
203
|
+
<p className={t.price}>
|
|
204
|
+
<span className={t.amount}>{p.price}</span>
|
|
205
|
+
{p.period ? <span className={t.period}>{p.period}</span> : null}
|
|
206
|
+
</p>
|
|
207
|
+
{p.description ? <p className={t.description}>{p.description}</p> : null}
|
|
208
|
+
{p.features.length ? (
|
|
209
|
+
<ul className={t.features}>
|
|
210
|
+
{p.features.map((f, j) => (
|
|
211
|
+
<li key={j} className={t.feature}>
|
|
212
|
+
<span aria-hidden className={t.check} />
|
|
213
|
+
{f}
|
|
214
|
+
</li>
|
|
215
|
+
))}
|
|
216
|
+
</ul>
|
|
217
|
+
) : null}
|
|
218
|
+
{p.cta ? (
|
|
219
|
+
<div className={t.actions}>
|
|
220
|
+
<Button link={p.cta} variant={p.highlight ? "primary" : "secondary"} />
|
|
221
|
+
</div>
|
|
222
|
+
) : null}
|
|
223
|
+
</Part>
|
|
195
224
|
</li>
|
|
196
225
|
))}
|
|
197
226
|
</ul>
|
|
@@ -206,14 +235,18 @@ export function CallToAction({ block }: { block: BlockOf<"cta"> }) {
|
|
|
206
235
|
const content = (
|
|
207
236
|
<div className={t.row}>
|
|
208
237
|
<div className={t.text}>
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
<Part name="cta.heading" props={{ block }}>
|
|
239
|
+
<h2 className={t.title}>
|
|
240
|
+
<Emphasis text={d.heading} />
|
|
241
|
+
</h2>
|
|
242
|
+
{d.body ? <p className={t.body}>{d.body}</p> : null}
|
|
243
|
+
</Part>
|
|
213
244
|
</div>
|
|
214
245
|
<div className={t.actions}>
|
|
215
|
-
<
|
|
216
|
-
|
|
246
|
+
<Part name="cta.actions" props={{ block }}>
|
|
247
|
+
<Button link={d.primary} />
|
|
248
|
+
<Button link={d.secondary} variant="secondary" />
|
|
249
|
+
</Part>
|
|
217
250
|
</div>
|
|
218
251
|
</div>
|
|
219
252
|
);
|
|
@@ -225,7 +258,9 @@ export function CallToAction({ block }: { block: BlockOf<"cta"> }) {
|
|
|
225
258
|
);
|
|
226
259
|
return (
|
|
227
260
|
<section id={block.anchor} data-block="cta" {...ground("inverse")} className={cx(t.imageRoot, block.anchor && theme.section.anchored)}>
|
|
228
|
-
<
|
|
261
|
+
<Part name="cta.media" props={{ block }}>
|
|
262
|
+
<Img image={d.image} crop className={t.image} />
|
|
263
|
+
</Part>
|
|
229
264
|
<div className={t.overlay}>
|
|
230
265
|
<div className={t.overlayInner}>{content}</div>
|
|
231
266
|
</div>
|
|
@@ -237,15 +272,19 @@ export function Faq({ block }: { block: BlockOf<"faq"> }) {
|
|
|
237
272
|
const t = theme.faq;
|
|
238
273
|
return (
|
|
239
274
|
<Section block="faq" id={block.anchor}>
|
|
240
|
-
<
|
|
275
|
+
<Part name="faq.heading" props={{ block }}>
|
|
276
|
+
<SectionHeading heading={block.data.heading} />
|
|
277
|
+
</Part>
|
|
241
278
|
{block.data.display === "list" ? (
|
|
242
279
|
<dl className={t.terms}>
|
|
243
280
|
{block.data.items.map((q, i) => (
|
|
244
281
|
<div key={i} className={t.term}>
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
|
|
282
|
+
<Part name="faq.item" props={{ block, item: q, index: i }}>
|
|
283
|
+
<dt className={t.termQuestion}>{q.question}</dt>
|
|
284
|
+
<dd>
|
|
285
|
+
<Markdown className={t.termAnswer}>{q.answer}</Markdown>
|
|
286
|
+
</dd>
|
|
287
|
+
</Part>
|
|
249
288
|
</div>
|
|
250
289
|
))}
|
|
251
290
|
</dl>
|
|
@@ -253,11 +292,13 @@ export function Faq({ block }: { block: BlockOf<"faq"> }) {
|
|
|
253
292
|
<div className={t.list}>
|
|
254
293
|
{block.data.items.map((q, i) => (
|
|
255
294
|
<details key={i} className={t.item}>
|
|
256
|
-
<
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
295
|
+
<Part name="faq.item" props={{ block, item: q, index: i }}>
|
|
296
|
+
<summary className={t.question}>
|
|
297
|
+
{q.question}
|
|
298
|
+
<span aria-hidden className={t.icon} />
|
|
299
|
+
</summary>
|
|
300
|
+
<Markdown className={t.answer}>{q.answer}</Markdown>
|
|
301
|
+
</Part>
|
|
261
302
|
</details>
|
|
262
303
|
))}
|
|
263
304
|
</div>
|
|
@@ -6,6 +6,7 @@ import { Registration } from "../components/Registration";
|
|
|
6
6
|
import { VolunteerForm } from "../forms/VolunteerForm";
|
|
7
7
|
import { formatEventDate } from "../format";
|
|
8
8
|
import { overrides, type CardHeading } from "../overrides";
|
|
9
|
+
import { Part } from "../parts";
|
|
9
10
|
import { fill, Img, Section, SectionHeading, toTone } from "../ui";
|
|
10
11
|
|
|
11
12
|
export { formatEventDate };
|
|
@@ -39,12 +40,18 @@ export function EventCard({ event, heading: Heading = "h3" }: { event: HearthEve
|
|
|
39
40
|
/** Event cards (the site's own, if it replaces them). `heading` is the level of their titles; `emptyText` is what to say when there are none. */
|
|
40
41
|
export function EventList({ events, heading = "h3", emptyText }: { events: HearthEvent[]; heading?: CardHeading; emptyText?: string }) {
|
|
41
42
|
if (!events.length) return <p className={theme.eventCard.empty}>{emptyText || theme.text.noEvents}</p>;
|
|
42
|
-
const Card = overrides.EventCard
|
|
43
|
+
const Card = overrides.EventCard;
|
|
43
44
|
return (
|
|
44
45
|
<ul className={theme.eventCard.list}>
|
|
45
|
-
{events.map((e) =>
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
{events.map((e) =>
|
|
47
|
+
Card ? (
|
|
48
|
+
<Card key={e.slug} event={e} heading={heading}>
|
|
49
|
+
<EventCard event={e} heading={heading} />
|
|
50
|
+
</Card>
|
|
51
|
+
) : (
|
|
52
|
+
<EventCard key={e.slug} event={e} heading={heading} />
|
|
53
|
+
),
|
|
54
|
+
)}
|
|
48
55
|
</ul>
|
|
49
56
|
);
|
|
50
57
|
}
|
|
@@ -54,7 +61,9 @@ export function UpcomingEvents({ block }: { block: BlockOf<"upcomingEvents"> })
|
|
|
54
61
|
const href = d.collection?.path ?? "/events";
|
|
55
62
|
return (
|
|
56
63
|
<Section block="upcomingEvents" id={block.anchor}>
|
|
57
|
-
<
|
|
64
|
+
<Part name="upcomingEvents.heading" props={{ block }}>
|
|
65
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
66
|
+
</Part>
|
|
58
67
|
<EventList events={d.events} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
|
|
59
68
|
{/* A limit of 0 is every event already, and on the page it points at the link would go nowhere. */}
|
|
60
69
|
{d.events.length && d.limit !== 0 ? (
|
|
@@ -74,7 +83,9 @@ export function PastEvents({ block }: { block: BlockOf<"pastEvents"> }) {
|
|
|
74
83
|
if (!d.events.length && !d.emptyText) return null;
|
|
75
84
|
return (
|
|
76
85
|
<Section block="pastEvents" id={block.anchor} tone={toTone(theme.pastEvents.tone)}>
|
|
77
|
-
<
|
|
86
|
+
<Part name="pastEvents.heading" props={{ block }}>
|
|
87
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
88
|
+
</Part>
|
|
78
89
|
<EventList events={d.events} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
|
|
79
90
|
</Section>
|
|
80
91
|
);
|
|
@@ -94,12 +105,16 @@ export function EventSpotlight({ block }: { block: BlockOf<"eventSpotlight"> })
|
|
|
94
105
|
{e.location.name ? <p className={t.place}>{e.location.name}</p> : null}
|
|
95
106
|
{e.summary ? <p className={t.summary}>{e.summary}</p> : null}
|
|
96
107
|
<div className={t.actions}>
|
|
97
|
-
<
|
|
98
|
-
{theme.
|
|
99
|
-
|
|
108
|
+
<Part name="eventSpotlight.actions" props={{ block }}>
|
|
109
|
+
<Link href={e.path} className={theme.button.primary}>
|
|
110
|
+
{theme.text.eventDetails}
|
|
111
|
+
</Link>
|
|
112
|
+
</Part>
|
|
100
113
|
</div>
|
|
101
114
|
</div>
|
|
102
|
-
|
|
115
|
+
<Part name="eventSpotlight.media" props={{ block }}>
|
|
116
|
+
{e.image ? <Img image={e.image} crop={4 / 3} className={t.image} /> : null}
|
|
117
|
+
</Part>
|
|
103
118
|
</div>
|
|
104
119
|
</Section>
|
|
105
120
|
);
|
|
@@ -5,6 +5,7 @@ import { NotHere } from "../components/NotHere";
|
|
|
5
5
|
import { DonationForm } from "../forms/DonationForm";
|
|
6
6
|
import { money } from "../format";
|
|
7
7
|
import { overrides, type CardHeading } from "../overrides";
|
|
8
|
+
import { Part } from "../parts";
|
|
8
9
|
import { fill, Img, Section, SectionHeading, toTone } from "../ui";
|
|
9
10
|
|
|
10
11
|
export { money };
|
|
@@ -56,12 +57,18 @@ export function FundraiserCard({ fundraiser, heading: Heading = "h3" }: { fundra
|
|
|
56
57
|
/** Fundraiser cards (the site's own, if it replaces them). `heading` is the level of their titles; `emptyText` is what to say when there are none. */
|
|
57
58
|
export function FundraiserList({ fundraisers, heading = "h3", emptyText }: { fundraisers: Fundraiser[]; heading?: CardHeading; emptyText?: string }) {
|
|
58
59
|
if (!fundraisers.length) return <p className={theme.fundraiserCard.empty}>{emptyText || theme.text.noFundraisers}</p>;
|
|
59
|
-
const Card = overrides.FundraiserCard
|
|
60
|
+
const Card = overrides.FundraiserCard;
|
|
60
61
|
return (
|
|
61
62
|
<ul className={theme.fundraiserCard.list}>
|
|
62
|
-
{fundraisers.map((f) =>
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
{fundraisers.map((f) =>
|
|
64
|
+
Card ? (
|
|
65
|
+
<Card key={f.slug} fundraiser={f} heading={heading}>
|
|
66
|
+
<FundraiserCard fundraiser={f} heading={heading} />
|
|
67
|
+
</Card>
|
|
68
|
+
) : (
|
|
69
|
+
<FundraiserCard key={f.slug} fundraiser={f} heading={heading} />
|
|
70
|
+
),
|
|
71
|
+
)}
|
|
65
72
|
</ul>
|
|
66
73
|
);
|
|
67
74
|
}
|
|
@@ -70,7 +77,9 @@ export function FundraiserGrid({ block }: { block: BlockOf<"fundraiserGrid"> })
|
|
|
70
77
|
const d = block.data;
|
|
71
78
|
return (
|
|
72
79
|
<Section block="fundraiserGrid" id={block.anchor}>
|
|
73
|
-
<
|
|
80
|
+
<Part name="fundraiserGrid.heading" props={{ block }}>
|
|
81
|
+
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
82
|
+
</Part>
|
|
74
83
|
<FundraiserList fundraisers={d.fundraisers} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
|
|
75
84
|
{d.fundraisers.length ? (
|
|
76
85
|
<NotHere href="/fundraisers">
|
|
@@ -96,17 +105,21 @@ export function FundraiserSpotlight({ block }: { block: BlockOf<"fundraiserSpotl
|
|
|
96
105
|
{f.summary ? <p className={t.summary}>{f.summary}</p> : null}
|
|
97
106
|
<Progress fundraiser={f} large />
|
|
98
107
|
<p className={t.actions}>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{theme.
|
|
108
|
+
<Part name="fundraiserSpotlight.actions" props={{ block }}>
|
|
109
|
+
{f.status === "active" ? (
|
|
110
|
+
<Link href={`${f.path}#donate`} className={theme.button.primary}>
|
|
111
|
+
{theme.text.donate}
|
|
112
|
+
</Link>
|
|
113
|
+
) : null}
|
|
114
|
+
<Link href={f.path} className={t.learnMore}>
|
|
115
|
+
{theme.text.learnMore}
|
|
102
116
|
</Link>
|
|
103
|
-
|
|
104
|
-
<Link href={f.path} className={t.learnMore}>
|
|
105
|
-
{theme.text.learnMore}
|
|
106
|
-
</Link>
|
|
117
|
+
</Part>
|
|
107
118
|
</p>
|
|
108
119
|
</div>
|
|
109
|
-
|
|
120
|
+
<Part name="fundraiserSpotlight.media" props={{ block }}>
|
|
121
|
+
{f.image ? <Img image={f.image} crop={4 / 3} className={t.image} /> : null}
|
|
122
|
+
</Part>
|
|
110
123
|
</div>
|
|
111
124
|
</Section>
|
|
112
125
|
);
|
|
@@ -118,7 +131,9 @@ export function Donate({ block }: { block: BlockOf<"donate"> }) {
|
|
|
118
131
|
return (
|
|
119
132
|
<Section block="donate" id={block.anchor}>
|
|
120
133
|
<div className={t.wrap}>
|
|
121
|
-
<
|
|
134
|
+
<Part name="donate.heading" props={{ block }}>
|
|
135
|
+
<SectionHeading heading={d.heading} intro={d.intro} />
|
|
136
|
+
</Part>
|
|
122
137
|
{d.fundraiser ? <p className={t.supporting}>{fill(theme.text.supporting, { fundraiser: d.fundraiser.title })}</p> : null}
|
|
123
138
|
<DonationForm presetsCents={d.presetsCents} minCents={d.minCents} maxCents={d.maxCents} fundraiser={d.fundraiser?.slug} submitLabel={d.submitLabel} />
|
|
124
139
|
</div>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { BlockOf, ResolvedBlock } from "@the-smithy/hearth";
|
|
2
2
|
import { HearthBlocks, type BlockComponents } from "@the-smithy/hearth/react";
|
|
3
|
-
import type { ComponentType } from "react";
|
|
3
|
+
import type { ComponentType, ReactNode } from "react";
|
|
4
4
|
import { theme } from "@/theme";
|
|
5
5
|
import { EmberFormBlock } from "../forms/EmberFormBlock";
|
|
6
6
|
import { HearthForm } from "../forms/ContactForm";
|
|
7
7
|
import { overrides } from "../overrides";
|
|
8
|
+
import { Part } from "../parts";
|
|
8
9
|
import { Section, SectionHeading } from "../ui";
|
|
9
10
|
import { CollectionList } from "./collections";
|
|
10
11
|
import { CallToAction, CardGrid, Faq, Hero, MediaText, Pricing, Projects, Quote, RichText, Stats } from "./content";
|
|
@@ -18,7 +19,9 @@ function FormBlock({ block }: { block: BlockOf<"form"> }) {
|
|
|
18
19
|
return (
|
|
19
20
|
<Section block="form" id={block.anchor}>
|
|
20
21
|
<div className={theme.formBlock.wrap}>
|
|
21
|
-
<
|
|
22
|
+
<Part name="form.heading" props={{ block }}>
|
|
23
|
+
<SectionHeading heading={block.data.heading} intro={block.data.intro} />
|
|
24
|
+
</Part>
|
|
22
25
|
{/* Set to an Ember form in Hearth: Ember renders it and keeps the leads; the site's form is the fallback. */}
|
|
23
26
|
{block.data.emberFormId ? (
|
|
24
27
|
<EmberFormBlock formId={block.data.emberFormId} fallback={<HearthForm form={block.data} />} />
|
|
@@ -32,7 +35,7 @@ function FormBlock({ block }: { block: BlockOf<"form"> }) {
|
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* One component per Hearth block type. Sites style them through the theme (src/theme/index.ts); a site that has to
|
|
35
|
-
*
|
|
38
|
+
* change one adds to it, replaces one of its parts or replaces it in src/theme/overrides.tsx. Types without a component (including ones Hearth adds
|
|
36
39
|
* later) render nothing.
|
|
37
40
|
*/
|
|
38
41
|
export const blocks: BlockComponents = {
|
|
@@ -66,15 +69,21 @@ export const blocks: BlockComponents = {
|
|
|
66
69
|
custom: CustomBlock,
|
|
67
70
|
};
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
type Drawn = ComponentType<{ block: ResolvedBlock; children?: ReactNode }>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A site's block, given Hearth's own as its children, to keep, add to or leave out. Anchors from Hearth keep working
|
|
76
|
+
* around it, whether or not its code knows about them, so Hearth's inside it goes without one.
|
|
77
|
+
*/
|
|
78
|
+
const extended = (Component: Drawn, Hearths: Drawn | undefined) =>
|
|
79
|
+
function Extended({ block }: { block: ResolvedBlock }) {
|
|
80
|
+
const drawn = <Component block={block}>{Hearths ? <Hearths block={{ ...block, anchor: undefined }} /> : null}</Component>;
|
|
72
81
|
return block.anchor ? (
|
|
73
82
|
<div id={block.anchor} className={theme.section.anchored}>
|
|
74
|
-
|
|
83
|
+
{drawn}
|
|
75
84
|
</div>
|
|
76
85
|
) : (
|
|
77
|
-
|
|
86
|
+
drawn
|
|
78
87
|
);
|
|
79
88
|
};
|
|
80
89
|
|
|
@@ -87,7 +96,9 @@ let components: BlockComponents | undefined;
|
|
|
87
96
|
function allBlocks() {
|
|
88
97
|
components ??= {
|
|
89
98
|
...blocks,
|
|
90
|
-
...Object.fromEntries(
|
|
99
|
+
...Object.fromEntries(
|
|
100
|
+
Object.entries(overrides.blocks ?? {}).map(([type, component]) => [type, extended(component as Drawn, blocks[type as keyof BlockComponents] as Drawn | undefined)]),
|
|
101
|
+
),
|
|
91
102
|
};
|
|
92
103
|
return components;
|
|
93
104
|
}
|