PB-driven editorial copy + Hampers & Boxes category (Q1a, Q2, Q5)
- collection_rows: hampers-boxes row (seed default + ensure-row migration); /chocolate shows the button, /collections/hampers-boxes gets the two-tone hero (last-word script rule) + live Shopify hampers + row blurb as lead - gifting_page: tabs/hampers/seasonal heading fields (seed + fill-missing migration, never overwrites admin edits); gifting.tsx consumes them - site_settings: home seasonal + universe copy keys (seed + ensure-keys migration); index loader passes them to SeasonalTeaser/UniverseSection - hero lead on collection pages falls back to the PB row blurb
This commit is contained in:
@@ -205,6 +205,15 @@ const COLLECTION_ROWS = [
|
|||||||
sort: 7,
|
sort: 7,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
shopify_handle: "hampers-boxes",
|
||||||
|
name_override: "Hampers & Boxes",
|
||||||
|
blurb: "Celebration hampers and keepsake boxes, composed to order",
|
||||||
|
carousel_urls:
|
||||||
|
"/products/mozi20.jpg\n/products/mozi-33.jpg\n/products/mozi21.jpg",
|
||||||
|
sort: 8,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const GIFTING_PAGE = [
|
const GIFTING_PAGE = [
|
||||||
@@ -214,6 +223,14 @@ const GIFTING_PAGE = [
|
|||||||
hero_script: "giving",
|
hero_script: "giving",
|
||||||
hero_lead:
|
hero_lead:
|
||||||
"Some gifts are opened. Others are remembered. Every Mozimo hamper is composed by hand — chocolate first, packaging worthy of it second.",
|
"Some gifts are opened. Others are remembered. Every Mozimo hamper is composed by hand — chocolate first, packaging worthy of it second.",
|
||||||
|
tabs_eyebrow: "Who is it for?",
|
||||||
|
tabs_title: "Gifts,",
|
||||||
|
tabs_script: "price by price",
|
||||||
|
hampers_title: "Hampers &",
|
||||||
|
hampers_script: "boxes",
|
||||||
|
seasonal_eyebrow: "The Seasonal Edit",
|
||||||
|
seasonal_title: "Chocolate for the",
|
||||||
|
seasonal_script: "calendar",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -296,6 +313,14 @@ const SCHEMAS = {
|
|||||||
{ name: "hero_title", type: "text" },
|
{ name: "hero_title", type: "text" },
|
||||||
{ name: "hero_script", type: "text" },
|
{ name: "hero_script", type: "text" },
|
||||||
{ name: "hero_lead", type: "text" },
|
{ name: "hero_lead", type: "text" },
|
||||||
|
{ name: "tabs_eyebrow", type: "text" },
|
||||||
|
{ name: "tabs_title", type: "text" },
|
||||||
|
{ name: "tabs_script", type: "text" },
|
||||||
|
{ name: "hampers_title", type: "text" },
|
||||||
|
{ name: "hampers_script", type: "text" },
|
||||||
|
{ name: "seasonal_eyebrow", type: "text" },
|
||||||
|
{ name: "seasonal_title", type: "text" },
|
||||||
|
{ name: "seasonal_script", type: "text" },
|
||||||
{ name: "sort", type: "number" },
|
{ name: "sort", type: "number" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -308,6 +333,41 @@ const SITE_SETTINGS = [
|
|||||||
"Complimentary delivery in Chandigarh Tricity on orders above ₹1,500 · Intercity shipped at actuals",
|
"Complimentary delivery in Chandigarh Tricity on orders above ₹1,500 · Intercity shipped at actuals",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "home_seasonal_eyebrow",
|
||||||
|
value: "The Seasonal Edit",
|
||||||
|
sort: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "home_seasonal_title",
|
||||||
|
value: "Chocolate for the",
|
||||||
|
sort: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "home_seasonal_script",
|
||||||
|
value: "calendar",
|
||||||
|
sort: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "home_universe_eyebrow",
|
||||||
|
value: "Our World",
|
||||||
|
sort: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "home_universe_title",
|
||||||
|
value: "Discover the universe of",
|
||||||
|
sort: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "home_universe_script",
|
||||||
|
value: "Mozimo",
|
||||||
|
sort: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "home_universe_cta",
|
||||||
|
value: "Explore our world",
|
||||||
|
sort: 7,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const CORPORATE_CHILDREN = [
|
const CORPORATE_CHILDREN = [
|
||||||
@@ -475,6 +535,54 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migration: fill section copy that arrived after the record was seeded
|
||||||
|
// (only empty fields — anything edited in the admin is never overwritten)
|
||||||
|
if (name === "gifting_page") {
|
||||||
|
const rec = (await authed(`/api/collections/${name}/records?perPage=1`)).body.items?.[0];
|
||||||
|
if (rec) {
|
||||||
|
const patch = {};
|
||||||
|
for (const [k, v] of Object.entries(SEEDS[name][0] ?? {})) {
|
||||||
|
if (k !== "sort" && !rec[k]) patch[k] = v;
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length) {
|
||||||
|
const patched = await authed(`/api/collections/${name}/records/${rec.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
});
|
||||||
|
if (patched.ok)
|
||||||
|
console.log(`[pb-seed] gifting_page: filled ${Object.keys(patch).length} empty field(s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// migration: make sure editorial rows/settings that shipped later exist
|
||||||
|
if (name === "collection_rows") {
|
||||||
|
const all = (await authed(`/api/collections/${name}/records?perPage=100`)).body.items ?? [];
|
||||||
|
for (const row of COLLECTION_ROWS) {
|
||||||
|
if (!all.some((r) => r.shopify_handle === row.shopify_handle)) {
|
||||||
|
const inserted = await authed(`/api/collections/${name}/records`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(row),
|
||||||
|
});
|
||||||
|
if (inserted.ok)
|
||||||
|
console.log(`[pb-seed] collection_rows: added missing row "${row.name_override}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name === "site_settings") {
|
||||||
|
const all = (await authed(`/api/collections/${name}/records?perPage=100`)).body.items ?? [];
|
||||||
|
for (const setting of SITE_SETTINGS) {
|
||||||
|
if (!all.some((r) => r.key === setting.key)) {
|
||||||
|
const inserted = await authed(`/api/collections/${name}/records`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(setting),
|
||||||
|
});
|
||||||
|
if (inserted.ok)
|
||||||
|
console.log(`[pb-seed] site_settings: added "${setting.key}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const record of SEEDS[name]) {
|
for (const record of SEEDS[name]) {
|
||||||
|
|||||||
+15
-8
@@ -14,7 +14,8 @@ export const Route = createFileRoute("/gifting")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function GiftingPage() {
|
function GiftingPage() {
|
||||||
const { hero, tabs, hampers, seasonal } = Route.useLoaderData();
|
const { hero, sections, tabs, hampers, seasonal } =
|
||||||
|
Route.useLoaderData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-ivory-50">
|
<div className="min-h-screen bg-ivory-50">
|
||||||
@@ -51,7 +52,7 @@ function GiftingPage() {
|
|||||||
>
|
>
|
||||||
<div className="wrap section-pad pb-0!">
|
<div className="wrap section-pad pb-0!">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<p className="eyebrow text-orange-500">Who is it for?</p>
|
<p className="eyebrow text-orange-500">{sections.tabs.eyebrow}</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.05}>
|
<Reveal delay={0.05}>
|
||||||
<h2 className="display mt-5 max-w-2xl text-[clamp(1.9rem,3.8vw,2.9rem)] text-ink-900">
|
<h2 className="display mt-5 max-w-2xl text-[clamp(1.9rem,3.8vw,2.9rem)] text-ink-900">
|
||||||
@@ -71,13 +72,15 @@ function GiftingPage() {
|
|||||||
<div className="flex flex-wrap items-end justify-between gap-6">
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<h2 className="display text-[clamp(2rem,4vw,3.4rem)]">
|
<h2 className="display text-[clamp(2rem,4vw,3.4rem)]">
|
||||||
Hampers &{" "}
|
{sections.hampers.title}{" "}
|
||||||
<em className="italic text-orange-500">boxes</em>
|
<em className="italic text-orange-500">
|
||||||
|
{sections.hampers.script}
|
||||||
|
</em>
|
||||||
</h2>
|
</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.1}>
|
<Reveal delay={0.1}>
|
||||||
<a
|
<a
|
||||||
href="/collections/gift-collection"
|
href="/collections/hampers-boxes"
|
||||||
className="link-quiet eyebrow text-ink-700 hover:text-orange-500"
|
className="link-quiet eyebrow text-ink-700 hover:text-orange-500"
|
||||||
>
|
>
|
||||||
Full gifting collection
|
Full gifting collection
|
||||||
@@ -118,12 +121,16 @@ function GiftingPage() {
|
|||||||
<section id="seasonal" className="relative bg-orange-500">
|
<section id="seasonal" className="relative bg-orange-500">
|
||||||
<div className="wrap section-pad">
|
<div className="wrap section-pad">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<p className="eyebrow text-ivory-100">The Seasonal Edit</p>
|
<p className="eyebrow text-ivory-100">
|
||||||
|
{sections.seasonal.eyebrow}
|
||||||
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.1}>
|
<Reveal delay={0.1}>
|
||||||
<h2 className="display mt-5 max-w-3xl text-[clamp(2.1rem,4.5vw,3.9rem)] text-ivory-50">
|
<h2 className="display mt-5 max-w-3xl text-[clamp(2.1rem,4.5vw,3.9rem)] text-ivory-50">
|
||||||
Chocolate for the{" "}
|
{sections.seasonal.title}{" "}
|
||||||
<em className="italic text-orange-300">calendar</em>
|
<em className="italic text-orange-300">
|
||||||
|
{sections.seasonal.script}
|
||||||
|
</em>
|
||||||
</h2>
|
</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -15,8 +15,12 @@ import SeasonalTeaser from "@/components/sections/SeasonalTeaser";
|
|||||||
import ExperiencesTeaser from "@/components/sections/ExperiencesTeaser";
|
import ExperiencesTeaser from "@/components/sections/ExperiencesTeaser";
|
||||||
import JournalTeaser from "@/components/sections/JournalTeaser";
|
import JournalTeaser from "@/components/sections/JournalTeaser";
|
||||||
import { PressStrip, InstagramSection } from "@/components/sections/Social";
|
import { PressStrip, InstagramSection } from "@/components/sections/Social";
|
||||||
|
import { getHomeCopy, type HomeCopy } from "@/lib/content";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({ component: Home });
|
export const Route = createFileRoute("/")({
|
||||||
|
component: Home,
|
||||||
|
loader: (): Promise<HomeCopy> => getHomeCopy(),
|
||||||
|
});
|
||||||
|
|
||||||
const TICKER = [
|
const TICKER = [
|
||||||
"Single Origin",
|
"Single Origin",
|
||||||
@@ -28,6 +32,7 @@ const TICKER = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
|
const copy = Route.useLoaderData();
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-ivory-50">
|
<div className="min-h-screen bg-ivory-50">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -37,12 +42,12 @@ function Home() {
|
|||||||
<BrandStatement />
|
<BrandStatement />
|
||||||
<ExperienceTiles />
|
<ExperienceTiles />
|
||||||
<MozimoEdit />
|
<MozimoEdit />
|
||||||
<UniverseSection />
|
<UniverseSection copy={copy.universe} />
|
||||||
<Signatures />
|
<Signatures />
|
||||||
<CraftCinematic />
|
<CraftCinematic />
|
||||||
<IngredientStory />
|
<IngredientStory />
|
||||||
<GiftingSection />
|
<GiftingSection />
|
||||||
<SeasonalTeaser />
|
<SeasonalTeaser copy={copy.seasonal} />
|
||||||
<ExperiencesTeaser />
|
<ExperiencesTeaser />
|
||||||
<JournalTeaser />
|
<JournalTeaser />
|
||||||
<PressStrip />
|
<PressStrip />
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import Reveal from "@/components/Reveal";
|
import Reveal from "@/components/Reveal";
|
||||||
import { seasonal } from "@/data/catalog";
|
import { seasonal } from "@/data/catalog";
|
||||||
|
import type { HomeCopy } from "@/lib/content";
|
||||||
|
|
||||||
export default function SeasonalTeaser() {
|
export default function SeasonalTeaser({
|
||||||
|
copy,
|
||||||
|
}: {
|
||||||
|
copy?: HomeCopy["seasonal"];
|
||||||
|
}) {
|
||||||
|
const eyebrow = copy?.eyebrow ?? "The Seasonal Edit";
|
||||||
|
const title = copy?.title ?? "Chocolate for the";
|
||||||
|
const script = copy?.script ?? "calendar";
|
||||||
return (
|
return (
|
||||||
<section id="seasonal" className="relative bg-orange-500">
|
<section id="seasonal" className="relative bg-orange-500">
|
||||||
<div className="wrap section-pad">
|
<div className="wrap section-pad">
|
||||||
<div className="flex flex-wrap items-end justify-between gap-6">
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<p className="eyebrow text-ivory-100">The Seasonal Edit</p>
|
<p className="eyebrow text-ivory-100">{eyebrow}</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.1}>
|
<Reveal delay={0.1}>
|
||||||
<h2 className="display mt-5 max-w-3xl text-[clamp(2.1rem,5vw,4.2rem)] text-ivory-50">
|
<h2 className="display mt-5 max-w-3xl text-[clamp(2.1rem,5vw,4.2rem)] text-ivory-50">
|
||||||
Chocolate for the{" "}
|
{title}{" "}
|
||||||
<em className="italic text-ivory-100">calendar</em>
|
<em className="italic text-ivory-100">{script}</em>
|
||||||
</h2>
|
</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Reveal from "@/components/Reveal";
|
import Reveal from "@/components/Reveal";
|
||||||
|
import type { HomeCopy } from "@/lib/content";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Discover the universe of Mozimo" — Hermes-inspired universe gateway.
|
* "Discover the universe of Mozimo" — Hermes-inspired universe gateway.
|
||||||
@@ -6,7 +7,15 @@ import Reveal from "@/components/Reveal";
|
|||||||
* link into /our-world. Imagery is swappable in one line once the new
|
* link into /our-world. Imagery is swappable in one line once the new
|
||||||
* cacao/pastel photography lands.
|
* cacao/pastel photography lands.
|
||||||
*/
|
*/
|
||||||
export default function UniverseSection() {
|
export default function UniverseSection({
|
||||||
|
copy,
|
||||||
|
}: {
|
||||||
|
copy?: HomeCopy["universe"];
|
||||||
|
}) {
|
||||||
|
const eyebrow = copy?.eyebrow ?? "Our World";
|
||||||
|
const title = copy?.title ?? "Discover the universe of";
|
||||||
|
const script = copy?.script ?? "Mozimo";
|
||||||
|
const cta = copy?.cta ?? "Explore our world";
|
||||||
return (
|
return (
|
||||||
<section className="relative" aria-label="The universe of Mozimo">
|
<section className="relative" aria-label="The universe of Mozimo">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
@@ -21,11 +30,11 @@ export default function UniverseSection() {
|
|||||||
/>
|
/>
|
||||||
<span className="absolute inset-0 bg-gradient-to-t from-bean-950/80 via-bean-950/20 to-transparent" />
|
<span className="absolute inset-0 bg-gradient-to-t from-bean-950/80 via-bean-950/20 to-transparent" />
|
||||||
<span className="absolute inset-x-0 bottom-0 p-8 sm:p-14">
|
<span className="absolute inset-x-0 bottom-0 p-8 sm:p-14">
|
||||||
<span className="eyebrow block text-ivory-100/70">Our World</span>
|
<span className="eyebrow block text-ivory-100/70">{eyebrow}</span>
|
||||||
<span className="display mt-4 block max-w-3xl text-[clamp(2rem,4.5vw,3.8rem)] text-ivory-50">
|
<span className="display mt-4 block max-w-3xl text-[clamp(2rem,4.5vw,3.8rem)] text-ivory-50">
|
||||||
Discover the universe of{" "}
|
{title}{" "}
|
||||||
<em className="font-script text-[1.12em] font-normal italic text-orange-300">
|
<em className="font-script text-[1.12em] font-normal italic text-orange-300">
|
||||||
Mozimo
|
{script}
|
||||||
</em>
|
</em>
|
||||||
</span>
|
</span>
|
||||||
<span className="link-quiet eyebrow mt-6 inline-block text-ivory-100/80 transition-colors group-hover:text-orange-300">
|
<span className="link-quiet eyebrow mt-6 inline-block text-ivory-100/80 transition-colors group-hover:text-orange-300">
|
||||||
|
|||||||
@@ -161,6 +161,14 @@ export type CmsGiftingPage = {
|
|||||||
heroTitle?: string;
|
heroTitle?: string;
|
||||||
heroScript?: string;
|
heroScript?: string;
|
||||||
heroLead?: string;
|
heroLead?: string;
|
||||||
|
tabsEyebrow?: string;
|
||||||
|
tabsTitle?: string;
|
||||||
|
tabsScript?: string;
|
||||||
|
hampersTitle?: string;
|
||||||
|
hampersScript?: string;
|
||||||
|
seasonalEyebrow?: string;
|
||||||
|
seasonalTitle?: string;
|
||||||
|
seasonalScript?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getCmsGiftingPage(): Promise<CmsGiftingPage | null> {
|
export async function getCmsGiftingPage(): Promise<CmsGiftingPage | null> {
|
||||||
@@ -172,6 +180,14 @@ export async function getCmsGiftingPage(): Promise<CmsGiftingPage | null> {
|
|||||||
heroTitle: str(r.hero_title) || undefined,
|
heroTitle: str(r.hero_title) || undefined,
|
||||||
heroScript: str(r.hero_script) || undefined,
|
heroScript: str(r.hero_script) || undefined,
|
||||||
heroLead: str(r.hero_lead) || undefined,
|
heroLead: str(r.hero_lead) || undefined,
|
||||||
|
tabsEyebrow: str(r.tabs_eyebrow) || undefined,
|
||||||
|
tabsTitle: str(r.tabs_title) || undefined,
|
||||||
|
tabsScript: str(r.tabs_script) || undefined,
|
||||||
|
hampersTitle: str(r.hampers_title) || undefined,
|
||||||
|
hampersScript: str(r.hampers_script) || undefined,
|
||||||
|
seasonalEyebrow: str(r.seasonal_eyebrow) || undefined,
|
||||||
|
seasonalTitle: str(r.seasonal_title) || undefined,
|
||||||
|
seasonalScript: str(r.seasonal_script) || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+84
-2
@@ -113,11 +113,29 @@ export type ResolvedGiftingTab = Omit<GiftingTab, "cta"> & {
|
|||||||
|
|
||||||
export type GiftingPageData = {
|
export type GiftingPageData = {
|
||||||
hero: GiftingHero;
|
hero: GiftingHero;
|
||||||
|
sections: GiftingSections;
|
||||||
tabs: ResolvedGiftingTab[];
|
tabs: ResolvedGiftingTab[];
|
||||||
hampers: Product[];
|
hampers: Product[];
|
||||||
seasonal: typeof defaultSeasonal;
|
seasonal: typeof defaultSeasonal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The /gifting section headings — PB-editable, bundled fallbacks below. */
|
||||||
|
export type GiftingSections = {
|
||||||
|
tabs: { eyebrow: string; title: string; script: string };
|
||||||
|
hampers: { title: string; script: string };
|
||||||
|
seasonal: { eyebrow: string; title: string; script: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackSections: GiftingSections = {
|
||||||
|
tabs: { eyebrow: "Who is it for?", title: "Gifts,", script: "price by price" },
|
||||||
|
hampers: { title: "Hampers &", script: "boxes" },
|
||||||
|
seasonal: {
|
||||||
|
eyebrow: "The Seasonal Edit",
|
||||||
|
title: "Chocolate for the",
|
||||||
|
script: "calendar",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const fallbackTabs = (): ResolvedGiftingTab[] =>
|
const fallbackTabs = (): ResolvedGiftingTab[] =>
|
||||||
defaultGiftingTabs.map((t) => ({
|
defaultGiftingTabs.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
@@ -242,6 +260,24 @@ export const getGiftingPageData = createServerFn({ method: "GET" }).handler(
|
|||||||
lead: cmsHero.heroLead ?? fallbackHero.lead,
|
lead: cmsHero.heroLead ?? fallbackHero.lead,
|
||||||
}
|
}
|
||||||
: fallbackHero,
|
: fallbackHero,
|
||||||
|
sections: cmsHero
|
||||||
|
? {
|
||||||
|
tabs: {
|
||||||
|
eyebrow: cmsHero.tabsEyebrow ?? fallbackSections.tabs.eyebrow,
|
||||||
|
title: cmsHero.tabsTitle ?? fallbackSections.tabs.title,
|
||||||
|
script: cmsHero.tabsScript ?? fallbackSections.tabs.script,
|
||||||
|
},
|
||||||
|
hampers: {
|
||||||
|
title: cmsHero.hampersTitle ?? fallbackSections.hampers.title,
|
||||||
|
script: cmsHero.hampersScript ?? fallbackSections.hampers.script,
|
||||||
|
},
|
||||||
|
seasonal: {
|
||||||
|
eyebrow: cmsHero.seasonalEyebrow ?? fallbackSections.seasonal.eyebrow,
|
||||||
|
title: cmsHero.seasonalTitle ?? fallbackSections.seasonal.title,
|
||||||
|
script: cmsHero.seasonalScript ?? fallbackSections.seasonal.script,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: fallbackSections,
|
||||||
tabs: cmsTabs ? await resolveTabs(cmsTabs) : fallbackTabs(),
|
tabs: cmsTabs ? await resolveTabs(cmsTabs) : fallbackTabs(),
|
||||||
hampers,
|
hampers,
|
||||||
seasonal: defaultSeasonal,
|
seasonal: defaultSeasonal,
|
||||||
@@ -263,7 +299,11 @@ export type CollectionPageData = {
|
|||||||
export const getCollectionPage = createServerFn({ method: "GET" })
|
export const getCollectionPage = createServerFn({ method: "GET" })
|
||||||
.validator((handle: string) => handle)
|
.validator((handle: string) => handle)
|
||||||
.handler(async ({ data: handle }): Promise<CollectionPageData | null> => {
|
.handler(async ({ data: handle }): Promise<CollectionPageData | null> => {
|
||||||
const shop = await getShopifyCollection(handle, 50);
|
const [shop, rows] = await Promise.all([
|
||||||
|
getShopifyCollection(handle, 50),
|
||||||
|
getCmsCollectionRows(),
|
||||||
|
]);
|
||||||
|
const row = rows?.find((r) => r.shopifyHandle === handle);
|
||||||
if (!shop) {
|
if (!shop) {
|
||||||
// graceful fallback for the handful of curated handles
|
// graceful fallback for the handful of curated handles
|
||||||
const def = defaultCollections.find((c) =>
|
const def = defaultCollections.find((c) =>
|
||||||
@@ -282,7 +322,7 @@ export const getCollectionPage = createServerFn({ method: "GET" })
|
|||||||
return {
|
return {
|
||||||
handle,
|
handle,
|
||||||
title: shop.title,
|
title: shop.title,
|
||||||
description: shop.description,
|
description: shop.description || row?.blurb || "",
|
||||||
image: shop.image,
|
image: shop.image,
|
||||||
products: shop.products.map(toProduct),
|
products: shop.products.map(toProduct),
|
||||||
};
|
};
|
||||||
@@ -307,6 +347,48 @@ export const getAnnouncement = createServerFn({ method: "GET" }).handler(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* ── homepage editorial copy (PB `site_settings`, bundled fallbacks) ── */
|
||||||
|
|
||||||
|
export type HomeCopy = {
|
||||||
|
seasonal: { eyebrow: string; title: string; script: string };
|
||||||
|
universe: { eyebrow: string; title: string; script: string; cta: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackHomeCopy: HomeCopy = {
|
||||||
|
seasonal: {
|
||||||
|
eyebrow: "The Seasonal Edit",
|
||||||
|
title: "Chocolate for the",
|
||||||
|
script: "calendar",
|
||||||
|
},
|
||||||
|
universe: {
|
||||||
|
eyebrow: "Our World",
|
||||||
|
title: "Discover the universe of",
|
||||||
|
script: "Mozimo",
|
||||||
|
cta: "Explore our world",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHomeCopy = createServerFn({ method: "GET" }).handler(
|
||||||
|
async (): Promise<HomeCopy> => {
|
||||||
|
const settings = await getCmsSiteSettings();
|
||||||
|
if (!settings) return fallbackHomeCopy;
|
||||||
|
const pick = (key: string, fb: string) => settings[key]?.trim() || fb;
|
||||||
|
return {
|
||||||
|
seasonal: {
|
||||||
|
eyebrow: pick("home_seasonal_eyebrow", fallbackHomeCopy.seasonal.eyebrow),
|
||||||
|
title: pick("home_seasonal_title", fallbackHomeCopy.seasonal.title),
|
||||||
|
script: pick("home_seasonal_script", fallbackHomeCopy.seasonal.script),
|
||||||
|
},
|
||||||
|
universe: {
|
||||||
|
eyebrow: pick("home_universe_eyebrow", fallbackHomeCopy.universe.eyebrow),
|
||||||
|
title: pick("home_universe_title", fallbackHomeCopy.universe.title),
|
||||||
|
script: pick("home_universe_script", fallbackHomeCopy.universe.script),
|
||||||
|
cta: pick("home_universe_cta", fallbackHomeCopy.universe.cta),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/* ── predictive search ─────────────────────────────────────── */
|
/* ── predictive search ─────────────────────────────────────── */
|
||||||
|
|
||||||
export type SearchResults = {
|
export type SearchResults = {
|
||||||
|
|||||||
Reference in New Issue
Block a user