@sprintup-cms/sdk 1.8.57 → 1.8.62

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 CHANGED
@@ -17,7 +17,7 @@ Official SDK for **SprintUp Forge CMS** — typed API client, Next.js App Router
17
17
  4. [Verify your connection](#verify-your-connection)
18
18
  5. [Adding new pages](#adding-new-pages)
19
19
  6. [Custom page layouts](#custom-page-layouts)
20
- 7. [Navigation and footer](#navigation-and-footer)
20
+ 7. [Navigation and footer](#navigation-and-footer) — automatically rendered by the SDK via CMS Globals
21
21
  8. [Extending with custom blocks](#extending-with-custom-blocks)
22
22
  9. [ISR and caching strategy](#isr-and-caching-strategy)
23
23
  10. [API reference](#api-reference)
@@ -288,48 +288,60 @@ export default async function Page({ params }: { params: Promise<{ slug: string[
288
288
 
289
289
  ## Navigation and footer
290
290
 
291
- Pull live nav and footer data from the CMS site structure:
291
+ > **As of v1.8.62**, navigation and footer are managed as **CMS Globals** — configured entirely in the CMS Admin under **Globals → Navigation** and **Globals → Footer**. You do not need to write any header or footer code. The SDK renders them automatically.
292
292
 
293
- ```tsx
294
- // components/layout/header.tsx
295
- import { cmsClient } from '@sprintup-cms/sdk'
296
- import type { CMSMenuItem } from '@sprintup-cms/sdk'
297
- import Link from 'next/link'
293
+ ### How it works
298
294
 
299
- export async function Header() {
300
- const structure = await cmsClient.getSiteStructure()
301
- const navItems: CMSMenuItem[] = structure?.menus.header ?? []
295
+ `CMSCatchAllPage` (the SDK's catch-all page handler) automatically fetches the active globals for your app and renders:
296
+ - `<CMSHeader>` a sticky top nav bar with your logo, nav links, and CTA buttons
297
+ - `<CMSFooter>` a configurable section grid with optional footer bottom bar
302
298
 
303
- return (
304
- <header className="border-b">
305
- <nav className="max-w-5xl mx-auto px-6 py-4 flex gap-6">
306
- {navItems.map(item => (
307
- <Link key={item.id} href={item.href} target={item.openInNewTab ? '_blank' : undefined}>
308
- {item.label}
309
- </Link>
310
- ))}
311
- </nav>
312
- </header>
313
- )
314
- }
315
- ```
299
+ **You do not need to create `components/layout/header.tsx` or `components/layout/footer.tsx`.**
300
+
301
+ ### Configuring navigation in the CMS
302
+
303
+ Go to **CMS Admin Globals → Navigation** and add items:
304
+
305
+ | Item type | Rendered as | Options |
306
+ |---|---|---|
307
+ | `link` | Nav link in the centre area | Label, URL, open in new tab |
308
+ | `button` | CTA button, right-aligned | Label, URL, variant: `primary` / `outline` / `ghost` |
309
+ | `dropdown` | Link with children | Label, URL, child links |
310
+
311
+ ### Configuring footer in the CMS
312
+
313
+ Go to **CMS Admin → Globals → Footer** and add sections in any order:
314
+
315
+ | Section type | Description |
316
+ |---|---|
317
+ | `brand` | Logo image and tagline |
318
+ | `links` | A column of links with a custom title — add as many as you need |
319
+ | `contact` | Email, phone, address with a custom section title |
320
+ | `social` | Facebook, X, Instagram, LinkedIn, YouTube icons |
321
+
322
+ Optionally enable **Footer Bottom** (a full-width legal bar):
323
+ - Copyright text
324
+ - Legal links (Privacy Policy, Terms, etc.)
325
+
326
+ ### Links: internal vs external
327
+
328
+ When adding links inside footer sections, the link input auto-searches your published CMS pages as you type. Select a page to create an internal link, or type a full URL (`https://...`) for an external link. External links open in a new tab automatically.
329
+
330
+ ### Custom layout: accessing globals manually
331
+
332
+ If you are building a custom page outside `CMSCatchAllPage`, you can access globals directly:
316
333
 
317
334
  ```tsx
318
- // app/layout.tsx
319
- import { Header } from '@/components/layout/header'
335
+ // Only needed for custom layouts — CMSCatchAllPage handles this automatically
336
+ import { cmsClient } from '@sprintup-cms/sdk'
320
337
 
321
- export default function RootLayout({ children }: { children: React.ReactNode }) {
322
- return (
323
- <html lang="en">
324
- <body>
325
- <Header />
326
- {children}
327
- </body>
328
- </html>
329
- )
330
- }
338
+ const globals = await cmsClient.getGlobals()
339
+ const nav = globals?.nav // CMSPage with sectionData.items[]
340
+ const footer = globals?.footer // CMSPage with sectionData.sections[]
331
341
  ```
332
342
 
343
+ > **Deprecated:** `cmsClient.getSiteStructure()` and `menus.header` / `menus.footer` are the old approach and should not be used for navigation or footer.
344
+
333
345
  ---
334
346
 
335
347
  ## Extending with custom blocks
@@ -370,9 +382,10 @@ The `custom` prop is a `Record<blockType, (block: CMSBlock) => React.ReactNode>`
370
382
  | Individual page | 60 seconds | `cms-page-{slug}` |
371
383
  | All pages list | 60 seconds | `cms-pages-{appId}` |
372
384
  | Page type schema | 3600 seconds | `cms-page-type-{id}` |
373
- | Site structure (nav/footer) | 300 seconds | `site-structure-{appId}` |
385
+ | Globals (nav + footer) | 60 seconds | `cms-globals-{appId}` |
374
386
  | Sitemap | 3600 seconds | — |
375
387
  | Status check | No cache | — |
388
+ | ~~Site structure~~ | ~~300 seconds~~ | Deprecated — use Globals |
376
389
 
377
390
  The revalidation webhook calls `revalidateTag('cms-page-{slug}')` and `revalidatePath('/{slug}')` when the CMS publishes a page, giving you **instant updates** without a full rebuild.
378
391
 
@@ -390,11 +403,12 @@ The revalidation webhook calls `revalidateTag('cms-page-{slug}')` and `revalidat
390
403
  | `getEvents()` | Shorthand for `getPages({ type: 'event-page' })` |
391
404
  | `getAnnouncements()` | Shorthand for `getPages({ type: 'announcement-page' })` |
392
405
  | `getPageType(id)` | Fetch a page type schema by ID |
393
- | `getSiteStructure()` | Fetch nav, footer, and page tree |
406
+ | `getGlobals()` | Fetch navigation and footer globals — returns `{ nav, footer }`. CMSCatchAllPage calls this automatically. |
394
407
  | `getPreviewPage(token)` | Fetch a draft page for preview mode |
395
408
  | `getPageWithPreview(slug, token?)` | Fetch page — preview if token present, live otherwise |
396
409
  | `getSitemap()` | Fetch all published slugs with sitemap metadata |
397
410
  | `getStatus()` | Connectivity check — returns counts and page list |
411
+ | ~~`getSiteStructure()`~~ | **Deprecated.** Used the old Site Structure editor. Navigation and footer are now CMS Globals — use `getGlobals()` or rely on CMSCatchAllPage. |
398
412
 
399
413
  ---
400
414
 
@@ -439,6 +453,13 @@ The revalidation webhook calls `revalidateTag('cms-page-{slug}')` and `revalidat
439
453
  - `CMS_WEBHOOK_SECRET` on both sides must match exactly.
440
454
  - Check your deployment logs for `[sprintup-cms] revalidate error:` messages.
441
455
 
456
+ **Navigation or footer not showing**
457
+
458
+ - Go to **CMS Admin → Globals → Navigation** (or Footer) and check that content is published (status = Published).
459
+ - Ensure at least one item/section is added. An empty globals document renders nothing.
460
+ - Changes are cached for 60 seconds. Hard-refresh or wait a moment after publishing.
461
+ - Do not use `getSiteStructure()` — it does not return globals data.
462
+
442
463
  **Preview mode not working**
443
464
 
444
465
  - The `/api/cms-preview/exit` route must exist.
@@ -1151,100 +1151,154 @@ function ServerProductListBlock({ block }) {
1151
1151
  }) })
1152
1152
  ] }) });
1153
1153
  }
1154
- function parseNavItems(html) {
1155
- if (!html) return [];
1156
- const text = html.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ");
1157
- return text.split(/\r?\n/).filter(Boolean).map((line) => {
1158
- const [label, url] = line.split("|").map((s) => s.trim());
1159
- return { label: label || "", url: url || "#" };
1160
- });
1154
+ function getSD(doc) {
1155
+ const structuredBlock = Array.isArray(doc?.blocks) && doc.blocks[0]?.type === "__structured__" ? doc.blocks[0].content : null;
1156
+ return doc?.sectionData ?? structuredBlock ?? null;
1161
1157
  }
1162
1158
  function CMSHeader({ nav }) {
1163
- if (!nav) return null;
1164
- const structuredBlock = Array.isArray(nav.blocks) && nav.blocks[0]?.type === "__structured__" ? nav.blocks[0].content : null;
1165
- const sd = nav.sectionData ?? structuredBlock;
1159
+ const sd = getSD(nav);
1166
1160
  if (!sd) return null;
1167
- const primary = sd.primary ?? {};
1168
- const navItems = parseNavItems(primary.nav_items || "");
1169
- const logoUrl = primary.logo_url || "";
1170
- const logoAlt = primary.logo_alt || "";
1171
- if (!logoUrl && !logoAlt && navItems.length === 0) return null;
1172
- return /* @__PURE__ */ jsxRuntime.jsx("header", { className: "w-full border-b border-border bg-background/95 backdrop-blur sticky top-0 z-50", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-6xl mx-auto px-4 h-16 flex items-center justify-between gap-4", children: [
1173
- /* @__PURE__ */ jsxRuntime.jsxs("a", { href: "/", className: "flex items-center gap-2 shrink-0", children: [
1174
- logoUrl && /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: logoAlt || "Logo", className: "h-8 w-auto" }),
1175
- !logoUrl && logoAlt && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold text-base", children: logoAlt })
1176
- ] }),
1177
- navItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "hidden md:flex items-center gap-6", children: navItems.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: item.url, className: "text-sm text-muted-foreground hover:text-foreground transition-colors", children: item.label }, i)) })
1161
+ const logoUrl = sd.logo_url?.trim() || "";
1162
+ const logoAlt = sd.logo_alt?.trim() || "";
1163
+ const items = Array.isArray(sd.items) ? sd.items : [];
1164
+ if (!logoUrl && !logoAlt && items.length === 0) return null;
1165
+ const links = items.filter((i) => i.type === "link" || i.type === "dropdown");
1166
+ const buttons = items.filter((i) => i.type === "button");
1167
+ const btnBase = { textDecoration: "none", padding: "0.4rem 1rem", borderRadius: "6px", fontWeight: 600, fontSize: "0.875rem", whiteSpace: "nowrap", transition: "opacity 0.15s" };
1168
+ const btnVariant = (v) => ({
1169
+ primary: { ...btnBase, background: "var(--foreground, #111)", color: "#fff", border: "1px solid transparent" },
1170
+ outline: { ...btnBase, background: "transparent", color: "var(--foreground, #111)", border: "1px solid var(--border, #e5e7eb)" },
1171
+ ghost: { ...btnBase, background: "transparent", color: "var(--muted-foreground, #6b7280)", border: "none", fontWeight: 500 }
1172
+ })[v] ?? btnBase;
1173
+ return /* @__PURE__ */ jsxRuntime.jsx("header", { style: { position: "sticky", top: 0, zIndex: 50, width: "100%", borderBottom: "1px solid var(--border, #e5e7eb)", background: "rgba(255,255,255,0.97)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { maxWidth: "1200px", margin: "0 auto", padding: "0 1.5rem", height: "64px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "2rem" }, children: [
1174
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: "/", style: { display: "flex", alignItems: "center", gap: "0.5rem", textDecoration: "none", flexShrink: 0 }, children: logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: logoAlt || "Logo", style: { height: "32px", width: "auto", display: "block" } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 700, fontSize: "1.125rem", color: "var(--foreground, #111)" }, children: logoAlt }) }),
1175
+ links.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("nav", { style: { display: "flex", alignItems: "center", gap: "1.75rem", flex: 1 }, children: links.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
1176
+ "a",
1177
+ {
1178
+ href: item.url || "#",
1179
+ target: item.openInNewTab ? "_blank" : void 0,
1180
+ rel: item.openInNewTab ? "noopener noreferrer" : void 0,
1181
+ style: { fontSize: "0.9rem", color: "var(--muted-foreground, #6b7280)", textDecoration: "none", fontWeight: 500 },
1182
+ children: item.label
1183
+ },
1184
+ item.id
1185
+ )) }),
1186
+ buttons.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", gap: "0.625rem", flexShrink: 0 }, children: buttons.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
1187
+ "a",
1188
+ {
1189
+ href: item.url || "#",
1190
+ target: item.openInNewTab ? "_blank" : void 0,
1191
+ rel: item.openInNewTab ? "noopener noreferrer" : void 0,
1192
+ style: btnVariant(item.variant || "ghost"),
1193
+ children: item.label
1194
+ },
1195
+ item.id
1196
+ )) })
1178
1197
  ] }) });
