@the-smithy/create-hearth-site 0.24.1 → 0.26.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/README.md +2 -1
- package/bin/create-hearth-site.js +1 -1
- package/package.json +4 -3
- package/template/AGENTS.md +191 -9
- package/template/CLAUDE.md +2 -124
- package/template/GEMINI.md +3 -0
- package/template/HOSTING.md +88 -0
- package/template/README.md +30 -29
- package/template/SITE.md +1 -1
- package/template/_gitignore +2 -1
- package/template/package.json +3 -2
- package/template/scripts/hearth.mjs +82 -31
- package/template/src/app/donate/thank-you/page.tsx +1 -1
- package/template/src/app/globals.css +1 -0
- package/template/src/app/layout.tsx +1 -1
- package/template/src/app/not-found.tsx +1 -1
- package/template/src/app/registered/page.tsx +1 -1
- package/template/src/hearth/base.css +66 -0
- package/template/src/hearth/blocks/collections.tsx +6 -5
- package/template/src/hearth/blocks/content.tsx +64 -28
- package/template/src/hearth/blocks/custom.tsx +9 -1
- package/template/src/hearth/blocks/events.tsx +25 -17
- package/template/src/hearth/blocks/giving.tsx +24 -20
- package/template/src/hearth/blocks/index.tsx +29 -4
- package/template/src/hearth/blocks/info.tsx +15 -13
- package/template/src/hearth/blocks/media.tsx +10 -9
- package/template/src/hearth/components/EventView.tsx +61 -29
- package/template/src/hearth/components/FundraiserView.tsx +77 -0
- package/template/src/hearth/components/NotHere.tsx +9 -0
- package/template/src/hearth/components/PageContent.tsx +23 -0
- package/template/src/hearth/components/Registration.tsx +3 -2
- package/template/src/hearth/components/Share.tsx +11 -11
- package/template/src/hearth/format.ts +5 -3
- package/template/src/hearth/forms/ContactForm.tsx +5 -3
- package/template/src/hearth/forms/DonateForm.tsx +49 -32
- package/template/src/hearth/forms/DonationForm.tsx +13 -0
- package/template/src/hearth/forms/RegistrationForm.tsx +16 -14
- package/template/src/hearth/forms/VolunteerForm.tsx +10 -8
- package/template/src/hearth/forms/fields.tsx +17 -5
- package/template/src/hearth/hearth.css +3 -47
- package/template/src/hearth/layout/MobileNav.tsx +1 -1
- package/template/src/hearth/layout/NavLink.tsx +6 -2
- package/template/src/hearth/layout/RootLayout.tsx +8 -32
- package/template/src/hearth/layout/SiteFooter.tsx +13 -6
- package/template/src/hearth/layout/SiteFrame.tsx +48 -0
- package/template/src/hearth/layout/SiteHeader.tsx +9 -4
- package/template/src/hearth/manifest.json +3 -1
- package/template/src/hearth/overrides.ts +48 -0
- package/template/src/hearth/pages/donate.tsx +24 -15
- package/template/src/hearth/pages/event.tsx +3 -2
- package/template/src/hearth/pages/events.tsx +22 -10
- package/template/src/hearth/pages/fundraiser.tsx +10 -63
- package/template/src/hearth/pages/fundraisers.tsx +21 -11
- package/template/src/hearth/pages/not-found.tsx +28 -8
- package/template/src/hearth/pages/page.tsx +9 -15
- package/template/src/hearth/pages/registered.tsx +23 -10
- package/template/src/hearth/pages/thank-you.tsx +23 -10
- package/template/src/hearth/theme/defaults.ts +125 -2
- package/template/src/hearth/ui.tsx +20 -5
- package/template/src/theme/overrides.tsx +19 -5
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import type { BlockOf, HearthEvent } from "@the-smithy/hearth";
|
|
3
3
|
import { theme } from "@/theme";
|
|
4
|
+
import { NotHere } from "../components/NotHere";
|
|
4
5
|
import { Registration } from "../components/Registration";
|
|
5
6
|
import { VolunteerForm } from "../forms/VolunteerForm";
|
|
6
7
|
import { formatEventDate } from "../format";
|
|
8
|
+
import { overrides, type CardHeading } from "../overrides";
|
|
7
9
|
import { fill, Img, Section, SectionHeading, toTone } from "../ui";
|
|
8
10
|
|
|
9
11
|
export { formatEventDate };
|
|
10
12
|
|
|
11
|
-
export function EventCard({ event }: { event: HearthEvent }) {
|
|
13
|
+
export function EventCard({ event, heading: Heading = "h3" }: { event: HearthEvent; heading?: CardHeading }) {
|
|
12
14
|
const t = theme.eventCard;
|
|
13
15
|
const cancelled = event.status === "cancelled";
|
|
14
16
|
return (
|
|
@@ -19,11 +21,11 @@ export function EventCard({ event }: { event: HearthEvent }) {
|
|
|
19
21
|
{cancelled ? <span className={t.cancelled}>{theme.text.cancelled}</span> : null}
|
|
20
22
|
<time dateTime={event.startsAt}>{formatEventDate(event)}</time>
|
|
21
23
|
</p>
|
|
22
|
-
<
|
|
24
|
+
<Heading className={t.title}>
|
|
23
25
|
<Link href={event.path} className={t.link}>
|
|
24
26
|
{event.title}
|
|
25
27
|
</Link>
|
|
26
|
-
</
|
|
28
|
+
</Heading>
|
|
27
29
|
{event.location.name ? <p className={t.place}>{event.location.name}</p> : null}
|
|
28
30
|
{event.summary ? <p className={t.summary}>{event.summary}</p> : null}
|
|
29
31
|
<div className={t.actions}>
|
|
@@ -34,12 +36,14 @@ export function EventCard({ event }: { event: HearthEvent }) {
|
|
|
34
36
|
);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
/** 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
|
+
export function EventList({ events, heading = "h3", emptyText }: { events: HearthEvent[]; heading?: CardHeading; emptyText?: string }) {
|
|
41
|
+
if (!events.length) return <p className={theme.eventCard.empty}>{emptyText || theme.text.noEvents}</p>;
|
|
42
|
+
const Card = overrides.EventCard ?? EventCard;
|
|
39
43
|
return (
|
|
40
44
|
<ul className={theme.eventCard.list}>
|
|
41
45
|
{events.map((e) => (
|
|
42
|
-
<
|
|
46
|
+
<Card key={e.slug} event={e} heading={heading} />
|
|
43
47
|
))}
|
|
44
48
|
</ul>
|
|
45
49
|
);
|
|
@@ -47,14 +51,18 @@ export function EventList({ events }: { events: HearthEvent[] }) {
|
|
|
47
51
|
|
|
48
52
|
export function UpcomingEvents({ block }: { block: BlockOf<"upcomingEvents"> }) {
|
|
49
53
|
const d = block.data;
|
|
54
|
+
const href = d.collection?.path ?? "/events";
|
|
50
55
|
return (
|
|
51
|
-
<Section block="upcomingEvents">
|
|
56
|
+
<Section block="upcomingEvents" id={block.anchor}>
|
|
52
57
|
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
53
|
-
<EventList events={d.events} />
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
<EventList events={d.events} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
|
|
59
|
+
{/* A limit of 0 is every event already, and on the page it points at the link would go nowhere. */}
|
|
60
|
+
{d.events.length && d.limit !== 0 ? (
|
|
61
|
+
<NotHere href={href}>
|
|
62
|
+
<Link href={href} className={theme.upcomingEvents.more}>
|
|
63
|
+
{d.collection ? fill(theme.text.allCollectionEvents, { collection: d.collection.name.toLowerCase() }) : theme.text.allEvents}
|
|
64
|
+
</Link>
|
|
65
|
+
</NotHere>
|
|
58
66
|
) : null}
|
|
59
67
|
</Section>
|
|
60
68
|
);
|
|
@@ -63,11 +71,11 @@ export function UpcomingEvents({ block }: { block: BlockOf<"upcomingEvents"> })
|
|
|
63
71
|
/** Events that have ended, most recent first (a talk archive). They keep their pages, so each still links to it. */
|
|
64
72
|
export function PastEvents({ block }: { block: BlockOf<"pastEvents"> }) {
|
|
65
73
|
const d = block.data;
|
|
66
|
-
if (!d.events.length) return null;
|
|
74
|
+
if (!d.events.length && !d.emptyText) return null;
|
|
67
75
|
return (
|
|
68
|
-
<Section block="pastEvents" tone={toTone(theme.pastEvents.tone)}>
|
|
76
|
+
<Section block="pastEvents" id={block.anchor} tone={toTone(theme.pastEvents.tone)}>
|
|
69
77
|
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
70
|
-
<EventList events={d.events} />
|
|
78
|
+
<EventList events={d.events} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
|
|
71
79
|
</Section>
|
|
72
80
|
);
|
|
73
81
|
}
|
|
@@ -77,7 +85,7 @@ export function EventSpotlight({ block }: { block: BlockOf<"eventSpotlight"> })
|
|
|
77
85
|
const t = theme.eventSpotlight;
|
|
78
86
|
if (!e) return null;
|
|
79
87
|
return (
|
|
80
|
-
<Section block="eventSpotlight" tone={toTone(t.tone)}>
|
|
88
|
+
<Section block="eventSpotlight" id={block.anchor} tone={toTone(t.tone)}>
|
|
81
89
|
<div className={t.grid}>
|
|
82
90
|
<div>
|
|
83
91
|
{block.data.heading ? <p className={t.label}>{block.data.heading}</p> : null}
|
|
@@ -110,7 +118,7 @@ export function VolunteerSignup({ block }: { block: BlockOf<"volunteerSignup"> }
|
|
|
110
118
|
const t = theme.volunteerSignup;
|
|
111
119
|
const intro = block.data.intro || v.intro;
|
|
112
120
|
return (
|
|
113
|
-
<Section block="volunteerSignup" id={volunteerAnchor(e.slug)}>
|
|
121
|
+
<Section block="volunteerSignup" id={block.anchor ?? volunteerAnchor(e.slug)}>
|
|
114
122
|
<div className={t.wrap}>
|
|
115
123
|
<h2 className={t.title}>{block.data.heading || theme.text.volunteer}</h2>
|
|
116
124
|
{block.data.eventSlug ? (
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import type { BlockOf, Fundraiser } from "@the-smithy/hearth";
|
|
3
3
|
import { theme } from "@/theme";
|
|
4
|
-
import {
|
|
4
|
+
import { NotHere } from "../components/NotHere";
|
|
5
|
+
import { DonationForm } from "../forms/DonationForm";
|
|
5
6
|
import { money } from "../format";
|
|
7
|
+
import { overrides, type CardHeading } from "../overrides";
|
|
6
8
|
import { fill, Img, Section, SectionHeading, toTone } from "../ui";
|
|
7
9
|
|
|
8
10
|
export { money };
|
|
@@ -16,34 +18,32 @@ export function Progress({ fundraiser, large }: { fundraiser: Fundraiser; large?
|
|
|
16
18
|
<div className={t.root}>
|
|
17
19
|
<p className={t.amounts} data-size={large ? "large" : undefined}>
|
|
18
20
|
<span className={t.raised}>{money(raisedCents)}</span>
|
|
19
|
-
<span className={t.goal}>{goalCents ?
|
|
21
|
+
<span className={t.goal}> {goalCents ? fill(theme.text.raisedOfGoal, { goal: money(goalCents) }) : theme.text.raised}</span>
|
|
20
22
|
</p>
|
|
21
23
|
{pct !== null ? (
|
|
22
|
-
<div className={t.bar} role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} aria-label=
|
|
24
|
+
<div className={t.bar} role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} aria-label={theme.text.progressLabel}>
|
|
23
25
|
<div className={t.fill} style={{ width: `${pct}%` }} />
|
|
24
26
|
</div>
|
|
25
27
|
) : null}
|
|
26
28
|
{donorCount ? (
|
|
27
|
-
<p className={t.donors}>
|
|
28
|
-
{donorCount} online donation{donorCount === 1 ? "" : "s"}
|
|
29
|
-
</p>
|
|
29
|
+
<p className={t.donors}>{fill(donorCount === 1 ? theme.text.oneDonation : theme.text.donations, { count: String(donorCount) })}</p>
|
|
30
30
|
) : null}
|
|
31
31
|
</div>
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function FundraiserCard({ fundraiser }: { fundraiser: Fundraiser }) {
|
|
35
|
+
export function FundraiserCard({ fundraiser, heading: Heading = "h3" }: { fundraiser: Fundraiser; heading?: CardHeading }) {
|
|
36
36
|
const t = theme.fundraiserCard;
|
|
37
37
|
return (
|
|
38
38
|
<li className={t.card} data-status={fundraiser.status}>
|
|
39
39
|
{fundraiser.image ? <Img image={fundraiser.image} crop={16 / 9} className={t.image} /> : null}
|
|
40
40
|
<div className={t.body}>
|
|
41
41
|
{fundraiser.status === "completed" ? <p className={t.completed}>{theme.text.completed}</p> : null}
|
|
42
|
-
<
|
|
42
|
+
<Heading className={t.title}>
|
|
43
43
|
<Link href={fundraiser.path} className={t.link}>
|
|
44
44
|
{fundraiser.title}
|
|
45
45
|
</Link>
|
|
46
|
-
</
|
|
46
|
+
</Heading>
|
|
47
47
|
{fundraiser.summary ? <p className={t.summary}>{fundraiser.summary}</p> : null}
|
|
48
48
|
<div className={t.progress}>
|
|
49
49
|
<Progress fundraiser={fundraiser} />
|
|
@@ -53,12 +53,14 @@ export function FundraiserCard({ fundraiser }: { fundraiser: Fundraiser }) {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
/** 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
|
+
export function FundraiserList({ fundraisers, heading = "h3", emptyText }: { fundraisers: Fundraiser[]; heading?: CardHeading; emptyText?: string }) {
|
|
58
|
+
if (!fundraisers.length) return <p className={theme.fundraiserCard.empty}>{emptyText || theme.text.noFundraisers}</p>;
|
|
59
|
+
const Card = overrides.FundraiserCard ?? FundraiserCard;
|
|
58
60
|
return (
|
|
59
61
|
<ul className={theme.fundraiserCard.list}>
|
|
60
62
|
{fundraisers.map((f) => (
|
|
61
|
-
<
|
|
63
|
+
<Card key={f.slug} fundraiser={f} heading={heading} />
|
|
62
64
|
))}
|
|
63
65
|
</ul>
|
|
64
66
|
);
|
|
@@ -67,13 +69,15 @@ export function FundraiserList({ fundraisers }: { fundraisers: Fundraiser[] }) {
|
|
|
67
69
|
export function FundraiserGrid({ block }: { block: BlockOf<"fundraiserGrid"> }) {
|
|
68
70
|
const d = block.data;
|
|
69
71
|
return (
|
|
70
|
-
<Section block="fundraiserGrid">
|
|
72
|
+
<Section block="fundraiserGrid" id={block.anchor}>
|
|
71
73
|
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
72
|
-
<FundraiserList fundraisers={d.fundraisers} />
|
|
74
|
+
<FundraiserList fundraisers={d.fundraisers} heading={d.heading ? "h3" : "h2"} emptyText={d.emptyText} />
|
|
73
75
|
{d.fundraisers.length ? (
|
|
74
|
-
<
|
|
75
|
-
{theme.
|
|
76
|
-
|
|
76
|
+
<NotHere href="/fundraisers">
|
|
77
|
+
<Link href="/fundraisers" className={theme.fundraiserGrid.more}>
|
|
78
|
+
{theme.text.allFundraisers}
|
|
79
|
+
</Link>
|
|
80
|
+
</NotHere>
|
|
77
81
|
) : null}
|
|
78
82
|
</Section>
|
|
79
83
|
);
|
|
@@ -84,7 +88,7 @@ export function FundraiserSpotlight({ block }: { block: BlockOf<"fundraiserSpotl
|
|
|
84
88
|
const t = theme.fundraiserSpotlight;
|
|
85
89
|
if (!f) return null;
|
|
86
90
|
return (
|
|
87
|
-
<Section block="fundraiserSpotlight" tone={toTone(t.tone)}>
|
|
91
|
+
<Section block="fundraiserSpotlight" id={block.anchor} tone={toTone(t.tone)}>
|
|
88
92
|
<div className={t.grid}>
|
|
89
93
|
<div className={t.details}>
|
|
90
94
|
{block.data.heading ? <p className={t.label}>{block.data.heading}</p> : null}
|
|
@@ -112,11 +116,11 @@ export function Donate({ block }: { block: BlockOf<"donate"> }) {
|
|
|
112
116
|
const d = block.data;
|
|
113
117
|
const t = theme.donate;
|
|
114
118
|
return (
|
|
115
|
-
<Section block="donate">
|
|
119
|
+
<Section block="donate" id={block.anchor}>
|
|
116
120
|
<div className={t.wrap}>
|
|
117
121
|
<SectionHeading heading={d.heading} intro={d.intro} />
|
|
118
122
|
{d.fundraiser ? <p className={t.supporting}>{fill(theme.text.supporting, { fundraiser: d.fundraiser.title })}</p> : null}
|
|
119
|
-
<
|
|
123
|
+
<DonationForm presetsCents={d.presetsCents} minCents={d.minCents} maxCents={d.maxCents} fundraiser={d.fundraiser?.slug} submitLabel={d.submitLabel} />
|
|
120
124
|
</div>
|
|
121
125
|
</Section>
|
|
122
126
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
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
4
|
import { theme } from "@/theme";
|
|
4
|
-
import { blocks as overrides } from "@/theme/overrides";
|
|
5
5
|
import { EmberFormBlock } from "../forms/EmberFormBlock";
|
|
6
6
|
import { HearthForm } from "../forms/ContactForm";
|
|
7
|
+
import { overrides } from "../overrides";
|
|
7
8
|
import { Section, SectionHeading } from "../ui";
|
|
8
9
|
import { CollectionList } from "./collections";
|
|
9
10
|
import { CallToAction, CardGrid, Faq, Hero, MediaText, Pricing, Projects, Quote, RichText, Stats } from "./content";
|
|
@@ -15,7 +16,7 @@ import { Embed, Gallery, Logos, People, Video } from "./media";
|
|
|
15
16
|
|
|
16
17
|
function FormBlock({ block }: { block: BlockOf<"form"> }) {
|
|
17
18
|
return (
|
|
18
|
-
<Section block="form">
|
|
19
|
+
<Section block="form" id={block.anchor}>
|
|
19
20
|
<div className={theme.formBlock.wrap}>
|
|
20
21
|
<SectionHeading heading={block.data.heading} intro={block.data.intro} />
|
|
21
22
|
{/* Set to an Ember form in Hearth: Ember renders it and keeps the leads; the site's form is the fallback. */}
|
|
@@ -65,8 +66,32 @@ export const blocks: BlockComponents = {
|
|
|
65
66
|
custom: CustomBlock,
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
/** A site's replaced block keeps working with anchors from Hearth, whether or not its code knows about them. */
|
|
70
|
+
const anchored = (Component: ComponentType<{ block: ResolvedBlock }>) =>
|
|
71
|
+
function Anchored({ block }: { block: ResolvedBlock }) {
|
|
72
|
+
return block.anchor ? (
|
|
73
|
+
<div id={block.anchor} className={theme.section.anchored}>
|
|
74
|
+
<Component block={block} />
|
|
75
|
+
</div>
|
|
76
|
+
) : (
|
|
77
|
+
<Component block={block} />
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let components: BlockComponents | undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hearth's blocks with the site's in their place. Made on first use, not as this file loads: an override in
|
|
85
|
+
* src/theme/overrides.tsx may import Blocks from here, and then that file hasn't finished loading at this point.
|
|
86
|
+
*/
|
|
87
|
+
function allBlocks() {
|
|
88
|
+
components ??= {
|
|
89
|
+
...blocks,
|
|
90
|
+
...Object.fromEntries(Object.entries(overrides.blocks ?? {}).map(([type, component]) => [type, anchored(component as ComponentType<{ block: ResolvedBlock }>)])),
|
|
91
|
+
};
|
|
92
|
+
return components;
|
|
93
|
+
}
|
|
69
94
|
|
|
70
95
|
export function Blocks({ blocks: list }: { blocks: ResolvedBlock[] }) {
|
|
71
|
-
return <HearthBlocks blocks={list} components={
|
|
96
|
+
return <HearthBlocks blocks={list} components={allBlocks()} fallback={() => null} />;
|
|
72
97
|
}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import { directionsUrl, pinned, type BlockOf } from "@the-smithy/hearth";
|
|
2
2
|
import { theme } from "@/theme";
|
|
3
|
-
import { Markdown, Section, SectionHeading, SmartLink } from "../ui";
|
|
3
|
+
import { ExternalLink, fill, Markdown, Section, SectionHeading, SmartLink } from "../ui";
|
|
4
4
|
import { OpenMap } from "./OpenMap";
|
|
5
5
|
|
|
6
6
|
export function Location({ block }: { block: BlockOf<"location"> }) {
|
|
7
7
|
const d = block.data;
|
|
8
8
|
const t = theme.location;
|
|
9
|
+
const Title = d.heading ? "h3" : "h2";
|
|
9
10
|
return (
|
|
10
|
-
<Section block="location">
|
|
11
|
+
<Section block="location" id={block.anchor}>
|
|
11
12
|
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} />
|
|
12
13
|
<div className={t.grid}>
|
|
13
14
|
<div className={t.details}>
|
|
14
|
-
<
|
|
15
|
+
<Title className={t.name}>{d.name}</Title>
|
|
15
16
|
<address className={t.address}>{d.address}</address>
|
|
16
17
|
{d.hours ? <p className={t.hours}>{d.hours}</p> : null}
|
|
17
18
|
{d.notes ? <Markdown className={t.notes}>{d.notes}</Markdown> : null}
|
|
18
|
-
<
|
|
19
|
+
<ExternalLink href={directionsUrl(d)} rel="noopener" className={t.directions}>
|
|
19
20
|
{theme.text.directions}
|
|
20
|
-
</
|
|
21
|
+
</ExternalLink>
|
|
21
22
|
</div>
|
|
22
23
|
{/* 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={
|
|
24
|
+
{d.showMap && d.position ? <OpenMap places={[{ name: d.name, address: d.address, position: d.position }]} label={fill(theme.text.mapOf, { name: d.name })} className={t.map} /> : null}
|
|
24
25
|
</div>
|
|
25
26
|
</Section>
|
|
26
27
|
);
|
|
@@ -31,23 +32,24 @@ export function Locations({ block }: { block: BlockOf<"locations"> }) {
|
|
|
31
32
|
const d = block.data;
|
|
32
33
|
const t = theme.locations;
|
|
33
34
|
const onMap = d.showMap ? pinned(d.items) : [];
|
|
35
|
+
const Title = d.heading ? "h3" : "h2";
|
|
34
36
|
return (
|
|
35
|
-
<Section block="locations">
|
|
37
|
+
<Section block="locations" id={block.anchor}>
|
|
36
38
|
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
37
39
|
<div className={t.grid} data-map={onMap.length ? "" : undefined}>
|
|
38
|
-
{onMap.length ? <OpenMap places={onMap} label={d.heading ?
|
|
40
|
+
{onMap.length ? <OpenMap places={onMap} label={d.heading ? fill(theme.text.mapHeading, { heading: d.heading }) : theme.text.mapPlaces} className={t.map} /> : null}
|
|
39
41
|
<ul className={onMap.length ? t.list : t.listNoMap}>
|
|
40
42
|
{d.items.map((place, i) => (
|
|
41
43
|
<li key={i} className={t.item}>
|
|
42
|
-
<
|
|
44
|
+
<Title className={t.name}>{place.name}</Title>
|
|
43
45
|
<address className={t.address}>{place.address}</address>
|
|
44
46
|
{place.hours ? <p className={t.hours}>{place.hours}</p> : null}
|
|
45
47
|
{place.notes ? <p className={t.notes}>{place.notes}</p> : null}
|
|
46
48
|
<p className={t.links}>
|
|
47
|
-
<
|
|
49
|
+
<ExternalLink href={directionsUrl(place)} rel="noopener" className={t.link}>
|
|
48
50
|
{theme.text.directions}
|
|
49
|
-
<span className="sr-only">
|
|
50
|
-
</
|
|
51
|
+
<span className="sr-only"> {fill(theme.text.directionsTo, { name: place.name })}</span>
|
|
52
|
+
</ExternalLink>
|
|
51
53
|
{place.link ? (
|
|
52
54
|
<SmartLink href={place.link.href} className={t.link}>
|
|
53
55
|
{place.link.label}
|
|
@@ -66,7 +68,7 @@ export function Schedule({ block }: { block: BlockOf<"schedule"> }) {
|
|
|
66
68
|
const d = block.data;
|
|
67
69
|
const t = theme.schedule;
|
|
68
70
|
return (
|
|
69
|
-
<Section block="schedule">
|
|
71
|
+
<Section block="schedule" id={block.anchor}>
|
|
70
72
|
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
71
73
|
<ol className={t.list}>
|
|
72
74
|
{d.items.map((item, i) => (
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { videoEmbedUrl, type BlockOf } from "@the-smithy/hearth";
|
|
2
2
|
import { HearthEmbed } from "@the-smithy/hearth/react";
|
|
3
3
|
import { theme } from "@/theme";
|
|
4
|
-
import { Img, Section, SectionHeading } from "../ui";
|
|
4
|
+
import { ExternalLink, Img, Section, SectionHeading } from "../ui";
|
|
5
5
|
|
|
6
6
|
export function Gallery({ block }: { block: BlockOf<"gallery"> }) {
|
|
7
7
|
const d = block.data;
|
|
8
8
|
const t = theme.gallery;
|
|
9
9
|
return (
|
|
10
|
-
<Section block="gallery">
|
|
10
|
+
<Section block="gallery" id={block.anchor}>
|
|
11
11
|
<SectionHeading heading={d.heading} intro={d.intro} />
|
|
12
12
|
<ul className={t.list} data-columns={d.columns}>
|
|
13
13
|
{d.images.map((item, i) => (
|
|
@@ -29,7 +29,7 @@ export function Video({ block }: { block: BlockOf<"video"> }) {
|
|
|
29
29
|
const src = videoEmbedUrl(d.url);
|
|
30
30
|
if (!src) return null;
|
|
31
31
|
return (
|
|
32
|
-
<Section block="video">
|
|
32
|
+
<Section block="video" id={block.anchor}>
|
|
33
33
|
<SectionHeading heading={d.heading} />
|
|
34
34
|
<figure className={t.figure}>
|
|
35
35
|
<div className={t.frame}>
|
|
@@ -52,7 +52,7 @@ export function Video({ block }: { block: BlockOf<"video"> }) {
|
|
|
52
52
|
export function Embed({ block }: { block: BlockOf<"embed"> }) {
|
|
53
53
|
const t = theme.embed;
|
|
54
54
|
return (
|
|
55
|
-
<Section block="embed">
|
|
55
|
+
<Section block="embed" id={block.anchor}>
|
|
56
56
|
<figure className={t.figure} data-width={block.data.width}>
|
|
57
57
|
<HearthEmbed block={block} className={t.frame} />
|
|
58
58
|
{block.data.caption ? <figcaption className={t.caption}>{block.data.caption}</figcaption> : null}
|
|
@@ -64,15 +64,16 @@ export function Embed({ block }: { block: BlockOf<"embed"> }) {
|
|
|
64
64
|
export function People({ block }: { block: BlockOf<"people"> }) {
|
|
65
65
|
const d = block.data;
|
|
66
66
|
const t = theme.people;
|
|
67
|
+
const Title = d.heading ? "h3" : "h2";
|
|
67
68
|
return (
|
|
68
|
-
<Section block="people">
|
|
69
|
+
<Section block="people" id={block.anchor}>
|
|
69
70
|
<SectionHeading eyebrow={d.eyebrow} heading={d.heading} intro={d.intro} />
|
|
70
71
|
<ul className={t.list}>
|
|
71
72
|
{d.people.map((p, i) => (
|
|
72
73
|
<li key={i} className={t.person}>
|
|
73
74
|
{p.image ? <Img image={p.image} crop={1} className={t.image} /> : null}
|
|
74
75
|
<div>
|
|
75
|
-
<
|
|
76
|
+
<Title className={t.name}>{p.name}</Title>
|
|
76
77
|
{p.role ? <p className={t.role}>{p.role}</p> : null}
|
|
77
78
|
</div>
|
|
78
79
|
{p.bio ? <p className={t.bio}>{p.bio}</p> : null}
|
|
@@ -87,7 +88,7 @@ export function Logos({ block }: { block: BlockOf<"logos"> }) {
|
|
|
87
88
|
const d = block.data;
|
|
88
89
|
const t = theme.logos;
|
|
89
90
|
return (
|
|
90
|
-
<Section block="logos">
|
|
91
|
+
<Section block="logos" id={block.anchor}>
|
|
91
92
|
<SectionHeading heading={d.heading} intro={d.intro} />
|
|
92
93
|
<ul className={t.list}>
|
|
93
94
|
{d.logos.map((l, i) => {
|
|
@@ -95,9 +96,9 @@ export function Logos({ block }: { block: BlockOf<"logos"> }) {
|
|
|
95
96
|
return (
|
|
96
97
|
<li key={i}>
|
|
97
98
|
{l.url ? (
|
|
98
|
-
<
|
|
99
|
+
<ExternalLink href={l.url} rel="noopener" className={t.link}>
|
|
99
100
|
{mark}
|
|
100
|
-
</
|
|
101
|
+
</ExternalLink>
|
|
101
102
|
) : (
|
|
102
103
|
mark
|
|
103
104
|
)}
|
|
@@ -8,18 +8,65 @@ import { hearthNext } from "../client";
|
|
|
8
8
|
import { formatEventDate } from "../format";
|
|
9
9
|
import { RegistrationForm } from "../forms/RegistrationForm";
|
|
10
10
|
import { VolunteerForm } from "../forms/VolunteerForm";
|
|
11
|
+
import { overrides } from "../overrides";
|
|
11
12
|
import { siteUrl } from "../site";
|
|
12
13
|
import { fill, Img } from "../ui";
|
|
13
14
|
import { Registration } from "./Registration";
|
|
14
15
|
import { Share } from "./Share";
|
|
15
16
|
|
|
17
|
+
/** An event's page: its structured data, and the site's EventPage override or Hearth's own. Used at /events/… and /<collection>/…. */
|
|
18
|
+
export function EventView({ event }: { event: HearthEventDetail }) {
|
|
19
|
+
const Page = overrides.EventPage ?? EventContent;
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<JsonLd data={event.meta.jsonLd} />
|
|
23
|
+
<Page event={event} />
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Registration on the event's page (sites with Tickets), while it's open. For an EventPage override to reuse. */
|
|
29
|
+
export function EventRegisterSection({ event }: { event: HearthEventDetail }) {
|
|
30
|
+
const t = theme.event;
|
|
31
|
+
if (!event.registration?.open) return null;
|
|
32
|
+
return (
|
|
33
|
+
<section id="register" className={t.section}>
|
|
34
|
+
<div className={t.sectionInner}>
|
|
35
|
+
<h2 className={t.sectionTitle}>{theme.text.register}</h2>
|
|
36
|
+
{event.registration.fundraiser ? <p className={t.sectionNote}>{fill(theme.text.ticketsSupport, { fundraiser: event.registration.fundraiser.title })}</p> : null}
|
|
37
|
+
<div className={t.form}>
|
|
38
|
+
<RegistrationForm slug={event.slug} tickets={event.registration.tickets} />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Volunteer sign-up on the event's page, while it's open. For an EventPage override to reuse; leave it out if the event's blocks have a Volunteer sign-up. */
|
|
46
|
+
export function EventVolunteerSection({ event }: { event: HearthEventDetail }) {
|
|
47
|
+
const t = theme.event;
|
|
48
|
+
if (!event.volunteering?.open) return null;
|
|
49
|
+
return (
|
|
50
|
+
<section id="volunteer" className={t.section}>
|
|
51
|
+
<div className={t.sectionInner}>
|
|
52
|
+
<h2 className={t.sectionTitle}>{theme.text.volunteer}</h2>
|
|
53
|
+
{event.volunteering.intro ? <p className={t.sectionIntro}>{event.volunteering.intro}</p> : null}
|
|
54
|
+
{event.volunteering.fundraiser ? <p className={t.sectionNote}>{fill(theme.text.supports, { fundraiser: event.volunteering.fundraiser.title })}</p> : null}
|
|
55
|
+
<div className={t.form}>
|
|
56
|
+
<VolunteerForm slug={event.slug} timezone={event.timezone} volunteering={event.volunteering} />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</section>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
16
63
|
/**
|
|
17
|
-
*
|
|
64
|
+
* Hearth's own event page: header with date, place and registration, then its blocks.
|
|
18
65
|
* When the event's blocks start with a hero, the hero is the header (its heading is the page's only h1, the event's
|
|
19
66
|
* name its eyebrow, the event's image its picture if it has none) and the details sit in a band under it.
|
|
20
67
|
* Volunteer sign-up goes at the bottom, unless a Volunteer sign-up block for the event is placed on the page.
|
|
21
68
|
*/
|
|
22
|
-
export async function
|
|
69
|
+
export async function EventContent({ event }: { event: HearthEventDetail }) {
|
|
23
70
|
const t = theme.event;
|
|
24
71
|
const site = await hearthNext.getSite();
|
|
25
72
|
const base = siteUrl(site);
|
|
@@ -27,7 +74,7 @@ export async function EventView({ event }: { event: HearthEventDetail }) {
|
|
|
27
74
|
const [first, ...rest] = event.blocks;
|
|
28
75
|
const hero = first?.type === "hero" ? (first as BlockOf<"hero">) : null;
|
|
29
76
|
// A Volunteer sign-up block for this event puts the form where the editor placed it, instead of at the bottom.
|
|
30
|
-
const volunteerBlock = event.blocks.
|
|
77
|
+
const volunteerBlock = event.blocks.find((b) => b.type === "volunteerSignup" && (b as BlockOf<"volunteerSignup">).data.event.slug === event.slug);
|
|
31
78
|
const online = event.attendance === "online" ? theme.text.online : event.attendance === "mixed" ? theme.text.onlineAndInPerson : null;
|
|
32
79
|
|
|
33
80
|
const cancelled = event.status === "cancelled" ? <p className={t.cancelled}>{theme.text.cancelledEvent}</p> : null;
|
|
@@ -49,17 +96,23 @@ export async function EventView({ event }: { event: HearthEventDetail }) {
|
|
|
49
96
|
<div className={t.actions}>
|
|
50
97
|
<Registration event={event} />
|
|
51
98
|
{event.volunteering?.open ? (
|
|
52
|
-
<a href={volunteerBlock ? `#${volunteerAnchor(event.slug)}` : "#volunteer"} className={t.volunteer}>
|
|
99
|
+
<a href={volunteerBlock ? `#${volunteerBlock.anchor ?? volunteerAnchor(event.slug)}` : "#volunteer"} className={t.volunteer}>
|
|
53
100
|
{theme.text.volunteer}
|
|
54
101
|
</a>
|
|
55
102
|
) : null}
|
|
56
|
-
{base && event.status !== "cancelled" ?
|
|
103
|
+
{base && event.status !== "cancelled" ? (
|
|
104
|
+
<Share
|
|
105
|
+
title={event.title}
|
|
106
|
+
url={`${base}${event.path}`}
|
|
107
|
+
message={fill(theme.text.shareEventMessage, { title: event.title, date: formatEventDate(event), url: `${base}${event.path}` })}
|
|
108
|
+
heading={fill(theme.text.shareEventHeading, { title: event.title })}
|
|
109
|
+
/>
|
|
110
|
+
) : null}
|
|
57
111
|
</div>
|
|
58
112
|
);
|
|
59
113
|
|
|
60
114
|
return (
|
|
61
115
|
<>
|
|
62
|
-
<JsonLd data={event.meta.jsonLd} />
|
|
63
116
|
{hero ? (
|
|
64
117
|
<>
|
|
65
118
|
<Hero block={{ ...hero, data: { ...hero.data, eyebrow: hero.data.eyebrow ?? event.title, image: hero.data.image ?? event.image ?? undefined } }} />
|
|
@@ -92,29 +145,8 @@ export async function EventView({ event }: { event: HearthEventDetail }) {
|
|
|
92
145
|
</section>
|
|
93
146
|
)}
|
|
94
147
|
<Blocks blocks={hero ? rest : event.blocks} />
|
|
95
|
-
{event
|
|
96
|
-
|
|
97
|
-
<div className={t.sectionInner}>
|
|
98
|
-
<h2 className={t.sectionTitle}>{theme.text.register}</h2>
|
|
99
|
-
{event.registration.fundraiser ? <p className={t.sectionNote}>{fill(theme.text.ticketsSupport, { fundraiser: event.registration.fundraiser.title })}</p> : null}
|
|
100
|
-
<div className={t.form}>
|
|
101
|
-
<RegistrationForm slug={event.slug} tickets={event.registration.tickets} />
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</section>
|
|
105
|
-
) : null}
|
|
106
|
-
{event.volunteering?.open && !volunteerBlock ? (
|
|
107
|
-
<section id="volunteer" className={t.section}>
|
|
108
|
-
<div className={t.sectionInner}>
|
|
109
|
-
<h2 className={t.sectionTitle}>{theme.text.volunteer}</h2>
|
|
110
|
-
{event.volunteering.intro ? <p className={t.sectionIntro}>{event.volunteering.intro}</p> : null}
|
|
111
|
-
{event.volunteering.fundraiser ? <p className={t.sectionNote}>{fill(theme.text.supports, { fundraiser: event.volunteering.fundraiser.title })}</p> : null}
|
|
112
|
-
<div className={t.form}>
|
|
113
|
-
<VolunteerForm slug={event.slug} timezone={event.timezone} volunteering={event.volunteering} />
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</section>
|
|
117
|
-
) : null}
|
|
148
|
+
<EventRegisterSection event={event} />
|
|
149
|
+
{volunteerBlock ? null : <EventVolunteerSection event={event} />}
|
|
118
150
|
</>
|
|
119
151
|
);
|
|
120
152
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { FundraiserDetail } from "@the-smithy/hearth";
|
|
2
|
+
import { JsonLd } from "@the-smithy/hearth/react";
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { Blocks } from "../blocks";
|
|
5
|
+
import { Progress } from "../blocks/giving";
|
|
6
|
+
import { hearthNext } from "../client";
|
|
7
|
+
import { DonationForm } from "../forms/DonationForm";
|
|
8
|
+
import { money } from "../format";
|
|
9
|
+
import { overrides } from "../overrides";
|
|
10
|
+
import { siteUrl } from "../site";
|
|
11
|
+
import { fill, Img, Section } from "../ui";
|
|
12
|
+
import { Share } from "./Share";
|
|
13
|
+
|
|
14
|
+
/** A fundraiser's page: its structured data, and the site's FundraiserPage override or Hearth's own. */
|
|
15
|
+
export function FundraiserView({ fundraiser }: { fundraiser: FundraiserDetail }) {
|
|
16
|
+
const Page = overrides.FundraiserPage ?? FundraiserContent;
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<JsonLd data={fundraiser.meta.jsonLd} />
|
|
20
|
+
<Page fundraiser={fundraiser} />
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Hearth's own fundraiser page: progress, Donate and Share, then its story blocks and the donation form. */
|
|
26
|
+
export async function FundraiserContent({ fundraiser: f }: { fundraiser: FundraiserDetail }) {
|
|
27
|
+
const site = await hearthNext.getSite();
|
|
28
|
+
const giving = site.settings.giving;
|
|
29
|
+
const t = theme.fundraiser;
|
|
30
|
+
const base = siteUrl(site);
|
|
31
|
+
const url = base ? `${base}${f.path}` : null;
|
|
32
|
+
const active = f.status === "active";
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<section className={t.header}>
|
|
37
|
+
<div className={t.headerInner}>
|
|
38
|
+
<div className={t.details}>
|
|
39
|
+
<p className={t.label}>{active ? theme.text.fundraiserLabel : theme.text.completedFundraiserLabel}</p>
|
|
40
|
+
<h1 className={t.title}>{f.title}</h1>
|
|
41
|
+
{f.summary ? <p className={t.summary}>{f.summary}</p> : null}
|
|
42
|
+
<Progress fundraiser={f} large />
|
|
43
|
+
{active ? (
|
|
44
|
+
<div className={t.actions}>
|
|
45
|
+
<a href="#donate" className={theme.button.primary}>
|
|
46
|
+
{theme.text.donate}
|
|
47
|
+
</a>
|
|
48
|
+
{url ? (
|
|
49
|
+
<Share
|
|
50
|
+
title={f.title}
|
|
51
|
+
url={url}
|
|
52
|
+
message={
|
|
53
|
+
f.goalCents
|
|
54
|
+
? fill(theme.text.shareFundraiserGoalMessage, { title: f.title, raised: money(f.raisedCents), goal: money(f.goalCents), url })
|
|
55
|
+
: fill(theme.text.shareFundraiserMessage, { title: f.title, url })
|
|
56
|
+
}
|
|
57
|
+
heading={theme.text.shareFundraiserHeading}
|
|
58
|
+
/>
|
|
59
|
+
) : null}
|
|
60
|
+
</div>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
{f.image ? <Img image={f.image} crop={4 / 3} className={t.image} /> : null}
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
<Blocks blocks={f.blocks} />
|
|
67
|
+
{active ? (
|
|
68
|
+
<Section block="fundraiserDonate">
|
|
69
|
+
<div className={t.give}>
|
|
70
|
+
<h2 className={t.giveTitle}>{fill(theme.text.giveTo, { fundraiser: f.title })}</h2>
|
|
71
|
+
<DonationForm presetsCents={giving.presetsCents} minCents={giving.minCents} maxCents={giving.maxCents} fundraiser={f.slug} />
|
|
72
|
+
</div>
|
|
73
|
+
</Section>
|
|
74
|
+
) : null}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { usePathname } from "next/navigation";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
/** Draws its children everywhere but at `href`: a link to all of something isn't needed on the page it points at. */
|
|
7
|
+
export function NotHere({ href, children }: { href: string; children: ReactNode }) {
|
|
8
|
+
return usePathname() === href ? null : children;
|
|
9
|
+
}
|