create-nextblock 0.2.78 → 0.8.0
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 +740 -459
- 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
|
@@ -2,23 +2,142 @@
|
|
|
2
2
|
"use server";
|
|
3
3
|
|
|
4
4
|
import { sendEmail } from './email';
|
|
5
|
+
import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
|
|
5
6
|
|
|
6
|
-
interface FormSubmissionResult {
|
|
7
|
-
success: boolean;
|
|
8
|
-
message: string;
|
|
9
|
-
}
|
|
7
|
+
interface FormSubmissionResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
|
|
13
|
+
|
|
14
|
+
type FormSubmissionConfig = {
|
|
15
|
+
recipient: string;
|
|
16
|
+
botProtectionProvider?: BotProtectionProvider;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function normalizeSubmissionConfig(config: string | FormSubmissionConfig) {
|
|
20
|
+
if (typeof config === 'string') {
|
|
21
|
+
return {
|
|
22
|
+
recipient: config,
|
|
23
|
+
botProtectionProvider: undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleFormSubmission(
|
|
31
|
+
config: string | FormSubmissionConfig,
|
|
32
|
+
prevState: unknown,
|
|
33
|
+
formData: FormData
|
|
34
|
+
): Promise<FormSubmissionResult> {
|
|
35
|
+
const { recipient, botProtectionProvider } = normalizeSubmissionConfig(config);
|
|
36
|
+
|
|
37
|
+
// Phase 1: Honeypot Validation
|
|
38
|
+
const honeypot = formData.get('verification_secondary_email');
|
|
39
|
+
if (honeypot && typeof honeypot === 'string' && honeypot.length > 0) {
|
|
40
|
+
console.warn("[Bot Protection] Honeypot triggered. Discarding submission from bot.");
|
|
41
|
+
// Fool the bot by returning a fake success response immediately
|
|
42
|
+
return { success: true, message: "Submission successful!" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Phase 2: Advanced Captcha Verification
|
|
46
|
+
try {
|
|
47
|
+
const supabase = getServiceRoleSupabaseClient();
|
|
48
|
+
|
|
49
|
+
// Fetch global bot protection settings
|
|
50
|
+
const { data: publicSetting } = await supabase
|
|
51
|
+
.from('site_settings')
|
|
52
|
+
.select('value')
|
|
53
|
+
.eq('key', 'bot_protection_public')
|
|
54
|
+
.maybeSingle();
|
|
55
|
+
|
|
56
|
+
const { data: secretSetting } = await supabase
|
|
57
|
+
.from('site_settings')
|
|
58
|
+
.select('value')
|
|
59
|
+
.eq('key', 'bot_protection_secret')
|
|
60
|
+
.maybeSingle();
|
|
61
|
+
|
|
62
|
+
const publicVal = (publicSetting?.value || {}) as Record<string, any>;
|
|
63
|
+
const secretVal = (secretSetting?.value || {}) as Record<string, any>;
|
|
64
|
+
|
|
65
|
+
const blockProvider =
|
|
66
|
+
botProtectionProvider === 'turnstile' || botProtectionProvider === 'recaptcha'
|
|
67
|
+
? botProtectionProvider
|
|
68
|
+
: undefined;
|
|
69
|
+
const provider = blockProvider || publicVal.provider || 'none';
|
|
70
|
+
const secretKey = secretVal.secretKey ||
|
|
71
|
+
(provider === 'turnstile' ? process.env.TURNSTILE_SECRET_KEY : process.env.RECAPTCHA_SECRET_KEY) ||
|
|
72
|
+
'';
|
|
73
|
+
|
|
74
|
+
if (provider === 'turnstile') {
|
|
75
|
+
const token = formData.get('cf-turnstile-response') as string;
|
|
76
|
+
if (!token) {
|
|
77
|
+
return { success: false, message: "Security verification token is missing. Please try again." };
|
|
78
|
+
}
|
|
79
|
+
if (!secretKey) {
|
|
80
|
+
console.error("[Bot Protection] Turnstile secret key is not configured.");
|
|
81
|
+
return { success: false, message: "Bot protection is misconfigured. Please contact support." };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
88
|
+
},
|
|
89
|
+
body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
|
|
90
|
+
});
|
|
10
91
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
92
|
+
const outcome = await res.json();
|
|
93
|
+
if (!outcome.success) {
|
|
94
|
+
console.warn("[Bot Protection] Turnstile verification failed:", outcome);
|
|
95
|
+
return { success: false, message: "Security verification failed. Please try again." };
|
|
96
|
+
}
|
|
97
|
+
} else if (provider === 'recaptcha') {
|
|
98
|
+
const token = formData.get('g-recaptcha-response') as string;
|
|
99
|
+
if (!token) {
|
|
100
|
+
return { success: false, message: "Security verification token is missing. Please try again." };
|
|
101
|
+
}
|
|
102
|
+
if (!secretKey) {
|
|
103
|
+
console.error("[Bot Protection] reCAPTCHA secret key is not configured.");
|
|
104
|
+
return { success: false, message: "Bot protection is misconfigured. Please contact support." };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
111
|
+
},
|
|
112
|
+
body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const outcome = await res.json();
|
|
116
|
+
if (!outcome.success || outcome.score < 0.5) {
|
|
117
|
+
console.warn("[Bot Protection] reCAPTCHA verification failed:", outcome);
|
|
118
|
+
return { success: false, message: "Security verification failed. Please try again." };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("[Bot Protection] Error during validation:", error);
|
|
123
|
+
// If database or fetch error occurs, we gracefully degrade or warn, but let's be secure and fail open/closed depending on preference.
|
|
124
|
+
// The requirement says: "If the API indicates a verification failure or falls below a threshold... reject the operation securely."
|
|
125
|
+
// Let's return error message.
|
|
126
|
+
return { success: false, message: "Sorry, security verification could not be completed at this time." };
|
|
127
|
+
}
|
|
16
128
|
|
|
17
129
|
const data: Record<string, string | File> = {};
|
|
18
130
|
let submitterEmail = 'a user'; // Default value
|
|
19
131
|
|
|
20
132
|
formData.forEach((value, key) => {
|
|
21
|
-
|
|
133
|
+
// Avoid sending internal bot protection tokens and honeypots in the notification email
|
|
134
|
+
if (
|
|
135
|
+
typeof value === 'string' &&
|
|
136
|
+
!key.startsWith('$') &&
|
|
137
|
+
key !== 'verification_secondary_email' &&
|
|
138
|
+
key !== 'g-recaptcha-response' &&
|
|
139
|
+
key !== 'cf-turnstile-response'
|
|
140
|
+
) {
|
|
22
141
|
data[key] = value;
|
|
23
142
|
// Attempt to find a field that looks like an email address to use in the subject
|
|
24
143
|
if (key.toLowerCase().includes('email')) {
|
|
@@ -62,4 +181,4 @@ export async function handleFormSubmission(
|
|
|
62
181
|
console.error("Email sending failed:", error);
|
|
63
182
|
return { success: false, message: "Sorry, there was an error sending your message. Please try again later." };
|
|
64
183
|
}
|
|
65
|
-
}
|
|
184
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { createClient } from '@nextblock-cms/db';
|
|
4
4
|
import { cookies } from 'next/headers';
|
|
5
5
|
import { redirect } from 'next/navigation';
|
|
6
|
-
import { getLanguageByCode } from '
|
|
6
|
+
import { getLanguageByCode } from '../cms/settings/languages/actions';
|
|
7
7
|
|
|
8
8
|
export interface Language {
|
|
9
9
|
id: number;
|
|
@@ -33,43 +33,37 @@ export async function setCurrentLocaleCookie(locale: string) {
|
|
|
33
33
|
cookieStore.set('NEXT_LOCALE', locale, { path: '/' });
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export async function
|
|
36
|
+
export async function getContentTranslations(translationGroupId: string, type: 'pages' | 'posts' | 'products' = 'pages'): Promise<{ slug: string, language_code: string }[]> {
|
|
37
37
|
if (!translationGroupId) {
|
|
38
|
-
console.warn('
|
|
38
|
+
console.warn('getContentTranslations called without translationGroupId');
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
41
41
|
const supabase = createClient();
|
|
42
42
|
|
|
43
43
|
const { data, error } = await supabase
|
|
44
|
-
.from(
|
|
45
|
-
.select('slug,
|
|
44
|
+
.from(type)
|
|
45
|
+
.select('slug, languages(code)')
|
|
46
46
|
.eq('translation_group_id', translationGroupId)
|
|
47
|
-
.eq('status', 'published');
|
|
47
|
+
.eq('status', type === 'products' ? 'active' : 'published'); // Products use 'active' or 'draft' status usually, but let's check
|
|
48
48
|
|
|
49
49
|
if (error) {
|
|
50
|
-
console.error(
|
|
50
|
+
console.error(`Error fetching translations for ${type}:`, error);
|
|
51
51
|
return [];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
interface PageWithLanguage {
|
|
55
|
-
slug: string;
|
|
56
|
-
status: string; // Or your actual status type
|
|
57
|
-
languages: { code: string } | { code: string }[] | null; // Can be object, array of objects, or null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
54
|
// Map the data to the expected format { slug: string, language_code: string }
|
|
61
55
|
const formattedTranslations = data
|
|
62
|
-
? (data as
|
|
56
|
+
? (data as any[]).map(item => {
|
|
63
57
|
let langCode = '';
|
|
64
|
-
if (
|
|
65
|
-
if (Array.isArray(
|
|
66
|
-
langCode =
|
|
67
|
-
} else {
|
|
68
|
-
langCode =
|
|
58
|
+
if (item.languages) {
|
|
59
|
+
if (Array.isArray(item.languages)) {
|
|
60
|
+
langCode = item.languages[0]?.code || '';
|
|
61
|
+
} else {
|
|
62
|
+
langCode = item.languages.code || '';
|
|
69
63
|
}
|
|
70
64
|
}
|
|
71
65
|
return {
|
|
72
|
-
slug:
|
|
66
|
+
slug: item.slug,
|
|
73
67
|
language_code: langCode,
|
|
74
68
|
};
|
|
75
69
|
}).filter(t => t.language_code)
|
|
@@ -78,6 +72,10 @@ export async function getPageTranslations(translationGroupId: string): Promise<{
|
|
|
78
72
|
return formattedTranslations;
|
|
79
73
|
}
|
|
80
74
|
|
|
75
|
+
export async function getPageTranslations(translationGroupId: string): Promise<{ slug: string, language_code: string }[]> {
|
|
76
|
+
return getContentTranslations(translationGroupId, 'pages');
|
|
77
|
+
}
|
|
78
|
+
|
|
81
79
|
// Helper to get language details by code, potentially used by LanguageSwitcher or other components
|
|
82
80
|
export async function getLanguageDetails(localeCode: string): Promise<Language | null> {
|
|
83
81
|
const { data, error } = await getLanguageByCode(localeCode);
|
|
@@ -89,9 +87,9 @@ export async function getLanguageDetails(localeCode: string): Promise<Language |
|
|
|
89
87
|
return data;
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
export async function
|
|
90
|
+
export async function getContentMetadataBySlugAndLocale(slug: string, localeCode: string, type: 'pages' | 'posts' | 'products' = 'pages'): Promise<{ slug: string; translation_group_id: string | null; type: string } | null> {
|
|
93
91
|
if (!slug || !localeCode) {
|
|
94
|
-
console.warn('
|
|
92
|
+
console.warn('getContentMetadataBySlugAndLocale called without slug or localeCode');
|
|
95
93
|
return null;
|
|
96
94
|
}
|
|
97
95
|
const supabase = createClient();
|
|
@@ -102,24 +100,27 @@ export async function getPageMetadataBySlugAndLocale(slug: string, localeCode: s
|
|
|
102
100
|
return null;
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
const { data:
|
|
106
|
-
.from(
|
|
103
|
+
const { data: item, error } = await supabase
|
|
104
|
+
.from(type)
|
|
107
105
|
.select('slug, translation_group_id')
|
|
108
106
|
.eq('slug', slug)
|
|
109
107
|
.eq('language_id', languageData.id)
|
|
110
108
|
.maybeSingle();
|
|
111
109
|
|
|
112
110
|
if (error) {
|
|
113
|
-
console.error(`Error fetching
|
|
111
|
+
console.error(`Error fetching metadata for ${type} slug ${slug} and locale ${localeCode}:`, error);
|
|
114
112
|
return null;
|
|
115
113
|
}
|
|
116
|
-
if (!
|
|
117
|
-
// It's possible the slug is for a different content type (e.g. blog post) or doesn't exist.
|
|
118
|
-
// For now, we only search 'pages'. This might need to be expanded or handled gracefully.
|
|
119
|
-
console.warn(`No page found for slug ${slug} and locale ${localeCode}`);
|
|
114
|
+
if (!item) {
|
|
120
115
|
return null;
|
|
121
116
|
}
|
|
122
|
-
return
|
|
117
|
+
return { ...item, type };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function getPageMetadataBySlugAndLocale(slug: string, localeCode: string): Promise<{ slug: string; translation_group_id: string | null } | null> {
|
|
121
|
+
const metadata = await getContentMetadataBySlugAndLocale(slug, localeCode, 'pages');
|
|
122
|
+
if (!metadata) return null;
|
|
123
|
+
return { slug: metadata.slug, translation_group_id: metadata.translation_group_id };
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
export async function changeLanguage(newLocale: string, currentPath: string) {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@supabase/supabase-js';
|
|
4
|
+
import { NEXTBLOCK_PACKAGES } from '@nextblock-cms/utils';
|
|
5
|
+
import { headers } from 'next/headers';
|
|
6
|
+
import { revalidatePath } from 'next/cache';
|
|
7
|
+
|
|
8
|
+
// Freemius handles both Sandbox and Production keys on the same API domain.
|
|
9
|
+
// The key itself determines the environment.
|
|
10
|
+
const FM_API_URL = 'https://api.freemius.com/v1';
|
|
11
|
+
|
|
12
|
+
// Helper to get service role client
|
|
13
|
+
const getServiceRoleClient = () => {
|
|
14
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
15
|
+
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
16
|
+
|
|
17
|
+
if (!supabaseUrl || !supabaseServiceKey) {
|
|
18
|
+
console.error('Missing Supabase credentials');
|
|
19
|
+
throw new Error('Missing Supabase credentials (Service Key required for activation).');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (supabaseServiceKey === process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
|
23
|
+
console.warn('CRITICAL WARNING: SUPABASE_SERVICE_ROLE_KEY matches NEXT_PUBLIC_SUPABASE_ANON_KEY. This will likely cause Permission Denied errors as RLS cannot be bypassed.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return createClient(supabaseUrl, supabaseServiceKey, {
|
|
27
|
+
auth: {
|
|
28
|
+
persistSession: false,
|
|
29
|
+
autoRefreshToken: false,
|
|
30
|
+
detectSessionInUrl: false,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function activatePackage(key: string) {
|
|
36
|
+
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
37
|
+
return { error: 'License activation is disabled in Sandbox mode. To purchase a real license, visit nextblock.ca' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!key) {
|
|
41
|
+
return { error: 'License key is required.' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const headerList = await headers();
|
|
45
|
+
// instance_name is usually the domain, for local dev use 'localhost' or actual host
|
|
46
|
+
const instanceName = headerList.get('host') || 'nextblock-instance';
|
|
47
|
+
|
|
48
|
+
// Freemius requires a 32-char unique identifier for the install.
|
|
49
|
+
// We hash the instance (domain) to ensure reactivations on the same domain use the same UID.
|
|
50
|
+
const crypto = require('crypto');
|
|
51
|
+
const uid = crypto.createHash('md5').update(instanceName).digest('hex');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
let data = null;
|
|
55
|
+
let pkg = null;
|
|
56
|
+
let fmProductId = null;
|
|
57
|
+
let hasLicenseError = false;
|
|
58
|
+
let specificErrorMsg: string | null = null;
|
|
59
|
+
|
|
60
|
+
// We don't know the exact package just from the license key, so we try activating
|
|
61
|
+
// against our known Freemius Product IDs from the NEXTBLOCK_PACKAGES registry.
|
|
62
|
+
const packages = Object.values(NEXTBLOCK_PACKAGES);
|
|
63
|
+
|
|
64
|
+
for (const p of packages) {
|
|
65
|
+
if (!p.fm_product_id) continue;
|
|
66
|
+
|
|
67
|
+
const siteUrl = encodeURIComponent(`http://${instanceName}`);
|
|
68
|
+
const response = await fetch(`${FM_API_URL}/products/${p.fm_product_id}/licenses/activate.json?uid=${uid}&license_key=${encodeURIComponent(key)}&url=${siteUrl}`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Accept': 'application/json',
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const responseData = await response.json();
|
|
77
|
+
|
|
78
|
+
// Freemius returns the license object directly if successful, or an error/api_response
|
|
79
|
+
if (response.ok && responseData.install_id) {
|
|
80
|
+
data = responseData;
|
|
81
|
+
pkg = p;
|
|
82
|
+
fmProductId = p.fm_product_id;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const errorCode = responseData?.error?.code;
|
|
87
|
+
if (errorCode === 'not_found' || errorCode === 'invalid_license_key') {
|
|
88
|
+
hasLicenseError = true;
|
|
89
|
+
} else if (responseData?.error?.message) {
|
|
90
|
+
specificErrorMsg = responseData.error.message;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!data || !pkg) {
|
|
95
|
+
if (hasLicenseError && process.env.NEXT_PUBLIC_IS_SANDBOX !== 'true' && !specificErrorMsg) {
|
|
96
|
+
return { error: 'Sorry, this is a sandbox key. Please purchase the real key at nextblock.ca' };
|
|
97
|
+
}
|
|
98
|
+
return { error: specificErrorMsg || 'Activation failed. Invalid key, wrong product, or limit reached.' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Store in DB - USE SERVICE ROLE
|
|
102
|
+
const supabase = getServiceRoleClient();
|
|
103
|
+
|
|
104
|
+
const { error: dbError } = await supabase
|
|
105
|
+
.from('package_activations')
|
|
106
|
+
.upsert({
|
|
107
|
+
license_key: key,
|
|
108
|
+
instance_name: instanceName,
|
|
109
|
+
package_id: pkg.id,
|
|
110
|
+
status: 'active',
|
|
111
|
+
meta: {
|
|
112
|
+
...data,
|
|
113
|
+
fm_product_id: fmProductId,
|
|
114
|
+
fm_install_id: data.install_id,
|
|
115
|
+
fm_uid: uid
|
|
116
|
+
},
|
|
117
|
+
last_validated_at: new Date().toISOString(),
|
|
118
|
+
}, { onConflict: 'license_key, package_id' });
|
|
119
|
+
|
|
120
|
+
if (dbError) {
|
|
121
|
+
console.error('DB Error activating package:', dbError);
|
|
122
|
+
return { error: 'Activation successful, but local saving failed: ' + dbError.message };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
revalidatePath('/cms/settings/packages');
|
|
126
|
+
return { success: true, package: pkg.name };
|
|
127
|
+
|
|
128
|
+
} catch (err: any) {
|
|
129
|
+
console.error('Activation Action Error:', err);
|
|
130
|
+
return { error: err.message || 'An unexpected error occurred.' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function deactivatePackage(packageId: string) {
|
|
135
|
+
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
136
|
+
return { error: 'License deactivation is disabled in Sandbox mode.' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const supabase = getServiceRoleClient();
|
|
140
|
+
|
|
141
|
+
// 1. Get current activation
|
|
142
|
+
const { data: activation, error: fetchError } = await supabase
|
|
143
|
+
.from('package_activations')
|
|
144
|
+
.select('id, license_key, instance_name, meta')
|
|
145
|
+
.eq('package_id', packageId)
|
|
146
|
+
.eq('status', 'active')
|
|
147
|
+
.single();
|
|
148
|
+
|
|
149
|
+
if (fetchError || !activation) {
|
|
150
|
+
return { error: 'No active license found for this package.' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. Deactivate at Freemius
|
|
154
|
+
try {
|
|
155
|
+
const fmProductId = activation.meta?.fm_product_id;
|
|
156
|
+
const uid = activation.meta?.fm_uid;
|
|
157
|
+
const installId = activation.meta?.fm_install_id;
|
|
158
|
+
|
|
159
|
+
if (fmProductId && uid && installId) {
|
|
160
|
+
await fetch(`${FM_API_URL}/products/${fmProductId}/licenses/deactivate.json?uid=${uid}&install_id=${installId}&license_key=${encodeURIComponent(activation.license_key)}`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'Accept': 'application/json',
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.warn('Freemius Deactivation failed (network?), removing locally anyway.', err);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 3. Remove/Update local DB
|
|
172
|
+
const { error: deleteError } = await supabase
|
|
173
|
+
.from('package_activations')
|
|
174
|
+
.delete()
|
|
175
|
+
.eq('id', activation.id);
|
|
176
|
+
|
|
177
|
+
if (deleteError) {
|
|
178
|
+
return { error: 'Failed to remove local activation record.' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
revalidatePath('/cms/settings/packages');
|
|
182
|
+
return { success: true };
|
|
183
|
+
}
|
|
@@ -1,48 +1,147 @@
|
|
|
1
|
-
'use server';
|
|
2
|
-
|
|
3
|
-
import { cache } from 'react';
|
|
4
|
-
import { createClient } from '@nextblock-cms/db/server';
|
|
5
|
-
import { revalidatePath } from 'next/cache';
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { cache } from 'react';
|
|
4
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
5
|
+
import { revalidatePath } from 'next/cache';
|
|
6
|
+
import type { Database } from '@nextblock-cms/db';
|
|
7
|
+
import type { PostWithMediaDimensions } from '../../components/blocks/types';
|
|
8
|
+
import { resolveMediaUrl } from '../../lib/media/resolveMediaUrl';
|
|
9
|
+
import { estimateReadTimeMinutesFromBlocks } from '../../lib/posts/readTime';
|
|
10
|
+
|
|
11
|
+
type PostRow = Database['public']['Tables']['posts']['Row'];
|
|
12
|
+
type BlockRow = Database['public']['Tables']['blocks']['Row'];
|
|
13
|
+
type FeatureMediaSelection = {
|
|
14
|
+
object_key: string;
|
|
15
|
+
width?: number | null;
|
|
16
|
+
height?: number | null;
|
|
17
|
+
blur_data_url?: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type PostQueryRow = PostRow & {
|
|
21
|
+
feature_media_object?: FeatureMediaSelection | FeatureMediaSelection[] | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type PostWithFeatureMedia = PostRow & {
|
|
25
|
+
feature_media_object?: FeatureMediaSelection | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function normalizeFeatureMediaObject(
|
|
29
|
+
mediaObject: PostQueryRow['feature_media_object']
|
|
30
|
+
): FeatureMediaSelection | null {
|
|
31
|
+
if (Array.isArray(mediaObject)) {
|
|
32
|
+
return mediaObject[0] ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return mediaObject ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getEstimatedReadTimeMap(
|
|
39
|
+
supabase: ReturnType<typeof createClient>,
|
|
40
|
+
postIds: number[]
|
|
41
|
+
) {
|
|
42
|
+
if (postIds.length === 0) {
|
|
43
|
+
return new Map<number, number>();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { data: textBlocks, error } = await supabase
|
|
47
|
+
.from('blocks')
|
|
48
|
+
.select('post_id, block_type, content')
|
|
49
|
+
.in('post_id', postIds)
|
|
50
|
+
.eq('block_type', 'text')
|
|
51
|
+
.order('order', { ascending: true });
|
|
52
|
+
|
|
53
|
+
if (error) {
|
|
54
|
+
console.error('Error fetching post text blocks for read-time estimation:', error);
|
|
55
|
+
return new Map(postIds.map((postId) => [postId, 1]));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const blocksByPostId = new Map<number, Array<Pick<BlockRow, 'block_type' | 'content'>>>();
|
|
59
|
+
postIds.forEach((postId) => blocksByPostId.set(postId, []));
|
|
60
|
+
|
|
61
|
+
(textBlocks || []).forEach((block) => {
|
|
62
|
+
if (typeof block.post_id !== 'number') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const blocks = blocksByPostId.get(block.post_id) || [];
|
|
67
|
+
blocks.push(block as Pick<BlockRow, 'block_type' | 'content'>);
|
|
68
|
+
blocksByPostId.set(block.post_id, blocks);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return new Map(
|
|
72
|
+
postIds.map((postId) => [
|
|
73
|
+
postId,
|
|
74
|
+
estimateReadTimeMinutesFromBlocks(blocksByPostId.get(postId)),
|
|
75
|
+
])
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizePostsForCards(
|
|
80
|
+
posts: PostWithFeatureMedia[],
|
|
81
|
+
estimatedReadTimeMap: Map<number, number>
|
|
82
|
+
) {
|
|
83
|
+
return posts.map((post) => {
|
|
84
|
+
const mediaObject = post.feature_media_object || null;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
...post,
|
|
88
|
+
feature_image_url: resolveMediaUrl(mediaObject?.object_key),
|
|
89
|
+
feature_image_width: mediaObject?.width || null,
|
|
90
|
+
feature_image_height: mediaObject?.height || null,
|
|
91
|
+
blur_data_url: mediaObject?.blur_data_url || null,
|
|
92
|
+
estimated_read_time_minutes: estimatedReadTimeMap.get(post.id) || 1,
|
|
93
|
+
} satisfies PostWithMediaDimensions;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function fetchPublishedPostsPage(languageId: number, page: number, limit: number) {
|
|
98
|
+
const supabase = createClient();
|
|
99
|
+
const offset = (page - 1) * limit;
|
|
100
|
+
|
|
101
|
+
const { data: posts, error, count } = await supabase
|
|
102
|
+
.from('posts')
|
|
103
|
+
.select(
|
|
104
|
+
'id, title, slug, label, excerpt, subtitle, published_at, language_id, status, created_at, updated_at, translation_group_id, feature_image_id, version, author_id, meta_title, meta_description, feature_media_object:media!feature_image_id(object_key, width, height, blur_data_url)',
|
|
105
|
+
{ count: 'exact' }
|
|
106
|
+
)
|
|
107
|
+
.eq('status', 'published')
|
|
108
|
+
.eq('language_id', languageId)
|
|
109
|
+
.order('published_at', { ascending: false })
|
|
110
|
+
.range(offset, offset + limit - 1);
|
|
111
|
+
|
|
112
|
+
if (error) {
|
|
113
|
+
console.error('Error fetching published posts:', error);
|
|
114
|
+
return { posts: [], totalCount: 0, error: error.message };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalizedPosts: PostWithFeatureMedia[] = ((posts as PostQueryRow[] | null) ?? []).map(
|
|
118
|
+
(post) => ({
|
|
119
|
+
...post,
|
|
120
|
+
feature_media_object: normalizeFeatureMediaObject(post.feature_media_object),
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
const postIds = normalizedPosts.map((post) => post.id);
|
|
124
|
+
const estimatedReadTimeMap = await getEstimatedReadTimeMap(supabase, postIds);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
posts: normalizePostsForCards(normalizedPosts, estimatedReadTimeMap),
|
|
128
|
+
totalCount: count || 0,
|
|
129
|
+
error: undefined,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function fetchPaginatedPublishedPosts(languageId: number, page: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string }> {
|
|
134
|
+
return fetchPublishedPostsPage(languageId, page, limit);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const fetchInitialPublishedPosts = cache(async (languageId: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string | null }> => {
|
|
138
|
+
const result = await fetchPublishedPostsPage(languageId, 1, limit);
|
|
139
|
+
return {
|
|
140
|
+
posts: result.posts,
|
|
141
|
+
totalCount: result.totalCount,
|
|
142
|
+
error: result.error ?? null,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
46
145
|
export async function revalidateAndLog(path: string): Promise<{ success: boolean; error?: string }> {
|
|
47
146
|
try {
|
|
48
147
|
// Step 1: Revalidate the path
|
|
@@ -69,12 +168,11 @@ export async function revalidateAndLog(path: string): Promise<{ success: boolean
|
|
|
69
168
|
throw new Error(`Failed to log revalidation: ${response.status} ${response.statusText} - ${errorBody.error}`);
|
|
70
169
|
}
|
|
71
170
|
|
|
72
|
-
|
|
73
|
-
return { success: true };
|
|
171
|
+
return { success: true };
|
|
74
172
|
|
|
75
173
|
} catch (error) {
|
|
76
174
|
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
77
175
|
console.error(`Error in revalidateAndLog for path "${path}":`, errorMessage);
|
|
78
176
|
return { success: false, error: errorMessage };
|
|
79
177
|
}
|
|
80
|
-
}
|
|
178
|
+
}
|