1179
1198
  }
1180
- function parseFooterColumns(html) {
1181
- if (!html) return [];
1182
- const text = html.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ");
1183
- const lines = text.split(/\r?\n/).filter(Boolean);
1184
- const cols = [];
1185
- let current = null;
1186
- for (const line of lines) {
1187
- const parts = line.split("|").map((s) => s.trim());
1188
- if (parts.length === 1) {
1189
- if (current) cols.push(current);
1190
- current = { title: parts[0], links: [] };
1191
- } else if (parts.length >= 2) {
1192
- if (!current) current = { title: "", links: [] };
1193
- current.links.push({ label: parts[0], url: parts[1] || "#" });
1194
- }
1199
+ var SOCIAL_ICONS = [
1200
+ { key: "facebook", label: "Facebook", d: "M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" },
1201
+ { key: "twitter", label: "X", d: "M18 6L6 18M6 6l12 12" },
1202
+ { key: "instagram", label: "Instagram", d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37zm1.5-4.87h.01M6.5 2h11A4.5 4.5 0 0 1 22 6.5v11A4.5 4.5 0 0 1 17.5 22h-11A4.5 4.5 0 0 1 2 17.5v-11A4.5 4.5 0 0 1 6.5 2z" },
1203
+ { key: "linkedin", label: "LinkedIn", d: "M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-4 0v7h-4v-7a6 6 0 0 1 6-6zM2 9h4v12H2zm2-3a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" },
1204
+ { key: "youtube", label: "YouTube", d: "M22.54 6.42a2.78 2.78 0 0 0-1.94-1.96C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 1.96A29 29 0 0 0 1 12a29 29 0 0 0 .46 5.58 2.78 2.78 0 0 0 1.94 1.96C5.12 20 12 20 12 20s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-1.96A29 29 0 0 0 23 12a29 29 0 0 0-.46-5.58zM9.75 15.02V8.98L15.5 12l-5.75 3.02z" }
1205
+ ];
1206
+ function renderFooterSection(sec, mutedLink) {
1207
+ if (sec.type === "brand") {
1208
+ if (!sec.logo_url && !sec.tagline) return null;
1209
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
1210
+ sec.logo_url && /* @__PURE__ */ jsxRuntime.jsx("img", { src: sec.logo_url, alt: "Logo", style: { height: "32px", width: "auto", objectFit: "contain" } }),
1211
+ sec.tagline && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.875rem", color: "var(--muted-foreground, #6b7280)", lineHeight: 1.6, margin: 0 }, children: sec.tagline })
1212
+ ] });
1195
1213
  }
1196
- if (current) cols.push(current);
1197
- return cols;
1198
- }
1199
- function parseLegalLinks(html) {
1200
- if (!html) return [];
1201
- const text = html.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ");
1202
- return text.split(/\r?\n/).filter(Boolean).map((line) => {
1203
- const [label, url] = line.split("|").map((s) => s.trim());
1204
- return { label: label || "", url: url || "#" };
1205
- });
1214
+ if (sec.type === "links") {
1215
+ const links = (sec.links || []).filter((l) => l.label);
1216
+ if (!links.length && !sec.title) return null;
1217
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1218
+ sec.title && /* @__PURE__ */ jsxRuntime.jsx("h4", { style: { fontWeight: 600, fontSize: "0.875rem", margin: "0 0 0.875rem", color: "var(--foreground, #111)" }, children: sec.title }),
1219
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: "0.5rem" }, children: links.map((link) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
1220
+ "a",
1221
+ {
1222
+ href: link.url || "#",
1223
+ target: link.external ? "_blank" : void 0,
1224
+ rel: link.external ? "noopener noreferrer" : void 0,
1225
+ style: mutedLink,
1226
+ children: link.label
1227
+ }
1228
+ ) }, link.id || link.label)) })
1229
+ ] });
1230
+ }
1231
+ if (sec.type === "contact") {
1232
+ if (!sec.email && !sec.phone && !sec.address) return null;
1233
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1234
+ sec.title && /* @__PURE__ */ jsxRuntime.jsx("h4", { style: { fontWeight: 600, fontSize: "0.875rem", margin: "0 0 0.875rem", color: "var(--foreground, #111)" }, children: sec.title }),
1235
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.4rem" }, children: [
1236
+ sec.email && /* @__PURE__ */ jsxRuntime.jsx("a", { href: `mailto:${sec.email}`, style: mutedLink, children: sec.email }),
1237
+ sec.phone && /* @__PURE__ */ jsxRuntime.jsx("a", { href: `tel:${sec.phone}`, style: mutedLink, children: sec.phone }),
1238
+ sec.address && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { ...mutedLink, whiteSpace: "pre-line", margin: 0 }, children: sec.address })
1239
+ ] })
1240
+ ] });
1241
+ }
1242
+ if (sec.type === "social") {
1243
+ const active = SOCIAL_ICONS.filter((s) => sec[s.key]?.trim());
1244
+ if (!active.length) return null;
1245
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1246
+ sec.title && /* @__PURE__ */ jsxRuntime.jsx("h4", { style: { fontWeight: 600, fontSize: "0.875rem", margin: "0 0 0.875rem", color: "var(--foreground, #111)" }, children: sec.title }),
1247
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "0.625rem" }, children: active.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
1248
+ "a",
1249
+ {
1250
+ href: sec[s.key],
1251
+ target: "_blank",
1252
+ rel: "noopener noreferrer",
1253
+ "aria-label": s.label,
1254
+ style: { display: "flex", alignItems: "center", justifyContent: "center", width: "34px", height: "34px", borderRadius: "8px", border: "1px solid var(--border, #e5e7eb)", color: "var(--muted-foreground, #6b7280)", textDecoration: "none" },
1255
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: s.d }) })
1256
+ },
1257
+ s.key
1258
+ )) })
1259
+ ] });
1260
+ }
1261
+ return null;
1206
1262
  }
