@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
|
@@ -1,35 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { DonateForm } from "@/components/DonateForm";
|
|
4
|
-
import { Section } from "@/components/ui";
|
|
5
|
-
import { hearthNext } from "@/lib/hearth";
|
|
6
|
-
import { hasFeature } from "@/lib/site";
|
|
7
|
-
|
|
8
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
9
|
-
const site = await hearthNext.getSite();
|
|
10
|
-
return {
|
|
11
|
-
title: "Donate",
|
|
12
|
-
description: `Support ${site.name} with a donation.`,
|
|
13
|
-
alternates: { canonical: "/donate" },
|
|
14
|
-
openGraph: { title: "Donate", url: "/donate" },
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A general donation page (sites with Giving). Ask staff to reserve "donate" on the site's Plan card in Hearth so
|
|
20
|
-
* no page takes this address. For a page built in Hearth instead, use the Donation form block there.
|
|
21
|
-
*/
|
|
22
|
-
export default async function DonatePage() {
|
|
23
|
-
const site = await hearthNext.getSite();
|
|
24
|
-
if (!hasFeature(site, "giving")) notFound();
|
|
25
|
-
const giving = site.settings.giving;
|
|
26
|
-
return (
|
|
27
|
-
<Section>
|
|
28
|
-
<div className="max-w-2xl">
|
|
29
|
-
<h1 className="mb-4 text-[clamp(32px,4vw,48px)] font-bold tracking-tight">Donate</h1>
|
|
30
|
-
<p className="mb-10 text-lg opacity-85">Your gift supports {giving.legalName || site.name}.</p>
|
|
31
|
-
<DonateForm presetsCents={giving.presetsCents} minCents={giving.minCents} maxCents={giving.maxCents} />
|
|
32
|
-
</div>
|
|
33
|
-
</Section>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/donate.tsx.
|
|
2
|
+
export { default, generateMetadata } from "@/hearth/pages/donate";
|
|
@@ -1,22 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { Section } from "@/components/ui";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = { title: "Thank you", robots: { index: false, follow: false } };
|
|
6
|
-
|
|
7
|
-
/** Where the payment page sends donors back. The receipt is emailed once the payment completes. */
|
|
8
|
-
export default function ThankYouPage() {
|
|
9
|
-
return (
|
|
10
|
-
<Section>
|
|
11
|
-
<div className="max-w-2xl">
|
|
12
|
-
<h1 className="mb-4 text-[clamp(32px,4vw,48px)] font-bold tracking-tight">Thank you</h1>
|
|
13
|
-
<p className="text-lg opacity-85">Your gift makes a difference. If you gave your email, a receipt is on its way.</p>
|
|
14
|
-
<p className="mt-8">
|
|
15
|
-
<Link href="/" className="font-semibold underline underline-offset-4">
|
|
16
|
-
Back to the homepage
|
|
17
|
-
</Link>
|
|
18
|
-
</p>
|
|
19
|
-
</div>
|
|
20
|
-
</Section>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/thank-you.tsx.
|
|
2
|
+
export { default, metadata } from "@/hearth/pages/thank-you";
|
|
@@ -1,26 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { EventView } from "@/components/EventView";
|
|
4
|
-
import { hearthNext } from "@/lib/hearth";
|
|
5
|
-
import { hasFeature } from "@/lib/site";
|
|
6
|
-
|
|
7
|
-
type Props = { params: Promise<{ slug: string }> };
|
|
8
|
-
|
|
9
|
-
async function load(slug: string) {
|
|
10
|
-
const site = await hearthNext.getSite();
|
|
11
|
-
return hasFeature(site, "events") ? hearthNext.getEvent(slug) : null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function generateMetadata({ params }: Props) {
|
|
15
|
-
const event = await load((await params).slug);
|
|
16
|
-
return event ? metadataFromMeta(event.meta) : {};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Events without a type live here. Events in a collection (e.g. Talks) live at their collection's address. */
|
|
20
|
-
export default async function EventPage({ params }: Props) {
|
|
21
|
-
const slug = (await params).slug;
|
|
22
|
-
const event = await load(slug);
|
|
23
|
-
if (!event) return hearthNext.redirectOrNotFound(`/events/${slug}`);
|
|
24
|
-
if (event.path !== `/events/${event.slug}`) permanentRedirect(event.path);
|
|
25
|
-
return <EventView event={event} />;
|
|
26
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/event.tsx.
|
|
2
|
+
export { default, generateMetadata } from "@/hearth/pages/event";
|
|
@@ -1,29 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { EventList } from "@/components/blocks/events";
|
|
4
|
-
import { Section } from "@/components/ui";
|
|
5
|
-
import { hearthNext } from "@/lib/hearth";
|
|
6
|
-
import { hasFeature } from "@/lib/site";
|
|
7
|
-
|
|
8
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
9
|
-
const site = await hearthNext.getSite();
|
|
10
|
-
return {
|
|
11
|
-
title: "Events",
|
|
12
|
-
description: `Upcoming events from ${site.name}.`,
|
|
13
|
-
alternates: { canonical: "/events" },
|
|
14
|
-
openGraph: { title: "Events", url: "/events" },
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Upcoming events (only on sites with the Events feature in Hearth). */
|
|
19
|
-
export default async function EventsPage() {
|
|
20
|
-
const site = await hearthNext.getSite();
|
|
21
|
-
if (!hasFeature(site, "events")) notFound();
|
|
22
|
-
const events = await hearthNext.getEvents({ when: "upcoming" });
|
|
23
|
-
return (
|
|
24
|
-
<Section>
|
|
25
|
-
<h1 className="mb-10 text-[clamp(32px,4vw,48px)] font-bold tracking-tight">Events</h1>
|
|
26
|
-
<EventList events={events} />
|
|
27
|
-
</Section>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/events.tsx.
|
|
2
|
+
export { default, generateMetadata } from "@/hearth/pages/events";
|
|
@@ -1,55 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { Blocks } from "@/components/blocks";
|
|
4
|
-
import { Progress } from "@/components/blocks/giving";
|
|
5
|
-
import { DonateForm } from "@/components/DonateForm";
|
|
6
|
-
import { Img, Section } from "@/components/ui";
|
|
7
|
-
import { hearthNext } from "@/lib/hearth";
|
|
8
|
-
import { hasFeature } from "@/lib/site";
|
|
9
|
-
|
|
10
|
-
type Props = { params: Promise<{ slug: string }> };
|
|
11
|
-
|
|
12
|
-
async function load(slug: string) {
|
|
13
|
-
const site = await hearthNext.getSite();
|
|
14
|
-
return hasFeature(site, "giving") ? { site, fundraiser: await hearthNext.getFundraiser(slug) } : null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function generateMetadata({ params }: Props) {
|
|
18
|
-
const loaded = await load((await params).slug);
|
|
19
|
-
return loaded?.fundraiser ? metadataFromMeta(loaded.fundraiser.meta) : {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** A fundraiser's page: progress and a donation form, then its story blocks. */
|
|
23
|
-
export default async function FundraiserPage({ params }: Props) {
|
|
24
|
-
const slug = (await params).slug;
|
|
25
|
-
const loaded = await load(slug);
|
|
26
|
-
if (!loaded?.fundraiser) return hearthNext.redirectOrNotFound(`/fundraisers/${slug}`);
|
|
27
|
-
const { site, fundraiser: f } = loaded;
|
|
28
|
-
const giving = site.settings.giving;
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<>
|
|
32
|
-
<JsonLd data={f.meta.jsonLd} />
|
|
33
|
-
<section className="bg-muted">
|
|
34
|
-
<div className="mx-auto grid max-w-site items-center gap-10 px-4 py-16 sm:px-6 md:grid-cols-2">
|
|
35
|
-
<div className="flex flex-col gap-4">
|
|
36
|
-
<p className="text-sm font-semibold uppercase tracking-[0.18em] opacity-75">{f.status === "completed" ? "Completed fundraiser" : "Fundraiser"}</p>
|
|
37
|
-
<h1 className="text-[clamp(32px,4.4vw,52px)] font-bold leading-tight tracking-tight">{f.title}</h1>
|
|
38
|
-
{f.summary ? <p className="text-lg opacity-85">{f.summary}</p> : null}
|
|
39
|
-
<Progress fundraiser={f} large />
|
|
40
|
-
</div>
|
|
41
|
-
{f.image ? <Img image={f.image} crop={4 / 3} className="aspect-[4/3] w-full rounded-xl object-cover" /> : null}
|
|
42
|
-
</div>
|
|
43
|
-
</section>
|
|
44
|
-
<Blocks blocks={f.blocks} />
|
|
45
|
-
{f.status === "active" ? (
|
|
46
|
-
<Section>
|
|
47
|
-
<div className="max-w-2xl">
|
|
48
|
-
<h2 className="mb-6 text-2xl font-bold">Give to {f.title}</h2>
|
|
49
|
-
<DonateForm presetsCents={giving.presetsCents} minCents={giving.minCents} maxCents={giving.maxCents} fundraiser={f.slug} />
|
|
50
|
-
</div>
|
|
51
|
-
</Section>
|
|
52
|
-
) : null}
|
|
53
|
-
</>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/fundraiser.tsx.
|
|
2
|
+
export { default, generateMetadata } from "@/hearth/pages/fundraiser";
|
|
@@ -1,37 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { FundraiserList } from "@/components/blocks/giving";
|
|
4
|
-
import { Section } from "@/components/ui";
|
|
5
|
-
import { hearthNext } from "@/lib/hearth";
|
|
6
|
-
import { hasFeature } from "@/lib/site";
|
|
7
|
-
|
|
8
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
9
|
-
const site = await hearthNext.getSite();
|
|
10
|
-
return {
|
|
11
|
-
title: "Fundraisers",
|
|
12
|
-
description: `Fundraisers and campaigns from ${site.name}: see how each is doing and give.`,
|
|
13
|
-
alternates: { canonical: "/fundraisers" },
|
|
14
|
-
openGraph: { title: "Fundraisers", url: "/fundraisers" },
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Active fundraisers, then completed ones (only on sites with the Giving feature in Hearth). */
|
|
19
|
-
export default async function FundraisersPage() {
|
|
20
|
-
const site = await hearthNext.getSite();
|
|
21
|
-
if (!hasFeature(site, "giving")) notFound();
|
|
22
|
-
const [active, completed] = await Promise.all([hearthNext.getFundraisers({ status: "active" }), hearthNext.getFundraisers({ status: "completed" })]);
|
|
23
|
-
return (
|
|
24
|
-
<>
|
|
25
|
-
<Section>
|
|
26
|
-
<h1 className="mb-10 text-[clamp(32px,4vw,48px)] font-bold tracking-tight">Fundraisers</h1>
|
|
27
|
-
<FundraiserList fundraisers={active} />
|
|
28
|
-
</Section>
|
|
29
|
-
{completed.length ? (
|
|
30
|
-
<Section tone="muted">
|
|
31
|
-
<h2 className="mb-8 text-2xl font-bold">Completed</h2>
|
|
32
|
-
<FundraiserList fundraisers={completed} />
|
|
33
|
-
</Section>
|
|
34
|
-
) : null}
|
|
35
|
-
</>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/fundraisers.tsx.
|
|
2
|
+
export { default, generateMetadata } from "@/hearth/pages/fundraisers";
|
|
@@ -1,78 +1,4 @@
|
|
|
1
|
+
/* Managed by Hearth (pnpm hearth:update replaces it). The site's tokens and CSS go in src/theme/theme.css. */
|
|
1
2
|
@import "tailwindcss";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* The site's brand. Blocks only use these tokens, so restyling a new site starts here.
|
|
5
|
-
* Hearth's "tone" options map to them: default, muted, inverse and accent.
|
|
6
|
-
*/
|
|
7
|
-
@theme {
|
|
8
|
-
--color-bg: #fbfaf7;
|
|
9
|
-
--color-ink: #1c1c1a;
|
|
10
|
-
--color-muted-ink: #5b5b55;
|
|
11
|
-
/* Dividers and card edges: decorative, so they can be faint. */
|
|
12
|
-
--color-line: #e3e0d8;
|
|
13
|
-
/* Form field and option borders: the border is how people see the field, so it needs 3:1 on surface, bg and muted. */
|
|
14
|
-
--color-field: #8a867e;
|
|
15
|
-
--color-surface: #ffffff;
|
|
16
|
-
--color-muted: #f1eee7;
|
|
17
|
-
--color-inverse: #1c1c1a;
|
|
18
|
-
--color-inverse-ink: #f6f4ee;
|
|
19
|
-
--color-accent: #2f5d50;
|
|
20
|
-
--color-accent-ink: #ffffff;
|
|
21
|
-
/* Highlighted words in headings (*like this*) and links. Check contrast on every ground. */
|
|
22
|
-
--color-highlight: #2f5d50;
|
|
23
|
-
--color-danger: #a12a1f;
|
|
24
|
-
|
|
25
|
-
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
26
|
-
--container-site: 72rem;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
body {
|
|
30
|
-
background: var(--color-bg);
|
|
31
|
-
color: var(--color-ink);
|
|
32
|
-
font-family: var(--font-sans);
|
|
33
|
-
-webkit-font-smoothing: antialiased;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/* Markdown from Hearth (rich text, FAQ answers, media + text). */
|
|
37
|
-
.prose {
|
|
38
|
-
line-height: 1.7;
|
|
39
|
-
}
|
|
40
|
-
.prose > * + * {
|
|
41
|
-
margin-top: 1em;
|
|
42
|
-
}
|
|
43
|
-
.prose h2 {
|
|
44
|
-
font-size: 1.75rem;
|
|
45
|
-
font-weight: 700;
|
|
46
|
-
line-height: 1.25;
|
|
47
|
-
margin-top: 1.6em;
|
|
48
|
-
}
|
|
49
|
-
.prose h3 {
|
|
50
|
-
font-size: 1.35rem;
|
|
51
|
-
font-weight: 700;
|
|
52
|
-
margin-top: 1.4em;
|
|
53
|
-
}
|
|
54
|
-
.prose a {
|
|
55
|
-
text-decoration: underline;
|
|
56
|
-
text-underline-offset: 3px;
|
|
57
|
-
}
|
|
58
|
-
.prose ul {
|
|
59
|
-
list-style: disc;
|
|
60
|
-
padding-left: 1.4em;
|
|
61
|
-
}
|
|
62
|
-
.prose ol {
|
|
63
|
-
list-style: decimal;
|
|
64
|
-
padding-left: 1.4em;
|
|
65
|
-
}
|
|
66
|
-
.prose blockquote {
|
|
67
|
-
border-left: 3px solid var(--color-line);
|
|
68
|
-
padding-left: 1em;
|
|
69
|
-
font-style: italic;
|
|
70
|
-
}
|
|
71
|
-
.prose img {
|
|
72
|
-
border-radius: 0.75rem;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
:focus-visible {
|
|
76
|
-
outline: 2px solid var(--color-accent);
|
|
77
|
-
outline-offset: 2px;
|
|
78
|
-
}
|
|
3
|
+
@import "../hearth/hearth.css";
|
|
4
|
+
@import "../theme/theme.css";
|
|
@@ -1,38 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { HearthAnalytics } from "@the-smithy/hearth/analytics";
|
|
3
|
-
import { siteLang } from "@the-smithy/hearth/next";
|
|
4
|
-
import { HearthPreview } from "@the-smithy/hearth/preview";
|
|
5
|
-
import { SiteFooter } from "@/components/SiteFooter";
|
|
6
|
-
import { SiteHeader } from "@/components/SiteHeader";
|
|
7
|
-
import { hearthNext } from "@/lib/hearth";
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it): the layout is src/hearth/layout/RootLayout.tsx, styled in src/theme.
|
|
8
2
|
import "./globals.css";
|
|
9
3
|
|
|
10
|
-
|
|
11
|
-
* What every page starts from: full links, the title pattern, the default description and share image, the site's
|
|
12
|
-
* language, Google and Bing verification, and no indexing on preview deploys or while the site is hidden in Hearth.
|
|
13
|
-
* Pages from Hearth then set their own title, description, canonical address and share image.
|
|
14
|
-
*/
|
|
15
|
-
export const generateMetadata = () => hearthNext.siteMetadata();
|
|
16
|
-
|
|
17
|
-
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
18
|
-
const site = await hearthNext.getSite();
|
|
19
|
-
const preview = await hearthNext.isPreview();
|
|
20
|
-
return (
|
|
21
|
-
<html lang={siteLang(site)}>
|
|
22
|
-
<body className="flex min-h-dvh flex-col">
|
|
23
|
-
<a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded focus:bg-surface focus:px-4 focus:py-2">
|
|
24
|
-
Skip to content
|
|
25
|
-
</a>
|
|
26
|
-
{/* Live draft preview from Hearth: shows while an editor previews, refreshes as they edit. */}
|
|
27
|
-
{preview ? <HearthPreview /> : null}
|
|
28
|
-
<SiteHeader site={site} />
|
|
29
|
-
<main id="main" className="flex-1">
|
|
30
|
-
{children}
|
|
31
|
-
</main>
|
|
32
|
-
<SiteFooter site={site} />
|
|
33
|
-
{/* Hearth's built-in visitor analytics (cookieless, no banner needed): off in draft preview and when the site turns it off. */}
|
|
34
|
-
{site.analytics && !preview ? <HearthAnalytics config={site.analytics} /> : null}
|
|
35
|
-
</body>
|
|
36
|
-
</html>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
4
|
+
export { default, generateMetadata } from "@/hearth/layout/RootLayout";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { hearthNext } from "@/hearth/client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The summary AI tools read (https://llmstxt.org): the organization, its contact details and hours, and every
|
|
5
|
+
* page search engines may list, with a line on each. Not found on preview deploys and while the site is hidden.
|
|
6
|
+
*/
|
|
7
|
+
export const GET = hearthNext.llmsTxtHandler();
|
|
@@ -1,14 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export default function NotFound() {
|
|
5
|
-
return (
|
|
6
|
-
<Section className="py-28">
|
|
7
|
-
<h1 className="text-4xl font-bold">Page not found</h1>
|
|
8
|
-
<p className="mt-4 text-lg opacity-80">The page you're looking for has moved or no longer exists.</p>
|
|
9
|
-
<Link href="/" className="mt-8 inline-block rounded-lg bg-accent px-6 py-3.5 font-semibold text-accent-ink hover:opacity-90">
|
|
10
|
-
Go to the homepage
|
|
11
|
-
</Link>
|
|
12
|
-
</Section>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/not-found.tsx.
|
|
2
|
+
export { default } from "@/hearth/pages/not-found";
|
|
@@ -1,22 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { Section } from "@/components/ui";
|
|
4
|
-
|
|
5
|
-
export const metadata: Metadata = { title: "You're registered", robots: { index: false, follow: false } };
|
|
6
|
-
|
|
7
|
-
/** Where the payment page sends people after paying for a ticket. The confirmation is emailed once payment completes. */
|
|
8
|
-
export default function RegisteredPage() {
|
|
9
|
-
return (
|
|
10
|
-
<Section>
|
|
11
|
-
<div className="max-w-2xl">
|
|
12
|
-
<h1 className="mb-4 text-[clamp(32px,4vw,48px)] font-bold tracking-tight">You're registered</h1>
|
|
13
|
-
<p className="text-lg opacity-85">Thanks for signing up. A confirmation is on its way to your email.</p>
|
|
14
|
-
<p className="mt-8">
|
|
15
|
-
<Link href="/events" className="font-semibold underline underline-offset-4">
|
|
16
|
-
See all events
|
|
17
|
-
</Link>
|
|
18
|
-
</p>
|
|
19
|
-
</div>
|
|
20
|
-
</Section>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
1
|
+
// Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/registered.tsx.
|
|
2
|
+
export { default, metadata } from "@/hearth/pages/registered";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { hearthNext } from "@/
|
|
2
|
-
import { hasFeature } from "@/
|
|
1
|
+
import { hearthNext } from "@/hearth/client";
|
|
2
|
+
import { hasFeature } from "@/hearth/site";
|
|
3
3
|
|
|
4
4
|
/** Every indexable page, event and fundraiser in Hearth, plus the pages this site's own code serves. */
|
|
5
5
|
export default async function sitemap() {
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { directionsUrl, type Position } from "@the-smithy/hearth";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import "maplibre-gl/dist/maplibre-gl.css";
|
|
6
|
+
import { theme } from "@/theme";
|
|
7
|
+
import { cx } from "../theme";
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
|
9
|
+
const t = theme.map;
|
|
9
10
|
|
|
10
11
|
export type MapPlace = { name: string; address: string; position: Position };
|
|
11
12
|
|
|
@@ -31,7 +32,8 @@ export function OpenMap({ places, label, className = "" }: { places: MapPlace[];
|
|
|
31
32
|
const list: MapPlace[] = JSON.parse(key);
|
|
32
33
|
const map = new ml.Map({
|
|
33
34
|
container: el,
|
|
34
|
-
style
|
|
35
|
+
// Free OpenStreetMap tiles from OpenFreeMap by default (no key, no cookies); the theme's map.style swaps them.
|
|
36
|
+
style: t.style,
|
|
35
37
|
center: [list[0].position.lng, list[0].position.lat],
|
|
36
38
|
zoom: 15,
|
|
37
39
|
attributionControl: { compact: true },
|
|
@@ -51,7 +53,7 @@ export function OpenMap({ places, label, className = "" }: { places: MapPlace[];
|
|
|
51
53
|
bounds.extend(at);
|
|
52
54
|
const pin = document.createElement("button");
|
|
53
55
|
pin.type = "button";
|
|
54
|
-
pin.className =
|
|
56
|
+
pin.className = t.pin;
|
|
55
57
|
pin.setAttribute("aria-label", place.name);
|
|
56
58
|
new ml.Marker({ element: pin }).setLngLat(at).setPopup(new ml.Popup({ offset: 14, closeButton: true }).setDOMContent(popup(place))).addTo(map);
|
|
57
59
|
}
|
|
@@ -75,10 +77,10 @@ export function OpenMap({ places, label, className = "" }: { places: MapPlace[];
|
|
|
75
77
|
}, [key, label]);
|
|
76
78
|
|
|
77
79
|
return (
|
|
78
|
-
<div className={
|
|
80
|
+
<div className={cx(t.frame, className)}>
|
|
79
81
|
{/* Sized by the frame: MapLibre makes its container position: relative. */}
|
|
80
82
|
<div ref={container} className="size-full" />
|
|
81
|
-
{failed ? <p className=
|
|
83
|
+
{failed ? <p className={t.failed}>{theme.text.mapFailed}</p> : null}
|
|
82
84
|
</div>
|
|
83
85
|
);
|
|
84
86
|
}
|
|
@@ -86,7 +88,7 @@ export function OpenMap({ places, label, className = "" }: { places: MapPlace[];
|
|
|
86
88
|
/** A pin's popup, built as elements (never HTML strings) so names and addresses stay plain text. */
|
|
87
89
|
function popup(place: MapPlace) {
|
|
88
90
|
const box = document.createElement("div");
|
|
89
|
-
box.className =
|
|
91
|
+
box.className = t.popup;
|
|
90
92
|
const name = document.createElement("strong");
|
|
91
93
|
name.textContent = place.name;
|
|
92
94
|
const address = document.createElement("span");
|
|
@@ -95,8 +97,8 @@ function popup(place: MapPlace) {
|
|
|
95
97
|
const link = document.createElement("a");
|
|
96
98
|
link.href = directionsUrl(place);
|
|
97
99
|
link.rel = "noopener";
|
|
98
|
-
link.className =
|
|
99
|
-
link.textContent =
|
|
100
|
+
link.className = t.popupLink;
|
|
101
|
+
link.textContent = theme.text.directions;
|
|
100
102
|
box.append(name, address, link);
|
|
101
103
|
return box;
|
|
102
104
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { BlockOf, CollectionEntry } from "@the-smithy/hearth";
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { formatEntryDate } from "../format";
|
|
5
|
+
import { fill, Img, Section, SectionHeading } from "../ui";
|
|
6
|
+
|
|
7
|
+
const t = theme.collectionList;
|
|
8
|
+
|
|
9
|
+
function Card({ entry }: { entry: CollectionEntry }) {
|
|
10
|
+
return (
|
|
11
|
+
<li className={t.card}>
|
|
12
|
+
{entry.image ? <Img image={entry.image} crop={16 / 10} className={t.image} /> : null}
|
|
13
|
+
<div className={t.body}>
|
|
14
|
+
{entry.date ? <p className={t.date}>{formatEntryDate(entry.date)}</p> : null}
|
|
15
|
+
<h3 className={t.title}>
|
|
16
|
+
<Link href={entry.path} className={t.link}>
|
|
17
|
+
{entry.title}
|
|
18
|
+
</Link>
|
|
19
|
+
</h3>
|
|
20
|
+
{entry.summary ? <p className={t.summary}>{entry.summary}</p> : null}
|
|
21
|
+
{entry.tags.length ? (
|
|
22
|
+
<ul className={t.tags}>
|
|
23
|
+
{entry.tags.map((tag) => (
|
|
24
|
+
<li key={tag} className={t.tag}>
|
|
25
|
+
{tag}
|
|
26
|
+
</li>
|
|
27
|
+
))}
|
|
28
|
+
</ul>
|
|
29
|
+
) : null}
|
|
30
|
+
</div>
|
|
31
|
+
</li>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Entries of a page collection (a blog, case studies…), as cards or a simple list. */
|
|
36
|
+
export function CollectionList({ block }: { block: BlockOf<"collectionList"> }) {
|
|
37
|
+
const { entries, collection, layout } = block.data;
|
|
38
|
+
return (
|
|
39
|
+
<Section block="collectionList">
|
|
40
|
+
<SectionHeading eyebrow={block.data.eyebrow} heading={block.data.heading} intro={block.data.intro} />
|
|
41
|
+
{!entries.length ? (
|
|
42
|
+
<p className={t.empty}>{theme.text.noEntries}</p>
|
|
43
|
+
) : layout === "list" ? (
|
|
44
|
+
<ul className={t.list}>
|
|
45
|
+
{entries.map((e) => (
|
|
46
|
+
<li key={e.path} className={t.listItem}>
|
|
47
|
+
<Link href={e.path} className={t.listLink}>
|
|
48
|
+
{e.title}
|
|
49
|
+
</Link>
|
|
50
|
+
{e.date || e.summary ? (
|
|
51
|
+
<p className={t.listMeta}>
|
|
52
|
+
{e.date ? <span className={t.listDate}>{formatEntryDate(e.date)}</span> : null}
|
|
53
|
+
{e.date && e.summary ? " · " : null}
|
|
54
|
+
{e.summary}
|
|
55
|
+
</p>
|
|
56
|
+
) : null}
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
) : (
|
|
61
|
+
<ul className={t.cards}>
|
|
62
|
+
{entries.map((e) => (
|
|
63
|
+
<Card key={e.path} entry={e} />
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
)}
|
|
67
|
+
{collection && entries.length >= block.data.limit ? (
|
|
68
|
+
<Link href={collection.path} className={t.more}>
|
|
69
|
+
{fill(theme.text.allEntries, { collection: collection.name.toLowerCase() })}
|
|
70
|
+
</Link>
|
|
71
|
+
) : null}
|
|
72
|
+
</Section>
|
|
73
|
+
);
|
|
74
|
+
}
|