create-nextblock 0.16.1 → 0.16.3
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/docker-template/.dockerignore +1 -1
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -221
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +4 -4
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +4542 -2904
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/api/view/route.ts +114 -114
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +624 -618
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -123
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +251 -244
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +157 -140
- package/templates/nextblock-template/lib/seo/page-document.test.ts +384 -350
- package/templates/nextblock-template/lib/seo/page-document.ts +463 -412
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -22
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
|
@@ -1,612 +1,612 @@
|
|
|
1
|
-
// components/blocks/renderers/SectionBlockRenderer.tsx
|
|
2
|
-
import React from "react";
|
|
3
|
-
import Image from "next/image";
|
|
4
|
-
import type { SectionBlockContent } from "../../../lib/blocks/blockRegistry";
|
|
5
|
-
import { buildVisualEditAttributes } from "../../../lib/visual-editing/edit-info";
|
|
6
|
-
import type {
|
|
7
|
-
VisualEditAttributes,
|
|
8
|
-
VisualEditingDocumentContext,
|
|
9
|
-
} from "../../../lib/visual-editing/types";
|
|
10
|
-
import { getPublicBlockRendererLoader } from "../publicRendererLoaders";
|
|
11
|
-
import SectionSlider from "./SectionSlider";
|
|
12
|
-
import { getCachedCustomBlockDefinitionBySlug } from "../../../lib/custom-block-definitions";
|
|
13
|
-
import { CachedDynamicLayoutEngine } from "../../renderers/CachedDynamicLayoutEngine";
|
|
14
|
-
import { resolveBlockRelations } from "../../../lib/resolve-block-relations";
|
|
15
|
-
|
|
16
|
-
// Static imports for core block renderers for LCP/performance optimization
|
|
17
|
-
import TextBlockRenderer from "./TextBlockRenderer";
|
|
18
|
-
import HeadingBlockRenderer from "./HeadingBlockRenderer";
|
|
19
|
-
import ImageBlockRenderer from "./ImageBlockRenderer";
|
|
20
|
-
import ButtonBlockRenderer from "./ButtonBlockRenderer";
|
|
21
|
-
import { StockPhotoCredit } from "./StockPhotoCredit";
|
|
22
|
-
|
|
23
|
-
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
24
|
-
const BACKGROUND_COMPOSITING_CLASSES =
|
|
25
|
-
"isolate transform-gpu [backface-visibility:hidden] [transform-style:preserve-3d]";
|
|
26
|
-
const ABSOLUTE_BACKGROUND_CLASSES =
|
|
27
|
-
"pointer-events-none absolute inset-[-1px] -z-10 transform-gpu [backface-visibility:hidden] [transform-style:preserve-3d]";
|
|
28
|
-
const ECOMMERCE_BLOCK_TYPES = new Set([
|
|
29
|
-
"product_grid",
|
|
30
|
-
"featured_product",
|
|
31
|
-
"cart",
|
|
32
|
-
"checkout",
|
|
33
|
-
"product_details",
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
function loadEcommerceBlockRenderer(blockType: string) {
|
|
37
|
-
return import("../ecommerceRendererLoaders").then((module) =>
|
|
38
|
-
module.loadEcommerceBlockRenderer(blockType)
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type SectionBackgroundImage = NonNullable<
|
|
43
|
-
NonNullable<SectionBlockContent["background"]>["image"]
|
|
44
|
-
>;
|
|
45
|
-
|
|
46
|
-
// Renders a section's background image. External https URLs (e.g. AI-inserted
|
|
47
|
-
// stock photos) use a plain <img> so any allowlisted host works without Next
|
|
48
|
-
// image remotePatterns config; stored R2 media keeps the optimized next/image
|
|
49
|
-
// path. Returns null when neither a URL nor an object key is present.
|
|
50
|
-
function renderBackgroundImageElement(image: SectionBackgroundImage, priority: boolean) {
|
|
51
|
-
const externalUrl =
|
|
52
|
-
typeof image.external_url === "string" && /^https?:\/\//i.test(image.external_url.trim())
|
|
53
|
-
? image.external_url.trim()
|
|
54
|
-
: null;
|
|
55
|
-
const objectFit: "cover" | "contain" = image.size === "contain" ? "contain" : "cover";
|
|
56
|
-
const objectPosition = image.position || "center";
|
|
57
|
-
|
|
58
|
-
if (externalUrl) {
|
|
59
|
-
return (
|
|
60
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
61
|
-
<img
|
|
62
|
-
src={externalUrl}
|
|
63
|
-
alt={image.alt_text || ""}
|
|
64
|
-
loading={priority ? "eager" : "lazy"}
|
|
65
|
-
fetchPriority={priority ? "high" : "auto"}
|
|
66
|
-
decoding="async"
|
|
67
|
-
className="absolute inset-0 h-full w-full"
|
|
68
|
-
style={{ objectFit, objectPosition }}
|
|
69
|
-
/>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!image.object_key) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<Image
|
|
79
|
-
src={`${R2_BASE_URL}/${image.object_key}`}
|
|
80
|
-
alt={image.alt_text || ""}
|
|
81
|
-
fill
|
|
82
|
-
priority={priority}
|
|
83
|
-
fetchPriority={priority ? "high" : "auto"}
|
|
84
|
-
placeholder={image.blur_data_url ? "blur" : "empty"}
|
|
85
|
-
blurDataURL={image.blur_data_url || undefined}
|
|
86
|
-
quality={image.quality || 80}
|
|
87
|
-
sizes="100vw"
|
|
88
|
-
style={{ objectFit, objectPosition }}
|
|
89
|
-
/>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface SectionBlockRendererProps {
|
|
94
|
-
content: SectionBlockContent;
|
|
95
|
-
languageId: number;
|
|
96
|
-
visualEditAttributes?: VisualEditAttributes;
|
|
97
|
-
visualEditing?: VisualEditingDocumentContext;
|
|
98
|
-
parentBlockId?: number;
|
|
99
|
-
parentBlockIndex?: number;
|
|
100
|
-
blockType?: "section";
|
|
101
|
-
botProtectionPublic?: BotProtectionPublicSettings;
|
|
102
|
-
scriptNonce?: string;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
type BotProtectionPublicSettings = {
|
|
106
|
-
provider: 'none' | 'turnstile' | 'recaptcha';
|
|
107
|
-
siteKey: string;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// Container class mapping
|
|
111
|
-
const containerClasses = {
|
|
112
|
-
'full-width': 'w-full',
|
|
113
|
-
'container': 'container mx-auto px-4',
|
|
114
|
-
'container-sm': 'container mx-auto px-4 max-w-screen-sm',
|
|
115
|
-
'container-lg': 'container mx-auto px-4 max-w-screen-lg',
|
|
116
|
-
'container-xl': 'container mx-auto px-4 max-w-screen-xl'
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Column grid classes
|
|
120
|
-
const columnClasses = {
|
|
121
|
-
1: 'grid-cols-1',
|
|
122
|
-
2: 'grid-cols-1 md:grid-cols-2',
|
|
123
|
-
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
124
|
-
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Gap classes
|
|
128
|
-
const gapClasses = {
|
|
129
|
-
none: 'gap-0',
|
|
130
|
-
sm: 'gap-2',
|
|
131
|
-
md: 'gap-4',
|
|
132
|
-
lg: 'gap-6',
|
|
133
|
-
xl: 'gap-8'
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// Padding classes
|
|
137
|
-
const paddingClasses = {
|
|
138
|
-
none: '',
|
|
139
|
-
sm: 'py-2',
|
|
140
|
-
md: 'py-4',
|
|
141
|
-
lg: 'py-8',
|
|
142
|
-
xl: 'py-12'
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Vertical alignment classes
|
|
146
|
-
const verticalAlignmentClasses = {
|
|
147
|
-
start: 'items-start',
|
|
148
|
-
center: 'items-center',
|
|
149
|
-
end: 'items-end',
|
|
150
|
-
stretch: 'items-stretch'
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
function formatMinHeight(value?: string) {
|
|
154
|
-
if (!value) return undefined;
|
|
155
|
-
const trimmed = value.trim();
|
|
156
|
-
if (/^\d+$/.test(trimmed)) {
|
|
157
|
-
return `${trimmed}px`;
|
|
158
|
-
}
|
|
159
|
-
return trimmed;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function formatStopPosition(position: number) {
|
|
163
|
-
return Number.isInteger(position) ? String(position) : position.toFixed(1);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function formatGradientStops(stops: Array<{ color: string; position: number }>) {
|
|
167
|
-
return stops
|
|
168
|
-
.map((stop, index) => {
|
|
169
|
-
const previous = stops[index - 1];
|
|
170
|
-
const next = stops[index + 1];
|
|
171
|
-
let position = stop.position;
|
|
172
|
-
|
|
173
|
-
if (next && next.position === stop.position && stop.position > 0) {
|
|
174
|
-
position = stop.position - 0.1;
|
|
175
|
-
} else if (previous && previous.position === stop.position && stop.position < 100) {
|
|
176
|
-
position = stop.position + 0.1;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return `${stop.color} ${formatStopPosition(position)}%`;
|
|
180
|
-
})
|
|
181
|
-
.join(', ');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Background style generator (handles solid, theme and gradients; image handled in JSX)
|
|
185
|
-
function generateBackgroundStyles(background: SectionBlockContent['background']) {
|
|
186
|
-
const styles: React.CSSProperties = {};
|
|
187
|
-
let className = '';
|
|
188
|
-
|
|
189
|
-
switch (background.type) {
|
|
190
|
-
case 'theme': {
|
|
191
|
-
// Theme-based backgrounds using CSS classes
|
|
192
|
-
const themeClasses = {
|
|
193
|
-
primary: 'bg-primary text-primary-foreground',
|
|
194
|
-
secondary: 'bg-secondary text-secondary-foreground',
|
|
195
|
-
muted: 'bg-muted text-muted-foreground',
|
|
196
|
-
accent: 'bg-accent text-accent-foreground',
|
|
197
|
-
destructive: 'bg-destructive text-destructive-foreground'
|
|
198
|
-
};
|
|
199
|
-
className = background.theme ? themeClasses[background.theme] || '' : '';
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
case 'solid':
|
|
204
|
-
styles.backgroundColor = background.solid_color;
|
|
205
|
-
break;
|
|
206
|
-
|
|
207
|
-
case 'gradient':
|
|
208
|
-
if (background.gradient) {
|
|
209
|
-
const { type, direction, stops } = background.gradient;
|
|
210
|
-
const gradientStops = formatGradientStops(stops);
|
|
211
|
-
styles.background = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
|
|
212
|
-
}
|
|
213
|
-
break;
|
|
214
|
-
|
|
215
|
-
case 'image':
|
|
216
|
-
// Handled via absolute next/image in the render tree for LCP & sizing optimization
|
|
217
|
-
break;
|
|
218
|
-
|
|
219
|
-
default:
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return { styles, className };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function generateGradientOverlayStyle(gradient: any): React.CSSProperties {
|
|
227
|
-
const { type, direction, stops } = gradient;
|
|
228
|
-
const gradientStops = formatGradientStops(stops);
|
|
229
|
-
return {
|
|
230
|
-
background: `${type}-gradient(${direction || 'to right'}, ${gradientStops})`,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
interface NestedBlockRendererProps {
|
|
235
|
-
block: SectionBlockContent['column_blocks'][0][0];
|
|
236
|
-
languageId: number;
|
|
237
|
-
parentBlockId?: number;
|
|
238
|
-
parentBlockIndex?: number;
|
|
239
|
-
visualEditing?: VisualEditingDocumentContext;
|
|
240
|
-
visualEditAttributes?: VisualEditAttributes;
|
|
241
|
-
botProtectionPublic?: BotProtectionPublicSettings;
|
|
242
|
-
scriptNonce?: string;
|
|
243
|
-
priority?: boolean;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* A form nested inside a section has no `blocks` row of its own — it lives in the
|
|
248
|
-
* section's jsonb — so the same strip the top-level renderer performs has to happen
|
|
249
|
-
* here too. See BlockRenderer.stripServerOnlyContent for why.
|
|
250
|
-
*/
|
|
251
|
-
function stripServerOnlyContent(blockType: string, content: unknown): unknown {
|
|
252
|
-
if (blockType !== 'form' || !content || typeof content !== 'object') return content;
|
|
253
|
-
if (!('recipient_email' in (content as Record<string, unknown>))) return content;
|
|
254
|
-
const { recipient_email: _dropped, ...safe } = content as Record<string, unknown>;
|
|
255
|
-
return safe;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function renderNestedBlock({
|
|
259
|
-
block,
|
|
260
|
-
languageId,
|
|
261
|
-
parentBlockId,
|
|
262
|
-
parentBlockIndex,
|
|
263
|
-
visualEditing,
|
|
264
|
-
visualEditAttributes,
|
|
265
|
-
botProtectionPublic,
|
|
266
|
-
scriptNonce,
|
|
267
|
-
priority = false,
|
|
268
|
-
}: NestedBlockRendererProps) {
|
|
269
|
-
// Statically resolve core block types first to avoid dynamic imports overhead
|
|
270
|
-
if (block.block_type === 'text') {
|
|
271
|
-
return (
|
|
272
|
-
<TextBlockRenderer
|
|
273
|
-
content={block.content as any}
|
|
274
|
-
languageId={languageId}
|
|
275
|
-
visualEditAttributes={visualEditAttributes}
|
|
276
|
-
renderContext="section"
|
|
277
|
-
/>
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (block.block_type === 'heading') {
|
|
282
|
-
return (
|
|
283
|
-
<HeadingBlockRenderer
|
|
284
|
-
content={block.content as any}
|
|
285
|
-
languageId={languageId}
|
|
286
|
-
visualEditAttributes={visualEditAttributes}
|
|
287
|
-
/>
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (block.block_type === 'button') {
|
|
292
|
-
return (
|
|
293
|
-
<ButtonBlockRenderer
|
|
294
|
-
content={block.content as any}
|
|
295
|
-
languageId={languageId}
|
|
296
|
-
visualEditAttributes={visualEditAttributes}
|
|
297
|
-
/>
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (block.block_type === 'image') {
|
|
302
|
-
return (
|
|
303
|
-
<ImageBlockRenderer
|
|
304
|
-
content={block.content as any}
|
|
305
|
-
languageId={languageId}
|
|
306
|
-
priority={priority}
|
|
307
|
-
visualEditAttributes={visualEditAttributes}
|
|
308
|
-
/>
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Fallback to dynamic loader for custom/other block types
|
|
313
|
-
const rendererLoader = getPublicBlockRendererLoader(block.block_type);
|
|
314
|
-
|
|
315
|
-
if (!rendererLoader) {
|
|
316
|
-
if (ECOMMERCE_BLOCK_TYPES.has(block.block_type)) {
|
|
317
|
-
const { default: EcommerceRendererComponent } = await loadEcommerceBlockRenderer(
|
|
318
|
-
block.block_type
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
return (
|
|
322
|
-
<EcommerceRendererComponent
|
|
323
|
-
content={block.content}
|
|
324
|
-
languageId={languageId}
|
|
325
|
-
visualEditAttributes={visualEditAttributes}
|
|
326
|
-
/>
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Custom block definitions are data-rendered the same way as at the top
|
|
331
|
-
// level. A custom block's block_type is the definition slug.
|
|
332
|
-
const definition = await getCachedCustomBlockDefinitionBySlug(block.block_type);
|
|
333
|
-
if (definition) {
|
|
334
|
-
const resolvedBlock = (await resolveBlockRelations({
|
|
335
|
-
data: block.content as Record<string, any>,
|
|
336
|
-
fields: definition.fields,
|
|
337
|
-
})) as any;
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
<div {...visualEditAttributes}>
|
|
341
|
-
<CachedDynamicLayoutEngine
|
|
342
|
-
definition={definition}
|
|
343
|
-
layoutSchema={definition.layout_schema}
|
|
344
|
-
fields={definition.fields}
|
|
345
|
-
data={{
|
|
346
|
-
...(resolvedBlock.data || {}),
|
|
347
|
-
resolved_relations: resolvedBlock.resolved_relations || {},
|
|
348
|
-
}}
|
|
349
|
-
/>
|
|
350
|
-
</div>
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Unknown type with no matching definition: keep the diagnostic out of the
|
|
355
|
-
// public-facing page (only show it inside the live editor).
|
|
356
|
-
if (!visualEditing?.enabled) {
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return (
|
|
361
|
-
<div
|
|
362
|
-
className="p-2 border rounded bg-destructive/10 text-destructive text-sm"
|
|
363
|
-
{...visualEditAttributes}
|
|
364
|
-
>
|
|
365
|
-
<strong>Unsupported block type:</strong> {block.block_type}
|
|
366
|
-
</div>
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const { default: RendererComponent } = await rendererLoader();
|
|
371
|
-
|
|
372
|
-
// Handle different prop requirements for different renderers
|
|
373
|
-
if (block.block_type === 'posts_grid') {
|
|
374
|
-
return (
|
|
375
|
-
<RendererComponent
|
|
376
|
-
content={block.content}
|
|
377
|
-
languageId={languageId}
|
|
378
|
-
block={{ ...block, id: 0, language_id: languageId, order: 0, created_at: '', updated_at: '' }}
|
|
379
|
-
visualEditAttributes={visualEditAttributes}
|
|
380
|
-
/>
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return (
|
|
385
|
-
<RendererComponent
|
|
386
|
-
content={stripServerOnlyContent(block.block_type, block.content)}
|
|
387
|
-
languageId={languageId}
|
|
388
|
-
visualEditAttributes={visualEditAttributes}
|
|
389
|
-
visualEditing={visualEditing}
|
|
390
|
-
parentBlockId={parentBlockId}
|
|
391
|
-
parentBlockIndex={parentBlockIndex}
|
|
392
|
-
botProtectionPublic={botProtectionPublic}
|
|
393
|
-
scriptNonce={scriptNonce}
|
|
394
|
-
// Hero sections also carry LCP-eligible media in dynamically loaded
|
|
395
|
-
// renderers (e.g. video_embed's poster frame), not just nested images.
|
|
396
|
-
priority={priority}
|
|
397
|
-
/>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export default async function SectionBlockRenderer({
|
|
402
|
-
content,
|
|
403
|
-
languageId,
|
|
404
|
-
visualEditAttributes,
|
|
405
|
-
visualEditing,
|
|
406
|
-
parentBlockId,
|
|
407
|
-
parentBlockIndex,
|
|
408
|
-
blockType,
|
|
409
|
-
botProtectionPublic,
|
|
410
|
-
scriptNonce,
|
|
411
|
-
}: SectionBlockRendererProps) {
|
|
412
|
-
const isHero = content.is_hero === true;
|
|
413
|
-
|
|
414
|
-
// Build CSS classes
|
|
415
|
-
const containerClass = containerClasses[content.container_type] || containerClasses.container;
|
|
416
|
-
const gridClass = columnClasses[content.responsive_columns.desktop] || columnClasses[3];
|
|
417
|
-
const gapClass = gapClasses[content.column_gap] || gapClasses.md;
|
|
418
|
-
const paddingTopClass = paddingClasses[content.padding.top] || paddingClasses.md;
|
|
419
|
-
const paddingBottomClass = paddingClasses[content.padding.bottom] || paddingClasses.md;
|
|
420
|
-
const alignmentClass = content.vertical_alignment ? verticalAlignmentClasses[content.vertical_alignment] : 'items-start';
|
|
421
|
-
|
|
422
|
-
// --- Render Slide-based Carousel Section ---
|
|
423
|
-
if (content.slider && content.slides && content.slides.length > 0) {
|
|
424
|
-
const renderedSlides = await Promise.all(
|
|
425
|
-
content.slides.map(async (slide, slideIndex) => {
|
|
426
|
-
const isFirstSlide = slideIndex === 0;
|
|
427
|
-
// Priority loading for the first slide of a hero carousel
|
|
428
|
-
const slidePriority = isHero && isFirstSlide;
|
|
429
|
-
|
|
430
|
-
const slideBackground = slide.background || { type: "none" };
|
|
431
|
-
const { styles: slideBgStyles, className: slideBgClassName } = generateBackgroundStyles(slideBackground);
|
|
432
|
-
|
|
433
|
-
const renderedColumns = await Promise.all(
|
|
434
|
-
slide.column_blocks.map(async (columnBlocks, columnIndex) => {
|
|
435
|
-
const blocks = Array.isArray(columnBlocks) ? columnBlocks : [];
|
|
436
|
-
const renderedBlocks = await Promise.all(
|
|
437
|
-
blocks.map(async (block, blockIndex) => ({
|
|
438
|
-
key: `${block.block_type}-${columnIndex}-${blockIndex}`,
|
|
439
|
-
node: await renderNestedBlock({
|
|
440
|
-
block,
|
|
441
|
-
languageId,
|
|
442
|
-
parentBlockId,
|
|
443
|
-
parentBlockIndex,
|
|
444
|
-
visualEditing,
|
|
445
|
-
botProtectionPublic,
|
|
446
|
-
scriptNonce,
|
|
447
|
-
priority: slidePriority, // Pass down priority
|
|
448
|
-
visualEditAttributes:
|
|
449
|
-
typeof parentBlockId === "number" && typeof parentBlockIndex === "number"
|
|
450
|
-
? buildVisualEditAttributes(visualEditing, {
|
|
451
|
-
kind: "nested",
|
|
452
|
-
parentBlockId,
|
|
453
|
-
parentBlockIndex,
|
|
454
|
-
parentBlockType: "section",
|
|
455
|
-
columnIndex,
|
|
456
|
-
blockIndex,
|
|
457
|
-
blockType: block.block_type,
|
|
458
|
-
})
|
|
459
|
-
: undefined,
|
|
460
|
-
}),
|
|
461
|
-
}))
|
|
462
|
-
);
|
|
463
|
-
|
|
464
|
-
return { columnIndex, renderedBlocks };
|
|
465
|
-
})
|
|
466
|
-
);
|
|
467
|
-
|
|
468
|
-
return (
|
|
469
|
-
<div
|
|
470
|
-
key={`slide-${slideIndex}`}
|
|
471
|
-
className={`relative w-full flex items-center ${BACKGROUND_COMPOSITING_CLASSES} ${paddingTopClass} ${paddingBottomClass} ${slideBgClassName}`}
|
|
472
|
-
style={{
|
|
473
|
-
...slideBgStyles,
|
|
474
|
-
minHeight: formatMinHeight(slideBackground.min_height) || '400px'
|
|
475
|
-
}}
|
|
476
|
-
>
|
|
477
|
-
{/* Background image Layer for slide */}
|
|
478
|
-
{slideBackground.type === 'image' && slideBackground.image && (
|
|
479
|
-
<div className={ABSOLUTE_BACKGROUND_CLASSES}>
|
|
480
|
-
{renderBackgroundImageElement(slideBackground.image, slidePriority)}
|
|
481
|
-
{slideBackground.image.overlay && slideBackground.image.overlay.gradient && (
|
|
482
|
-
<div
|
|
483
|
-
className="absolute inset-0 transform-gpu [backface-visibility:hidden]"
|
|
484
|
-
style={generateGradientOverlayStyle(slideBackground.image.overlay.gradient)}
|
|
485
|
-
/>
|
|
486
|
-
)}
|
|
487
|
-
</div>
|
|
488
|
-
)}
|
|
489
|
-
|
|
490
|
-
<div className={`${containerClass} w-full`}>
|
|
491
|
-
<div className={`grid ${gridClass} ${gapClass} ${alignmentClass}`}>
|
|
492
|
-
{renderedColumns.map(({ columnIndex, renderedBlocks }) => (
|
|
493
|
-
<div key={`column-${columnIndex}`} className="min-h-0 space-y-4">
|
|
494
|
-
{renderedBlocks.map(({ key, node }) => (
|
|
495
|
-
<React.Fragment key={key}>{node}</React.Fragment>
|
|
496
|
-
))}
|
|
497
|
-
</div>
|
|
498
|
-
))}
|
|
499
|
-
</div>
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
);
|
|
503
|
-
})
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
// Determine the slider's container min-height from configured slides
|
|
507
|
-
let sliderMinHeight = '400px';
|
|
508
|
-
if (content.slides && content.slides.length > 0) {
|
|
509
|
-
const configuredHeight = content.slides
|
|
510
|
-
.map(s => s.background?.min_height)
|
|
511
|
-
.find(Boolean);
|
|
512
|
-
if (configuredHeight) {
|
|
513
|
-
sliderMinHeight = formatMinHeight(configuredHeight) || '400px';
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return (
|
|
518
|
-
<section
|
|
519
|
-
className={`relative w-full overflow-hidden ${BACKGROUND_COMPOSITING_CLASSES}`}
|
|
520
|
-
{...visualEditAttributes}
|
|
521
|
-
>
|
|
522
|
-
<SectionSlider
|
|
523
|
-
autoplay={content.autoplay}
|
|
524
|
-
timeframe={content.timeframe}
|
|
525
|
-
minHeight={sliderMinHeight}
|
|
526
|
-
>
|
|
527
|
-
{renderedSlides}
|
|
528
|
-
</SectionSlider>
|
|
529
|
-
</section>
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// --- Render Standard Single Layout Section ---
|
|
534
|
-
const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
|
|
535
|
-
|
|
536
|
-
const renderedColumns = await Promise.all(
|
|
537
|
-
content.column_blocks.map(async (columnBlocks, columnIndex) => {
|
|
538
|
-
const blocks = Array.isArray(columnBlocks) ? columnBlocks : [];
|
|
539
|
-
const renderedBlocks = await Promise.all(
|
|
540
|
-
blocks.map(async (block, blockIndex) => ({
|
|
541
|
-
key: `${block.block_type}-${columnIndex}-${blockIndex}`,
|
|
542
|
-
node: await renderNestedBlock({
|
|
543
|
-
block,
|
|
544
|
-
languageId,
|
|
545
|
-
parentBlockId,
|
|
546
|
-
parentBlockIndex,
|
|
547
|
-
visualEditing,
|
|
548
|
-
botProtectionPublic,
|
|
549
|
-
scriptNonce,
|
|
550
|
-
priority: isHero, // Pass priority down to nested image blocks
|
|
551
|
-
visualEditAttributes:
|
|
552
|
-
typeof parentBlockId === "number" && typeof parentBlockIndex === "number"
|
|
553
|
-
? buildVisualEditAttributes(visualEditing, {
|
|
554
|
-
kind: "nested",
|
|
555
|
-
parentBlockId,
|
|
556
|
-
parentBlockIndex,
|
|
557
|
-
parentBlockType: "section",
|
|
558
|
-
columnIndex,
|
|
559
|
-
blockIndex,
|
|
560
|
-
blockType: block.block_type,
|
|
561
|
-
})
|
|
562
|
-
: undefined,
|
|
563
|
-
}),
|
|
564
|
-
}))
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
return { columnIndex, renderedBlocks };
|
|
568
|
-
})
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
return (
|
|
572
|
-
<section
|
|
573
|
-
className={`relative w-full ${BACKGROUND_COMPOSITING_CLASSES} ${paddingTopClass} ${paddingBottomClass} ${backgroundClassName}`.trim()}
|
|
574
|
-
style={{
|
|
575
|
-
...styles,
|
|
576
|
-
minHeight: formatMinHeight(content.background?.min_height)
|
|
577
|
-
}}
|
|
578
|
-
{...visualEditAttributes}
|
|
579
|
-
>
|
|
580
|
-
{/* Background image Layer */}
|
|
581
|
-
{content.background?.type === 'image' && content.background.image?.attribution && (
|
|
582
|
-
<StockPhotoCredit
|
|
583
|
-
attribution={content.background.image.attribution}
|
|
584
|
-
className="pointer-events-auto absolute bottom-1 right-2 z-10 rounded bg-black/40 px-1.5 py-0.5 text-[10px] text-white/90 backdrop-blur-sm [&_a]:text-white"
|
|
585
|
-
/>
|
|
586
|
-
)}
|
|
587
|
-
{content.background?.type === 'image' && content.background.image && (
|
|
588
|
-
<div className={ABSOLUTE_BACKGROUND_CLASSES}>
|
|
589
|
-
{renderBackgroundImageElement(content.background.image, isHero)}
|
|
590
|
-
{content.background.image.overlay && content.background.image.overlay.gradient && (
|
|
591
|
-
<div
|
|
592
|
-
className="absolute inset-0 transform-gpu [backface-visibility:hidden]"
|
|
593
|
-
style={generateGradientOverlayStyle(content.background.image.overlay.gradient)}
|
|
594
|
-
/>
|
|
595
|
-
)}
|
|
596
|
-
</div>
|
|
597
|
-
)}
|
|
598
|
-
|
|
599
|
-
<div className={containerClass}>
|
|
600
|
-
<div className={`grid ${gridClass} ${gapClass} ${alignmentClass}`}>
|
|
601
|
-
{renderedColumns.map(({ columnIndex, renderedBlocks }) => (
|
|
602
|
-
<div key={`column-${columnIndex}`} className="min-h-0 space-y-4">
|
|
603
|
-
{renderedBlocks.map(({ key, node }) => (
|
|
604
|
-
<React.Fragment key={key}>{node}</React.Fragment>
|
|
605
|
-
))}
|
|
606
|
-
</div>
|
|
607
|
-
))}
|
|
608
|
-
</div>
|
|
609
|
-
</div>
|
|
610
|
-
</section>
|
|
611
|
-
);
|
|
612
|
-
}
|
|
1
|
+
// components/blocks/renderers/SectionBlockRenderer.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import type { SectionBlockContent } from "../../../lib/blocks/blockRegistry";
|
|
5
|
+
import { buildVisualEditAttributes } from "../../../lib/visual-editing/edit-info";
|
|
6
|
+
import type {
|
|
7
|
+
VisualEditAttributes,
|
|
8
|
+
VisualEditingDocumentContext,
|
|
9
|
+
} from "../../../lib/visual-editing/types";
|
|
10
|
+
import { getPublicBlockRendererLoader } from "../publicRendererLoaders";
|
|
11
|
+
import SectionSlider from "./SectionSlider";
|
|
12
|
+
import { getCachedCustomBlockDefinitionBySlug } from "../../../lib/custom-block-definitions";
|
|
13
|
+
import { CachedDynamicLayoutEngine } from "../../renderers/CachedDynamicLayoutEngine";
|
|
14
|
+
import { resolveBlockRelations } from "../../../lib/resolve-block-relations";
|
|
15
|
+
|
|
16
|
+
// Static imports for core block renderers for LCP/performance optimization
|
|
17
|
+
import TextBlockRenderer from "./TextBlockRenderer";
|
|
18
|
+
import HeadingBlockRenderer from "./HeadingBlockRenderer";
|
|
19
|
+
import ImageBlockRenderer from "./ImageBlockRenderer";
|
|
20
|
+
import ButtonBlockRenderer from "./ButtonBlockRenderer";
|
|
21
|
+
import { StockPhotoCredit } from "./StockPhotoCredit";
|
|
22
|
+
|
|
23
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
24
|
+
const BACKGROUND_COMPOSITING_CLASSES =
|
|
25
|
+
"isolate transform-gpu [backface-visibility:hidden] [transform-style:preserve-3d]";
|
|
26
|
+
const ABSOLUTE_BACKGROUND_CLASSES =
|
|
27
|
+
"pointer-events-none absolute inset-[-1px] -z-10 transform-gpu [backface-visibility:hidden] [transform-style:preserve-3d]";
|
|
28
|
+
const ECOMMERCE_BLOCK_TYPES = new Set([
|
|
29
|
+
"product_grid",
|
|
30
|
+
"featured_product",
|
|
31
|
+
"cart",
|
|
32
|
+
"checkout",
|
|
33
|
+
"product_details",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function loadEcommerceBlockRenderer(blockType: string) {
|
|
37
|
+
return import("../ecommerceRendererLoaders").then((module) =>
|
|
38
|
+
module.loadEcommerceBlockRenderer(blockType)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type SectionBackgroundImage = NonNullable<
|
|
43
|
+
NonNullable<SectionBlockContent["background"]>["image"]
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
// Renders a section's background image. External https URLs (e.g. AI-inserted
|
|
47
|
+
// stock photos) use a plain <img> so any allowlisted host works without Next
|
|
48
|
+
// image remotePatterns config; stored R2 media keeps the optimized next/image
|
|
49
|
+
// path. Returns null when neither a URL nor an object key is present.
|
|
50
|
+
function renderBackgroundImageElement(image: SectionBackgroundImage, priority: boolean) {
|
|
51
|
+
const externalUrl =
|
|
52
|
+
typeof image.external_url === "string" && /^https?:\/\//i.test(image.external_url.trim())
|
|
53
|
+
? image.external_url.trim()
|
|
54
|
+
: null;
|
|
55
|
+
const objectFit: "cover" | "contain" = image.size === "contain" ? "contain" : "cover";
|
|
56
|
+
const objectPosition = image.position || "center";
|
|
57
|
+
|
|
58
|
+
if (externalUrl) {
|
|
59
|
+
return (
|
|
60
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
61
|
+
<img
|
|
62
|
+
src={externalUrl}
|
|
63
|
+
alt={image.alt_text || ""}
|
|
64
|
+
loading={priority ? "eager" : "lazy"}
|
|
65
|
+
fetchPriority={priority ? "high" : "auto"}
|
|
66
|
+
decoding="async"
|
|
67
|
+
className="absolute inset-0 h-full w-full"
|
|
68
|
+
style={{ objectFit, objectPosition }}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!image.object_key) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Image
|
|
79
|
+
src={`${R2_BASE_URL}/${image.object_key}`}
|
|
80
|
+
alt={image.alt_text || ""}
|
|
81
|
+
fill
|
|
82
|
+
priority={priority}
|
|
83
|
+
fetchPriority={priority ? "high" : "auto"}
|
|
84
|
+
placeholder={image.blur_data_url ? "blur" : "empty"}
|
|
85
|
+
blurDataURL={image.blur_data_url || undefined}
|
|
86
|
+
quality={image.quality || 80}
|
|
87
|
+
sizes="100vw"
|
|
88
|
+
style={{ objectFit, objectPosition }}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface SectionBlockRendererProps {
|
|
94
|
+
content: SectionBlockContent;
|
|
95
|
+
languageId: number;
|
|
96
|
+
visualEditAttributes?: VisualEditAttributes;
|
|
97
|
+
visualEditing?: VisualEditingDocumentContext;
|
|
98
|
+
parentBlockId?: number;
|
|
99
|
+
parentBlockIndex?: number;
|
|
100
|
+
blockType?: "section";
|
|
101
|
+
botProtectionPublic?: BotProtectionPublicSettings;
|
|
102
|
+
scriptNonce?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type BotProtectionPublicSettings = {
|
|
106
|
+
provider: 'none' | 'turnstile' | 'recaptcha';
|
|
107
|
+
siteKey: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Container class mapping
|
|
111
|
+
const containerClasses = {
|
|
112
|
+
'full-width': 'w-full',
|
|
113
|
+
'container': 'container mx-auto px-4',
|
|
114
|
+
'container-sm': 'container mx-auto px-4 max-w-screen-sm',
|
|
115
|
+
'container-lg': 'container mx-auto px-4 max-w-screen-lg',
|
|
116
|
+
'container-xl': 'container mx-auto px-4 max-w-screen-xl'
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Column grid classes
|
|
120
|
+
const columnClasses = {
|
|
121
|
+
1: 'grid-cols-1',
|
|
122
|
+
2: 'grid-cols-1 md:grid-cols-2',
|
|
123
|
+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
124
|
+
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Gap classes
|
|
128
|
+
const gapClasses = {
|
|
129
|
+
none: 'gap-0',
|
|
130
|
+
sm: 'gap-2',
|
|
131
|
+
md: 'gap-4',
|
|
132
|
+
lg: 'gap-6',
|
|
133
|
+
xl: 'gap-8'
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Padding classes
|
|
137
|
+
const paddingClasses = {
|
|
138
|
+
none: '',
|
|
139
|
+
sm: 'py-2',
|
|
140
|
+
md: 'py-4',
|
|
141
|
+
lg: 'py-8',
|
|
142
|
+
xl: 'py-12'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Vertical alignment classes
|
|
146
|
+
const verticalAlignmentClasses = {
|
|
147
|
+
start: 'items-start',
|
|
148
|
+
center: 'items-center',
|
|
149
|
+
end: 'items-end',
|
|
150
|
+
stretch: 'items-stretch'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function formatMinHeight(value?: string) {
|
|
154
|
+
if (!value) return undefined;
|
|
155
|
+
const trimmed = value.trim();
|
|
156
|
+
if (/^\d+$/.test(trimmed)) {
|
|
157
|
+
return `${trimmed}px`;
|
|
158
|
+
}
|
|
159
|
+
return trimmed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatStopPosition(position: number) {
|
|
163
|
+
return Number.isInteger(position) ? String(position) : position.toFixed(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function formatGradientStops(stops: Array<{ color: string; position: number }>) {
|
|
167
|
+
return stops
|
|
168
|
+
.map((stop, index) => {
|
|
169
|
+
const previous = stops[index - 1];
|
|
170
|
+
const next = stops[index + 1];
|
|
171
|
+
let position = stop.position;
|
|
172
|
+
|
|
173
|
+
if (next && next.position === stop.position && stop.position > 0) {
|
|
174
|
+
position = stop.position - 0.1;
|
|
175
|
+
} else if (previous && previous.position === stop.position && stop.position < 100) {
|
|
176
|
+
position = stop.position + 0.1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return `${stop.color} ${formatStopPosition(position)}%`;
|
|
180
|
+
})
|
|
181
|
+
.join(', ');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Background style generator (handles solid, theme and gradients; image handled in JSX)
|
|
185
|
+
function generateBackgroundStyles(background: SectionBlockContent['background']) {
|
|
186
|
+
const styles: React.CSSProperties = {};
|
|
187
|
+
let className = '';
|
|
188
|
+
|
|
189
|
+
switch (background.type) {
|
|
190
|
+
case 'theme': {
|
|
191
|
+
// Theme-based backgrounds using CSS classes
|
|
192
|
+
const themeClasses = {
|
|
193
|
+
primary: 'bg-primary text-primary-foreground',
|
|
194
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
195
|
+
muted: 'bg-muted text-muted-foreground',
|
|
196
|
+
accent: 'bg-accent text-accent-foreground',
|
|
197
|
+
destructive: 'bg-destructive text-destructive-foreground'
|
|
198
|
+
};
|
|
199
|
+
className = background.theme ? themeClasses[background.theme] || '' : '';
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case 'solid':
|
|
204
|
+
styles.backgroundColor = background.solid_color;
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case 'gradient':
|
|
208
|
+
if (background.gradient) {
|
|
209
|
+
const { type, direction, stops } = background.gradient;
|
|
210
|
+
const gradientStops = formatGradientStops(stops);
|
|
211
|
+
styles.background = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case 'image':
|
|
216
|
+
// Handled via absolute next/image in the render tree for LCP & sizing optimization
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
default:
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { styles, className };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function generateGradientOverlayStyle(gradient: any): React.CSSProperties {
|
|
227
|
+
const { type, direction, stops } = gradient;
|
|
228
|
+
const gradientStops = formatGradientStops(stops);
|
|
229
|
+
return {
|
|
230
|
+
background: `${type}-gradient(${direction || 'to right'}, ${gradientStops})`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface NestedBlockRendererProps {
|
|
235
|
+
block: SectionBlockContent['column_blocks'][0][0];
|
|
236
|
+
languageId: number;
|
|
237
|
+
parentBlockId?: number;
|
|
238
|
+
parentBlockIndex?: number;
|
|
239
|
+
visualEditing?: VisualEditingDocumentContext;
|
|
240
|
+
visualEditAttributes?: VisualEditAttributes;
|
|
241
|
+
botProtectionPublic?: BotProtectionPublicSettings;
|
|
242
|
+
scriptNonce?: string;
|
|
243
|
+
priority?: boolean;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A form nested inside a section has no `blocks` row of its own — it lives in the
|
|
248
|
+
* section's jsonb — so the same strip the top-level renderer performs has to happen
|
|
249
|
+
* here too. See BlockRenderer.stripServerOnlyContent for why.
|
|
250
|
+
*/
|
|
251
|
+
function stripServerOnlyContent(blockType: string, content: unknown): unknown {
|
|
252
|
+
if (blockType !== 'form' || !content || typeof content !== 'object') return content;
|
|
253
|
+
if (!('recipient_email' in (content as Record<string, unknown>))) return content;
|
|
254
|
+
const { recipient_email: _dropped, ...safe } = content as Record<string, unknown>;
|
|
255
|
+
return safe;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function renderNestedBlock({
|
|
259
|
+
block,
|
|
260
|
+
languageId,
|
|
261
|
+
parentBlockId,
|
|
262
|
+
parentBlockIndex,
|
|
263
|
+
visualEditing,
|
|
264
|
+
visualEditAttributes,
|
|
265
|
+
botProtectionPublic,
|
|
266
|
+
scriptNonce,
|
|
267
|
+
priority = false,
|
|
268
|
+
}: NestedBlockRendererProps) {
|
|
269
|
+
// Statically resolve core block types first to avoid dynamic imports overhead
|
|
270
|
+
if (block.block_type === 'text') {
|
|
271
|
+
return (
|
|
272
|
+
<TextBlockRenderer
|
|
273
|
+
content={block.content as any}
|
|
274
|
+
languageId={languageId}
|
|
275
|
+
visualEditAttributes={visualEditAttributes}
|
|
276
|
+
renderContext="section"
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (block.block_type === 'heading') {
|
|
282
|
+
return (
|
|
283
|
+
<HeadingBlockRenderer
|
|
284
|
+
content={block.content as any}
|
|
285
|
+
languageId={languageId}
|
|
286
|
+
visualEditAttributes={visualEditAttributes}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (block.block_type === 'button') {
|
|
292
|
+
return (
|
|
293
|
+
<ButtonBlockRenderer
|
|
294
|
+
content={block.content as any}
|
|
295
|
+
languageId={languageId}
|
|
296
|
+
visualEditAttributes={visualEditAttributes}
|
|
297
|
+
/>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (block.block_type === 'image') {
|
|
302
|
+
return (
|
|
303
|
+
<ImageBlockRenderer
|
|
304
|
+
content={block.content as any}
|
|
305
|
+
languageId={languageId}
|
|
306
|
+
priority={priority}
|
|
307
|
+
visualEditAttributes={visualEditAttributes}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Fallback to dynamic loader for custom/other block types
|
|
313
|
+
const rendererLoader = getPublicBlockRendererLoader(block.block_type);
|
|
314
|
+
|
|
315
|
+
if (!rendererLoader) {
|
|
316
|
+
if (ECOMMERCE_BLOCK_TYPES.has(block.block_type)) {
|
|
317
|
+
const { default: EcommerceRendererComponent } = await loadEcommerceBlockRenderer(
|
|
318
|
+
block.block_type
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<EcommerceRendererComponent
|
|
323
|
+
content={block.content}
|
|
324
|
+
languageId={languageId}
|
|
325
|
+
visualEditAttributes={visualEditAttributes}
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Custom block definitions are data-rendered the same way as at the top
|
|
331
|
+
// level. A custom block's block_type is the definition slug.
|
|
332
|
+
const definition = await getCachedCustomBlockDefinitionBySlug(block.block_type);
|
|
333
|
+
if (definition) {
|
|
334
|
+
const resolvedBlock = (await resolveBlockRelations({
|
|
335
|
+
data: block.content as Record<string, any>,
|
|
336
|
+
fields: definition.fields,
|
|
337
|
+
})) as any;
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div {...visualEditAttributes}>
|
|
341
|
+
<CachedDynamicLayoutEngine
|
|
342
|
+
definition={definition}
|
|
343
|
+
layoutSchema={definition.layout_schema}
|
|
344
|
+
fields={definition.fields}
|
|
345
|
+
data={{
|
|
346
|
+
...(resolvedBlock.data || {}),
|
|
347
|
+
resolved_relations: resolvedBlock.resolved_relations || {},
|
|
348
|
+
}}
|
|
349
|
+
/>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Unknown type with no matching definition: keep the diagnostic out of the
|
|
355
|
+
// public-facing page (only show it inside the live editor).
|
|
356
|
+
if (!visualEditing?.enabled) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div
|
|
362
|
+
className="p-2 border rounded bg-destructive/10 text-destructive text-sm"
|
|
363
|
+
{...visualEditAttributes}
|
|
364
|
+
>
|
|
365
|
+
<strong>Unsupported block type:</strong> {block.block_type}
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const { default: RendererComponent } = await rendererLoader();
|
|
371
|
+
|
|
372
|
+
// Handle different prop requirements for different renderers
|
|
373
|
+
if (block.block_type === 'posts_grid') {
|
|
374
|
+
return (
|
|
375
|
+
<RendererComponent
|
|
376
|
+
content={block.content}
|
|
377
|
+
languageId={languageId}
|
|
378
|
+
block={{ ...block, id: 0, language_id: languageId, order: 0, created_at: '', updated_at: '' }}
|
|
379
|
+
visualEditAttributes={visualEditAttributes}
|
|
380
|
+
/>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<RendererComponent
|
|
386
|
+
content={stripServerOnlyContent(block.block_type, block.content)}
|
|
387
|
+
languageId={languageId}
|
|
388
|
+
visualEditAttributes={visualEditAttributes}
|
|
389
|
+
visualEditing={visualEditing}
|
|
390
|
+
parentBlockId={parentBlockId}
|
|
391
|
+
parentBlockIndex={parentBlockIndex}
|
|
392
|
+
botProtectionPublic={botProtectionPublic}
|
|
393
|
+
scriptNonce={scriptNonce}
|
|
394
|
+
// Hero sections also carry LCP-eligible media in dynamically loaded
|
|
395
|
+
// renderers (e.g. video_embed's poster frame), not just nested images.
|
|
396
|
+
priority={priority}
|
|
397
|
+
/>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export default async function SectionBlockRenderer({
|
|
402
|
+
content,
|
|
403
|
+
languageId,
|
|
404
|
+
visualEditAttributes,
|
|
405
|
+
visualEditing,
|
|
406
|
+
parentBlockId,
|
|
407
|
+
parentBlockIndex,
|
|
408
|
+
blockType,
|
|
409
|
+
botProtectionPublic,
|
|
410
|
+
scriptNonce,
|
|
411
|
+
}: SectionBlockRendererProps) {
|
|
412
|
+
const isHero = content.is_hero === true;
|
|
413
|
+
|
|
414
|
+
// Build CSS classes
|
|
415
|
+
const containerClass = containerClasses[content.container_type] || containerClasses.container;
|
|
416
|
+
const gridClass = columnClasses[content.responsive_columns.desktop] || columnClasses[3];
|
|
417
|
+
const gapClass = gapClasses[content.column_gap] || gapClasses.md;
|
|
418
|
+
const paddingTopClass = paddingClasses[content.padding.top] || paddingClasses.md;
|
|
419
|
+
const paddingBottomClass = paddingClasses[content.padding.bottom] || paddingClasses.md;
|
|
420
|
+
const alignmentClass = content.vertical_alignment ? verticalAlignmentClasses[content.vertical_alignment] : 'items-start';
|
|
421
|
+
|
|
422
|
+
// --- Render Slide-based Carousel Section ---
|
|
423
|
+
if (content.slider && content.slides && content.slides.length > 0) {
|
|
424
|
+
const renderedSlides = await Promise.all(
|
|
425
|
+
content.slides.map(async (slide, slideIndex) => {
|
|
426
|
+
const isFirstSlide = slideIndex === 0;
|
|
427
|
+
// Priority loading for the first slide of a hero carousel
|
|
428
|
+
const slidePriority = isHero && isFirstSlide;
|
|
429
|
+
|
|
430
|
+
const slideBackground = slide.background || { type: "none" };
|
|
431
|
+
const { styles: slideBgStyles, className: slideBgClassName } = generateBackgroundStyles(slideBackground);
|
|
432
|
+
|
|
433
|
+
const renderedColumns = await Promise.all(
|
|
434
|
+
slide.column_blocks.map(async (columnBlocks, columnIndex) => {
|
|
435
|
+
const blocks = Array.isArray(columnBlocks) ? columnBlocks : [];
|
|
436
|
+
const renderedBlocks = await Promise.all(
|
|
437
|
+
blocks.map(async (block, blockIndex) => ({
|
|
438
|
+
key: `${block.block_type}-${columnIndex}-${blockIndex}`,
|
|
439
|
+
node: await renderNestedBlock({
|
|
440
|
+
block,
|
|
441
|
+
languageId,
|
|
442
|
+
parentBlockId,
|
|
443
|
+
parentBlockIndex,
|
|
444
|
+
visualEditing,
|
|
445
|
+
botProtectionPublic,
|
|
446
|
+
scriptNonce,
|
|
447
|
+
priority: slidePriority, // Pass down priority
|
|
448
|
+
visualEditAttributes:
|
|
449
|
+
typeof parentBlockId === "number" && typeof parentBlockIndex === "number"
|
|
450
|
+
? buildVisualEditAttributes(visualEditing, {
|
|
451
|
+
kind: "nested",
|
|
452
|
+
parentBlockId,
|
|
453
|
+
parentBlockIndex,
|
|
454
|
+
parentBlockType: "section",
|
|
455
|
+
columnIndex,
|
|
456
|
+
blockIndex,
|
|
457
|
+
blockType: block.block_type,
|
|
458
|
+
})
|
|
459
|
+
: undefined,
|
|
460
|
+
}),
|
|
461
|
+
}))
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
return { columnIndex, renderedBlocks };
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<div
|
|
470
|
+
key={`slide-${slideIndex}`}
|
|
471
|
+
className={`relative w-full flex items-center ${BACKGROUND_COMPOSITING_CLASSES} ${paddingTopClass} ${paddingBottomClass} ${slideBgClassName}`}
|
|
472
|
+
style={{
|
|
473
|
+
...slideBgStyles,
|
|
474
|
+
minHeight: formatMinHeight(slideBackground.min_height) || '400px'
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
{/* Background image Layer for slide */}
|
|
478
|
+
{slideBackground.type === 'image' && slideBackground.image && (
|
|
479
|
+
<div className={ABSOLUTE_BACKGROUND_CLASSES}>
|
|
480
|
+
{renderBackgroundImageElement(slideBackground.image, slidePriority)}
|
|
481
|
+
{slideBackground.image.overlay && slideBackground.image.overlay.gradient && (
|
|
482
|
+
<div
|
|
483
|
+
className="absolute inset-0 transform-gpu [backface-visibility:hidden]"
|
|
484
|
+
style={generateGradientOverlayStyle(slideBackground.image.overlay.gradient)}
|
|
485
|
+
/>
|
|
486
|
+
)}
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
489
|
+
|
|
490
|
+
<div className={`${containerClass} w-full`}>
|
|
491
|
+
<div className={`grid ${gridClass} ${gapClass} ${alignmentClass}`}>
|
|
492
|
+
{renderedColumns.map(({ columnIndex, renderedBlocks }) => (
|
|
493
|
+
<div key={`column-${columnIndex}`} className="min-h-0 space-y-4">
|
|
494
|
+
{renderedBlocks.map(({ key, node }) => (
|
|
495
|
+
<React.Fragment key={key}>{node}</React.Fragment>
|
|
496
|
+
))}
|
|
497
|
+
</div>
|
|
498
|
+
))}
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Determine the slider's container min-height from configured slides
|
|
507
|
+
let sliderMinHeight = '400px';
|
|
508
|
+
if (content.slides && content.slides.length > 0) {
|
|
509
|
+
const configuredHeight = content.slides
|
|
510
|
+
.map(s => s.background?.min_height)
|
|
511
|
+
.find(Boolean);
|
|
512
|
+
if (configuredHeight) {
|
|
513
|
+
sliderMinHeight = formatMinHeight(configuredHeight) || '400px';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<section
|
|
519
|
+
className={`relative w-full overflow-hidden ${BACKGROUND_COMPOSITING_CLASSES}`}
|
|
520
|
+
{...visualEditAttributes}
|
|
521
|
+
>
|
|
522
|
+
<SectionSlider
|
|
523
|
+
autoplay={content.autoplay}
|
|
524
|
+
timeframe={content.timeframe}
|
|
525
|
+
minHeight={sliderMinHeight}
|
|
526
|
+
>
|
|
527
|
+
{renderedSlides}
|
|
528
|
+
</SectionSlider>
|
|
529
|
+
</section>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// --- Render Standard Single Layout Section ---
|
|
534
|
+
const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
|
|
535
|
+
|
|
536
|
+
const renderedColumns = await Promise.all(
|
|
537
|
+
content.column_blocks.map(async (columnBlocks, columnIndex) => {
|
|
538
|
+
const blocks = Array.isArray(columnBlocks) ? columnBlocks : [];
|
|
539
|
+
const renderedBlocks = await Promise.all(
|
|
540
|
+
blocks.map(async (block, blockIndex) => ({
|
|
541
|
+
key: `${block.block_type}-${columnIndex}-${blockIndex}`,
|
|
542
|
+
node: await renderNestedBlock({
|
|
543
|
+
block,
|
|
544
|
+
languageId,
|
|
545
|
+
parentBlockId,
|
|
546
|
+
parentBlockIndex,
|
|
547
|
+
visualEditing,
|
|
548
|
+
botProtectionPublic,
|
|
549
|
+
scriptNonce,
|
|
550
|
+
priority: isHero, // Pass priority down to nested image blocks
|
|
551
|
+
visualEditAttributes:
|
|
552
|
+
typeof parentBlockId === "number" && typeof parentBlockIndex === "number"
|
|
553
|
+
? buildVisualEditAttributes(visualEditing, {
|
|
554
|
+
kind: "nested",
|
|
555
|
+
parentBlockId,
|
|
556
|
+
parentBlockIndex,
|
|
557
|
+
parentBlockType: "section",
|
|
558
|
+
columnIndex,
|
|
559
|
+
blockIndex,
|
|
560
|
+
blockType: block.block_type,
|
|
561
|
+
})
|
|
562
|
+
: undefined,
|
|
563
|
+
}),
|
|
564
|
+
}))
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
return { columnIndex, renderedBlocks };
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
return (
|
|
572
|
+
<section
|
|
573
|
+
className={`relative w-full ${BACKGROUND_COMPOSITING_CLASSES} ${paddingTopClass} ${paddingBottomClass} ${backgroundClassName}`.trim()}
|
|
574
|
+
style={{
|
|
575
|
+
...styles,
|
|
576
|
+
minHeight: formatMinHeight(content.background?.min_height)
|
|
577
|
+
}}
|
|
578
|
+
{...visualEditAttributes}
|
|
579
|
+
>
|
|
580
|
+
{/* Background image Layer */}
|
|
581
|
+
{content.background?.type === 'image' && content.background.image?.attribution && (
|
|
582
|
+
<StockPhotoCredit
|
|
583
|
+
attribution={content.background.image.attribution}
|
|
584
|
+
className="pointer-events-auto absolute bottom-1 right-2 z-10 rounded bg-black/40 px-1.5 py-0.5 text-[10px] text-white/90 backdrop-blur-sm [&_a]:text-white"
|
|
585
|
+
/>
|
|
586
|
+
)}
|
|
587
|
+
{content.background?.type === 'image' && content.background.image && (
|
|
588
|
+
<div className={ABSOLUTE_BACKGROUND_CLASSES}>
|
|
589
|
+
{renderBackgroundImageElement(content.background.image, isHero)}
|
|
590
|
+
{content.background.image.overlay && content.background.image.overlay.gradient && (
|
|
591
|
+
<div
|
|
592
|
+
className="absolute inset-0 transform-gpu [backface-visibility:hidden]"
|
|
593
|
+
style={generateGradientOverlayStyle(content.background.image.overlay.gradient)}
|
|
594
|
+
/>
|
|
595
|
+
)}
|
|
596
|
+
</div>
|
|
597
|
+
)}
|
|
598
|
+
|
|
599
|
+
<div className={containerClass}>
|
|
600
|
+
<div className={`grid ${gridClass} ${gapClass} ${alignmentClass}`}>
|
|
601
|
+
{renderedColumns.map(({ columnIndex, renderedBlocks }) => (
|
|
602
|
+
<div key={`column-${columnIndex}`} className="min-h-0 space-y-4">
|
|
603
|
+
{renderedBlocks.map(({ key, node }) => (
|
|
604
|
+
<React.Fragment key={key}>{node}</React.Fragment>
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
607
|
+
))}
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
</section>
|
|
611
|
+
);
|
|
612
|
+
}
|