@the-smithy/create-hearth-site 0.20.0 → 0.21.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 +51 -23
- 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/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 +36 -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,77 @@
|
|
|
1
|
+
import { metadataFromMeta } from "@the-smithy/hearth/next";
|
|
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 { Share } from "../components/Share";
|
|
8
|
+
import { DonateForm } from "../forms/DonateForm";
|
|
9
|
+
import { money } from "../format";
|
|
10
|
+
import { hasFeature, siteUrl } from "../site";
|
|
11
|
+
import { fill, Img, Section } from "../ui";
|
|
12
|
+
|
|
13
|
+
type Props = { params: Promise<{ slug: string }> };
|
|
14
|
+
|
|
15
|
+
async function load(slug: string) {
|
|
16
|
+
const site = await hearthNext.getSite();
|
|
17
|
+
return hasFeature(site, "giving") ? { site, fundraiser: await hearthNext.getFundraiser(slug) } : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function generateMetadata({ params }: Props) {
|
|
21
|
+
const loaded = await load((await params).slug);
|
|
22
|
+
return loaded?.fundraiser ? metadataFromMeta(loaded.fundraiser.meta) : {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A fundraiser's page: progress, Donate and Share, then its story blocks and the donation form. */
|
|
26
|
+
export default async function FundraiserPage({ params }: Props) {
|
|
27
|
+
const slug = (await params).slug;
|
|
28
|
+
const loaded = await load(slug);
|
|
29
|
+
if (!loaded?.fundraiser) return hearthNext.redirectOrNotFound(`/fundraisers/${slug}`);
|
|
30
|
+
const { site, fundraiser: f } = loaded;
|
|
31
|
+
const giving = site.settings.giving;
|
|
32
|
+
const t = theme.fundraiser;
|
|
33
|
+
const base = siteUrl(site);
|
|
34
|
+
const url = base ? `${base}${f.path}` : null;
|
|
35
|
+
const active = f.status === "active";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<JsonLd data={f.meta.jsonLd} />
|
|
40
|
+
<section className={t.header}>
|
|
41
|
+
<div className={t.headerInner}>
|
|
42
|
+
<div className={t.details}>
|
|
43
|
+
<p className={t.label}>{active ? "Fundraiser" : "Completed fundraiser"}</p>
|
|
44
|
+
<h1 className={t.title}>{f.title}</h1>
|
|
45
|
+
{f.summary ? <p className={t.summary}>{f.summary}</p> : null}
|
|
46
|
+
<Progress fundraiser={f} large />
|
|
47
|
+
{active ? (
|
|
48
|
+
<div className={t.actions}>
|
|
49
|
+
<a href="#donate" className={theme.button.primary}>
|
|
50
|
+
{theme.text.donate}
|
|
51
|
+
</a>
|
|
52
|
+
{url ? (
|
|
53
|
+
<Share
|
|
54
|
+
title={f.title}
|
|
55
|
+
url={url}
|
|
56
|
+
message={`I'm supporting ${f.title}${f.goalCents ? `: ${money(f.raisedCents)} raised of ${money(f.goalCents)} so far` : ""}. Give here: ${url}`}
|
|
57
|
+
heading="Share this fundraiser"
|
|
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
|
+
<DonateForm presetsCents={giving.presetsCents} minCents={giving.minCents} maxCents={giving.maxCents} fundraiser={f.slug} />
|
|
72
|
+
</div>
|
|
73
|
+
</Section>
|
|
74
|
+
) : null}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { notFound } from "next/navigation";
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { FundraiserList } from "../blocks/giving";
|
|
5
|
+
import { hearthNext } from "../client";
|
|
6
|
+
import { hasFeature } from "../site";
|
|
7
|
+
import { Section } from "../ui";
|
|
8
|
+
|
|
9
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
10
|
+
const site = await hearthNext.getSite();
|
|
11
|
+
return {
|
|
12
|
+
title: "Fundraisers",
|
|
13
|
+
description: `Fundraisers and campaigns from ${site.name}: see how each is doing and give.`,
|
|
14
|
+
alternates: { canonical: "/fundraisers" },
|
|
15
|
+
openGraph: { title: "Fundraisers", url: "/fundraisers" },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Active fundraisers, then completed ones (only on sites with the Giving feature in Hearth). */
|
|
20
|
+
export default async function FundraisersPage() {
|
|
21
|
+
const site = await hearthNext.getSite();
|
|
22
|
+
if (!hasFeature(site, "giving")) notFound();
|
|
23
|
+
const [active, completed] = await Promise.all([hearthNext.getFundraisers({ status: "active" }), hearthNext.getFundraisers({ status: "completed" })]);
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<Section block="fundraisers">
|
|
27
|
+
<h1 className={theme.page.title}>Fundraisers</h1>
|
|
28
|
+
<FundraiserList fundraisers={active} />
|
|
29
|
+
</Section>
|
|
30
|
+
{completed.length ? (
|
|
31
|
+
<Section block="fundraisers" tone="muted">
|
|
32
|
+
<h2 className={theme.fundraiser.completedTitle}>Completed</h2>
|
|
33
|
+
<FundraiserList fundraisers={completed} />
|
|
34
|
+
</Section>
|
|
35
|
+
) : null}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { theme } from "@/theme";
|
|
3
|
+
import { Section } from "../ui";
|
|
4
|
+
|
|
5
|
+
export default function NotFound() {
|
|
6
|
+
return (
|
|
7
|
+
<Section block="notFound" innerClassName={theme.page.notFound}>
|
|
8
|
+
<h1 className={theme.page.title}>Page not found</h1>
|
|
9
|
+
<p className={theme.page.lead}>The page you're looking for has moved or no longer exists.</p>
|
|
10
|
+
<Link href="/" className={theme.button.primary}>
|
|
11
|
+
Go to the homepage
|
|
12
|
+
</Link>
|
|
13
|
+
</Section>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { normalizeSlug } from "@the-smithy/hearth";
|
|
2
|
+
import { metadataFromMeta } from "@the-smithy/hearth/next";
|
|
3
|
+
import { JsonLd } from "@the-smithy/hearth/react";
|
|
4
|
+
import type { Metadata } from "next";
|
|
5
|
+
import { Blocks } from "../blocks";
|
|
6
|
+
import { hearthNext } from "../client";
|
|
7
|
+
import { EventView } from "../components/EventView";
|
|
8
|
+
import { hasFeature } from "../site";
|
|
9
|
+
|
|
10
|
+
type Props = { params: Promise<{ slug?: string[] }> };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An event in an event collection, e.g. /talks/<event> for a "Talks" collection. Pages are checked first,
|
|
14
|
+
* so a page at the same address wins.
|
|
15
|
+
*/
|
|
16
|
+
async function collectionEvent(slug: string[] | undefined) {
|
|
17
|
+
if (!slug || slug.length < 2) return null;
|
|
18
|
+
const site = await hearthNext.getSite();
|
|
19
|
+
if (!hasFeature(site, "events")) return null;
|
|
20
|
+
const prefix = normalizeSlug(slug.slice(0, -1));
|
|
21
|
+
const collection = (await hearthNext.getCollections()).find((c) => c.kind === "events" && c.slug === prefix);
|
|
22
|
+
if (!collection) return null;
|
|
23
|
+
const event = await hearthNext.getEvent(normalizeSlug(slug.at(-1)));
|
|
24
|
+
return event?.collection?.id === collection.id ? event : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Every page in Hearth at its path ("/" is "home", "/about" is "about", "/blog/post" is a blog entry), plus events in collections. */
|
|
28
|
+
export default async function Page({ params }: Props) {
|
|
29
|
+
const slug = (await params).slug;
|
|
30
|
+
const page = await hearthNext.getPage(slug);
|
|
31
|
+
if (page) {
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<JsonLd data={page.meta.jsonLd} />
|
|
35
|
+
{/* Pages usually open with a Hero (the visible title); this keeps a heading for the rest. */}
|
|
36
|
+
{page.blocks[0]?.type === "hero" ? null : <h1 className="sr-only">{page.title}</h1>}
|
|
37
|
+
<Blocks blocks={page.blocks} />
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const event = await collectionEvent(slug);
|
|
43
|
+
if (event) return <EventView event={event} />;
|
|
44
|
+
// An old address (a renamed page, or the site before it moved to Hearth) redirects; anything else is a 404.
|
|
45
|
+
return hearthNext.redirectOrNotFound(slug);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
49
|
+
const slug = (await params).slug;
|
|
50
|
+
const metadata = await hearthNext.generatePageMetadata(slug);
|
|
51
|
+
if (Object.keys(metadata).length) return metadata;
|
|
52
|
+
const event = await collectionEvent(slug);
|
|
53
|
+
return event ? metadataFromMeta(event.meta) : {};
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { Section } from "../ui";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = { title: "You're registered", robots: { index: false, follow: false } };
|
|
7
|
+
|
|
8
|
+
/** Where the payment page sends people after paying for a ticket. The confirmation is emailed once payment completes. */
|
|
9
|
+
export default function RegisteredPage() {
|
|
10
|
+
return (
|
|
11
|
+
<Section block="registered">
|
|
12
|
+
<h1 className={theme.page.title}>You're registered</h1>
|
|
13
|
+
<p className={theme.page.lead}>Thanks for signing up. A confirmation is on its way to your email.</p>
|
|
14
|
+
<Link href="/events" className={theme.page.back}>
|
|
15
|
+
See all events
|
|
16
|
+
</Link>
|
|
17
|
+
</Section>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { theme } from "@/theme";
|
|
4
|
+
import { Section } from "../ui";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = { title: "Thank you", robots: { index: false, follow: false } };
|
|
7
|
+
|
|
8
|
+
/** Where the payment page sends donors back. The receipt is emailed once the payment completes. */
|
|
9
|
+
export default function ThankYouPage() {
|
|
10
|
+
return (
|
|
11
|
+
<Section block="thankYou">
|
|
12
|
+
<h1 className={theme.page.title}>Thank you</h1>
|
|
13
|
+
<p className={theme.page.lead}>Your gift makes a difference. If you gave your email, a receipt is on its way.</p>
|
|
14
|
+
<Link href="/" className={theme.page.back}>
|
|
15
|
+
Back to the homepage
|
|
16
|
+
</Link>
|
|
17
|
+
</Section>
|
|
18
|
+
);
|
|
19
|
+
}
|