1207
1263
  function CMSFooter({ footer }) {
1208
- if (!footer) return null;
1209
- const structuredBlock = Array.isArray(footer.blocks) && footer.blocks[0]?.type === "__structured__" ? footer.blocks[0].content : null;
1210
- const sd = footer.sectionData ?? structuredBlock;
1264
+ const sd = getSD(footer);
1211
1265
  if (!sd) return null;
1212
- const c = sd.content ?? {};
1213
- const contact = sd.contact ?? {};
1214
- const social = sd.social ?? {};
1215
- const legal = sd.legal ?? {};
1216
- const columns = parseFooterColumns(c.columns || "");
1217
- const legalLinks = parseLegalLinks(legal.legal_links || "");
1218
- const hasSocial = social.facebook || social.twitter || social.instagram || social.linkedin || social.youtube;
1219
- const isEmpty = !c.tagline && !c.logo_url && columns.length === 0 && !contact.email && !contact.phone && !hasSocial && !legal.copyright && legalLinks.length === 0;
1220
- if (isEmpty) return null;
1221
- return /* @__PURE__ */ jsxRuntime.jsx("footer", { className: "border-t border-border bg-muted/30 pt-12 pb-6 mt-16", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-6xl mx-auto px-4", children: [
1222
- (c.tagline || c.logo_url || columns.length > 0 || contact.email) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `grid gap-8 mb-10 grid-cols-1 sm:grid-cols-2 ${columns.length > 0 ? "lg:grid-cols-4" : "lg:grid-cols-2"}`, children: [
1223
- (c.logo_url || c.tagline) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1224
- c.logo_url && /* @__PURE__ */ jsxRuntime.jsx("img", { src: c.logo_url, alt: "Logo", className: "h-8 w-auto" }),
1225
- c.tagline && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground leading-relaxed", children: c.tagline })
1226
- ] }),
1227
- columns.map((col, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1228
- col.title && /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-semibold text-sm mb-3", children: col.title }),
1229
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-2", children: col.links.map((link, j) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: link.url, className: "text-sm text-muted-foreground hover:text-foreground transition-colors", children: link.label }) }, j)) })
1230
- ] }, i)),
1231
- (contact.email || contact.phone || contact.address) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1232
- /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-semibold text-sm mb-3", children: "Contact" }),
1233
- contact.email && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: contact.email }),
1234
- contact.phone && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: contact.phone }),
1235
- contact.address && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground whitespace-pre-line", children: contact.address })
1236
- ] })
1237
- ] }),
1238
- hasSocial && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-4 mb-6", children: [
1239
- social.facebook && /* @__PURE__ */ jsxRuntime.jsx("a", { href: social.facebook, className: "text-sm text-muted-foreground hover:text-foreground transition-colors", children: "Facebook" }),
1240
- social.twitter && /* @__PURE__ */ jsxRuntime.jsx("a", { href: social.twitter, className: "text-sm text-muted-foreground hover:text-foreground transition-colors", children: "X" }),
1241
- social.instagram && /* @__PURE__ */ jsxRuntime.jsx("a", { href: social.instagram, className: "text-sm text-muted-foreground hover:text-foreground transition-colors", children: "Instagram" }),
1242
- social.linkedin && /* @__PURE__ */ jsxRuntime.jsx("a", { href: social.linkedin, className: "text-sm text-muted-foreground hover:text-foreground transition-colors", children: "LinkedIn" }),
1243
- social.youtube && /* @__PURE__ */ jsxRuntime.jsx("a", { href: social.youtube, className: "text-sm text-muted-foreground hover:text-foreground transition-colors", children: "YouTube" })
1244
- ] }),
1245
- (legal.copyright || legalLinks.length > 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4 pt-6 border-t border-border", children: [
1246
- legal.copyright && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: legal.copyright }),
1247
- legalLinks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-4", children: legalLinks.map((link, i) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: link.url, className: "text-xs text-muted-foreground hover:text-foreground transition-colors", children: link.label }, i)) })
1266
+ const sections = Array.isArray(sd.sections) ? sd.sections : [];
1267
+ let renderSections = sections;
1268
+ if (renderSections.length === 0 && (sd.brand || sd.columns || sd.contact || sd.social || sd.legal)) {
1269
+ renderSections = [];
1270
+ if (sd.brand?.logo_url || sd.brand?.tagline) renderSections.push({ id: "b", type: "brand", ...sd.brand });
1271
+ (sd.columns || []).forEach((col) => renderSections.push({ id: col.id || col.title, type: "links", title: col.title, links: col.links }));
1272
+ if (sd.contact?.email || sd.contact?.phone) renderSections.push({ id: "c", type: "contact", ...sd.contact });
1273
+ if (Object.values(sd.social || {}).some(Boolean)) renderSections.push({ id: "s", type: "social", ...sd.social });
1274
+ }
1275
+ const legalSec = sd.sections ? sd.sections.find((s) => s.type === "legal") : sd.legal;
1276
+ const mainSections = renderSections.filter((s) => s.type !== "legal");
1277
+ if (mainSections.length === 0 && !legalSec) return null;
1278
+ const gridCount = mainSections.length;
1279
+ const gridCols = gridCount <= 1 ? "1fr" : gridCount === 2 ? "repeat(2,1fr)" : gridCount === 3 ? "repeat(3,1fr)" : "repeat(4,1fr)";
1280
+ const mutedLink = { fontSize: "0.875rem", color: "var(--muted-foreground, #6b7280)", textDecoration: "none" };
1281
+ const copyright = legalSec?.copyright || "";
1282
+ const legalLinks = legalSec?.legal_links || legalSec?.links || [];
1283
+ const hasBottom = copyright || legalLinks.filter((l) => l.label).length > 0;
1284
+ return /* @__PURE__ */ jsxRuntime.jsx("footer", { style: { borderTop: "1px solid var(--border, #e5e7eb)", background: "var(--muted, #f9fafb)", marginTop: "4rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { maxWidth: "1200px", margin: "0 auto", padding: "3rem 1.5rem 1.5rem" }, children: [
1285
+ mainSections.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gridTemplateColumns: gridCols, gap: "2.5rem", marginBottom: hasBottom ? "2.5rem" : 0 }, children: mainSections.map((sec) => {
1286
+ const rendered = renderFooterSection(sec, mutedLink);
1287
+ return rendered ? /* @__PURE__ */ jsxRuntime.jsx("div", { children: rendered }, sec.id) : null;
1288
+ }) }),
1289
+ hasBottom && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderTop: "1px solid var(--border, #e5e7eb)", paddingTop: "1.25rem", display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "space-between", gap: "1rem" }, children: [
1290
+ copyright && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: "var(--muted-foreground, #6b7280)", margin: 0 }, children: copyright }),
1291
+ legalLinks.filter((l) => l.label).length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "1.25rem" }, children: legalLinks.filter((l) => l.label).map((link) => /* @__PURE__ */ jsxRuntime.jsx(
1292
+ "a",
1293
+ {
1294
+ href: link.url || "#",
1295
+ target: link.external ? "_blank" : void 0,
1296
+ rel: link.external ? "noopener noreferrer" : void 0,
1297
+ style: { fontSize: "0.8rem", color: "var(--muted-foreground, #6b7280)", textDecoration: "none" },
1298
+ children: link.label
1299
+ },
1300
+ link.id || link.label
1301
+ )) })
1248
1302
  ] })
1249
1303
  ] }) });
1250
1304
  }