create-nextblock 0.2.78 → 0.8.1
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/bin/create-nextblock.js +793 -472
- package/package.json +1 -2
- package/scripts/sync-template.js +18 -1
- package/templates/nextblock-template/.browserslistrc +11 -0
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/README.md +23 -114
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
- package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
- package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
- package/templates/nextblock-template/app/actions/consent.ts +57 -0
- package/templates/nextblock-template/app/actions/formActions.ts +130 -11
- package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
- package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
- package/templates/nextblock-template/app/actions/postActions.ts +146 -48
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
- package/templates/nextblock-template/app/actions.ts +67 -12
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
- package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
- package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
- package/templates/nextblock-template/app/api/draft/route.ts +93 -0
- package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
- package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
- package/templates/nextblock-template/app/api/search/route.ts +43 -0
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
- package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
- package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
- package/templates/nextblock-template/app/cart/page.tsx +7 -0
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
- package/templates/nextblock-template/app/checkout/page.tsx +52 -0
- package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
- package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
- package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
- package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
- package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
- package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
- package/templates/nextblock-template/app/cms/layout.tsx +29 -10
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
- package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
- package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
- package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
- package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
- package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
- package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
- package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
- package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
- package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
- package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
- package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
- package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
- package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
- package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
- package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
- package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
- package/templates/nextblock-template/app/globals.css +9 -0
- package/templates/nextblock-template/app/layout.tsx +372 -120
- package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
- package/templates/nextblock-template/app/lib/seo.ts +279 -0
- package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
- package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
- package/templates/nextblock-template/app/page.tsx +165 -73
- package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
- package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
- package/templates/nextblock-template/app/profile/account-data.ts +37 -0
- package/templates/nextblock-template/app/profile/account-links.ts +22 -0
- package/templates/nextblock-template/app/profile/account-types.ts +11 -0
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
- package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
- package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
- package/templates/nextblock-template/app/profile/page.tsx +51 -0
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
- package/templates/nextblock-template/app/providers.tsx +55 -17
- package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
- package/templates/nextblock-template/app/sitemap.ts +128 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
- package/templates/nextblock-template/components/AppShell.tsx +154 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
- package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
- package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
- package/templates/nextblock-template/components/Header.tsx +49 -41
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
- package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
- package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/types.ts +7 -6
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -26
- package/templates/nextblock-template/components/header-auth.tsx +69 -17
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
- package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
- package/templates/nextblock-template/context/AuthContext.tsx +23 -90
- package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
- package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
- package/templates/nextblock-template/context/language-rest-client.ts +31 -0
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
- package/templates/nextblock-template/docs/README.md +34 -0
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
- package/templates/nextblock-template/index.d.ts +2 -0
- package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
- package/templates/nextblock-template/lib/ai-client.ts +247 -0
- package/templates/nextblock-template/lib/ai-config.ts +81 -0
- package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
- package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
- package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
- package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
- package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
- package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
- package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
- package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
- package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
- package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
- package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
- package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
- package/templates/nextblock-template/lib/blocks/README.md +13 -670
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
- package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
- package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
- package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
- package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
- package/templates/nextblock-template/lib/privacy/types.ts +67 -0
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
- package/templates/nextblock-template/lib/promotions/server.ts +741 -0
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
- package/templates/nextblock-template/lib/search/server.ts +585 -0
- package/templates/nextblock-template/lib/search/types.ts +27 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
- package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
- package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
- package/templates/nextblock-template/lib/zod-config.ts +5 -0
- package/templates/nextblock-template/next.config.js +190 -66
- package/templates/nextblock-template/package.json +34 -30
- package/templates/nextblock-template/proxy.ts +435 -253
- package/templates/nextblock-template/public/images/NBcover.webp +0 -0
- package/templates/nextblock-template/public/images/cap.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
- package/templates/nextblock-template/public/images/extensibility.webp +0 -0
- package/templates/nextblock-template/public/images/goals.webp +0 -0
- package/templates/nextblock-template/public/images/included.webp +0 -0
- package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
- package/templates/nextblock-template/public/images/pants.webp +0 -0
- package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
- package/templates/nextblock-template/tailwind.config.js +1 -0
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
- package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
- package/templates/nextblock-template/tsconfig.json +52 -66
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -0
- package/templates/nextblock-template/app/force-styles.tsx +0 -31
- package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
- package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
- package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
- package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
- package/templates/nextblock-template/docs/files-structure.md +0 -426
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
|
@@ -4,14 +4,16 @@ import React, { useState, lazy } from 'react';
|
|
|
4
4
|
import { cn } from '@nextblock-cms/utils';
|
|
5
5
|
import { Button } from '@nextblock-cms/ui';
|
|
6
6
|
import { PlusCircle, Trash2, Edit2, GripVertical, Image as ImageIcon } from "lucide-react";
|
|
7
|
-
import
|
|
8
|
-
import { availableBlockTypes, getBlockDefinition, getInitialContent, BlockType } from '
|
|
7
|
+
import { SectionBlockContent } from '../../../../lib/blocks/blockRegistry';
|
|
8
|
+
import { availableBlockTypes, getBlockDefinition, getInitialContent, BlockType } from '../../../../lib/blocks/blockRegistry';
|
|
9
9
|
import { useDroppable } from "@dnd-kit/core";
|
|
10
10
|
import { useSortable } from "@dnd-kit/sortable";
|
|
11
11
|
import { CSS } from "@dnd-kit/utilities";
|
|
12
12
|
import { BlockEditorModal } from './BlockEditorModal';
|
|
13
13
|
import { ConfirmationDialog } from '@nextblock-cms/ui';
|
|
14
14
|
import BlockTypeSelector from './BlockTypeSelector';
|
|
15
|
+
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
16
|
+
import { CustomBlockEditorPreview } from './CustomBlockEditorPreview';
|
|
15
17
|
|
|
16
18
|
type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
|
|
17
19
|
|
|
@@ -22,7 +24,7 @@ interface SortableColumnBlockProps {
|
|
|
22
24
|
columnIndex: number;
|
|
23
25
|
onEdit: () => void;
|
|
24
26
|
onDelete: () => void;
|
|
25
|
-
blockType: 'section'
|
|
27
|
+
blockType: 'section';
|
|
26
28
|
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
27
29
|
sectionBackground?: SectionBlockContent['background'];
|
|
28
30
|
}
|
|
@@ -51,9 +53,6 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
|
|
|
51
53
|
transition,
|
|
52
54
|
opacity: isDragging ? 0.5 : 1,
|
|
53
55
|
};
|
|
54
|
-
|
|
55
|
-
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
56
|
-
|
|
57
56
|
// Helper to check for dark background
|
|
58
57
|
const isDarkBackground = React.useMemo(() => {
|
|
59
58
|
if (!sectionBackground) return false;
|
|
@@ -181,7 +180,7 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
|
|
|
181
180
|
);
|
|
182
181
|
}
|
|
183
182
|
case 'image': {
|
|
184
|
-
const imageUrl = block.content.object_key
|
|
183
|
+
const imageUrl = resolveMediaUrl(block.content.object_key) || block.content.src;
|
|
185
184
|
return (
|
|
186
185
|
<div className="flex gap-3">
|
|
187
186
|
<div className="flex-shrink-0 h-10 w-10 bg-muted/20 rounded overflow-hidden flex items-center justify-center border border-white/10">
|
|
@@ -255,10 +254,26 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
|
|
|
255
254
|
</div>
|
|
256
255
|
</div>
|
|
257
256
|
);
|
|
258
|
-
default:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
default: {
|
|
258
|
+
const formattedLabel = block.block_type
|
|
259
|
+
.split(/[-_]/)
|
|
260
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
261
|
+
.join(" ");
|
|
262
|
+
const placeholder = (
|
|
263
|
+
<div className={cn("text-xs flex flex-col gap-0.5", isDarkBackground ? "text-white/70" : "text-muted-foreground")}>
|
|
264
|
+
<span className="font-medium">{formattedLabel}</span>
|
|
265
|
+
<span className="text-[10px] opacity-70">Click edit to configure</span>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
// Custom blocks (block_type === definition slug) render their real layout.
|
|
269
|
+
return (
|
|
270
|
+
<CustomBlockEditorPreview
|
|
271
|
+
blockType={block.block_type}
|
|
272
|
+
content={(block.content || {}) as Record<string, any>}
|
|
273
|
+
fallback={placeholder}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
262
277
|
}
|
|
263
278
|
};
|
|
264
279
|
|
|
@@ -301,7 +316,7 @@ export interface ColumnEditorProps {
|
|
|
301
316
|
columnIndex: number;
|
|
302
317
|
blocks: ColumnBlock[];
|
|
303
318
|
onBlocksChange: (newBlocks: ColumnBlock[]) => void;
|
|
304
|
-
blockType: 'section'
|
|
319
|
+
blockType: 'section';
|
|
305
320
|
sectionBackground?: SectionBlockContent['background'];
|
|
306
321
|
}
|
|
307
322
|
|
|
@@ -374,7 +389,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
374
389
|
const initialContent = getInitialContent(selectedBlockType);
|
|
375
390
|
const newBlock: ColumnBlock = {
|
|
376
391
|
block_type: selectedBlockType,
|
|
377
|
-
content: initialContent || {},
|
|
392
|
+
content: (initialContent || {}) as Record<string, any>,
|
|
378
393
|
temp_id: `temp-${Date.now()}-${Math.random()}`
|
|
379
394
|
};
|
|
380
395
|
onBlocksChange([...blocks, newBlock]);
|
|
@@ -406,7 +421,9 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
406
421
|
const handleStartEdit = (block: ColumnBlock, index: number) => {
|
|
407
422
|
const blockDef = getBlockDefinition(block.block_type);
|
|
408
423
|
if (!blockDef) {
|
|
409
|
-
|
|
424
|
+
const Editor = lazy(() => import(`../editors/DynamicCustomBlockEditor`));
|
|
425
|
+
setLazyEditor(Editor);
|
|
426
|
+
setEditingBlock({ ...block, index });
|
|
410
427
|
return;
|
|
411
428
|
}
|
|
412
429
|
|
|
@@ -534,9 +551,9 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
534
551
|
onOpenChange={setIsBlockSelectorOpen}
|
|
535
552
|
onSelectBlockType={handleSelectBlockType}
|
|
536
553
|
allowedBlockTypes={availableBlockTypes.filter(
|
|
537
|
-
(type) => type !== 'section'
|
|
554
|
+
(type) => type !== 'section'
|
|
538
555
|
)}
|
|
539
556
|
/>
|
|
540
557
|
</div>
|
|
541
558
|
);
|
|
542
|
-
}
|
|
559
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
|
|
6
|
+
|
|
7
|
+
// Module-level cache so every block preview shares a single network request for
|
|
8
|
+
// the custom block definitions instead of re-fetching per card.
|
|
9
|
+
type EditorDefinition = {
|
|
10
|
+
slug: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
fields: any[];
|
|
14
|
+
layout_schema: any;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const definitionCache: { definitions: EditorDefinition[]; promise: Promise<EditorDefinition[]> | null } = {
|
|
18
|
+
definitions: [],
|
|
19
|
+
promise: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function loadCustomBlockDefinitions(): Promise<EditorDefinition[]> {
|
|
23
|
+
if (definitionCache.definitions.length > 0) {
|
|
24
|
+
return Promise.resolve(definitionCache.definitions);
|
|
25
|
+
}
|
|
26
|
+
if (!definitionCache.promise) {
|
|
27
|
+
definitionCache.promise = fetch("/api/custom-blocks/editor-definitions")
|
|
28
|
+
.then((res) => (res.ok ? res.json() : { definitions: [] }))
|
|
29
|
+
.then((data) => {
|
|
30
|
+
definitionCache.definitions = (data?.definitions ?? []) as EditorDefinition[];
|
|
31
|
+
return definitionCache.definitions;
|
|
32
|
+
})
|
|
33
|
+
.catch(() => []);
|
|
34
|
+
}
|
|
35
|
+
return definitionCache.promise;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function useCustomBlockDefinition(slug: string) {
|
|
39
|
+
const [definitions, setDefinitions] = useState<EditorDefinition[]>(definitionCache.definitions);
|
|
40
|
+
const [loading, setLoading] = useState(definitionCache.definitions.length === 0);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
loadCustomBlockDefinitions().then((defs) => {
|
|
45
|
+
if (!cancelled) {
|
|
46
|
+
setDefinitions(defs);
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const definition = useMemo(
|
|
56
|
+
() => definitions.find((def) => def.slug === slug) ?? null,
|
|
57
|
+
[definitions, slug]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return { definition, loading };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolve db_relation field values into records so the preview matches the front
|
|
64
|
+
// end (including relation images), mirroring the server-side hydration.
|
|
65
|
+
function useResolvedRelations(definition: EditorDefinition | null, content: Record<string, any>) {
|
|
66
|
+
const [resolvedRelations, setResolvedRelations] = useState<Record<string, any>>({});
|
|
67
|
+
|
|
68
|
+
const relationSignature = useMemo(() => {
|
|
69
|
+
if (!definition) return "";
|
|
70
|
+
return definition.fields
|
|
71
|
+
.filter((field) => field.type === "db_relation")
|
|
72
|
+
.map((field) => `${field.key}:${JSON.stringify(content?.[field.key] ?? null)}`)
|
|
73
|
+
.join("|");
|
|
74
|
+
}, [definition, content]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!definition) return;
|
|
78
|
+
const relationFields = definition.fields.filter((field) => field.type === "db_relation");
|
|
79
|
+
if (relationFields.length === 0) {
|
|
80
|
+
setResolvedRelations({});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
(async () => {
|
|
86
|
+
const next: Record<string, any> = {};
|
|
87
|
+
await Promise.all(
|
|
88
|
+
relationFields.map(async (field) => {
|
|
89
|
+
const raw = content?.[field.key];
|
|
90
|
+
const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
91
|
+
if (values.length === 0) return;
|
|
92
|
+
|
|
93
|
+
const params = new URLSearchParams({
|
|
94
|
+
table: field.table,
|
|
95
|
+
valueColumn: field.value_column || "id",
|
|
96
|
+
displayColumn: field.display_column || "title",
|
|
97
|
+
values: values.map(String).join(","),
|
|
98
|
+
limit: String(values.length),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`/api/custom-blocks/db-relations?${params.toString()}`, {
|
|
103
|
+
cache: "no-store",
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) return;
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
const items = data?.items ?? [];
|
|
108
|
+
next[field.key] = field.multiple ? items : items[0] ?? null;
|
|
109
|
+
} catch {
|
|
110
|
+
// Leave the relation unresolved; the engine falls back gracefully.
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!cancelled) {
|
|
116
|
+
setResolvedRelations(next);
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
};
|
|
123
|
+
}, [definition, relationSignature]);
|
|
124
|
+
|
|
125
|
+
return resolvedRelations;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface CustomBlockEditorPreviewProps {
|
|
129
|
+
blockType: string;
|
|
130
|
+
content: Record<string, any>;
|
|
131
|
+
fallback: React.ReactNode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function CustomBlockEditorPreview({ blockType, content, fallback }: CustomBlockEditorPreviewProps) {
|
|
135
|
+
const { definition, loading } = useCustomBlockDefinition(blockType);
|
|
136
|
+
const resolvedRelations = useResolvedRelations(definition, content);
|
|
137
|
+
|
|
138
|
+
if (!definition) {
|
|
139
|
+
if (loading) {
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex items-center gap-2 py-3 text-xs text-muted-foreground">
|
|
142
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
143
|
+
Loading block preview…
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return <>{fallback}</>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
// pointer-events-none lets clicks fall through to the card so editing still opens.
|
|
152
|
+
<div className="pointer-events-none w-full overflow-hidden">
|
|
153
|
+
<DynamicLayoutEngine
|
|
154
|
+
fields={definition.fields}
|
|
155
|
+
layoutSchema={definition.layout_schema}
|
|
156
|
+
data={{ ...(content || {}), resolved_relations: resolvedRelations }}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -8,12 +8,13 @@ import PostsGridBlockEditor from '../editors/PostsGridBlockEditor';
|
|
|
8
8
|
type Block = Database['public']['Tables']['blocks']['Row'];
|
|
9
9
|
import { Button, Card, CardContent, Avatar, AvatarImage, AvatarFallback } from "@nextblock-cms/ui";
|
|
10
10
|
import { GripVertical, Edit2, Image as ImageIcon, MessageSquareQuote } from "lucide-react";
|
|
11
|
-
import { getBlockDefinition, blockRegistry, BlockType } from
|
|
11
|
+
import { getBlockDefinition, blockRegistry, BlockType } from '../../../../lib/blocks/blockRegistry';
|
|
12
12
|
import { BlockEditorModal } from './BlockEditorModal';
|
|
13
13
|
import { DeleteBlockButtonClient } from './DeleteBlockButtonClient';
|
|
14
14
|
import { cn } from '@nextblock-cms/utils';
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
16
|
+
import { SimpleTiptapRenderer } from '@nextblock-cms/ecommerce';
|
|
17
|
+
import { CustomBlockEditorPreview } from './CustomBlockEditorPreview';
|
|
17
18
|
|
|
18
19
|
export interface EditableBlockProps {
|
|
19
20
|
block: Block;
|
|
@@ -39,7 +40,7 @@ export default function EditableBlock({
|
|
|
39
40
|
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<any>> | ComponentType<any> | null>(null);
|
|
40
41
|
|
|
41
42
|
const SectionEditor = useMemo(() => {
|
|
42
|
-
if (block?.block_type === 'section'
|
|
43
|
+
if (block?.block_type === 'section') {
|
|
43
44
|
const editorFilename = blockRegistry[block.block_type as BlockType]?.editorComponentFilename;
|
|
44
45
|
if (editorFilename) {
|
|
45
46
|
return lazy(() => import(`../editors/${editorFilename}`));
|
|
@@ -57,7 +58,7 @@ export default function EditableBlock({
|
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
const handleEditClick = () => {
|
|
60
|
-
if (block.block_type === 'section'
|
|
61
|
+
if (block.block_type === 'section') {
|
|
61
62
|
setIsConfigPanelOpen(prev => !prev);
|
|
62
63
|
} else {
|
|
63
64
|
const blockDef = getBlockDefinition(block.block_type as BlockType);
|
|
@@ -78,6 +79,11 @@ export default function EditableBlock({
|
|
|
78
79
|
setLazyEditor(Editor);
|
|
79
80
|
setEditingBlock(block);
|
|
80
81
|
}
|
|
82
|
+
else {
|
|
83
|
+
const Editor = lazy(() => import(`../editors/DynamicCustomBlockEditor`));
|
|
84
|
+
setLazyEditor(Editor);
|
|
85
|
+
setEditingBlock(block);
|
|
86
|
+
}
|
|
81
87
|
}
|
|
82
88
|
};
|
|
83
89
|
|
|
@@ -90,7 +96,7 @@ export default function EditableBlock({
|
|
|
90
96
|
|
|
91
97
|
// If the click was on the card's background (not a button), and it's an editable block type,
|
|
92
98
|
// then trigger the edit handler.
|
|
93
|
-
if (block.block_type !== 'section'
|
|
99
|
+
if (block.block_type !== 'section') {
|
|
94
100
|
handleEditClick();
|
|
95
101
|
}
|
|
96
102
|
};
|
|
@@ -109,19 +115,24 @@ export default function EditableBlock({
|
|
|
109
115
|
const isCentered = htmlContent.includes('text-align: center') || htmlContent.includes('class="text-center"');
|
|
110
116
|
const isRight = htmlContent.includes('text-align: right') || htmlContent.includes('class="text-right"');
|
|
111
117
|
const alignmentClass = isCentered ? 'text-center' : isRight ? 'text-right' : 'text-left';
|
|
118
|
+
const isJson = htmlContent.trim().startsWith('{') || htmlContent.trim().startsWith('[');
|
|
112
119
|
|
|
113
120
|
return (
|
|
114
121
|
<div className="py-2">
|
|
115
122
|
<div className={cn("text-xs w-full", alignmentClass)}>
|
|
116
123
|
{htmlContent ? (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
isJson ? (
|
|
125
|
+
<SimpleTiptapRenderer content={htmlContent} className="prose prose-sm max-w-none [&>p]:my-0 [&>h1]:my-0 [&>h2]:my-0 [&>h3]:my-0 dark:prose-invert" />
|
|
126
|
+
) : (
|
|
127
|
+
<div
|
|
128
|
+
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
|
129
|
+
className={cn(
|
|
130
|
+
"prose prose-sm max-w-none [&>p]:my-0 [&>h1]:my-0 [&>h2]:my-0 [&>h3]:my-0 dark:prose-invert",
|
|
131
|
+
isCentered && "[&_*]:text-center",
|
|
132
|
+
isRight && "[&_*]:text-right"
|
|
133
|
+
)}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
125
136
|
) : <span className="text-muted-foreground italic">Empty text block</span>}
|
|
126
137
|
</div>
|
|
127
138
|
</div>
|
|
@@ -168,7 +179,7 @@ export default function EditableBlock({
|
|
|
168
179
|
}
|
|
169
180
|
case 'image': {
|
|
170
181
|
const content = (block.content || {}) as any;
|
|
171
|
-
const imageUrl = content.object_key
|
|
182
|
+
const imageUrl = resolveMediaUrl(content.object_key) || content.src;
|
|
172
183
|
return (
|
|
173
184
|
<div className="flex gap-4 py-2">
|
|
174
185
|
<div className="flex-shrink-0 h-16 w-16 bg-muted rounded overflow-hidden flex items-center justify-center border">
|
|
@@ -257,7 +268,7 @@ export default function EditableBlock({
|
|
|
257
268
|
default: {
|
|
258
269
|
const blockDefinition = getBlockDefinition(currentBlockType as BlockType);
|
|
259
270
|
const blockLabel = blockDefinition?.label || currentBlockType;
|
|
260
|
-
|
|
271
|
+
const placeholder = (
|
|
261
272
|
<div
|
|
262
273
|
className="py-4 flex flex-col items-center justify-center space-y-2 min-h-[80px] border border-dashed rounded-md bg-muted/20 cursor-pointer hover:border-primary"
|
|
263
274
|
onClick={handleCardClick}
|
|
@@ -268,11 +279,19 @@ export default function EditableBlock({
|
|
|
268
279
|
</div>
|
|
269
280
|
</div>
|
|
270
281
|
);
|
|
282
|
+
// Custom blocks (block_type === definition slug) render their real layout.
|
|
283
|
+
return (
|
|
284
|
+
<CustomBlockEditorPreview
|
|
285
|
+
blockType={currentBlockType}
|
|
286
|
+
content={(block.content || {}) as Record<string, any>}
|
|
287
|
+
fallback={placeholder}
|
|
288
|
+
/>
|
|
289
|
+
);
|
|
271
290
|
}
|
|
272
291
|
}
|
|
273
292
|
};
|
|
274
293
|
|
|
275
|
-
const isSection = block?.block_type === 'section'
|
|
294
|
+
const isSection = block?.block_type === 'section';
|
|
276
295
|
const blockDefinition = getBlockDefinition(block.block_type as BlockType);
|
|
277
296
|
|
|
278
297
|
return (
|
|
@@ -313,7 +332,7 @@ export default function EditableBlock({
|
|
|
313
332
|
{isSection ? (
|
|
314
333
|
<div className="mt-2 min-h-[200px]">
|
|
315
334
|
<Suspense fallback={<div className="flex justify-center items-center h-full"><p>Loading Editor...</p></div>}>
|
|
316
|
-
{SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, any>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section'
|
|
335
|
+
{SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, any>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section'} isConfigPanelOpen={isConfigPanelOpen} />}
|
|
317
336
|
</Suspense>
|
|
318
337
|
</div>
|
|
319
338
|
) : renderPreview()}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useState, useCallback, useEffect } from 'react';
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
import { Editor } from '@tiptap/react';
|
|
5
5
|
import Image from 'next/image';
|
|
6
6
|
import { Image as ImageIconLucide, Search, CheckCircle } from 'lucide-react';
|
|
@@ -14,49 +14,113 @@ import {
|
|
|
14
14
|
DialogFooter,
|
|
15
15
|
DialogClose,
|
|
16
16
|
} from '@nextblock-cms/ui';
|
|
17
|
-
import { Input } from '@nextblock-cms/ui';
|
|
18
|
-
import type { Database } from '@nextblock-cms/db';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
type Media = Database['public']['Tables']['media']['Row'];
|
|
22
|
-
|
|
23
|
-
const
|
|
17
|
+
import { Input } from '@nextblock-cms/ui';
|
|
18
|
+
import type { Database } from '@nextblock-cms/db';
|
|
19
|
+
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
20
|
+
|
|
21
|
+
type Media = Database['public']['Tables']['media']['Row'];
|
|
22
|
+
|
|
23
|
+
const MEDIA_REQUEST_TIMEOUT_MS = 8000;
|
|
24
|
+
const MEDIA_LIBRARY_LIMIT = 20;
|
|
25
|
+
|
|
26
|
+
function resolveMediaPreviewPath(media: Media) {
|
|
27
|
+
return media.file_path || media.object_key || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveMediaPreviewSrc(path: string) {
|
|
31
|
+
return resolveMediaUrl(path);
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
interface MediaLibraryModalProps {
|
|
26
35
|
editor: Editor | null;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
30
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
31
|
-
const [mediaLibrary, setMediaLibrary] = useState<Media[]>([]);
|
|
32
|
-
const [isLoadingMedia, setIsLoadingMedia] = useState(false);
|
|
33
|
-
const [
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
39
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
40
|
+
const [mediaLibrary, setMediaLibrary] = useState<Media[]>([]);
|
|
41
|
+
const [isLoadingMedia, setIsLoadingMedia] = useState(false);
|
|
42
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
43
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
44
|
+
const requestIdRef = useRef(0);
|
|
45
|
+
|
|
46
|
+
const fetchLibrary = useCallback(async () => {
|
|
47
|
+
if (!isModalOpen) return;
|
|
48
|
+
const requestId = ++requestIdRef.current;
|
|
49
|
+
setIsLoadingMedia(true);
|
|
50
|
+
setLoadError(null);
|
|
51
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
52
|
+
try {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
timeoutId = setTimeout(
|
|
55
|
+
() => controller.abort(new Error('Media library request timed out.')),
|
|
56
|
+
MEDIA_REQUEST_TIMEOUT_MS
|
|
57
|
+
);
|
|
58
|
+
const params = new URLSearchParams({
|
|
59
|
+
limit: MEDIA_LIBRARY_LIMIT.toString(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (searchTerm.trim()) {
|
|
63
|
+
params.set('q', searchTerm.trim());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const response = await fetch(`/api/media/library?${params.toString()}`, {
|
|
67
|
+
method: 'GET',
|
|
68
|
+
cache: 'no-store',
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
});
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
|
|
73
|
+
const payload = (await response.json()) as {
|
|
74
|
+
items?: Media[];
|
|
75
|
+
error?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (requestId !== requestIdRef.current) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
console.error('Error fetching media library:', payload.error);
|
|
84
|
+
setMediaLibrary([]);
|
|
85
|
+
setLoadError(payload.error || 'We could not load the media library. Please retry.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setMediaLibrary(payload.items || []);
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
if (requestId !== requestIdRef.current) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.error('Error fetching media library:', error);
|
|
96
|
+
setMediaLibrary([]);
|
|
97
|
+
if (error?.name === 'AbortError' || error?.message === 'Media library request timed out.') {
|
|
98
|
+
setLoadError('The media library took too long to respond. Please retry.');
|
|
99
|
+
} else {
|
|
100
|
+
setLoadError('We could not load the media library. Please retry.');
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
if (timeoutId) {
|
|
104
|
+
clearTimeout(timeoutId);
|
|
105
|
+
}
|
|
106
|
+
if (requestId === requestIdRef.current) {
|
|
107
|
+
setIsLoadingMedia(false);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, [isModalOpen, searchTerm]);
|
|
48
111
|
|
|
49
112
|
useEffect(() => {
|
|
50
113
|
fetchLibrary();
|
|
51
114
|
}, [fetchLibrary]);
|
|
52
115
|
|
|
53
|
-
const handleSelectMedia = (mediaItem: Media) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
116
|
+
const handleSelectMedia = (mediaItem: Media) => {
|
|
117
|
+
const previewPath = resolveMediaPreviewPath(mediaItem);
|
|
118
|
+
const imageUrl = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
|
|
119
|
+
if (editor && mediaItem.file_type?.startsWith("image/") && imageUrl) {
|
|
120
|
+
editor.chain().focus().insertContent(`<img src="${imageUrl}" alt="${mediaItem.description || mediaItem.file_name}" />`).run();
|
|
121
|
+
}
|
|
122
|
+
setIsModalOpen(false);
|
|
123
|
+
};
|
|
60
124
|
|
|
61
125
|
return (
|
|
62
126
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
@@ -79,36 +143,54 @@ export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
|
79
143
|
/>
|
|
80
144
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
81
145
|
</div>
|
|
82
|
-
{isLoadingMedia ? (
|
|
83
|
-
<div className="flex-grow flex items-center justify-center"><p>Loading media...</p></div>
|
|
84
|
-
) :
|
|
85
|
-
<div className="flex-grow flex items-center justify-center
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
146
|
+
{isLoadingMedia ? (
|
|
147
|
+
<div className="flex-grow flex items-center justify-center"><p>Loading media...</p></div>
|
|
148
|
+
) : loadError ? (
|
|
149
|
+
<div className="flex-grow flex flex-col items-center justify-center gap-3 text-center">
|
|
150
|
+
<p className="max-w-sm text-sm text-muted-foreground">{loadError}</p>
|
|
151
|
+
<Button type="button" variant="outline" size="sm" onClick={() => void fetchLibrary()}>
|
|
152
|
+
Retry
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
) : mediaLibrary.length === 0 ? (
|
|
156
|
+
<div className="flex-grow flex items-center justify-center"><p>No media found.</p></div>
|
|
157
|
+
) : (
|
|
158
|
+
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 overflow-y-auto flex-grow pr-2">
|
|
159
|
+
{mediaLibrary.filter(m => m.file_type?.startsWith("image/")).map((media) => {
|
|
160
|
+
const previewPath = resolveMediaPreviewPath(media);
|
|
161
|
+
const previewSrc = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<button
|
|
165
|
+
key={media.id}
|
|
166
|
+
type="button"
|
|
167
|
+
className="relative aspect-square border rounded-md overflow-hidden group focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
|
168
|
+
onClick={() => handleSelectMedia(media)}
|
|
169
|
+
>
|
|
170
|
+
{previewSrc ? (
|
|
171
|
+
<Image
|
|
172
|
+
src={previewSrc}
|
|
173
|
+
alt={media.description || media.file_name}
|
|
174
|
+
width={200}
|
|
175
|
+
height={200}
|
|
176
|
+
className="h-full w-full object-cover"
|
|
177
|
+
/>
|
|
178
|
+
) : (
|
|
179
|
+
<div className="flex h-full w-full items-center justify-center bg-muted text-xs text-muted-foreground">
|
|
180
|
+
Preview unavailable
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity flex items-center justify-center">
|
|
184
|
+
<CheckCircle className="h-8 w-8 text-white" />
|
|
185
|
+
</div>
|
|
186
|
+
<p className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 truncate text-center">
|
|
187
|
+
{media.file_name}
|
|
188
|
+
</p>
|
|
189
|
+
</button>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
112
194
|
<DialogFooter className="mt-4">
|
|
113
195
|
<DialogClose asChild>
|
|
114
196
|
<Button type="button" variant="outline">Cancel</Button>
|
|
@@ -117,4 +199,4 @@ export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
|
117
199
|
</DialogContent>
|
|
118
200
|
</Dialog>
|
|
119
201
|
);
|
|
120
|
-
};
|
|
202
|
+
};
|