diff --git a/scripts/pb-seed.mjs b/scripts/pb-seed.mjs index d4e1ef0..ed6b681 100644 --- a/scripts/pb-seed.mjs +++ b/scripts/pb-seed.mjs @@ -205,6 +205,15 @@ const COLLECTION_ROWS = [ sort: 7, 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 = [ @@ -214,6 +223,14 @@ const GIFTING_PAGE = [ hero_script: "giving", hero_lead: "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, }, ]; @@ -296,6 +313,14 @@ const SCHEMAS = { { name: "hero_title", type: "text" }, { name: "hero_script", 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" }, ], }, @@ -308,6 +333,41 @@ const SITE_SETTINGS = [ "Complimentary delivery in Chandigarh Tricity on orders above ₹1,500 · Intercity shipped at actuals", 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 = [ @@ -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; } for (const record of SEEDS[name]) { diff --git a/src/app/gifting.tsx b/src/app/gifting.tsx index bccaf13..df7889d 100644 --- a/src/app/gifting.tsx +++ b/src/app/gifting.tsx @@ -14,7 +14,8 @@ export const Route = createFileRoute("/gifting")({ }); function GiftingPage() { - const { hero, tabs, hampers, seasonal } = Route.useLoaderData(); + const { hero, sections, tabs, hampers, seasonal } = + Route.useLoaderData(); return (
@@ -51,7 +52,7 @@ function GiftingPage() { >
-

Who is it for?

+

{sections.tabs.eyebrow}

@@ -71,13 +72,15 @@ function GiftingPage() {

- Hampers &{" "} - boxes + {sections.hampers.title}{" "} + + {sections.hampers.script} +

Full gifting collection @@ -118,12 +121,16 @@ function GiftingPage() {
-

The Seasonal Edit

+

+ {sections.seasonal.eyebrow} +

- Chocolate for the{" "} - calendar + {sections.seasonal.title}{" "} + + {sections.seasonal.script} +

diff --git a/src/app/index.tsx b/src/app/index.tsx index d7fa58c..4dfeda8 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -15,8 +15,12 @@ import SeasonalTeaser from "@/components/sections/SeasonalTeaser"; import ExperiencesTeaser from "@/components/sections/ExperiencesTeaser"; import JournalTeaser from "@/components/sections/JournalTeaser"; 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 => getHomeCopy(), +}); const TICKER = [ "Single Origin", @@ -28,6 +32,7 @@ const TICKER = [ ]; function Home() { + const copy = Route.useLoaderData(); return (
@@ -37,12 +42,12 @@ function Home() { - + - + diff --git a/src/components/sections/SeasonalTeaser.tsx b/src/components/sections/SeasonalTeaser.tsx index c43a6d4..4be3600 100644 --- a/src/components/sections/SeasonalTeaser.tsx +++ b/src/components/sections/SeasonalTeaser.tsx @@ -1,19 +1,27 @@ import Reveal from "@/components/Reveal"; 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 (
-

The Seasonal Edit

+

{eyebrow}

- Chocolate for the{" "} - calendar + {title}{" "} + {script}

diff --git a/src/components/sections/UniverseSection.tsx b/src/components/sections/UniverseSection.tsx index cafbff3..7dae279 100644 --- a/src/components/sections/UniverseSection.tsx +++ b/src/components/sections/UniverseSection.tsx @@ -1,4 +1,5 @@ import Reveal from "@/components/Reveal"; +import type { HomeCopy } from "@/lib/content"; /** * "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 * 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 (
@@ -21,11 +30,11 @@ export default function UniverseSection() { /> - Our World + {eyebrow} - Discover the universe of{" "} + {title}{" "} - Mozimo + {script} diff --git a/src/lib/cms.ts b/src/lib/cms.ts index 6ee91d5..133bbb0 100644 --- a/src/lib/cms.ts +++ b/src/lib/cms.ts @@ -161,6 +161,14 @@ export type CmsGiftingPage = { heroTitle?: string; heroScript?: string; heroLead?: string; + tabsEyebrow?: string; + tabsTitle?: string; + tabsScript?: string; + hampersTitle?: string; + hampersScript?: string; + seasonalEyebrow?: string; + seasonalTitle?: string; + seasonalScript?: string; }; export async function getCmsGiftingPage(): Promise { @@ -172,6 +180,14 @@ export async function getCmsGiftingPage(): Promise { heroTitle: str(r.hero_title) || undefined, heroScript: str(r.hero_script) || 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, }; } diff --git a/src/lib/content.ts b/src/lib/content.ts index 0a8bbfe..1e186a0 100644 --- a/src/lib/content.ts +++ b/src/lib/content.ts @@ -113,11 +113,29 @@ export type ResolvedGiftingTab = Omit & { export type GiftingPageData = { hero: GiftingHero; + sections: GiftingSections; tabs: ResolvedGiftingTab[]; hampers: Product[]; 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[] => defaultGiftingTabs.map((t) => ({ ...t, @@ -242,6 +260,24 @@ export const getGiftingPageData = createServerFn({ method: "GET" }).handler( lead: cmsHero.heroLead ?? fallbackHero.lead, } : 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(), hampers, seasonal: defaultSeasonal, @@ -263,7 +299,11 @@ export type CollectionPageData = { export const getCollectionPage = createServerFn({ method: "GET" }) .validator((handle: string) => handle) .handler(async ({ data: handle }): Promise => { - 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) { // graceful fallback for the handful of curated handles const def = defaultCollections.find((c) => @@ -282,7 +322,7 @@ export const getCollectionPage = createServerFn({ method: "GET" }) return { handle, title: shop.title, - description: shop.description, + description: shop.description || row?.blurb || "", image: shop.image, 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 => { + 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 ─────────────────────────────────────── */ export type SearchResults = {