create-nextblock 0.2.77 → 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 +191 -151
- 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 -116
- 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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import {
|
|
2
|
+
customBlockDefinitionCreateSchema,
|
|
3
|
+
customBlockFieldKeySchema,
|
|
4
|
+
customBlockSlugSchema,
|
|
5
|
+
} from '@nextblock-cms/utils';
|
|
6
|
+
|
|
7
|
+
import { z } from './zod-config';
|
|
8
|
+
|
|
9
|
+
export const CORTEX_WIDGET_ALLOWED_RELATION_TABLES = [
|
|
10
|
+
'pages',
|
|
11
|
+
'posts',
|
|
12
|
+
'products',
|
|
13
|
+
'media',
|
|
14
|
+
'categories',
|
|
15
|
+
'profiles',
|
|
16
|
+
'languages',
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
const htmlElementSchema = z
|
|
20
|
+
.enum([
|
|
21
|
+
'article',
|
|
22
|
+
'aside',
|
|
23
|
+
'blockquote',
|
|
24
|
+
'div',
|
|
25
|
+
'figure',
|
|
26
|
+
'figcaption',
|
|
27
|
+
'h2',
|
|
28
|
+
'h3',
|
|
29
|
+
'img',
|
|
30
|
+
'p',
|
|
31
|
+
'section',
|
|
32
|
+
'span',
|
|
33
|
+
])
|
|
34
|
+
.describe('A safe semantic element supported by the dynamic layout renderer.');
|
|
35
|
+
|
|
36
|
+
const tailwindClassSchema = z
|
|
37
|
+
.string()
|
|
38
|
+
.trim()
|
|
39
|
+
.max(4000)
|
|
40
|
+
.describe('Tailwind utility classes only. Do not include CSS, style tags, or JavaScript.');
|
|
41
|
+
|
|
42
|
+
const cortexWidgetFieldBaseSchema = z.strictObject({
|
|
43
|
+
description: z.string().trim().max(500).optional(),
|
|
44
|
+
key: customBlockFieldKeySchema.describe('Lowercase snake_case field key.'),
|
|
45
|
+
label: z.string().trim().min(1).max(120),
|
|
46
|
+
required: z.boolean().default(false),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const cortexWidgetTextFieldSchema = cortexWidgetFieldBaseSchema.extend({
|
|
50
|
+
default_value: z.string().max(5000).optional(),
|
|
51
|
+
max_length: z.number().int().positive().max(10000).optional(),
|
|
52
|
+
min_length: z.number().int().min(0).max(10000).optional(),
|
|
53
|
+
placeholder: z.string().max(250).optional(),
|
|
54
|
+
type: z.literal('text'),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const cortexWidgetRichTextFieldSchema = cortexWidgetFieldBaseSchema.extend({
|
|
58
|
+
default_value: z.string().max(50000).optional(),
|
|
59
|
+
placeholder: z.string().max(250).optional(),
|
|
60
|
+
type: z.literal('rich-text'),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const cortexWidgetImageR2FieldSchema = cortexWidgetFieldBaseSchema.extend({
|
|
64
|
+
accept: z.array(z.string().trim().min(1).max(120)).max(20).optional(),
|
|
65
|
+
default_value: z
|
|
66
|
+
.strictObject({
|
|
67
|
+
alt: z.string().max(300).optional(),
|
|
68
|
+
file_name: z.string().trim().min(1).max(255).optional(),
|
|
69
|
+
file_type: z.string().trim().min(1).max(120).optional(),
|
|
70
|
+
height: z.number().int().positive().optional(),
|
|
71
|
+
object_key: z.string().trim().min(1).max(1024),
|
|
72
|
+
size_bytes: z.number().int().positive().optional(),
|
|
73
|
+
url: z.string().trim().min(1).max(2048),
|
|
74
|
+
width: z.number().int().positive().optional(),
|
|
75
|
+
})
|
|
76
|
+
.optional(),
|
|
77
|
+
max_bytes: z.number().int().positive().max(50 * 1024 * 1024).optional(),
|
|
78
|
+
type: z.literal('image_r2'),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const cortexWidgetDbRelationFieldSchema = cortexWidgetFieldBaseSchema.extend({
|
|
82
|
+
default_value: z.union([z.string(), z.array(z.string()), z.null()]).optional(),
|
|
83
|
+
display_column: z.string().trim().min(1).max(80).default('title'),
|
|
84
|
+
filters: z.record(z.string(), z.unknown()).optional(),
|
|
85
|
+
multiple: z.boolean().default(false),
|
|
86
|
+
table: z.enum(CORTEX_WIDGET_ALLOWED_RELATION_TABLES),
|
|
87
|
+
type: z.literal('db_relation'),
|
|
88
|
+
value_column: z.string().trim().min(1).max(80).default('id'),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const cortexWidgetFieldSchema = z
|
|
92
|
+
.discriminatedUnion('type', [
|
|
93
|
+
cortexWidgetTextFieldSchema,
|
|
94
|
+
cortexWidgetRichTextFieldSchema,
|
|
95
|
+
cortexWidgetImageR2FieldSchema,
|
|
96
|
+
cortexWidgetDbRelationFieldSchema,
|
|
97
|
+
])
|
|
98
|
+
.describe('A NextBlock custom block field. Allowed types: text, rich-text, image_r2, db_relation.');
|
|
99
|
+
|
|
100
|
+
export type CortexWidgetLayoutNode =
|
|
101
|
+
| {
|
|
102
|
+
as?: z.infer<typeof htmlElementSchema>;
|
|
103
|
+
children: CortexWidgetLayoutNode[];
|
|
104
|
+
className?: string;
|
|
105
|
+
type: 'container';
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
as?: z.infer<typeof htmlElementSchema>;
|
|
109
|
+
className?: string;
|
|
110
|
+
column?: string;
|
|
111
|
+
emptyFallback?: string;
|
|
112
|
+
field_key: string;
|
|
113
|
+
type: 'field_render';
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const cortexWidgetLayoutNodeSchema: z.ZodType<CortexWidgetLayoutNode> = z.lazy(() =>
|
|
117
|
+
z.discriminatedUnion('type', [
|
|
118
|
+
z.strictObject({
|
|
119
|
+
as: htmlElementSchema.optional(),
|
|
120
|
+
children: z.array(cortexWidgetLayoutNodeSchema).max(200).default([]),
|
|
121
|
+
className: tailwindClassSchema.optional(),
|
|
122
|
+
type: z.literal('container'),
|
|
123
|
+
}),
|
|
124
|
+
z.strictObject({
|
|
125
|
+
as: htmlElementSchema.optional(),
|
|
126
|
+
className: tailwindClassSchema.optional(),
|
|
127
|
+
column: z
|
|
128
|
+
.string()
|
|
129
|
+
.trim()
|
|
130
|
+
.min(1)
|
|
131
|
+
.max(80)
|
|
132
|
+
.optional()
|
|
133
|
+
.describe('For a db_relation field, the related record column to display (e.g. title, price).'),
|
|
134
|
+
emptyFallback: z.string().max(300).optional(),
|
|
135
|
+
field_key: customBlockFieldKeySchema,
|
|
136
|
+
type: z.literal('field_render'),
|
|
137
|
+
}),
|
|
138
|
+
])
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export const cortexWidgetBuildRequestSchema = z.strictObject({
|
|
142
|
+
context: z.string().trim().max(3000).optional(),
|
|
143
|
+
modelId: z.string().trim().min(1).max(200).optional(),
|
|
144
|
+
prompt: z.string().trim().min(3).max(4000),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export type CortexWidgetBuildRequest = z.infer<typeof cortexWidgetBuildRequestSchema>;
|
|
148
|
+
|
|
149
|
+
function collectLayoutFieldKeys(node: CortexWidgetLayoutNode): string[] {
|
|
150
|
+
if (node.type === 'field_render') {
|
|
151
|
+
return [node.field_key];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return node.children.flatMap((child) => collectLayoutFieldKeys(child));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function assertCortexWidgetFieldKeys(
|
|
158
|
+
definition: {
|
|
159
|
+
fields: Array<{ key: string }>;
|
|
160
|
+
layout_schema: CortexWidgetLayoutNode;
|
|
161
|
+
},
|
|
162
|
+
context: z.RefinementCtx
|
|
163
|
+
) {
|
|
164
|
+
const seenFieldKeys = new Set<string>();
|
|
165
|
+
|
|
166
|
+
definition.fields.forEach((field, index) => {
|
|
167
|
+
if (seenFieldKeys.has(field.key)) {
|
|
168
|
+
context.addIssue({
|
|
169
|
+
code: 'custom',
|
|
170
|
+
message: `Duplicate field key "${field.key}".`,
|
|
171
|
+
path: ['fields', index, 'key'],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
seenFieldKeys.add(field.key);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
for (const fieldKey of collectLayoutFieldKeys(definition.layout_schema)) {
|
|
179
|
+
if (!seenFieldKeys.has(fieldKey)) {
|
|
180
|
+
context.addIssue({
|
|
181
|
+
code: 'custom',
|
|
182
|
+
message: `Layout references unknown field "${fieldKey}".`,
|
|
183
|
+
path: ['layout_schema'],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const cortexWidgetDefinitionSchema = z
|
|
190
|
+
.strictObject({
|
|
191
|
+
description: z.string().trim().max(1000).default(''),
|
|
192
|
+
fields: z.array(cortexWidgetFieldSchema).min(1).max(80),
|
|
193
|
+
is_original: z.boolean().default(true),
|
|
194
|
+
layout_schema: cortexWidgetLayoutNodeSchema,
|
|
195
|
+
name: z.string().trim().min(1).max(160),
|
|
196
|
+
slug: customBlockSlugSchema.describe('Lowercase kebab-case slug.'),
|
|
197
|
+
})
|
|
198
|
+
.superRefine(assertCortexWidgetFieldKeys)
|
|
199
|
+
.describe('A complete NextBlock custom block definition stored as database JSONB.');
|
|
200
|
+
|
|
201
|
+
export type CortexWidgetDefinition = z.infer<typeof customBlockDefinitionCreateSchema>;
|
|
202
|
+
|
|
203
|
+
export function validateCortexWidgetDefinitionOutput(value: unknown): CortexWidgetDefinition {
|
|
204
|
+
const parsed = cortexWidgetDefinitionSchema.parse(value);
|
|
205
|
+
|
|
206
|
+
return customBlockDefinitionCreateSchema.parse({
|
|
207
|
+
...parsed,
|
|
208
|
+
is_original: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function buildCortexWidgetBuilderSystemPrompt() {
|
|
213
|
+
return [
|
|
214
|
+
'You are NextBlock Cortex, an expert web platform engineer building database-rendered custom CMS widgets.',
|
|
215
|
+
'Return ONLY one clean raw JSON object with the exact structure described in the user message. Do not include markdown fences, comments, prose, or explanatory text.',
|
|
216
|
+
'Never emit TSX, JSX, React components, JavaScript, CSS blocks, style attributes, script tags, or runtime code.',
|
|
217
|
+
'Use only these field types: text, rich-text, image_r2, db_relation.',
|
|
218
|
+
`Use db_relation.table only from this allowlist: ${CORTEX_WIDGET_ALLOWED_RELATION_TABLES.join(', ')}.`,
|
|
219
|
+
'Use lowercase kebab-case for slug and lowercase snake_case for field keys.',
|
|
220
|
+
'Build layout_schema as a self-referential tree: container nodes may contain nested container or field_render nodes to any needed depth.',
|
|
221
|
+
'Use Tailwind utility classes in className strings. Use responsive utilities where helpful.',
|
|
222
|
+
'The "as" property of any node MUST be exactly one of: article, aside, blockquote, div, figure, figcaption, h2, h3, img, p, section, span. Never use a, button, ul, ol, li, table, or any other tag. For a call-to-action or "more info" button, use a span or p styled with button-like Tailwind classes (rounded, padded, colored background).',
|
|
223
|
+
'Every field_render.field_key must match one field key exactly.',
|
|
224
|
+
'For relation fields, set value_column to id and set display_column to a column that actually exists on the chosen table: use title for pages, posts, and products; sku for product_variants; full_name for profiles; name for categories and languages; file_name for media. Do not invent display columns.',
|
|
225
|
+
'Entity images: when a block displays an image that belongs to a related product, page, or post (for example a product card photo or a post thumbnail), do NOT add an image_r2 upload field for it. Instead add a single db_relation field to that table (products, product_variants, pages, or posts) and add a field_render node that references it with "as": "img". The renderer automatically resolves the related record\'s primary image — a product or variant main_image/object_key, or a page/post feature image — so keep the table\'s normal display_column (for example title for a products relation).',
|
|
226
|
+
'You may reference the same db_relation field from more than one field_render node: for example one node with "as": "img" for the image and another text node for its title, plus the relation value to drive a "more info" link. This builds a product/page/post card from a single relation field.',
|
|
227
|
+
'To display a SPECIFIC column of a related record (its title, price, sku, etc.), set the field_render node\'s "column" property to that column name. The "column" overrides the field display_column for that one node, so a single product relation can show its image (as "img"), title (column "title"), and price (column "price") from three field_render nodes.',
|
|
228
|
+
'Available record columns by table — only reference these in a node "column": products: title, sku, price, sale_price, stock, short_description, slug, status; product_variants: sku, price, sale_price, stock_quantity; pages: title, slug, status; posts: title, slug, excerpt, subtitle; profiles: full_name; categories: name, slug, description; media: file_name; languages: name, code. Use "as": "img" (no column) to show a record image.',
|
|
229
|
+
'Do NOT create a standalone text field for data that lives on a related record. For example, a product price must come from a products db_relation field rendered with column "price" — never a separate "text" field the editor types by hand.',
|
|
230
|
+
'Monetary columns (price, sale_price, price_adjustment) are stored in integer cents and are automatically formatted as currency on display, so reference them directly; never multiply, divide, or add currency symbols yourself.',
|
|
231
|
+
'Only use image_r2 for standalone images uploaded directly by the editor that are not tied to any database record (for example a decorative banner or icon). Only use text or rich-text fields for free-form copy the editor writes, not for values that exist on a related record.',
|
|
232
|
+
].join(' ');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function buildCortexWidgetBuilderPrompt(params: CortexWidgetBuildRequest) {
|
|
236
|
+
return [
|
|
237
|
+
'Create a NextBlock custom block definition for this request:',
|
|
238
|
+
params.prompt,
|
|
239
|
+
params.context ? `Additional CMS context:\n${params.context}` : null,
|
|
240
|
+
[
|
|
241
|
+
'Return ONLY a JSON object with EXACTLY these top-level keys:',
|
|
242
|
+
'- "name": string (human-friendly block name).',
|
|
243
|
+
'- "slug": lowercase kebab-case string.',
|
|
244
|
+
'- "description": short string.',
|
|
245
|
+
'- "is_original": true.',
|
|
246
|
+
'- "fields": a non-empty array of field objects. Each field is { "key": lowercase snake_case string, "label": string, "required": boolean, "type": one of "text" | "rich-text" | "image_r2" | "db_relation" }.',
|
|
247
|
+
` For "db_relation" fields also include "table" (one of: ${CORTEX_WIDGET_ALLOWED_RELATION_TABLES.join(', ')}), "display_column" (e.g. title, name, full_name, file_name, code), "value_column": "id", and "multiple": boolean.`,
|
|
248
|
+
'- "layout_schema": a single root layout node (a tree). Every node is one of:',
|
|
249
|
+
' container: { "type": "container", "as": an HTML tag like div/section/article/figure, "className": Tailwind utility classes, "children": array of nodes }.',
|
|
250
|
+
' field render: { "type": "field_render", "field_key": one of the field keys above, "as": an HTML tag like p/span/img/h2/h3/div, "className": Tailwind utility classes, "emptyFallback": optional placeholder string }.',
|
|
251
|
+
' Containers may nest other containers to any depth. Every field_render.field_key MUST match one of the fields. Render image_r2 fields with "as": "img".',
|
|
252
|
+
].join('\n'),
|
|
253
|
+
]
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join('\n\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function buildCortexProfileCardVerificationDefinition(): CortexWidgetDefinition {
|
|
259
|
+
return validateCortexWidgetDefinitionOutput({
|
|
260
|
+
description:
|
|
261
|
+
'A multi-tier profile card with an R2 image asset slot and a live customer relation list.',
|
|
262
|
+
fields: [
|
|
263
|
+
{
|
|
264
|
+
accept: ['image/png', 'image/jpeg', 'image/webp'],
|
|
265
|
+
key: 'profile_photo',
|
|
266
|
+
label: 'Profile Photo',
|
|
267
|
+
max_bytes: 10485760,
|
|
268
|
+
required: false,
|
|
269
|
+
type: 'image_r2',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
key: 'profile_name',
|
|
273
|
+
label: 'Profile Name',
|
|
274
|
+
placeholder: 'Ada Lovelace',
|
|
275
|
+
required: true,
|
|
276
|
+
type: 'text',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
key: 'profile_role',
|
|
280
|
+
label: 'Profile Role',
|
|
281
|
+
placeholder: 'Principal Architect',
|
|
282
|
+
required: false,
|
|
283
|
+
type: 'text',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
key: 'profile_summary',
|
|
287
|
+
label: 'Profile Summary',
|
|
288
|
+
placeholder: '<p>Short profile biography.</p>',
|
|
289
|
+
required: false,
|
|
290
|
+
type: 'rich-text',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
display_column: 'full_name',
|
|
294
|
+
key: 'customer_list',
|
|
295
|
+
label: 'Customer List',
|
|
296
|
+
multiple: true,
|
|
297
|
+
required: false,
|
|
298
|
+
table: 'profiles',
|
|
299
|
+
type: 'db_relation',
|
|
300
|
+
value_column: 'id',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
is_original: true,
|
|
304
|
+
layout_schema: {
|
|
305
|
+
as: 'article',
|
|
306
|
+
children: [
|
|
307
|
+
{
|
|
308
|
+
as: 'div',
|
|
309
|
+
children: [
|
|
310
|
+
{
|
|
311
|
+
as: 'div',
|
|
312
|
+
children: [
|
|
313
|
+
{
|
|
314
|
+
as: 'div',
|
|
315
|
+
children: [
|
|
316
|
+
{
|
|
317
|
+
as: 'img',
|
|
318
|
+
className:
|
|
319
|
+
'h-24 w-24 rounded-full border object-cover shadow-sm',
|
|
320
|
+
emptyFallback: 'Upload profile photo',
|
|
321
|
+
field_key: 'profile_photo',
|
|
322
|
+
type: 'field_render',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
as: 'span',
|
|
326
|
+
className:
|
|
327
|
+
'rounded-full bg-muted px-3 py-1 text-center text-xs font-medium text-muted-foreground',
|
|
328
|
+
emptyFallback: 'No customers linked',
|
|
329
|
+
field_key: 'customer_list',
|
|
330
|
+
type: 'field_render',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
className: 'flex flex-col items-center gap-4 md:w-48',
|
|
334
|
+
type: 'container',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
as: 'div',
|
|
338
|
+
children: [
|
|
339
|
+
{
|
|
340
|
+
as: 'div',
|
|
341
|
+
children: [
|
|
342
|
+
{
|
|
343
|
+
as: 'h2',
|
|
344
|
+
className: 'text-2xl font-semibold leading-tight',
|
|
345
|
+
emptyFallback: 'Untitled profile',
|
|
346
|
+
field_key: 'profile_name',
|
|
347
|
+
type: 'field_render',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
as: 'p',
|
|
351
|
+
className: 'text-sm font-medium text-muted-foreground',
|
|
352
|
+
emptyFallback: 'Role pending',
|
|
353
|
+
field_key: 'profile_role',
|
|
354
|
+
type: 'field_render',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
className: 'flex flex-col gap-1',
|
|
358
|
+
type: 'container',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
as: 'div',
|
|
362
|
+
children: [
|
|
363
|
+
{
|
|
364
|
+
as: 'div',
|
|
365
|
+
className: 'prose prose-sm max-w-none text-muted-foreground',
|
|
366
|
+
emptyFallback: '<p>Add a concise profile summary.</p>',
|
|
367
|
+
field_key: 'profile_summary',
|
|
368
|
+
type: 'field_render',
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
className: 'rounded-md border bg-muted/30 p-4',
|
|
372
|
+
type: 'container',
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
className: 'flex min-w-0 flex-1 flex-col gap-4',
|
|
376
|
+
type: 'container',
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
className: 'flex flex-col gap-6 md:flex-row',
|
|
380
|
+
type: 'container',
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
className: 'rounded-lg border bg-background p-6 shadow-sm',
|
|
384
|
+
type: 'container',
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
className: 'mx-auto max-w-3xl p-4',
|
|
388
|
+
type: 'container',
|
|
389
|
+
},
|
|
390
|
+
name: 'Cortex Profile Card',
|
|
391
|
+
slug: 'cortex-profile-card',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { getSsgSupabaseClient } from '@nextblock-cms/db/server';
|
|
4
|
+
import {
|
|
5
|
+
customBlockDefinitionRowSchema,
|
|
6
|
+
type CustomBlockDefinition,
|
|
7
|
+
} from '@nextblock-cms/utils';
|
|
8
|
+
import { unstable_cache } from 'next/cache';
|
|
9
|
+
|
|
10
|
+
export const CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG = 'custom-block-definitions';
|
|
11
|
+
const CUSTOM_BLOCK_SELECT = 'id, slug, name, description, fields, layout_schema, is_original';
|
|
12
|
+
|
|
13
|
+
export function getCustomBlockDefinitionCacheTag(idOrSlug: string) {
|
|
14
|
+
return `${CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG}:${idOrSlug}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseCustomBlockDefinitionRows(rows: unknown[]): CustomBlockDefinition[] {
|
|
18
|
+
return rows.map((row) => customBlockDefinitionRowSchema.parse(row));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function queryCustomBlockDefinitions() {
|
|
22
|
+
const supabase = getSsgSupabaseClient();
|
|
23
|
+
const { data, error } = await supabase
|
|
24
|
+
.from('custom_block_definitions')
|
|
25
|
+
.select(CUSTOM_BLOCK_SELECT)
|
|
26
|
+
.order('name', { ascending: true });
|
|
27
|
+
|
|
28
|
+
if (error) {
|
|
29
|
+
throw new Error(`Failed to load custom block definitions: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parseCustomBlockDefinitionRows(data ?? []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const getCachedCustomBlockDefinitions = unstable_cache(
|
|
36
|
+
queryCustomBlockDefinitions,
|
|
37
|
+
[CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
38
|
+
{
|
|
39
|
+
revalidate: 60,
|
|
40
|
+
tags: [CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const getCachedCustomBlockDefinitionBySlugInternal = unstable_cache(
|
|
45
|
+
async (slug: string) => {
|
|
46
|
+
const definitions = await queryCustomBlockDefinitions();
|
|
47
|
+
return definitions.find((definition) => definition.slug === slug) ?? null;
|
|
48
|
+
},
|
|
49
|
+
[`${CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG}:by-slug`],
|
|
50
|
+
{
|
|
51
|
+
revalidate: 60,
|
|
52
|
+
tags: [CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
async function queryCustomBlockDefinitionBySlug(slug: string): Promise<CustomBlockDefinition | null> {
|
|
57
|
+
const supabase = getSsgSupabaseClient();
|
|
58
|
+
const { data, error } = await supabase
|
|
59
|
+
.from('custom_block_definitions')
|
|
60
|
+
.select(CUSTOM_BLOCK_SELECT)
|
|
61
|
+
.eq('slug', slug)
|
|
62
|
+
.maybeSingle();
|
|
63
|
+
|
|
64
|
+
if (error) {
|
|
65
|
+
throw new Error(`Failed to load custom block definition "${slug}": ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return data ? customBlockDefinitionRowSchema.parse(data) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getCachedCustomBlockDefinitionBySlug(slug: string) {
|
|
72
|
+
const cached = await getCachedCustomBlockDefinitionBySlugInternal(slug);
|
|
73
|
+
if (cached) {
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The cached lookup can hold a stale "not found" result for a definition that
|
|
78
|
+
// was created or edited within the last revalidation window. Fall back to a
|
|
79
|
+
// direct read so a freshly saved custom block renders on the front end
|
|
80
|
+
// immediately instead of showing "Unsupported block type".
|
|
81
|
+
try {
|
|
82
|
+
return await queryCustomBlockDefinitionBySlug(slug);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[custom-block-definitions] Live definition fallback failed:', error);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const R2_PRESIGNED_UPLOAD_MAX_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
export const R2_PRESIGNED_UPLOAD_EXPIRES_IN_SECONDS = 300;
|
|
5
|
+
export const R2_PRESIGNED_UPLOAD_DEFAULT_FOLDER = 'custom-blocks';
|
|
6
|
+
|
|
7
|
+
export const R2_PRESIGNED_UPLOAD_ALLOWED_CONTENT_TYPES = [
|
|
8
|
+
'image/avif',
|
|
9
|
+
'image/gif',
|
|
10
|
+
'image/jpeg',
|
|
11
|
+
'image/png',
|
|
12
|
+
'image/webp',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const EXTENSION_BY_CONTENT_TYPE: Record<
|
|
16
|
+
(typeof R2_PRESIGNED_UPLOAD_ALLOWED_CONTENT_TYPES)[number],
|
|
17
|
+
string
|
|
18
|
+
> = {
|
|
19
|
+
'image/avif': 'avif',
|
|
20
|
+
'image/gif': 'gif',
|
|
21
|
+
'image/jpeg': 'jpg',
|
|
22
|
+
'image/png': 'png',
|
|
23
|
+
'image/webp': 'webp',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type R2PresignedUploadPayload = {
|
|
27
|
+
contentType?: unknown;
|
|
28
|
+
filename?: unknown;
|
|
29
|
+
folder?: unknown;
|
|
30
|
+
size?: unknown;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ValidR2PresignedUploadPayload = {
|
|
34
|
+
contentType: (typeof R2_PRESIGNED_UPLOAD_ALLOWED_CONTENT_TYPES)[number];
|
|
35
|
+
filename: string;
|
|
36
|
+
folder: string;
|
|
37
|
+
size: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type R2PresignedUploadResult = {
|
|
41
|
+
expiresIn: number;
|
|
42
|
+
headers: Record<string, string>;
|
|
43
|
+
method: 'PUT';
|
|
44
|
+
objectKey: string;
|
|
45
|
+
presignedUrl: string;
|
|
46
|
+
publicUrl: string;
|
|
47
|
+
uploadUrl: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class R2PresignedUploadError extends Error {
|
|
51
|
+
constructor(message: string, public readonly status = 400) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = 'R2PresignedUploadError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isAllowedContentType(
|
|
58
|
+
contentType: string
|
|
59
|
+
): contentType is ValidR2PresignedUploadPayload['contentType'] {
|
|
60
|
+
return R2_PRESIGNED_UPLOAD_ALLOWED_CONTENT_TYPES.includes(
|
|
61
|
+
contentType as ValidR2PresignedUploadPayload['contentType']
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function sanitizeR2UploadFolder(input?: unknown) {
|
|
66
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
67
|
+
const fallback = R2_PRESIGNED_UPLOAD_DEFAULT_FOLDER;
|
|
68
|
+
const normalized = (raw || fallback).replace(/\\/g, '/').replace(/^\/+/, '');
|
|
69
|
+
const segments = normalized
|
|
70
|
+
.split('/')
|
|
71
|
+
.map((segment) =>
|
|
72
|
+
segment
|
|
73
|
+
.trim()
|
|
74
|
+
.replace(/\.{2,}/g, '')
|
|
75
|
+
.replace(/[^a-zA-Z0-9_.-]+/g, '-')
|
|
76
|
+
.replace(/^-+|-+$/g, '')
|
|
77
|
+
)
|
|
78
|
+
.filter((segment) => segment && segment !== '.' && segment !== '..');
|
|
79
|
+
|
|
80
|
+
const folder = segments.join('/') || fallback;
|
|
81
|
+
return folder.endsWith('/') ? folder : `${folder}/`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sanitizeFilenameBase(filename: string) {
|
|
85
|
+
const lastDotIndex = filename.lastIndexOf('.');
|
|
86
|
+
const base = lastDotIndex > 0 ? filename.slice(0, lastDotIndex) : filename;
|
|
87
|
+
const sanitized = base
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/\s+/g, '-')
|
|
90
|
+
.replace(/[^a-z0-9_.-]+/g, '')
|
|
91
|
+
.replace(/^-+|-+$/g, '')
|
|
92
|
+
.slice(0, 120);
|
|
93
|
+
|
|
94
|
+
return sanitized || 'asset';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getFileExtension(
|
|
98
|
+
filename: string,
|
|
99
|
+
contentType: ValidR2PresignedUploadPayload['contentType']
|
|
100
|
+
) {
|
|
101
|
+
const lastDotIndex = filename.lastIndexOf('.');
|
|
102
|
+
const extension = lastDotIndex > 0 ? filename.slice(lastDotIndex + 1).toLowerCase() : '';
|
|
103
|
+
const normalizedExtension = extension.replace(/[^a-z0-9]+/g, '');
|
|
104
|
+
const expectedExtension = EXTENSION_BY_CONTENT_TYPE[contentType];
|
|
105
|
+
|
|
106
|
+
if (!normalizedExtension) {
|
|
107
|
+
return expectedExtension;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (['jpeg', 'jpg'].includes(normalizedExtension) && contentType === 'image/jpeg') {
|
|
111
|
+
return normalizedExtension;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return normalizedExtension === expectedExtension ? normalizedExtension : expectedExtension;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatUploadTimestamp(date: Date) {
|
|
118
|
+
const parts = [
|
|
119
|
+
date.getUTCFullYear(),
|
|
120
|
+
`${date.getUTCMonth() + 1}`.padStart(2, '0'),
|
|
121
|
+
`${date.getUTCDate()}`.padStart(2, '0'),
|
|
122
|
+
`${date.getUTCHours()}`.padStart(2, '0'),
|
|
123
|
+
`${date.getUTCMinutes()}`.padStart(2, '0'),
|
|
124
|
+
`${date.getUTCSeconds()}`.padStart(2, '0'),
|
|
125
|
+
`${date.getUTCMilliseconds()}`.padStart(3, '0'),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
return parts.join('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function validateR2PresignedUploadPayload(
|
|
132
|
+
payload: R2PresignedUploadPayload
|
|
133
|
+
): ValidR2PresignedUploadPayload {
|
|
134
|
+
const filename = typeof payload.filename === 'string' ? payload.filename.trim() : '';
|
|
135
|
+
const contentType =
|
|
136
|
+
typeof payload.contentType === 'string' ? payload.contentType.trim().toLowerCase() : '';
|
|
137
|
+
const size =
|
|
138
|
+
typeof payload.size === 'number'
|
|
139
|
+
? payload.size
|
|
140
|
+
: typeof payload.size === 'string'
|
|
141
|
+
? Number(payload.size)
|
|
142
|
+
: Number.NaN;
|
|
143
|
+
|
|
144
|
+
if (!filename || filename.length > 255) {
|
|
145
|
+
throw new R2PresignedUploadError('A valid filename is required.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!isAllowedContentType(contentType)) {
|
|
149
|
+
throw new R2PresignedUploadError('Only AVIF, GIF, JPEG, PNG, and WebP images are supported.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
153
|
+
throw new R2PresignedUploadError('A positive file size is required.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (size > R2_PRESIGNED_UPLOAD_MAX_BYTES) {
|
|
157
|
+
throw new R2PresignedUploadError('Image uploads are limited to 10 MB.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
contentType,
|
|
162
|
+
filename,
|
|
163
|
+
folder: sanitizeR2UploadFolder(payload.folder),
|
|
164
|
+
size,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildR2UploadObjectKey(
|
|
169
|
+
payload: ValidR2PresignedUploadPayload,
|
|
170
|
+
options: { now?: Date; nonce?: string } = {}
|
|
171
|
+
) {
|
|
172
|
+
const base = sanitizeFilenameBase(payload.filename);
|
|
173
|
+
const extension = getFileExtension(payload.filename, payload.contentType);
|
|
174
|
+
const timestamp = formatUploadTimestamp(options.now ?? new Date());
|
|
175
|
+
const nonce = (options.nonce ?? randomUUID().slice(0, 8)).replace(/[^a-zA-Z0-9-]/g, '');
|
|
176
|
+
|
|
177
|
+
return `${payload.folder}${base}-${timestamp}-${nonce}.${extension}`;
|
|
178
|
+
}
|