create-nextblock 0.16.1 → 0.16.2
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/sandboxResetSql.ts +3092 -3092
- 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/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/posts/components/PostForm.tsx +618 -618
- 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 +244 -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 +140 -140
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -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 +177 -177
- 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,56 +1,56 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The loading behaviour of an external script is the admin's choice, stored per row
|
|
3
|
-
* as `load_strategy` (default / defer / async). A blocking load is sometimes the
|
|
4
|
-
* required one — anti-flicker snippets and consent gates have to run before render —
|
|
5
|
-
* so this file renders what was asked for instead of forcing `defer` on everything.
|
|
6
|
-
*/
|
|
7
|
-
/* eslint-disable @next/next/no-sync-scripts */
|
|
8
|
-
import React from 'react';
|
|
9
|
-
|
|
10
|
-
import { escapeInlineScript, type SiteScript, type SiteScriptPlacement } from '../lib/site-scripts/types';
|
|
11
|
-
|
|
12
|
-
interface SiteScriptsProps {
|
|
13
|
-
nonce: string;
|
|
14
|
-
placement: SiteScriptPlacement;
|
|
15
|
-
scripts: SiteScript[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Render the admin-authored site scripts for one injection point.
|
|
20
|
-
*
|
|
21
|
-
* Emitted as plain <script> elements rather than next/script: these are arbitrary
|
|
22
|
-
* author-supplied snippets that frequently expect to run at a specific position in
|
|
23
|
-
* the document, and next/script's strategies would relocate them. The CSP nonce is
|
|
24
|
-
* applied here so the snippets satisfy the policy without it needing 'unsafe-inline'.
|
|
25
|
-
*/
|
|
26
|
-
export default function SiteScripts({ nonce, placement, scripts }: SiteScriptsProps) {
|
|
27
|
-
const forPlacement = scripts.filter((script) => script.placement === placement);
|
|
28
|
-
|
|
29
|
-
if (forPlacement.length === 0) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<>
|
|
35
|
-
{forPlacement.map((script) =>
|
|
36
|
-
script.src ? (
|
|
37
|
-
<script
|
|
38
|
-
key={script.id}
|
|
39
|
-
src={script.src}
|
|
40
|
-
nonce={nonce || undefined}
|
|
41
|
-
{...(script.load_strategy === 'async' ? { async: true } : {})}
|
|
42
|
-
{...(script.load_strategy === 'defer' ? { defer: true } : {})}
|
|
43
|
-
data-nb-script={script.id}
|
|
44
|
-
/>
|
|
45
|
-
) : (
|
|
46
|
-
<script
|
|
47
|
-
key={script.id}
|
|
48
|
-
nonce={nonce || undefined}
|
|
49
|
-
data-nb-script={script.id}
|
|
50
|
-
dangerouslySetInnerHTML={{ __html: escapeInlineScript(script.code) }}
|
|
51
|
-
/>
|
|
52
|
-
)
|
|
53
|
-
)}
|
|
54
|
-
</>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* The loading behaviour of an external script is the admin's choice, stored per row
|
|
3
|
+
* as `load_strategy` (default / defer / async). A blocking load is sometimes the
|
|
4
|
+
* required one — anti-flicker snippets and consent gates have to run before render —
|
|
5
|
+
* so this file renders what was asked for instead of forcing `defer` on everything.
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable @next/next/no-sync-scripts */
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import { escapeInlineScript, type SiteScript, type SiteScriptPlacement } from '../lib/site-scripts/types';
|
|
11
|
+
|
|
12
|
+
interface SiteScriptsProps {
|
|
13
|
+
nonce: string;
|
|
14
|
+
placement: SiteScriptPlacement;
|
|
15
|
+
scripts: SiteScript[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render the admin-authored site scripts for one injection point.
|
|
20
|
+
*
|
|
21
|
+
* Emitted as plain <script> elements rather than next/script: these are arbitrary
|
|
22
|
+
* author-supplied snippets that frequently expect to run at a specific position in
|
|
23
|
+
* the document, and next/script's strategies would relocate them. The CSP nonce is
|
|
24
|
+
* applied here so the snippets satisfy the policy without it needing 'unsafe-inline'.
|
|
25
|
+
*/
|
|
26
|
+
export default function SiteScripts({ nonce, placement, scripts }: SiteScriptsProps) {
|
|
27
|
+
const forPlacement = scripts.filter((script) => script.placement === placement);
|
|
28
|
+
|
|
29
|
+
if (forPlacement.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{forPlacement.map((script) =>
|
|
36
|
+
script.src ? (
|
|
37
|
+
<script
|
|
38
|
+
key={script.id}
|
|
39
|
+
src={script.src}
|
|
40
|
+
nonce={nonce || undefined}
|
|
41
|
+
{...(script.load_strategy === 'async' ? { async: true } : {})}
|
|
42
|
+
{...(script.load_strategy === 'defer' ? { defer: true } : {})}
|
|
43
|
+
data-nb-script={script.id}
|
|
44
|
+
/>
|
|
45
|
+
) : (
|
|
46
|
+
<script
|
|
47
|
+
key={script.id}
|
|
48
|
+
nonce={nonce || undefined}
|
|
49
|
+
data-nb-script={script.id}
|
|
50
|
+
dangerouslySetInnerHTML={{ __html: escapeInlineScript(script.code) }}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { createClient } from '@nextblock-cms/db';
|
|
5
|
-
import { useTranslations } from '@nextblock-cms/utils';
|
|
6
|
-
import { ShieldCheck } from 'lucide-react';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Staff answers rendered under the review or comment they belong to.
|
|
10
|
-
*
|
|
11
|
-
* A reply is stored as an ordinary approved `type='comment'` row carrying its parent's
|
|
12
|
-
* target, so it is readable through the same public RLS policy the parent is — no
|
|
13
|
-
* special access, nothing new exposed. It is fetched separately rather than joined
|
|
14
|
-
* because the parents arrive one page at a time.
|
|
15
|
-
*
|
|
16
|
-
* Shared by the product-reviews and post-comments sections, which are otherwise near
|
|
17
|
-
* duplicates of each other.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export interface StaffReply {
|
|
21
|
-
id: string;
|
|
22
|
-
content: string;
|
|
23
|
-
created_at: string;
|
|
24
|
-
parent_id: string;
|
|
25
|
-
profiles?: { full_name?: string | null } | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** One query for a whole page of parents, keyed by parent id. */
|
|
29
|
-
export function useStaffReplies(parentIds: string[]): Record<string, StaffReply[]> {
|
|
30
|
-
const [byParent, setByParent] = useState<Record<string, StaffReply[]>>({});
|
|
31
|
-
const key = parentIds.join(',');
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (parentIds.length === 0) {
|
|
35
|
-
setByParent({});
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let cancelled = false;
|
|
40
|
-
const supabase = createClient();
|
|
41
|
-
|
|
42
|
-
void supabase
|
|
43
|
-
.from('cms_interactions' as any)
|
|
44
|
-
.select('id, content, created_at, parent_id, profiles(full_name)')
|
|
45
|
-
.in('parent_id', parentIds)
|
|
46
|
-
.eq('status', 'approved')
|
|
47
|
-
.order('created_at', { ascending: true })
|
|
48
|
-
.then(({ data }: { data: StaffReply[] | null }) => {
|
|
49
|
-
if (cancelled || !data) return;
|
|
50
|
-
const grouped: Record<string, StaffReply[]> = {};
|
|
51
|
-
for (const reply of data) {
|
|
52
|
-
(grouped[reply.parent_id] ??= []).push(reply);
|
|
53
|
-
}
|
|
54
|
-
setByParent(grouped);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return () => {
|
|
58
|
-
cancelled = true;
|
|
59
|
-
};
|
|
60
|
-
// Keyed on the joined id list rather than the array: `parentIds` is a fresh array on
|
|
61
|
-
// every render, so depending on it directly would refetch forever.
|
|
62
|
-
}, [key]);
|
|
63
|
-
|
|
64
|
-
return byParent;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function StaffReplies({ replies }: { replies: StaffReply[] | undefined }) {
|
|
68
|
-
const { t, lang } = useTranslations();
|
|
69
|
-
|
|
70
|
-
if (!replies || replies.length === 0) return null;
|
|
71
|
-
|
|
72
|
-
const badge = t('interactions.staff_badge');
|
|
73
|
-
const badgeLabel = badge === 'interactions.staff_badge' ? 'Staff' : badge;
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<ol className="mt-4 space-y-3 border-l-2 border-primary/25 pl-4">
|
|
77
|
-
{replies.map((reply) => (
|
|
78
|
-
<li key={reply.id} className="rounded-xl bg-primary/5 p-3">
|
|
79
|
-
<div className="mb-1 flex flex-wrap items-baseline gap-2">
|
|
80
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-bold uppercase leading-none text-primary">
|
|
81
|
-
<ShieldCheck className="h-3 w-3" />
|
|
82
|
-
{badgeLabel}
|
|
83
|
-
</span>
|
|
84
|
-
<span className="text-xs font-semibold text-foreground">
|
|
85
|
-
{reply.profiles?.full_name || 'Support'}
|
|
86
|
-
</span>
|
|
87
|
-
<time className="text-[10px] text-muted-foreground" suppressHydrationWarning>
|
|
88
|
-
{new Date(reply.created_at).toLocaleDateString(lang, {
|
|
89
|
-
year: 'numeric',
|
|
90
|
-
month: 'long',
|
|
91
|
-
day: 'numeric',
|
|
92
|
-
})}
|
|
93
|
-
</time>
|
|
94
|
-
</div>
|
|
95
|
-
<p className="whitespace-pre-line text-sm leading-relaxed text-slate-600 dark:text-slate-350">
|
|
96
|
-
{reply.content}
|
|
97
|
-
</p>
|
|
98
|
-
</li>
|
|
99
|
-
))}
|
|
100
|
-
</ol>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { createClient } from '@nextblock-cms/db';
|
|
5
|
+
import { useTranslations } from '@nextblock-cms/utils';
|
|
6
|
+
import { ShieldCheck } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Staff answers rendered under the review or comment they belong to.
|
|
10
|
+
*
|
|
11
|
+
* A reply is stored as an ordinary approved `type='comment'` row carrying its parent's
|
|
12
|
+
* target, so it is readable through the same public RLS policy the parent is — no
|
|
13
|
+
* special access, nothing new exposed. It is fetched separately rather than joined
|
|
14
|
+
* because the parents arrive one page at a time.
|
|
15
|
+
*
|
|
16
|
+
* Shared by the product-reviews and post-comments sections, which are otherwise near
|
|
17
|
+
* duplicates of each other.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface StaffReply {
|
|
21
|
+
id: string;
|
|
22
|
+
content: string;
|
|
23
|
+
created_at: string;
|
|
24
|
+
parent_id: string;
|
|
25
|
+
profiles?: { full_name?: string | null } | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One query for a whole page of parents, keyed by parent id. */
|
|
29
|
+
export function useStaffReplies(parentIds: string[]): Record<string, StaffReply[]> {
|
|
30
|
+
const [byParent, setByParent] = useState<Record<string, StaffReply[]>>({});
|
|
31
|
+
const key = parentIds.join(',');
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (parentIds.length === 0) {
|
|
35
|
+
setByParent({});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
const supabase = createClient();
|
|
41
|
+
|
|
42
|
+
void supabase
|
|
43
|
+
.from('cms_interactions' as any)
|
|
44
|
+
.select('id, content, created_at, parent_id, profiles(full_name)')
|
|
45
|
+
.in('parent_id', parentIds)
|
|
46
|
+
.eq('status', 'approved')
|
|
47
|
+
.order('created_at', { ascending: true })
|
|
48
|
+
.then(({ data }: { data: StaffReply[] | null }) => {
|
|
49
|
+
if (cancelled || !data) return;
|
|
50
|
+
const grouped: Record<string, StaffReply[]> = {};
|
|
51
|
+
for (const reply of data) {
|
|
52
|
+
(grouped[reply.parent_id] ??= []).push(reply);
|
|
53
|
+
}
|
|
54
|
+
setByParent(grouped);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
cancelled = true;
|
|
59
|
+
};
|
|
60
|
+
// Keyed on the joined id list rather than the array: `parentIds` is a fresh array on
|
|
61
|
+
// every render, so depending on it directly would refetch forever.
|
|
62
|
+
}, [key]);
|
|
63
|
+
|
|
64
|
+
return byParent;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function StaffReplies({ replies }: { replies: StaffReply[] | undefined }) {
|
|
68
|
+
const { t, lang } = useTranslations();
|
|
69
|
+
|
|
70
|
+
if (!replies || replies.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
const badge = t('interactions.staff_badge');
|
|
73
|
+
const badgeLabel = badge === 'interactions.staff_badge' ? 'Staff' : badge;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<ol className="mt-4 space-y-3 border-l-2 border-primary/25 pl-4">
|
|
77
|
+
{replies.map((reply) => (
|
|
78
|
+
<li key={reply.id} className="rounded-xl bg-primary/5 p-3">
|
|
79
|
+
<div className="mb-1 flex flex-wrap items-baseline gap-2">
|
|
80
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-bold uppercase leading-none text-primary">
|
|
81
|
+
<ShieldCheck className="h-3 w-3" />
|
|
82
|
+
{badgeLabel}
|
|
83
|
+
</span>
|
|
84
|
+
<span className="text-xs font-semibold text-foreground">
|
|
85
|
+
{reply.profiles?.full_name || 'Support'}
|
|
86
|
+
</span>
|
|
87
|
+
<time className="text-[10px] text-muted-foreground" suppressHydrationWarning>
|
|
88
|
+
{new Date(reply.created_at).toLocaleDateString(lang, {
|
|
89
|
+
year: 'numeric',
|
|
90
|
+
month: 'long',
|
|
91
|
+
day: 'numeric',
|
|
92
|
+
})}
|
|
93
|
+
</time>
|
|
94
|
+
</div>
|
|
95
|
+
<p className="whitespace-pre-line text-sm leading-relaxed text-slate-600 dark:text-slate-350">
|
|
96
|
+
{reply.content}
|
|
97
|
+
</p>
|
|
98
|
+
</li>
|
|
99
|
+
))}
|
|
100
|
+
</ol>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { Skeleton } from '@nextblock-cms/ui';
|
|
1
|
+
import { Skeleton } from '@nextblock-cms/ui';
|
|
2
2
|
|
|
3
3
|
const PostCardSkeleton = () => {
|
|
4
4
|
return (
|
|
5
|
-
<div className="border rounded-lg overflow-hidden shadow-sm bg-card text-card-foreground">
|
|
6
|
-
<Skeleton className="h-48 w-full" />
|
|
7
|
-
<div className="p-4 space-y-3">
|
|
8
|
-
<div className="flex items-center gap-2">
|
|
9
|
-
<Skeleton className="h-6 w-20 rounded-full" />
|
|
10
|
-
<Skeleton className="h-4 w-16" />
|
|
11
|
-
</div>
|
|
12
|
-
<Skeleton className="h-5 w-3/4" />
|
|
13
|
-
<Skeleton className="h-4 w-full" />
|
|
14
|
-
<Skeleton className="h-4 w-5/6" />
|
|
5
|
+
<div className="border rounded-lg overflow-hidden shadow-sm bg-card text-card-foreground">
|
|
6
|
+
<Skeleton className="h-48 w-full" />
|
|
7
|
+
<div className="p-4 space-y-3">
|
|
8
|
+
<div className="flex items-center gap-2">
|
|
9
|
+
<Skeleton className="h-6 w-20 rounded-full" />
|
|
10
|
+
<Skeleton className="h-4 w-16" />
|
|
11
|
+
</div>
|
|
12
|
+
<Skeleton className="h-5 w-3/4" />
|
|
13
|
+
<Skeleton className="h-4 w-full" />
|
|
14
|
+
<Skeleton className="h-4 w-5/6" />
|
|
15
15
|
<Skeleton className="h-4 w-1/4 mt-2" />
|
|
16
16
|
</div>
|
|
17
17
|
</div>
|
|
18
18
|
);
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export default PostCardSkeleton;
|
|
21
|
+
export default PostCardSkeleton;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// components/blocks/PostsGridBlock.tsx
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import type { Database } from '@nextblock-cms/db';
|
|
4
|
-
|
|
5
|
-
type Block = Database['public']['Tables']['blocks']['Row'];
|
|
6
|
-
// import Link from 'next/link'; // Unused, PostsGridClient handles links
|
|
7
|
-
import PostsGridClient from './PostsGridClient';
|
|
8
|
-
import { fetchInitialPublishedPosts, fetchPaginatedPublishedPosts } from '../../app/actions/postActions';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { Database } from '@nextblock-cms/db';
|
|
4
|
+
|
|
5
|
+
type Block = Database['public']['Tables']['blocks']['Row'];
|
|
6
|
+
// import Link from 'next/link'; // Unused, PostsGridClient handles links
|
|
7
|
+
import PostsGridClient from './PostsGridClient';
|
|
8
|
+
import { fetchInitialPublishedPosts, fetchPaginatedPublishedPosts } from '../../app/actions/postActions';
|
|
9
9
|
|
|
10
10
|
interface PostsGridBlockProps {
|
|
11
11
|
block: Block;
|
|
@@ -20,11 +20,11 @@ const PostsGridBlock: React.FC<PostsGridBlockProps> = async ({ block, languageId
|
|
|
20
20
|
showPagination = true,
|
|
21
21
|
} = block.content as { title?: string, postsPerPage?: number, columns?: number, showPagination?: boolean };
|
|
22
22
|
|
|
23
|
-
const { posts: initialPosts, totalCount, error: postsError } = await fetchInitialPublishedPosts(languageId, postsPerPage);
|
|
24
|
-
|
|
25
|
-
if (postsError) {
|
|
26
|
-
return <div className="text-red-500">Error loading posts: {postsError}</div>;
|
|
27
|
-
}
|
|
23
|
+
const { posts: initialPosts, totalCount, error: postsError } = await fetchInitialPublishedPosts(languageId, postsPerPage);
|
|
24
|
+
|
|
25
|
+
if (postsError) {
|
|
26
|
+
return <div className="text-red-500">Error loading posts: {postsError}</div>;
|
|
27
|
+
}
|
|
28
28
|
|
|
29
29
|
if (!initialPosts || initialPosts.length === 0) {
|
|
30
30
|
return (
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// components/blocks/PostsGridClient.tsx
|
|
2
2
|
'use client';
|
|
3
3
|
|
|
4
|
-
import React, { useState, useEffect } from 'react';
|
|
5
|
-
import Link from 'next/link';
|
|
6
|
-
import { useLanguage } from '../../context/LanguageContext';
|
|
7
|
-
import type { PostWithMediaDimensions } from './types';
|
|
8
|
-
import Image from 'next/image';
|
|
9
|
-
import { Button } from '@nextblock-cms/ui';
|
|
10
|
-
import PostCardSkeleton from './PostCardSkeleton'; // Added import
|
|
4
|
+
import React, { useState, useEffect } from 'react';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { useLanguage } from '../../context/LanguageContext';
|
|
7
|
+
import type { PostWithMediaDimensions } from './types';
|
|
8
|
+
import Image from 'next/image';
|
|
9
|
+
import { Button } from '@nextblock-cms/ui';
|
|
10
|
+
import PostCardSkeleton from './PostCardSkeleton'; // Added import
|
|
11
11
|
|
|
12
12
|
interface PostsGridClientProps {
|
|
13
13
|
initialPosts: PostWithMediaDimensions[];
|
|
@@ -20,10 +20,10 @@ interface PostsGridClientProps {
|
|
|
20
20
|
fetchAction: (languageId: number, page: number, limit: number) => Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string }>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const DEFAULT_FEATURE_IMAGE_WIDTH = 1600;
|
|
24
|
-
const DEFAULT_FEATURE_IMAGE_HEIGHT = 900;
|
|
25
|
-
|
|
26
|
-
const PostsGridClient: React.FC<PostsGridClientProps> = ({
|
|
23
|
+
const DEFAULT_FEATURE_IMAGE_WIDTH = 1600;
|
|
24
|
+
const DEFAULT_FEATURE_IMAGE_HEIGHT = 900;
|
|
25
|
+
|
|
26
|
+
const PostsGridClient: React.FC<PostsGridClientProps> = ({
|
|
27
27
|
initialPosts,
|
|
28
28
|
initialPage,
|
|
29
29
|
postsPerPage,
|
|
@@ -32,10 +32,10 @@ const PostsGridClient: React.FC<PostsGridClientProps> = ({
|
|
|
32
32
|
languageId,
|
|
33
33
|
showPagination,
|
|
34
34
|
fetchAction,
|
|
35
|
-
}) => {
|
|
36
|
-
const { currentLocale } = useLanguage();
|
|
37
|
-
const [currentPage, setCurrentPage] = useState(initialPage);
|
|
38
|
-
const [posts, setPosts] = useState<PostWithMediaDimensions[]>(initialPosts);
|
|
35
|
+
}) => {
|
|
36
|
+
const { currentLocale } = useLanguage();
|
|
37
|
+
const [currentPage, setCurrentPage] = useState(initialPage);
|
|
38
|
+
const [posts, setPosts] = useState<PostWithMediaDimensions[]>(initialPosts);
|
|
39
39
|
const [isLoading, setIsLoading] = useState(false);
|
|
40
40
|
const [error, setError] = useState<string | null>(null);
|
|
41
41
|
// Initialize skeletonCount to postsPerPage, or a sensible minimum if initialPosts is empty.
|
|
@@ -112,40 +112,40 @@ const PostsGridClient: React.FC<PostsGridClientProps> = ({
|
|
|
112
112
|
<PostCardSkeleton key={`skeleton-${index}`} />
|
|
113
113
|
))
|
|
114
114
|
) : posts.length > 0 ? (
|
|
115
|
-
posts.map((post) => (
|
|
116
|
-
<Link href={`/article/${post.slug}`} key={post.id} className="block group h-full">
|
|
115
|
+
posts.map((post) => (
|
|
116
|
+
<Link href={`/article/${post.slug}`} key={post.id} className="block group h-full">
|
|
117
117
|
<div className="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow bg-card text-card-foreground h-full flex flex-col">
|
|
118
118
|
{/* Basic Post Card Structure - Enhanced with Feature Image */}
|
|
119
|
-
{post.feature_image_url ? (
|
|
120
|
-
<div className="aspect-video overflow-hidden">
|
|
121
|
-
<Image
|
|
122
|
-
src={post.feature_image_url}
|
|
123
|
-
alt={`Feature image for ${post.title}`}
|
|
124
|
-
width={post.feature_image_width && post.feature_image_width > 0 ? post.feature_image_width : DEFAULT_FEATURE_IMAGE_WIDTH}
|
|
125
|
-
height={post.feature_image_height && post.feature_image_height > 0 ? post.feature_image_height : DEFAULT_FEATURE_IMAGE_HEIGHT}
|
|
126
|
-
sizes={imageSizes}
|
|
127
|
-
loading="lazy"
|
|
128
|
-
placeholder={post.blur_data_url ? 'blur' : 'empty'}
|
|
129
|
-
blurDataURL={post.blur_data_url ?? undefined}
|
|
130
|
-
quality={60}
|
|
131
|
-
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
132
|
-
/>
|
|
133
|
-
</div>
|
|
134
|
-
) : null}
|
|
135
|
-
<div className="p-4 flex flex-1 flex-col">
|
|
136
|
-
<div className="mb-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
137
|
-
<span className="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-semibold text-slate-700">
|
|
138
|
-
{post.label?.trim() || 'Article'}
|
|
139
|
-
</span>
|
|
140
|
-
<span>
|
|
141
|
-
{post.estimated_read_time_minutes} {currentLocale === 'fr' ? 'min de lecture' : 'min read'}
|
|
142
|
-
</span>
|
|
143
|
-
</div>
|
|
144
|
-
<h3 className="text-lg font-semibold mb-2 group-hover:text-primary">{post.title}</h3>
|
|
145
|
-
{post.excerpt && <p className="text-sm text-muted-foreground mb-3 line-clamp-3">{post.excerpt}</p>}
|
|
146
|
-
<div className="mt-auto pt-2">
|
|
147
|
-
<span className="text-xs text-primary group-hover:underline">Read more</span>
|
|
148
|
-
</div>
|
|
119
|
+
{post.feature_image_url ? (
|
|
120
|
+
<div className="aspect-video overflow-hidden">
|
|
121
|
+
<Image
|
|
122
|
+
src={post.feature_image_url}
|
|
123
|
+
alt={`Feature image for ${post.title}`}
|
|
124
|
+
width={post.feature_image_width && post.feature_image_width > 0 ? post.feature_image_width : DEFAULT_FEATURE_IMAGE_WIDTH}
|
|
125
|
+
height={post.feature_image_height && post.feature_image_height > 0 ? post.feature_image_height : DEFAULT_FEATURE_IMAGE_HEIGHT}
|
|
126
|
+
sizes={imageSizes}
|
|
127
|
+
loading="lazy"
|
|
128
|
+
placeholder={post.blur_data_url ? 'blur' : 'empty'}
|
|
129
|
+
blurDataURL={post.blur_data_url ?? undefined}
|
|
130
|
+
quality={60}
|
|
131
|
+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
) : null}
|
|
135
|
+
<div className="p-4 flex flex-1 flex-col">
|
|
136
|
+
<div className="mb-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
137
|
+
<span className="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-semibold text-slate-700">
|
|
138
|
+
{post.label?.trim() || 'Article'}
|
|
139
|
+
</span>
|
|
140
|
+
<span>
|
|
141
|
+
{post.estimated_read_time_minutes} {currentLocale === 'fr' ? 'min de lecture' : 'min read'}
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
<h3 className="text-lg font-semibold mb-2 group-hover:text-primary">{post.title}</h3>
|
|
145
|
+
{post.excerpt && <p className="text-sm text-muted-foreground mb-3 line-clamp-3">{post.excerpt}</p>}
|
|
146
|
+
<div className="mt-auto pt-2">
|
|
147
|
+
<span className="text-xs text-primary group-hover:underline">Read more</span>
|
|
148
|
+
</div>
|
|
149
149
|
</div>
|
|
150
150
|
</div>
|
|
151
151
|
</Link>
|
|
@@ -181,4 +181,4 @@ const PostsGridClient: React.FC<PostsGridClientProps> = ({
|
|
|
181
181
|
);
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
-
export default PostsGridClient;
|
|
184
|
+
export default PostsGridClient;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { z } from '../../lib/zod-config';
|
|
3
|
-
import { BlockConfig, BlockProps, BlockEditorProps } from '@nextblock-cms/sdk';
|
|
4
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@nextblock-cms/ui';
|
|
5
|
-
import { Card, CardContent } from '@nextblock-cms/ui';
|
|
6
|
-
import { Input } from '@nextblock-cms/ui';
|
|
7
|
-
import { Label } from '@nextblock-cms/ui';
|
|
8
|
-
import { Textarea } from '@nextblock-cms/ui';
|
|
9
|
-
import { MessageSquareQuote } from 'lucide-react';
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { z } from '../../lib/zod-config';
|
|
3
|
+
import { BlockConfig, BlockProps, BlockEditorProps } from '@nextblock-cms/sdk';
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@nextblock-cms/ui';
|
|
5
|
+
import { Card, CardContent } from '@nextblock-cms/ui';
|
|
6
|
+
import { Input } from '@nextblock-cms/ui';
|
|
7
|
+
import { Label } from '@nextblock-cms/ui';
|
|
8
|
+
import { Textarea } from '@nextblock-cms/ui';
|
|
9
|
+
import { MessageSquareQuote } from 'lucide-react';
|
|
10
10
|
|
|
11
11
|
// 1. Define the Schema
|
|
12
12
|
export const TestimonialSchema = z.object({
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import type { ComponentType } from "react";
|
|
2
|
-
|
|
3
|
-
type PublicBlockRendererLoader = () => Promise<{
|
|
4
|
-
default: ComponentType<any>;
|
|
5
|
-
}>;
|
|
6
|
-
|
|
7
|
-
const ecommerceBlockRendererLoaders: Record<string, PublicBlockRendererLoader> = {
|
|
8
|
-
product_grid: () => import("./renderers/ProductGridBlockRenderer"),
|
|
9
|
-
featured_product: () => import("./renderers/FeaturedProductBlockRenderer"),
|
|
10
|
-
cart: () => import("./renderers/CartBlockRenderer"),
|
|
11
|
-
checkout: () => import("./renderers/CheckoutBlockRenderer"),
|
|
12
|
-
product_details: () => import("./renderers/ProductDetailsBlockRenderer"),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function loadEcommerceBlockRenderer(blockType: string) {
|
|
16
|
-
const loader = ecommerceBlockRendererLoaders[blockType];
|
|
17
|
-
|
|
18
|
-
if (!loader) {
|
|
19
|
-
return Promise.reject(new Error(`Unsupported ecommerce block type: ${blockType}`));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return loader();
|
|
23
|
-
}
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
|
|
3
|
+
type PublicBlockRendererLoader = () => Promise<{
|
|
4
|
+
default: ComponentType<any>;
|
|
5
|
+
}>;
|
|
6
|
+
|
|
7
|
+
const ecommerceBlockRendererLoaders: Record<string, PublicBlockRendererLoader> = {
|
|
8
|
+
product_grid: () => import("./renderers/ProductGridBlockRenderer"),
|
|
9
|
+
featured_product: () => import("./renderers/FeaturedProductBlockRenderer"),
|
|
10
|
+
cart: () => import("./renderers/CartBlockRenderer"),
|
|
11
|
+
checkout: () => import("./renderers/CheckoutBlockRenderer"),
|
|
12
|
+
product_details: () => import("./renderers/ProductDetailsBlockRenderer"),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function loadEcommerceBlockRenderer(blockType: string) {
|
|
16
|
+
const loader = ecommerceBlockRendererLoaders[blockType];
|
|
17
|
+
|
|
18
|
+
if (!loader) {
|
|
19
|
+
return Promise.reject(new Error(`Unsupported ecommerce block type: ${blockType}`));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return loader();
|
|
23
|
+
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { ComponentType } from "react";
|
|
2
|
-
import type { BlockType } from "../../lib/blocks/blockRegistry";
|
|
3
|
-
|
|
4
|
-
type PublicBlockRendererLoader = () => Promise<{
|
|
5
|
-
default: ComponentType<any>;
|
|
6
|
-
}>;
|
|
7
|
-
|
|
8
|
-
const publicBlockRendererLoaders: Partial<
|
|
9
|
-
Record<BlockType | "hero", PublicBlockRendererLoader>
|
|
10
|
-
> = {
|
|
11
|
-
text: () => import("./renderers/TextBlockRenderer"),
|
|
12
|
-
heading: () => import("./renderers/HeadingBlockRenderer"),
|
|
13
|
-
image: () => import("./renderers/ImageBlockRenderer"),
|
|
14
|
-
button: () => import("./renderers/ButtonBlockRenderer"),
|
|
15
|
-
posts_grid: () => import("./renderers/PostsGridBlockRenderer"),
|
|
16
|
-
video_embed: () => import("./renderers/VideoEmbedBlockRenderer"),
|
|
17
|
-
section: () => import("./renderers/SectionBlockRenderer"),
|
|
18
|
-
hero: () => import("./renderers/SectionBlockRenderer"),
|
|
19
|
-
form: () => import("./renderers/FormBlockRenderer"),
|
|
20
|
-
testimonial: () => import("./renderers/TestimonialBlockRenderer"),
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function getPublicBlockRendererLoader(blockType: string) {
|
|
24
|
-
return publicBlockRendererLoaders[blockType as BlockType | "hero"];
|
|
25
|
-
}
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { BlockType } from "../../lib/blocks/blockRegistry";
|
|
3
|
+
|
|
4
|
+
type PublicBlockRendererLoader = () => Promise<{
|
|
5
|
+
default: ComponentType<any>;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
const publicBlockRendererLoaders: Partial<
|
|
9
|
+
Record<BlockType | "hero", PublicBlockRendererLoader>
|
|
10
|
+
> = {
|
|
11
|
+
text: () => import("./renderers/TextBlockRenderer"),
|
|
12
|
+
heading: () => import("./renderers/HeadingBlockRenderer"),
|
|
13
|
+
image: () => import("./renderers/ImageBlockRenderer"),
|
|
14
|
+
button: () => import("./renderers/ButtonBlockRenderer"),
|
|
15
|
+
posts_grid: () => import("./renderers/PostsGridBlockRenderer"),
|
|
16
|
+
video_embed: () => import("./renderers/VideoEmbedBlockRenderer"),
|
|
17
|
+
section: () => import("./renderers/SectionBlockRenderer"),
|
|
18
|
+
hero: () => import("./renderers/SectionBlockRenderer"),
|
|
19
|
+
form: () => import("./renderers/FormBlockRenderer"),
|
|
20
|
+
testimonial: () => import("./renderers/TestimonialBlockRenderer"),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function getPublicBlockRendererLoader(blockType: string) {
|
|
24
|
+
return publicBlockRendererLoaders[blockType as BlockType | "hero"];
|
|
25
|
+
}
|