create-nextblock 0.2.78 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-nextblock.js +793 -472
- package/package.json +1 -2
- package/scripts/sync-template.js +18 -1
- package/templates/nextblock-template/.browserslistrc +11 -0
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/README.md +23 -114
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
- package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
- package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
- package/templates/nextblock-template/app/actions/consent.ts +57 -0
- package/templates/nextblock-template/app/actions/formActions.ts +130 -11
- package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
- package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
- package/templates/nextblock-template/app/actions/postActions.ts +146 -48
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
- package/templates/nextblock-template/app/actions.ts +67 -12
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
- package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
- package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
- package/templates/nextblock-template/app/api/draft/route.ts +93 -0
- package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
- package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
- package/templates/nextblock-template/app/api/search/route.ts +43 -0
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
- package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
- package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
- package/templates/nextblock-template/app/cart/page.tsx +7 -0
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
- package/templates/nextblock-template/app/checkout/page.tsx +52 -0
- package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
- package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
- package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
- package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
- package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
- package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
- package/templates/nextblock-template/app/cms/layout.tsx +29 -10
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
- package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
- package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
- package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
- package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
- package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
- package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
- package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
- package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
- package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
- package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
- package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
- package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
- package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
- package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
- package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
- package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
- package/templates/nextblock-template/app/globals.css +9 -0
- package/templates/nextblock-template/app/layout.tsx +372 -120
- package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
- package/templates/nextblock-template/app/lib/seo.ts +279 -0
- package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
- package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
- package/templates/nextblock-template/app/page.tsx +165 -73
- package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
- package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
- package/templates/nextblock-template/app/profile/account-data.ts +37 -0
- package/templates/nextblock-template/app/profile/account-links.ts +22 -0
- package/templates/nextblock-template/app/profile/account-types.ts +11 -0
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
- package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
- package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
- package/templates/nextblock-template/app/profile/page.tsx +51 -0
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
- package/templates/nextblock-template/app/providers.tsx +55 -17
- package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
- package/templates/nextblock-template/app/sitemap.ts +128 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
- package/templates/nextblock-template/components/AppShell.tsx +154 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
- package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
- package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
- package/templates/nextblock-template/components/Header.tsx +49 -41
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
- package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
- package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/types.ts +7 -6
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -26
- package/templates/nextblock-template/components/header-auth.tsx +69 -17
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
- package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
- package/templates/nextblock-template/context/AuthContext.tsx +23 -90
- package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
- package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
- package/templates/nextblock-template/context/language-rest-client.ts +31 -0
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
- package/templates/nextblock-template/docs/README.md +34 -0
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
- package/templates/nextblock-template/index.d.ts +2 -0
- package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
- package/templates/nextblock-template/lib/ai-client.ts +247 -0
- package/templates/nextblock-template/lib/ai-config.ts +81 -0
- package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
- package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
- package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
- package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
- package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
- package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
- package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
- package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
- package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
- package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
- package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
- package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
- package/templates/nextblock-template/lib/blocks/README.md +13 -670
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
- package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
- package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
- package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
- package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
- package/templates/nextblock-template/lib/privacy/types.ts +67 -0
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
- package/templates/nextblock-template/lib/promotions/server.ts +741 -0
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
- package/templates/nextblock-template/lib/search/server.ts +585 -0
- package/templates/nextblock-template/lib/search/types.ts +27 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
- package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
- package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
- package/templates/nextblock-template/lib/zod-config.ts +5 -0
- package/templates/nextblock-template/next.config.js +190 -66
- package/templates/nextblock-template/package.json +34 -30
- package/templates/nextblock-template/proxy.ts +435 -253
- package/templates/nextblock-template/public/images/NBcover.webp +0 -0
- package/templates/nextblock-template/public/images/cap.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
- package/templates/nextblock-template/public/images/extensibility.webp +0 -0
- package/templates/nextblock-template/public/images/goals.webp +0 -0
- package/templates/nextblock-template/public/images/included.webp +0 -0
- package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
- package/templates/nextblock-template/public/images/pants.webp +0 -0
- package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
- package/templates/nextblock-template/tailwind.config.js +1 -0
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
- package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
- package/templates/nextblock-template/tsconfig.json +52 -66
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -0
- package/templates/nextblock-template/app/force-styles.tsx +0 -31
- package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
- package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
- package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
- package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
- package/templates/nextblock-template/docs/files-structure.md +0 -426
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
|
@@ -0,0 +1,1522 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useMemo, useTransition } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { toast } from "react-hot-toast";
|
|
6
|
+
import {
|
|
7
|
+
Boxes,
|
|
8
|
+
Plus,
|
|
9
|
+
Trash2,
|
|
10
|
+
Settings,
|
|
11
|
+
Database,
|
|
12
|
+
ImageIcon,
|
|
13
|
+
Type,
|
|
14
|
+
Code,
|
|
15
|
+
ArrowLeft,
|
|
16
|
+
Save,
|
|
17
|
+
PlusCircle,
|
|
18
|
+
Eye,
|
|
19
|
+
ListTree,
|
|
20
|
+
ChevronDown,
|
|
21
|
+
ChevronRight,
|
|
22
|
+
Info,
|
|
23
|
+
FolderOpen,
|
|
24
|
+
Sparkles,
|
|
25
|
+
Layers,
|
|
26
|
+
Loader2,
|
|
27
|
+
GripVertical,
|
|
28
|
+
BookOpen,
|
|
29
|
+
} from "lucide-react";
|
|
30
|
+
import {
|
|
31
|
+
Button,
|
|
32
|
+
Card,
|
|
33
|
+
CardHeader,
|
|
34
|
+
CardTitle,
|
|
35
|
+
CardDescription,
|
|
36
|
+
CardContent,
|
|
37
|
+
CardFooter,
|
|
38
|
+
Badge,
|
|
39
|
+
Input,
|
|
40
|
+
Label,
|
|
41
|
+
Textarea,
|
|
42
|
+
Checkbox,
|
|
43
|
+
ConfirmationDialog,
|
|
44
|
+
Dialog,
|
|
45
|
+
DialogTrigger,
|
|
46
|
+
DialogContent,
|
|
47
|
+
DialogHeader,
|
|
48
|
+
DialogTitle,
|
|
49
|
+
DialogDescription,
|
|
50
|
+
} from "@nextblock-cms/ui";
|
|
51
|
+
import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
|
|
52
|
+
import { ImageR2Picker } from "./ImageR2Picker";
|
|
53
|
+
import { DBRelationSelect } from "./DBRelationSelect";
|
|
54
|
+
import {
|
|
55
|
+
createCustomBlockDefinition,
|
|
56
|
+
updateCustomBlockDefinition,
|
|
57
|
+
} from "../actions";
|
|
58
|
+
import { orderCustomBlockFieldsByLayout } from "@nextblock-cms/utils";
|
|
59
|
+
import type { CustomBlockDefinition, CustomBlockField } from "@nextblock-cms/utils";
|
|
60
|
+
|
|
61
|
+
// Allowed container and field tags
|
|
62
|
+
const CONTAINER_TAGS = ["div", "section", "article", "blockquote", "figure", "figcaption", "h2", "h3", "p", "span"];
|
|
63
|
+
const FIELD_TAGS = ["div", "span", "p", "blockquote", "img", "h2", "h3", "figcaption"];
|
|
64
|
+
|
|
65
|
+
const getFieldIcon = (type: string) => {
|
|
66
|
+
switch (type) {
|
|
67
|
+
case "text":
|
|
68
|
+
return <Type className="h-3 w-3 text-sky-500 shrink-0" />;
|
|
69
|
+
case "rich-text":
|
|
70
|
+
return <Code className="h-3 w-3 text-emerald-500 shrink-0" />;
|
|
71
|
+
case "image_r2":
|
|
72
|
+
return <ImageIcon className="h-3 w-3 text-amber-500 shrink-0" />;
|
|
73
|
+
case "db_relation":
|
|
74
|
+
return <Database className="h-3 w-3 text-violet-500 shrink-0" />;
|
|
75
|
+
default:
|
|
76
|
+
return <Info className="h-3 w-3 text-slate-500 shrink-0" />;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface BlockComposerProps {
|
|
81
|
+
initialData?: CustomBlockDefinition;
|
|
82
|
+
mode: "create" | "edit";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface RelationTableTarget {
|
|
86
|
+
table: string;
|
|
87
|
+
label: string;
|
|
88
|
+
displayColumn: string;
|
|
89
|
+
valueColumn: string;
|
|
90
|
+
valueType: string;
|
|
91
|
+
selectColumns?: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function BlockComposer({ initialData, mode }: BlockComposerProps) {
|
|
95
|
+
const router = useRouter();
|
|
96
|
+
const [isPending, startTransition] = useTransition();
|
|
97
|
+
|
|
98
|
+
// General Block Info
|
|
99
|
+
const [name, setName] = useState(initialData?.name || "");
|
|
100
|
+
const [slug, setSlug] = useState(initialData?.slug || "");
|
|
101
|
+
const [description, setDescription] = useState(initialData?.description || "");
|
|
102
|
+
// Provenance flag: true for newly authored blocks, preserved on edit, set to
|
|
103
|
+
// false automatically by the duplicate action. No longer user-editable.
|
|
104
|
+
const [isOriginal] = useState(initialData?.is_original !== false);
|
|
105
|
+
|
|
106
|
+
// Schema state
|
|
107
|
+
const [fields, setFields] = useState<CustomBlockField[]>(initialData?.fields || []);
|
|
108
|
+
const [layoutSchema, setLayoutSchema] = useState<any>(
|
|
109
|
+
initialData?.layout_schema || {
|
|
110
|
+
type: "container",
|
|
111
|
+
as: "div",
|
|
112
|
+
className: "rounded-xl border bg-card p-6 shadow-sm flex flex-col gap-4",
|
|
113
|
+
children: [],
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Active designer view states
|
|
118
|
+
const [activeTab, setActiveTab] = useState<"general" | "fields" | "layout">("general");
|
|
119
|
+
const [relationTables, setRelationTables] = useState<RelationTableTarget[]>([]);
|
|
120
|
+
const [selectedNodePath, setSelectedNodePath] = useState<number[] | null>(null);
|
|
121
|
+
|
|
122
|
+
// Drag and drop state
|
|
123
|
+
const [draggedPath, setDraggedPath] = useState<number[] | null>(null);
|
|
124
|
+
const [dragOverInfo, setDragOverInfo] = useState<{ path: number[]; position: "before" | "after" | "inside" } | null>(null);
|
|
125
|
+
|
|
126
|
+
// Real-time mockup values for preview form
|
|
127
|
+
const [previewValues, setPreviewValues] = useState<Record<string, any>>({});
|
|
128
|
+
const [mockRelationRecords, setMockRelationRecords] = useState<Record<string, any>>({});
|
|
129
|
+
|
|
130
|
+
// Tree helper expanded state
|
|
131
|
+
const [expandedPaths, setExpandedPaths] = useState<Record<string, boolean>>({ "[]": true });
|
|
132
|
+
|
|
133
|
+
// Fetch available table relations on load
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const fetchRelations = async () => {
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch("/api/custom-blocks/db-relations?mode=tables");
|
|
138
|
+
if (res.ok) {
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
if (data && data.tables) {
|
|
141
|
+
setRelationTables(data.tables);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error("Error loading relation target tables:", err);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
fetchRelations();
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
// Sync previews whenever fields list changes
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const freshMock: Record<string, any> = { ...previewValues };
|
|
154
|
+
const freshRelations: Record<string, any> = { ...mockRelationRecords };
|
|
155
|
+
|
|
156
|
+
fields.forEach((f) => {
|
|
157
|
+
if (freshMock[f.key] === undefined) {
|
|
158
|
+
if (f.type === "text") freshMock[f.key] = `Mock text value for ${f.key}`;
|
|
159
|
+
if (f.type === "rich-text") freshMock[f.key] = `<p>Mock <strong>Rich Text</strong> content for ${f.key}</p>`;
|
|
160
|
+
if (f.type === "image_r2") {
|
|
161
|
+
freshMock[f.key] = {
|
|
162
|
+
url: "/images/commerce-square.webp",
|
|
163
|
+
object_key: "images/commerce-square.webp",
|
|
164
|
+
alt: "Sample product image",
|
|
165
|
+
width: 400,
|
|
166
|
+
height: 400,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (f.type === "db_relation") {
|
|
170
|
+
freshMock[f.key] = "mock-id-1";
|
|
171
|
+
|
|
172
|
+
const mockRecord: Record<string, any> = {
|
|
173
|
+
id: "mock-id-1",
|
|
174
|
+
title: `Mock ${f.table.charAt(0).toUpperCase() + f.table.slice(1)} Title`,
|
|
175
|
+
name: `Mock ${f.table.charAt(0).toUpperCase() + f.table.slice(1)} Name`,
|
|
176
|
+
full_name: "Mock User Full Name",
|
|
177
|
+
sku: "MOCK-SKU-100",
|
|
178
|
+
short_description: "This is a short descriptive blurb for the preview card layout.",
|
|
179
|
+
file_name: "placeholder-file.png",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Attach realistic image URLs for tables that support images
|
|
183
|
+
if (f.table === "media") {
|
|
184
|
+
mockRecord.object_key = "https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=400&h=300&fit=crop";
|
|
185
|
+
mockRecord.file_name = "visual-banner.webp";
|
|
186
|
+
} else if (f.table === "profiles") {
|
|
187
|
+
mockRecord.avatar_url = "https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120&h=120&fit=crop";
|
|
188
|
+
mockRecord.full_name = "Clara Dupont";
|
|
189
|
+
} else if (f.table === "products") {
|
|
190
|
+
mockRecord.avatar_url = "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=300&fit=crop";
|
|
191
|
+
mockRecord.object_key = "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=400&h=300&fit=crop";
|
|
192
|
+
mockRecord.title = "Premium Wireless Headphones";
|
|
193
|
+
mockRecord.sku = "HP-WIRELESS-100";
|
|
194
|
+
mockRecord.price = 19900;
|
|
195
|
+
mockRecord.stock = 15;
|
|
196
|
+
mockRecord.short_description = "Active noise cancelling with 40-hour battery life.";
|
|
197
|
+
} else if (f.table === "product_variants") {
|
|
198
|
+
mockRecord.avatar_url = "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400&h=300&fit=crop";
|
|
199
|
+
mockRecord.object_key = "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400&h=300&fit=crop";
|
|
200
|
+
mockRecord.sku = "MOCK-VAR-RED-L";
|
|
201
|
+
mockRecord.price = 12900;
|
|
202
|
+
mockRecord.stock_quantity = 42;
|
|
203
|
+
} else if (f.table === "posts" || f.table === "pages") {
|
|
204
|
+
mockRecord.feature_image_id = "mock-image-id";
|
|
205
|
+
mockRecord.avatar_url = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop";
|
|
206
|
+
mockRecord.object_key = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
freshRelations[f.key] = {
|
|
210
|
+
record: mockRecord,
|
|
211
|
+
table: f.table,
|
|
212
|
+
value: "mock-id-1",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
setPreviewValues(freshMock);
|
|
218
|
+
setMockRelationRecords(freshRelations);
|
|
219
|
+
}, [fields]);
|
|
220
|
+
|
|
221
|
+
// Handle Slug generation from Name in create mode
|
|
222
|
+
const handleNameChange = (val: string) => {
|
|
223
|
+
setName(val);
|
|
224
|
+
if (mode === "create") {
|
|
225
|
+
setSlug(
|
|
226
|
+
val
|
|
227
|
+
.toLowerCase()
|
|
228
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
229
|
+
.replace(/(^-|-$)+/g, "")
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Pre-generate standard blueprint layout
|
|
235
|
+
const handleGenerateDefaultLayout = () => {
|
|
236
|
+
if (fields.length === 0) {
|
|
237
|
+
toast.error("Please add at least one field first.");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const defaultLayout = {
|
|
241
|
+
type: "container",
|
|
242
|
+
as: "div",
|
|
243
|
+
className: "rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 flex flex-col gap-4 shadow-sm",
|
|
244
|
+
children: fields.map((f) => {
|
|
245
|
+
if (f.type === "image_r2") {
|
|
246
|
+
return {
|
|
247
|
+
type: "field_render",
|
|
248
|
+
field_key: f.key,
|
|
249
|
+
as: "img",
|
|
250
|
+
className: "h-24 w-24 rounded-full object-cover shadow-sm",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
type: "field_render",
|
|
255
|
+
field_key: f.key,
|
|
256
|
+
as: f.type === "rich-text" ? "div" : "p",
|
|
257
|
+
className: f.type === "rich-text"
|
|
258
|
+
? "prose prose-sm max-w-none text-muted-foreground"
|
|
259
|
+
: f.type === "db_relation"
|
|
260
|
+
? "text-xs font-semibold uppercase tracking-wide bg-muted px-2 py-0.5 rounded text-muted-foreground w-fit"
|
|
261
|
+
: "text-slate-800 dark:text-slate-200 text-sm font-medium",
|
|
262
|
+
emptyFallback: `<p>Empty ${f.label}</p>`,
|
|
263
|
+
};
|
|
264
|
+
}),
|
|
265
|
+
};
|
|
266
|
+
setLayoutSchema(defaultLayout);
|
|
267
|
+
setSelectedNodePath(null);
|
|
268
|
+
toast.success("Generated layout blueprint based on your fields.");
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Add a field
|
|
272
|
+
const addField = () => {
|
|
273
|
+
const key = `field_${fields.length + 1}`;
|
|
274
|
+
const newField: CustomBlockField = {
|
|
275
|
+
key,
|
|
276
|
+
label: `Field ${fields.length + 1}`,
|
|
277
|
+
type: "text",
|
|
278
|
+
required: false,
|
|
279
|
+
};
|
|
280
|
+
setFields([...fields, newField]);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Rename every field_render reference in the layout tree from one key to another
|
|
284
|
+
const renameLayoutFieldKey = (node: any, fromKey: string, toKey: string): any => {
|
|
285
|
+
if (!node || typeof node !== "object") return node;
|
|
286
|
+
if (node.type === "field_render") {
|
|
287
|
+
return node.field_key === fromKey ? { ...node, field_key: toKey } : node;
|
|
288
|
+
}
|
|
289
|
+
if (node.type === "container" && Array.isArray(node.children)) {
|
|
290
|
+
return { ...node, children: node.children.map((child: any) => renameLayoutFieldKey(child, fromKey, toKey)) };
|
|
291
|
+
}
|
|
292
|
+
return node;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Migrate a record keyed by field key when that field is renamed
|
|
296
|
+
const migrateKey = <T,>(record: Record<string, T>, fromKey: string, toKey: string): Record<string, T> => {
|
|
297
|
+
if (!(fromKey in record) || fromKey === toKey) return record;
|
|
298
|
+
const next = { ...record };
|
|
299
|
+
next[toKey] = next[fromKey];
|
|
300
|
+
delete next[fromKey];
|
|
301
|
+
return next;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Update field parameters
|
|
305
|
+
const updateField = (index: number, updated: Partial<CustomBlockField>) => {
|
|
306
|
+
const list = [...fields];
|
|
307
|
+
const previousKey = list[index].key;
|
|
308
|
+
|
|
309
|
+
// Auto populate defaults for db_relation
|
|
310
|
+
if (updated.type === "db_relation" && list[index].type !== "db_relation") {
|
|
311
|
+
const defaultTable = relationTables[0]?.table || "pages";
|
|
312
|
+
const targetTable = relationTables.find(t => t.table === defaultTable);
|
|
313
|
+
updated.table = defaultTable;
|
|
314
|
+
updated.display_column = targetTable?.displayColumn || "title";
|
|
315
|
+
updated.value_column = "id";
|
|
316
|
+
updated.multiple = false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
list[index] = { ...list[index], ...updated } as CustomBlockField;
|
|
320
|
+
setFields(list);
|
|
321
|
+
|
|
322
|
+
// Keep layout references and preview maps in sync when a field key changes,
|
|
323
|
+
// otherwise the layout would reference an unknown field and saving would fail.
|
|
324
|
+
if (updated.key !== undefined && updated.key !== previousKey) {
|
|
325
|
+
const nextKey = updated.key;
|
|
326
|
+
setLayoutSchema((prev: any) => renameLayoutFieldKey(prev, previousKey, nextKey));
|
|
327
|
+
setPreviewValues((prev) => migrateKey(prev, previousKey, nextKey));
|
|
328
|
+
setMockRelationRecords((prev) => migrateKey(prev, previousKey, nextKey));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Delete a field
|
|
333
|
+
const deleteField = (index: number) => {
|
|
334
|
+
const fieldKey = fields[index].key;
|
|
335
|
+
const list = [...fields];
|
|
336
|
+
list.splice(index, 1);
|
|
337
|
+
setFields(list);
|
|
338
|
+
|
|
339
|
+
// Filter previews
|
|
340
|
+
const freshPreviews = { ...previewValues };
|
|
341
|
+
delete freshPreviews[fieldKey];
|
|
342
|
+
setPreviewValues(freshPreviews);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// --- Layout Tree Node Editing Functions ---
|
|
346
|
+
const getLayoutNodeByPath = (root: any, path: number[]): any => {
|
|
347
|
+
let current = root;
|
|
348
|
+
for (const idx of path) {
|
|
349
|
+
if (current?.children && current.children[idx]) {
|
|
350
|
+
current = current.children[idx];
|
|
351
|
+
} else {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return current;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const modifyLayoutTree = (
|
|
359
|
+
node: any,
|
|
360
|
+
path: number[],
|
|
361
|
+
action: "update" | "delete" | "insert",
|
|
362
|
+
payload?: any
|
|
363
|
+
): any => {
|
|
364
|
+
if (path.length === 0) {
|
|
365
|
+
if (action === "update") {
|
|
366
|
+
return { ...node, ...payload };
|
|
367
|
+
}
|
|
368
|
+
if (action === "insert") {
|
|
369
|
+
return {
|
|
370
|
+
...node,
|
|
371
|
+
children: [...(node.children || []), payload],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return node;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const [head, ...tail] = path;
|
|
378
|
+
if (node.type === "container" && node.children && node.children[head] !== undefined) {
|
|
379
|
+
if (tail.length === 0 && action === "delete") {
|
|
380
|
+
const newChildren = [...node.children];
|
|
381
|
+
newChildren.splice(head, 1);
|
|
382
|
+
return { ...node, children: newChildren };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const newChildren = [...node.children];
|
|
386
|
+
newChildren[head] = modifyLayoutTree(node.children[head], tail, action, payload);
|
|
387
|
+
return { ...node, children: newChildren };
|
|
388
|
+
}
|
|
389
|
+
return node;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const handleUpdateSelectedNode = (payload: any) => {
|
|
393
|
+
if (!selectedNodePath) return;
|
|
394
|
+
const updated = modifyLayoutTree(layoutSchema, selectedNodePath, "update", payload);
|
|
395
|
+
setLayoutSchema(updated);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const handleDeleteNode = (path: number[]) => {
|
|
399
|
+
const updated = modifyLayoutTree(layoutSchema, path, "delete");
|
|
400
|
+
setLayoutSchema(updated);
|
|
401
|
+
setSelectedNodePath(null);
|
|
402
|
+
toast.success("Removed layout node.");
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const handleInsertNode = (path: number[], type: "container" | "field_render") => {
|
|
406
|
+
const newNode =
|
|
407
|
+
type === "container"
|
|
408
|
+
? {
|
|
409
|
+
type: "container",
|
|
410
|
+
as: "div",
|
|
411
|
+
className: "flex flex-col gap-2 p-2",
|
|
412
|
+
children: [],
|
|
413
|
+
}
|
|
414
|
+
: {
|
|
415
|
+
type: "field_render",
|
|
416
|
+
field_key: fields[0]?.key || "",
|
|
417
|
+
as: "span",
|
|
418
|
+
className: "text-sm",
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const updated = modifyLayoutTree(layoutSchema, path, "insert", newNode);
|
|
422
|
+
setLayoutSchema(updated);
|
|
423
|
+
|
|
424
|
+
// Autoexpand the path
|
|
425
|
+
const pathKey = JSON.stringify(path);
|
|
426
|
+
setExpandedPaths({ ...expandedPaths, [pathKey]: true });
|
|
427
|
+
toast.success(`Inserted new ${type === "container" ? "Container" : "Field Render"}.`);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// --- Drag & Drop helpers ---
|
|
431
|
+
const isDescendantOrSelf = (parent: number[], child: number[]): boolean => {
|
|
432
|
+
if (child.length < parent.length) return false;
|
|
433
|
+
for (let i = 0; i < parent.length; i++) {
|
|
434
|
+
if (parent[i] !== child[i]) return false;
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const adjustPathAfterRemoval = (source: number[], target: number[]): number[] => {
|
|
440
|
+
let i = 0;
|
|
441
|
+
while (i < source.length && i < target.length) {
|
|
442
|
+
if (source[i] !== target[i]) {
|
|
443
|
+
if (source[i] < target[i]) {
|
|
444
|
+
const adjusted = [...target];
|
|
445
|
+
adjusted[i] = adjusted[i] - 1;
|
|
446
|
+
return adjusted;
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
i++;
|
|
451
|
+
}
|
|
452
|
+
return target;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const removeNodeFromTree = (root: any, path: number[]): { newRoot: any; removedNode: any } => {
|
|
456
|
+
const getNode = (curr: any, pathTail: number[]): any => {
|
|
457
|
+
let node = curr;
|
|
458
|
+
for (const idx of pathTail) {
|
|
459
|
+
node = node.children[idx];
|
|
460
|
+
}
|
|
461
|
+
return node;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const nodeToRemove = getNode(root, path);
|
|
465
|
+
const clonedNode = JSON.parse(JSON.stringify(nodeToRemove));
|
|
466
|
+
|
|
467
|
+
const remove = (node: any, pathTail: number[]): any => {
|
|
468
|
+
if (pathTail.length === 1) {
|
|
469
|
+
const idx = pathTail[0];
|
|
470
|
+
const newChildren = [...node.children];
|
|
471
|
+
newChildren.splice(idx, 1);
|
|
472
|
+
return { ...node, children: newChildren };
|
|
473
|
+
}
|
|
474
|
+
const [head, ...tail] = pathTail;
|
|
475
|
+
const newChildren = [...node.children];
|
|
476
|
+
newChildren[head] = remove(node.children[head], tail);
|
|
477
|
+
return { ...node, children: newChildren };
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
if (path.length === 0) return { newRoot: null, removedNode: clonedNode };
|
|
481
|
+
return { newRoot: remove(root, path), removedNode: clonedNode };
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const insertNodeIntoTree = (root: any, path: number[], nodeToInsert: any, position: "before" | "after" | "inside"): any => {
|
|
485
|
+
const insert = (node: any, pathTail: number[]): any => {
|
|
486
|
+
if (pathTail.length === 0) {
|
|
487
|
+
if (position === "inside") {
|
|
488
|
+
return { ...node, children: [...(node.children || []), nodeToInsert] };
|
|
489
|
+
}
|
|
490
|
+
return node;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (pathTail.length === 1) {
|
|
494
|
+
const idx = pathTail[0];
|
|
495
|
+
if (position === "inside") {
|
|
496
|
+
const newChildren = [...node.children];
|
|
497
|
+
const targetNode = newChildren[idx];
|
|
498
|
+
newChildren[idx] = { ...targetNode, children: [...(targetNode.children || []), nodeToInsert] };
|
|
499
|
+
return { ...node, children: newChildren };
|
|
500
|
+
} else {
|
|
501
|
+
const newChildren = [...node.children];
|
|
502
|
+
const insertIdx = position === "before" ? idx : idx + 1;
|
|
503
|
+
newChildren.splice(insertIdx, 0, nodeToInsert);
|
|
504
|
+
return { ...node, children: newChildren };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const [head, ...tail] = pathTail;
|
|
509
|
+
const newChildren = [...node.children];
|
|
510
|
+
newChildren[head] = insert(node.children[head], tail);
|
|
511
|
+
return { ...node, children: newChildren };
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
return insert(root, path);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const handleMoveNode = (source: number[], target: number[], position: "before" | "after" | "inside") => {
|
|
518
|
+
if (isDescendantOrSelf(source, target)) {
|
|
519
|
+
toast.error("Cannot move a parent node inside or relative to its own descendants.");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const { newRoot, removedNode } = removeNodeFromTree(layoutSchema, source);
|
|
524
|
+
if (!newRoot) return;
|
|
525
|
+
|
|
526
|
+
const adjustedTarget = adjustPathAfterRemoval(source, target);
|
|
527
|
+
const updatedSchema = insertNodeIntoTree(newRoot, adjustedTarget, removedNode, position);
|
|
528
|
+
setLayoutSchema(updatedSchema);
|
|
529
|
+
setSelectedNodePath(null);
|
|
530
|
+
toast.success("Rearranged layout blueprint nodes.");
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// --- Save / Submit Block ---
|
|
534
|
+
const handleSaveBlock = async () => {
|
|
535
|
+
if (!name.trim()) {
|
|
536
|
+
toast.error("Please provide a block name.");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!slug.trim()) {
|
|
540
|
+
toast.error("Please provide a block slug.");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (fields.length === 0) {
|
|
544
|
+
toast.error("Block must contain at least one field.");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const payload = {
|
|
549
|
+
name,
|
|
550
|
+
slug,
|
|
551
|
+
description: description || "",
|
|
552
|
+
is_original: isOriginal,
|
|
553
|
+
fields,
|
|
554
|
+
layout_schema: layoutSchema,
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const toastId = toast.loading(`${mode === "create" ? "Creating" : "Saving"} custom block...`);
|
|
558
|
+
try {
|
|
559
|
+
let res;
|
|
560
|
+
if (mode === "create") {
|
|
561
|
+
res = await createCustomBlockDefinition(payload);
|
|
562
|
+
} else {
|
|
563
|
+
res = await updateCustomBlockDefinition(initialData!.id, payload);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (res.success) {
|
|
567
|
+
toast.success(`Block "${name}" saved successfully!`, { id: toastId });
|
|
568
|
+
startTransition(() => {
|
|
569
|
+
router.push("/cms/custom-blocks");
|
|
570
|
+
router.refresh();
|
|
571
|
+
});
|
|
572
|
+
} else {
|
|
573
|
+
const detail =
|
|
574
|
+
res.issues && res.issues.length > 0
|
|
575
|
+
? `: ${res.issues.slice(0, 3).join("; ")}`
|
|
576
|
+
: "";
|
|
577
|
+
toast.error(`${res.error || "Failed to save block definition."}${detail}`, {
|
|
578
|
+
id: toastId,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.error(err);
|
|
583
|
+
toast.error("An unexpected error occurred while saving.", { id: toastId });
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// --- Rendering helper for Visual Layout Tree ---
|
|
588
|
+
const renderTreeItem = (node: any, path: number[] = []): React.ReactNode => {
|
|
589
|
+
const isSelected = selectedNodePath && JSON.stringify(selectedNodePath) === JSON.stringify(path);
|
|
590
|
+
const pathKey = JSON.stringify(path);
|
|
591
|
+
const isExpanded = expandedPaths[pathKey] !== false;
|
|
592
|
+
|
|
593
|
+
const toggleExpand = (e: React.MouseEvent) => {
|
|
594
|
+
e.stopPropagation();
|
|
595
|
+
setExpandedPaths({
|
|
596
|
+
...expandedPaths,
|
|
597
|
+
[pathKey]: !isExpanded,
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const nodeName =
|
|
602
|
+
node.type === "container"
|
|
603
|
+
? `Container (${node.as || "div"})`
|
|
604
|
+
: `Render Field: ${node.field_key || "unmapped"}`;
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<div key={pathKey} className="ml-4 pl-2 border-l border-slate-100 dark:border-slate-800 space-y-1 mt-1">
|
|
608
|
+
<div
|
|
609
|
+
draggable={true}
|
|
610
|
+
onDragStart={(e) => {
|
|
611
|
+
e.stopPropagation();
|
|
612
|
+
setDraggedPath(path);
|
|
613
|
+
e.dataTransfer.effectAllowed = "move";
|
|
614
|
+
e.dataTransfer.setData("text/plain", JSON.stringify(path));
|
|
615
|
+
}}
|
|
616
|
+
onDragOver={(e) => {
|
|
617
|
+
if (!draggedPath) return;
|
|
618
|
+
e.preventDefault();
|
|
619
|
+
e.stopPropagation();
|
|
620
|
+
|
|
621
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
622
|
+
const relativeY = e.clientY - rect.top;
|
|
623
|
+
const height = rect.height;
|
|
624
|
+
|
|
625
|
+
let position: "before" | "after" | "inside";
|
|
626
|
+
if (node.type === "container") {
|
|
627
|
+
if (relativeY < height * 0.25) {
|
|
628
|
+
position = "before";
|
|
629
|
+
} else if (relativeY > height * 0.75) {
|
|
630
|
+
position = "after";
|
|
631
|
+
} else {
|
|
632
|
+
position = "inside";
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
if (relativeY < height * 0.5) {
|
|
636
|
+
position = "before";
|
|
637
|
+
} else {
|
|
638
|
+
position = "after";
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (JSON.stringify(draggedPath) !== JSON.stringify(path)) {
|
|
643
|
+
setDragOverInfo({ path, position });
|
|
644
|
+
}
|
|
645
|
+
}}
|
|
646
|
+
onDragLeave={() => {
|
|
647
|
+
setDragOverInfo(null);
|
|
648
|
+
}}
|
|
649
|
+
onDrop={(e) => {
|
|
650
|
+
e.preventDefault();
|
|
651
|
+
e.stopPropagation();
|
|
652
|
+
if (!draggedPath) return;
|
|
653
|
+
|
|
654
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
655
|
+
const relativeY = e.clientY - rect.top;
|
|
656
|
+
const height = rect.height;
|
|
657
|
+
|
|
658
|
+
let position: "before" | "after" | "inside";
|
|
659
|
+
if (node.type === "container") {
|
|
660
|
+
if (relativeY < height * 0.25) {
|
|
661
|
+
position = "before";
|
|
662
|
+
} else if (relativeY > height * 0.75) {
|
|
663
|
+
position = "after";
|
|
664
|
+
} else {
|
|
665
|
+
position = "inside";
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
if (relativeY < height * 0.5) {
|
|
669
|
+
position = "before";
|
|
670
|
+
} else {
|
|
671
|
+
position = "after";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (JSON.stringify(draggedPath) !== JSON.stringify(path)) {
|
|
676
|
+
handleMoveNode(draggedPath, path, position);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
setDraggedPath(null);
|
|
680
|
+
setDragOverInfo(null);
|
|
681
|
+
}}
|
|
682
|
+
onDragEnd={() => {
|
|
683
|
+
setDraggedPath(null);
|
|
684
|
+
setDragOverInfo(null);
|
|
685
|
+
}}
|
|
686
|
+
onClick={() => setSelectedNodePath(path)}
|
|
687
|
+
className={`flex items-center justify-between p-2 rounded-lg text-xs font-medium cursor-grab active:cursor-grabbing transition-all group ${
|
|
688
|
+
isSelected
|
|
689
|
+
? "bg-primary/10 text-primary border border-primary/20"
|
|
690
|
+
: "hover:bg-slate-50 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-300 border border-transparent"
|
|
691
|
+
} ${
|
|
692
|
+
dragOverInfo && JSON.stringify(dragOverInfo.path) === JSON.stringify(path)
|
|
693
|
+
? dragOverInfo.position === "before"
|
|
694
|
+
? "border-t-2 border-primary scale-[1.01] bg-primary/5"
|
|
695
|
+
: dragOverInfo.position === "after"
|
|
696
|
+
? "border-b-2 border-primary scale-[1.01] bg-primary/5"
|
|
697
|
+
: "bg-primary/20 border border-primary/40 scale-[1.02]"
|
|
698
|
+
: ""
|
|
699
|
+
}`}
|
|
700
|
+
>
|
|
701
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
702
|
+
<GripVertical className="h-3 w-3 text-slate-400 dark:text-slate-600 shrink-0 cursor-grab group-hover:text-slate-500" />
|
|
703
|
+
{node.type === "container" ? (
|
|
704
|
+
<button onClick={toggleExpand} className="p-0.5 rounded hover:bg-slate-200 dark:hover:bg-slate-700">
|
|
705
|
+
{isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
706
|
+
</button>
|
|
707
|
+
) : (
|
|
708
|
+
<span className="w-3.5" />
|
|
709
|
+
)}
|
|
710
|
+
{node.type === "container" ? (
|
|
711
|
+
<Layers className="h-3.5 w-3.5 text-primary shrink-0" />
|
|
712
|
+
) : (
|
|
713
|
+
getFieldIcon(fields.find((f) => f.key === node.field_key)?.type || "text")
|
|
714
|
+
)}
|
|
715
|
+
<span className="truncate">{nodeName}</span>
|
|
716
|
+
{node.className && (
|
|
717
|
+
<span className="text-[10px] text-muted-foreground font-mono truncate max-w-[150px]">
|
|
718
|
+
.{node.className.split(" ")[0]}
|
|
719
|
+
</span>
|
|
720
|
+
)}
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0">
|
|
724
|
+
{node.type === "container" && (
|
|
725
|
+
<>
|
|
726
|
+
<button
|
|
727
|
+
onClick={(e) => {
|
|
728
|
+
e.stopPropagation();
|
|
729
|
+
handleInsertNode(path, "container");
|
|
730
|
+
}}
|
|
731
|
+
className="p-1 rounded text-slate-500 hover:text-primary hover:bg-slate-100 dark:hover:bg-slate-700"
|
|
732
|
+
title="Add Inner Container"
|
|
733
|
+
>
|
|
734
|
+
<Layers className="h-3.5 w-3.5" />
|
|
735
|
+
</button>
|
|
736
|
+
<button
|
|
737
|
+
onClick={(e) => {
|
|
738
|
+
e.stopPropagation();
|
|
739
|
+
handleInsertNode(path, "field_render");
|
|
740
|
+
}}
|
|
741
|
+
className="p-1 rounded text-slate-500 hover:text-primary hover:bg-slate-100 dark:hover:bg-slate-700"
|
|
742
|
+
title="Add Field Render"
|
|
743
|
+
>
|
|
744
|
+
<Plus className="h-3.5 w-3.5" />
|
|
745
|
+
</button>
|
|
746
|
+
</>
|
|
747
|
+
)}
|
|
748
|
+
{path.length > 0 && (
|
|
749
|
+
<button
|
|
750
|
+
onClick={(e) => {
|
|
751
|
+
e.stopPropagation();
|
|
752
|
+
handleDeleteNode(path);
|
|
753
|
+
}}
|
|
754
|
+
className="p-1 rounded text-slate-500 hover:text-destructive hover:bg-destructive/10"
|
|
755
|
+
title="Remove Node"
|
|
756
|
+
>
|
|
757
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
758
|
+
</button>
|
|
759
|
+
)}
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
|
|
763
|
+
{node.type === "container" && isExpanded && node.children && (
|
|
764
|
+
<div className="space-y-1">
|
|
765
|
+
{node.children.map((child: any, idx: number) => renderTreeItem(child, [...path, idx]))}
|
|
766
|
+
</div>
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
);
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
const selectedNode = selectedNodePath ? getLayoutNodeByPath(layoutSchema, selectedNodePath) : null;
|
|
773
|
+
|
|
774
|
+
// Every field_render node in the layout, in depth-first order, with its path.
|
|
775
|
+
const layoutFieldRefs = useMemo(() => {
|
|
776
|
+
const refs: { path: number[]; key: string }[] = [];
|
|
777
|
+
const walk = (node: any, path: number[]) => {
|
|
778
|
+
if (!node || typeof node !== "object") return;
|
|
779
|
+
if (node.type === "field_render") {
|
|
780
|
+
if (node.field_key) refs.push({ path, key: node.field_key });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (node.type === "container" && Array.isArray(node.children)) {
|
|
784
|
+
node.children.forEach((child: any, idx: number) => walk(child, [...path, idx]));
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
walk(layoutSchema, []);
|
|
788
|
+
return refs;
|
|
789
|
+
}, [layoutSchema]);
|
|
790
|
+
|
|
791
|
+
// Preview/property inputs follow the layout order (deduped, each field once),
|
|
792
|
+
// with any fields not yet placed in the layout appended at the end.
|
|
793
|
+
const orderedPreviewFields = useMemo(() => {
|
|
794
|
+
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
795
|
+
const seen = new Set<string>();
|
|
796
|
+
const ordered: CustomBlockField[] = [];
|
|
797
|
+
for (const ref of layoutFieldRefs) {
|
|
798
|
+
if (seen.has(ref.key)) continue;
|
|
799
|
+
const field = byKey.get(ref.key);
|
|
800
|
+
if (field) {
|
|
801
|
+
ordered.push(field);
|
|
802
|
+
seen.add(ref.key);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
for (const field of fields) {
|
|
806
|
+
if (!seen.has(field.key)) {
|
|
807
|
+
ordered.push(field);
|
|
808
|
+
seen.add(field.key);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return ordered;
|
|
812
|
+
}, [fields, layoutFieldRefs]);
|
|
813
|
+
|
|
814
|
+
// Field keys already mapped by a different field_render node, so a property is
|
|
815
|
+
// only used once across the layout blueprint.
|
|
816
|
+
const fieldKeysUsedElsewhere = useMemo(() => {
|
|
817
|
+
const selectedKey = selectedNodePath ? JSON.stringify(selectedNodePath) : null;
|
|
818
|
+
const used = new Set<string>();
|
|
819
|
+
for (const ref of layoutFieldRefs) {
|
|
820
|
+
if (selectedKey && JSON.stringify(ref.path) === selectedKey) continue;
|
|
821
|
+
used.add(ref.key);
|
|
822
|
+
}
|
|
823
|
+
return used;
|
|
824
|
+
}, [layoutFieldRefs, selectedNodePath]);
|
|
825
|
+
|
|
826
|
+
// Keep the Properties Schema list itself ordered to match the layout blueprint,
|
|
827
|
+
// so the fields editor mirrors the visual tree order everywhere.
|
|
828
|
+
useEffect(() => {
|
|
829
|
+
setFields((prev) => {
|
|
830
|
+
const ordered = orderCustomBlockFieldsByLayout(prev, layoutSchema);
|
|
831
|
+
const unchanged =
|
|
832
|
+
ordered.length === prev.length && ordered.every((field, index) => field === prev[index]);
|
|
833
|
+
return unchanged ? prev : ordered;
|
|
834
|
+
});
|
|
835
|
+
}, [layoutFieldRefs]);
|
|
836
|
+
|
|
837
|
+
return (
|
|
838
|
+
<div className="w-full flex flex-col gap-6">
|
|
839
|
+
{/* Header section */}
|
|
840
|
+
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-slate-200 dark:border-slate-800 pb-5">
|
|
841
|
+
<div className="flex items-center gap-3">
|
|
842
|
+
<Button variant="ghost" size="icon" onClick={() => router.push("/cms/custom-blocks")} className="h-9 w-9">
|
|
843
|
+
<ArrowLeft className="h-5 w-5" />
|
|
844
|
+
</Button>
|
|
845
|
+
<div>
|
|
846
|
+
<h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-50">
|
|
847
|
+
{mode === "create" ? "Build Custom Block" : `Edit block: ${name}`}
|
|
848
|
+
</h2>
|
|
849
|
+
<p className="text-sm text-muted-foreground mt-0.5">
|
|
850
|
+
Specify your properties fields schemas and map visual Tailwind nested layouts.
|
|
851
|
+
</p>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
|
|
855
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
856
|
+
<Dialog>
|
|
857
|
+
<DialogTrigger asChild>
|
|
858
|
+
<Button size="sm" variant="outline" className="flex items-center gap-1.5 text-slate-600 dark:text-slate-300">
|
|
859
|
+
<BookOpen className="h-4 w-4 text-sky-500" />
|
|
860
|
+
How to Use Guide
|
|
861
|
+
</Button>
|
|
862
|
+
</DialogTrigger>
|
|
863
|
+
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-2xl p-6">
|
|
864
|
+
<DialogHeader className="border-b border-slate-100 dark:border-slate-800 pb-4">
|
|
865
|
+
<DialogTitle className="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-white">
|
|
866
|
+
<Boxes className="h-5 w-5 text-indigo-500" />
|
|
867
|
+
NextBlock Custom Block Creator Guide
|
|
868
|
+
</DialogTitle>
|
|
869
|
+
<DialogDescription className="text-xs text-muted-foreground mt-1">
|
|
870
|
+
A quick step-by-step masterclass on designing, structuring, and visually rendering data-driven user blocks.
|
|
871
|
+
</DialogDescription>
|
|
872
|
+
</DialogHeader>
|
|
873
|
+
|
|
874
|
+
<div className="py-6 space-y-6 text-slate-700 dark:text-slate-300 text-xs leading-relaxed">
|
|
875
|
+
{/* Step 1 */}
|
|
876
|
+
<div className="space-y-2">
|
|
877
|
+
<div className="flex items-center gap-2">
|
|
878
|
+
<Badge className="bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 px-2 py-0.5 text-[10px] font-bold">STEP 1</Badge>
|
|
879
|
+
<h4 className="font-bold text-slate-900 dark:text-white text-sm">Configure Name and Slug Identifiers</h4>
|
|
880
|
+
</div>
|
|
881
|
+
<p className="pl-14 text-slate-600 dark:text-slate-400">
|
|
882
|
+
Choose a descriptive name (e.g. <code className="font-mono bg-muted px-1 py-0.5 rounded text-indigo-400">Product Showcase Card</code>).
|
|
883
|
+
The slug identifier will auto-populate (e.g. <code className="font-mono bg-muted px-1 py-0.5 rounded text-indigo-400">product-showcase-card</code>).
|
|
884
|
+
This slug acts as the database key and cannot be changed once saved.
|
|
885
|
+
</p>
|
|
886
|
+
</div>
|
|
887
|
+
|
|
888
|
+
{/* Step 2 */}
|
|
889
|
+
<div className="space-y-2">
|
|
890
|
+
<div className="flex items-center gap-2">
|
|
891
|
+
<Badge className="bg-sky-500/10 text-sky-400 border border-sky-500/20 px-2 py-0.5 text-[10px] font-bold">STEP 2</Badge>
|
|
892
|
+
<h4 className="font-bold text-slate-900 dark:text-white text-sm">Declare Properties Fields Schema</h4>
|
|
893
|
+
</div>
|
|
894
|
+
<p className="pl-14 text-slate-600 dark:text-slate-400 mb-2">
|
|
895
|
+
Properties define the editable fields editors will populate when adding the block to pages. Supported types:
|
|
896
|
+
</p>
|
|
897
|
+
<ul className="pl-14 list-disc space-y-1.5 text-slate-600 dark:text-slate-400">
|
|
898
|
+
<li><span className="font-bold text-slate-800 dark:text-slate-200">Text:</span> Single line plain string (headings, button labels, links).</li>
|
|
899
|
+
<li><span className="font-bold text-slate-800 dark:text-slate-200">Rich-Text:</span> Full HTML formatting (body text, custom paragraphs, lists).</li>
|
|
900
|
+
<li><span className="font-bold text-slate-800 dark:text-slate-200">R2 Image:</span> Media files uploaded directly to Cloudflare R2 object storage.</li>
|
|
901
|
+
<li><span className="font-bold text-slate-800 dark:text-slate-200">Live DB Relation Link:</span> Reference table rows directly (e.g. link custom pages or products) with dynamic lookup columns.</li>
|
|
902
|
+
</ul>
|
|
903
|
+
</div>
|
|
904
|
+
|
|
905
|
+
{/* Step 3 */}
|
|
906
|
+
<div className="space-y-2">
|
|
907
|
+
<div className="flex items-center gap-2">
|
|
908
|
+
<Badge className="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 text-[10px] font-bold">STEP 3</Badge>
|
|
909
|
+
<h4 className="font-bold text-slate-900 dark:text-white text-sm">Visual Layout Tree & Drag & Drop</h4>
|
|
910
|
+
</div>
|
|
911
|
+
<p className="pl-14 text-slate-600 dark:text-slate-400 mb-2">
|
|
912
|
+
The layout tree outlines how the block is structured inside the DOM. You can nest structural containers and map fields:
|
|
913
|
+
</p>
|
|
914
|
+
<ul className="pl-14 list-disc space-y-1.5 text-slate-600 dark:text-slate-400">
|
|
915
|
+
<li><span className="font-bold text-slate-800 dark:text-slate-200">Containers:</span> Represent tags like <code className="font-mono bg-muted px-1 rounded"><div></code>, <code className="font-mono bg-muted px-1 rounded"><section></code>. Style them with Tailwind CSS.</li>
|
|
916
|
+
<li><span className="font-bold text-slate-800 dark:text-slate-200">Field Renders:</span> Maps a field schema value to an HTML tag (e.g. map image to <code className="font-mono bg-muted px-1 rounded"><img></code>).</li>
|
|
917
|
+
<li><span className="font-bold text-slate-800 dark:text-slate-200">Drag & Drop:</span> Click and drag the <GripVertical className="inline-block h-3.5 w-3.5 mx-0.5 text-slate-400" /> handle of any node in the tree list. Hover over targets to drop:
|
|
918
|
+
<ul className="list-circle pl-6 mt-1 space-y-1">
|
|
919
|
+
<li>Hover near <span className="font-bold text-indigo-400">top</span> of a node to insert <span className="italic">before</span> it.</li>
|
|
920
|
+
<li>Hover near <span className="font-bold text-indigo-400">bottom</span> of a node to insert <span className="italic">after</span> it.</li>
|
|
921
|
+
<li>Hover over the <span className="font-bold text-indigo-400">middle</span> of a container node to nest it <span className="italic">inside</span> as a child.</li>
|
|
922
|
+
</ul>
|
|
923
|
+
</li>
|
|
924
|
+
</ul>
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
{/* Quick Styling Presets */}
|
|
928
|
+
<div className="space-y-2 bg-slate-50 dark:bg-slate-950 p-4 border rounded-xl border-slate-100 dark:border-slate-800/80">
|
|
929
|
+
<h5 className="font-bold text-slate-900 dark:text-white flex items-center gap-1">
|
|
930
|
+
<Sparkles className="h-3.5 w-3.5 text-amber-400" />
|
|
931
|
+
Styling Presets Tip
|
|
932
|
+
</h5>
|
|
933
|
+
<p className="text-slate-600 dark:text-slate-400">
|
|
934
|
+
Select a node in the Layout Tree to inspect it. Use the <span className="font-bold text-slate-800 dark:text-slate-200">Quick-Styling utility Presets</span> to apply grid columns, borders, shadow cards, flex columns, and center alignments with one click. Test inputs on the right visualizer to preview live rendering instantly.
|
|
935
|
+
</p>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
</DialogContent>
|
|
939
|
+
</Dialog>
|
|
940
|
+
|
|
941
|
+
<Button onClick={handleSaveBlock} disabled={isPending} className="shadow-sm">
|
|
942
|
+
{isPending ? (
|
|
943
|
+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
944
|
+
) : (
|
|
945
|
+
<Save className="h-4 w-4 mr-2" />
|
|
946
|
+
)}
|
|
947
|
+
Save Block Schema
|
|
948
|
+
</Button>
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
|
|
952
|
+
{/* Main Dual-Pane layout */}
|
|
953
|
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
|
954
|
+
{/* Left builder pane (7 columns) */}
|
|
955
|
+
<div className="lg:col-span-7 flex flex-col gap-6">
|
|
956
|
+
<div className="flex border-b border-slate-200 dark:border-slate-800">
|
|
957
|
+
<button
|
|
958
|
+
onClick={() => setActiveTab("general")}
|
|
959
|
+
className={`pb-3 px-4 text-sm font-semibold border-b-2 transition-all flex items-center gap-2 ${
|
|
960
|
+
activeTab === "general"
|
|
961
|
+
? "border-primary text-primary"
|
|
962
|
+
: "border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
|
963
|
+
}`}
|
|
964
|
+
>
|
|
965
|
+
<Settings className="h-4 w-4" /> Config metadata
|
|
966
|
+
</button>
|
|
967
|
+
<button
|
|
968
|
+
onClick={() => setActiveTab("fields")}
|
|
969
|
+
className={`pb-3 px-4 text-sm font-semibold border-b-2 transition-all flex items-center gap-2 ${
|
|
970
|
+
activeTab === "fields"
|
|
971
|
+
? "border-primary text-primary"
|
|
972
|
+
: "border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
|
973
|
+
}`}
|
|
974
|
+
>
|
|
975
|
+
<Database className="h-4 w-4" /> Fields Schema
|
|
976
|
+
</button>
|
|
977
|
+
<button
|
|
978
|
+
onClick={() => setActiveTab("layout")}
|
|
979
|
+
className={`pb-3 px-4 text-sm font-semibold border-b-2 transition-all flex items-center gap-2 ${
|
|
980
|
+
activeTab === "layout"
|
|
981
|
+
? "border-primary text-primary"
|
|
982
|
+
: "border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
|
983
|
+
}`}
|
|
984
|
+
>
|
|
985
|
+
<ListTree className="h-4 w-4" /> Layout Tree
|
|
986
|
+
</button>
|
|
987
|
+
</div>
|
|
988
|
+
|
|
989
|
+
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-6 min-h-[450px]">
|
|
990
|
+
{/* Tab 1: General Info */}
|
|
991
|
+
{activeTab === "general" && (
|
|
992
|
+
<div className="space-y-6">
|
|
993
|
+
<div className="space-y-2">
|
|
994
|
+
<Label htmlFor="block-name" className="text-sm font-bold">Block Name</Label>
|
|
995
|
+
<Input
|
|
996
|
+
id="block-name"
|
|
997
|
+
placeholder="e.g. Testimonial Card"
|
|
998
|
+
value={name}
|
|
999
|
+
onChange={(e) => handleNameChange(e.target.value)}
|
|
1000
|
+
/>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div className="space-y-2">
|
|
1003
|
+
<Label htmlFor="block-slug" className="text-sm font-bold">Slug Identifier</Label>
|
|
1004
|
+
<Input
|
|
1005
|
+
id="block-slug"
|
|
1006
|
+
placeholder="e.g. testimonial-card"
|
|
1007
|
+
value={slug}
|
|
1008
|
+
onChange={(e) => setSlug(e.target.value)}
|
|
1009
|
+
disabled={mode === "edit"}
|
|
1010
|
+
/>
|
|
1011
|
+
<p className="text-xs text-muted-foreground">
|
|
1012
|
+
Unique block identifier. Used in schemas and JSON models. Cannot be changed once created.
|
|
1013
|
+
</p>
|
|
1014
|
+
</div>
|
|
1015
|
+
<div className="space-y-2">
|
|
1016
|
+
<Label htmlFor="block-desc" className="text-sm font-bold">Description</Label>
|
|
1017
|
+
<Textarea
|
|
1018
|
+
id="block-desc"
|
|
1019
|
+
rows={4}
|
|
1020
|
+
placeholder="Describe what this custom block represents or does..."
|
|
1021
|
+
value={description}
|
|
1022
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
1023
|
+
/>
|
|
1024
|
+
<p className="text-xs text-muted-foreground">
|
|
1025
|
+
Optional. Admin-only note shown in the blocks library — it does not appear on the front end.
|
|
1026
|
+
</p>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
)}
|
|
1030
|
+
|
|
1031
|
+
{/* Tab 2: Fields Manager */}
|
|
1032
|
+
{activeTab === "fields" && (
|
|
1033
|
+
<div className="space-y-6">
|
|
1034
|
+
<div className="flex justify-between items-center">
|
|
1035
|
+
<h3 className="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
|
|
1036
|
+
Properties Schema
|
|
1037
|
+
</h3>
|
|
1038
|
+
<Button onClick={addField} size="sm" variant="outline">
|
|
1039
|
+
<Plus className="mr-1.5 h-4 w-4" /> Add Property Field
|
|
1040
|
+
</Button>
|
|
1041
|
+
</div>
|
|
1042
|
+
|
|
1043
|
+
{fields.length === 0 ? (
|
|
1044
|
+
<div className="text-center py-12 border border-dashed rounded-lg border-slate-200 dark:border-slate-800">
|
|
1045
|
+
<Database className="mx-auto h-10 w-10 text-muted-foreground" />
|
|
1046
|
+
<p className="text-sm text-slate-500 mt-2">No schema fields created.</p>
|
|
1047
|
+
<Button onClick={addField} variant="link" className="mt-2 text-primary font-semibold">
|
|
1048
|
+
Add a field properties selector
|
|
1049
|
+
</Button>
|
|
1050
|
+
</div>
|
|
1051
|
+
) : (
|
|
1052
|
+
<div className="space-y-2.5">
|
|
1053
|
+
{fields.map((field, idx) => (
|
|
1054
|
+
<div
|
|
1055
|
+
key={idx}
|
|
1056
|
+
className="overflow-hidden rounded-lg border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/40 shadow-sm"
|
|
1057
|
+
>
|
|
1058
|
+
{/* Identity row */}
|
|
1059
|
+
<div className="grid grid-cols-1 md:grid-cols-12 gap-2.5 items-end p-3">
|
|
1060
|
+
<div className="md:col-span-4 space-y-1">
|
|
1061
|
+
<Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground">Property Key</Label>
|
|
1062
|
+
<Input
|
|
1063
|
+
value={field.key}
|
|
1064
|
+
placeholder="e.g. quote"
|
|
1065
|
+
onChange={(e) => updateField(idx, { key: e.target.value })}
|
|
1066
|
+
className="h-8 font-mono text-xs"
|
|
1067
|
+
/>
|
|
1068
|
+
</div>
|
|
1069
|
+
<div className="md:col-span-4 space-y-1">
|
|
1070
|
+
<Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground">Label</Label>
|
|
1071
|
+
<Input
|
|
1072
|
+
value={field.label}
|
|
1073
|
+
placeholder="e.g. Author Name"
|
|
1074
|
+
onChange={(e) => updateField(idx, { label: e.target.value })}
|
|
1075
|
+
className="h-8 text-xs"
|
|
1076
|
+
/>
|
|
1077
|
+
</div>
|
|
1078
|
+
<div className="md:col-span-3 space-y-1">
|
|
1079
|
+
<Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground">Type</Label>
|
|
1080
|
+
<select
|
|
1081
|
+
value={field.type}
|
|
1082
|
+
onChange={(e) =>
|
|
1083
|
+
updateField(idx, {
|
|
1084
|
+
type: e.target.value as "text" | "rich-text" | "image_r2" | "db_relation",
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
className="w-full rounded-md border border-input bg-background px-2 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 h-8"
|
|
1088
|
+
>
|
|
1089
|
+
<option value="text">Text (single-line)</option>
|
|
1090
|
+
<option value="rich-text">Rich-Text (HTML)</option>
|
|
1091
|
+
<option value="image_r2">Cloudflare R2 Image</option>
|
|
1092
|
+
<option value="db_relation">Live DB Relation Link</option>
|
|
1093
|
+
</select>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div className="md:col-span-1 flex justify-end">
|
|
1096
|
+
<Button
|
|
1097
|
+
variant="ghost"
|
|
1098
|
+
size="icon"
|
|
1099
|
+
onClick={() => deleteField(idx)}
|
|
1100
|
+
className="h-8 w-8 text-slate-400 hover:text-destructive hover:bg-destructive/10"
|
|
1101
|
+
title="Remove field"
|
|
1102
|
+
>
|
|
1103
|
+
<Trash2 className="h-4 w-4" />
|
|
1104
|
+
</Button>
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
|
|
1108
|
+
{/* Config + flags strip: context on the left, toggles on the right */}
|
|
1109
|
+
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 border-t border-slate-100 dark:border-slate-800/60 bg-slate-50/70 dark:bg-slate-950/30 px-3 py-2">
|
|
1110
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
1111
|
+
{field.type === "db_relation" ? (
|
|
1112
|
+
<>
|
|
1113
|
+
<Label className="text-[10px] uppercase tracking-wide font-semibold text-muted-foreground shrink-0">
|
|
1114
|
+
Table
|
|
1115
|
+
</Label>
|
|
1116
|
+
<select
|
|
1117
|
+
value={field.table}
|
|
1118
|
+
onChange={(e) => {
|
|
1119
|
+
const table = e.target.value;
|
|
1120
|
+
const spec = relationTables.find((t) => t.table === table);
|
|
1121
|
+
updateField(idx, {
|
|
1122
|
+
table,
|
|
1123
|
+
display_column: spec?.displayColumn || "title",
|
|
1124
|
+
value_column: spec?.valueColumn || "id",
|
|
1125
|
+
});
|
|
1126
|
+
}}
|
|
1127
|
+
className="h-7 rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
1128
|
+
>
|
|
1129
|
+
{relationTables.map((t) => (
|
|
1130
|
+
<option key={t.table} value={t.table}>
|
|
1131
|
+
{t.label}
|
|
1132
|
+
</option>
|
|
1133
|
+
))}
|
|
1134
|
+
</select>
|
|
1135
|
+
<span className="hidden lg:inline truncate text-[11px] text-muted-foreground">
|
|
1136
|
+
· choose the column in the Layout Tree
|
|
1137
|
+
</span>
|
|
1138
|
+
</>
|
|
1139
|
+
) : (
|
|
1140
|
+
<span className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
1141
|
+
{getFieldIcon(field.type)}
|
|
1142
|
+
{field.type === "text"
|
|
1143
|
+
? "Single-line text"
|
|
1144
|
+
: field.type === "rich-text"
|
|
1145
|
+
? "Formatted HTML content"
|
|
1146
|
+
: "Direct image upload (Cloudflare R2)"}
|
|
1147
|
+
</span>
|
|
1148
|
+
)}
|
|
1149
|
+
</div>
|
|
1150
|
+
|
|
1151
|
+
<div className="flex items-center gap-4 shrink-0">
|
|
1152
|
+
{field.type === "db_relation" && (
|
|
1153
|
+
<div className="flex items-center gap-1.5">
|
|
1154
|
+
<Checkbox
|
|
1155
|
+
id={`multiple-${idx}`}
|
|
1156
|
+
checked={field.multiple === true}
|
|
1157
|
+
onCheckedChange={(checked) => updateField(idx, { multiple: checked === true })}
|
|
1158
|
+
/>
|
|
1159
|
+
<Label htmlFor={`multiple-${idx}`} className="text-xs font-medium cursor-pointer">
|
|
1160
|
+
Link multiple
|
|
1161
|
+
</Label>
|
|
1162
|
+
</div>
|
|
1163
|
+
)}
|
|
1164
|
+
<div className="flex items-center gap-1.5">
|
|
1165
|
+
<Checkbox
|
|
1166
|
+
id={`required-${idx}`}
|
|
1167
|
+
checked={field.required === true}
|
|
1168
|
+
onCheckedChange={(checked) => updateField(idx, { required: checked === true })}
|
|
1169
|
+
/>
|
|
1170
|
+
<Label htmlFor={`required-${idx}`} className="text-xs font-medium cursor-pointer">
|
|
1171
|
+
Required
|
|
1172
|
+
</Label>
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
</div>
|
|
1177
|
+
))}
|
|
1178
|
+
</div>
|
|
1179
|
+
)}
|
|
1180
|
+
</div>
|
|
1181
|
+
)}
|
|
1182
|
+
|
|
1183
|
+
{/* Tab 3: Visual Layout Tree Editor */}
|
|
1184
|
+
{activeTab === "layout" && (
|
|
1185
|
+
<div className="space-y-6">
|
|
1186
|
+
<div className="flex justify-between items-center border-b border-slate-100 dark:border-slate-800 pb-3">
|
|
1187
|
+
<div>
|
|
1188
|
+
<h3 className="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
|
|
1189
|
+
Layout Schema Blueprint
|
|
1190
|
+
</h3>
|
|
1191
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">
|
|
1192
|
+
Infinitely nest containers or render mapped schema properties fields.
|
|
1193
|
+
</p>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div className="flex items-center gap-2">
|
|
1196
|
+
<Button onClick={handleGenerateDefaultLayout} size="sm" variant="outline">
|
|
1197
|
+
<Sparkles className="mr-1.5 h-3.5 w-3.5 text-amber-500" />
|
|
1198
|
+
Reset Blueprint Layout
|
|
1199
|
+
</Button>
|
|
1200
|
+
</div>
|
|
1201
|
+
</div>
|
|
1202
|
+
|
|
1203
|
+
<div className="grid grid-cols-1 md:grid-cols-12 gap-6">
|
|
1204
|
+
{/* Left tree layout list (5 columns) */}
|
|
1205
|
+
<div className="md:col-span-6 border rounded-xl border-slate-200 dark:border-slate-800 p-4 bg-slate-50/50 dark:bg-slate-900/30 max-h-[380px] overflow-y-auto">
|
|
1206
|
+
<h4 className="text-xs font-semibold text-slate-500 dark:text-slate-400 mb-2 uppercase tracking-wide">
|
|
1207
|
+
Tree Nodes
|
|
1208
|
+
</h4>
|
|
1209
|
+
{renderTreeItem(layoutSchema, [])}
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
{/* Right inspector config (6 columns) */}
|
|
1213
|
+
<div className="md:col-span-6 border border-slate-200 dark:border-slate-800 rounded-xl p-4 bg-white dark:bg-slate-950 space-y-4">
|
|
1214
|
+
<h4 className="text-xs font-bold text-slate-800 dark:text-slate-200 border-b pb-2 uppercase tracking-wide">
|
|
1215
|
+
Node Properties Inspector
|
|
1216
|
+
</h4>
|
|
1217
|
+
|
|
1218
|
+
{!selectedNode ? (
|
|
1219
|
+
<div className="text-center py-10 text-muted-foreground flex flex-col items-center justify-center h-full">
|
|
1220
|
+
<Layers className="h-8 w-8 text-slate-300 dark:text-slate-700 mb-2" />
|
|
1221
|
+
<span className="text-xs italic">Select a layout node on the left to inspect or style.</span>
|
|
1222
|
+
</div>
|
|
1223
|
+
) : (
|
|
1224
|
+
<div className="space-y-4 text-xs">
|
|
1225
|
+
<div className="flex justify-between items-center bg-slate-50 dark:bg-slate-900 px-3 py-1.5 rounded border">
|
|
1226
|
+
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
|
1227
|
+
Node Type:
|
|
1228
|
+
</span>
|
|
1229
|
+
<Badge variant="secondary" className="uppercase font-mono text-[10px]">
|
|
1230
|
+
{selectedNode.type}
|
|
1231
|
+
</Badge>
|
|
1232
|
+
</div>
|
|
1233
|
+
|
|
1234
|
+
{selectedNode.type === "container" ? (
|
|
1235
|
+
<div className="space-y-3">
|
|
1236
|
+
<div className="space-y-1">
|
|
1237
|
+
<Label className="font-semibold">HTML Element Tag</Label>
|
|
1238
|
+
<select
|
|
1239
|
+
value={selectedNode.as || "div"}
|
|
1240
|
+
onChange={(e) => handleUpdateSelectedNode({ as: e.target.value })}
|
|
1241
|
+
className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
|
|
1242
|
+
>
|
|
1243
|
+
{CONTAINER_TAGS.map((tag) => (
|
|
1244
|
+
<option key={tag} value={tag}>
|
|
1245
|
+
<{tag}> Element
|
|
1246
|
+
</option>
|
|
1247
|
+
))}
|
|
1248
|
+
</select>
|
|
1249
|
+
</div>
|
|
1250
|
+
</div>
|
|
1251
|
+
) : (
|
|
1252
|
+
<div className="space-y-3">
|
|
1253
|
+
<div className="space-y-1">
|
|
1254
|
+
<Label className="font-semibold">Map Property Field</Label>
|
|
1255
|
+
<select
|
|
1256
|
+
value={selectedNode.field_key || ""}
|
|
1257
|
+
onChange={(e) => {
|
|
1258
|
+
const key = e.target.value;
|
|
1259
|
+
const type = fields.find((f) => f.key === key)?.type || "text";
|
|
1260
|
+
handleUpdateSelectedNode({
|
|
1261
|
+
field_key: key,
|
|
1262
|
+
as: type === "image_r2" ? "img" : type === "rich-text" ? "div" : "span",
|
|
1263
|
+
});
|
|
1264
|
+
}}
|
|
1265
|
+
className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
|
|
1266
|
+
>
|
|
1267
|
+
<option value="" disabled>Select property key...</option>
|
|
1268
|
+
{fields.map((f) => {
|
|
1269
|
+
// Relation fields may be reused by multiple nodes (e.g. image, title,
|
|
1270
|
+
// price); only single-value fields are limited to one placement.
|
|
1271
|
+
const usedElsewhere =
|
|
1272
|
+
fieldKeysUsedElsewhere.has(f.key) && f.type !== "db_relation";
|
|
1273
|
+
return (
|
|
1274
|
+
<option key={f.key} value={f.key} disabled={usedElsewhere}>
|
|
1275
|
+
{f.label} ({f.key}){usedElsewhere ? " — already used" : ""}
|
|
1276
|
+
</option>
|
|
1277
|
+
);
|
|
1278
|
+
})}
|
|
1279
|
+
</select>
|
|
1280
|
+
</div>
|
|
1281
|
+
{(() => {
|
|
1282
|
+
const mappedField = fields.find((f) => f.key === selectedNode.field_key);
|
|
1283
|
+
if (mappedField?.type !== "db_relation") return null;
|
|
1284
|
+
const spec = relationTables.find((t) => t.table === mappedField.table);
|
|
1285
|
+
const columns = spec?.selectColumns || [];
|
|
1286
|
+
return (
|
|
1287
|
+
<div className="space-y-1">
|
|
1288
|
+
<Label className="font-semibold">Relation Column</Label>
|
|
1289
|
+
<select
|
|
1290
|
+
value={selectedNode.column || ""}
|
|
1291
|
+
onChange={(e) => handleUpdateSelectedNode({ column: e.target.value || undefined })}
|
|
1292
|
+
className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
|
|
1293
|
+
>
|
|
1294
|
+
<option value="">Default ({mappedField.display_column})</option>
|
|
1295
|
+
{columns.map((col) => (
|
|
1296
|
+
<option key={col} value={col}>
|
|
1297
|
+
{col}
|
|
1298
|
+
</option>
|
|
1299
|
+
))}
|
|
1300
|
+
</select>
|
|
1301
|
+
<p className="text-[10px] text-muted-foreground">
|
|
1302
|
+
Which column of the related record to show. Render as <img> for its image; price columns display as currency.
|
|
1303
|
+
</p>
|
|
1304
|
+
</div>
|
|
1305
|
+
);
|
|
1306
|
+
})()}
|
|
1307
|
+
<div className="space-y-1">
|
|
1308
|
+
<Label className="font-semibold">Render Tag Element</Label>
|
|
1309
|
+
<select
|
|
1310
|
+
value={selectedNode.as || "span"}
|
|
1311
|
+
onChange={(e) => handleUpdateSelectedNode({ as: e.target.value })}
|
|
1312
|
+
className="w-full rounded-md border h-8 px-2 bg-transparent text-xs"
|
|
1313
|
+
>
|
|
1314
|
+
{FIELD_TAGS.map((tag) => (
|
|
1315
|
+
<option key={tag} value={tag}>
|
|
1316
|
+
<{tag}> Render Target
|
|
1317
|
+
</option>
|
|
1318
|
+
))}
|
|
1319
|
+
</select>
|
|
1320
|
+
</div>
|
|
1321
|
+
<div className="space-y-1">
|
|
1322
|
+
<Label className="font-semibold">Empty Fallback Copy</Label>
|
|
1323
|
+
<Input
|
|
1324
|
+
value={selectedNode.emptyFallback || ""}
|
|
1325
|
+
onChange={(e) => handleUpdateSelectedNode({ emptyFallback: e.target.value })}
|
|
1326
|
+
placeholder="e.g. Quote content goes here..."
|
|
1327
|
+
className="h-8 text-xs"
|
|
1328
|
+
/>
|
|
1329
|
+
</div>
|
|
1330
|
+
</div>
|
|
1331
|
+
)}
|
|
1332
|
+
|
|
1333
|
+
<div className="space-y-1.5">
|
|
1334
|
+
<Label className="font-semibold">Tailwind CSS Classes</Label>
|
|
1335
|
+
<Textarea
|
|
1336
|
+
value={selectedNode.className || ""}
|
|
1337
|
+
onChange={(e) => handleUpdateSelectedNode({ className: e.target.value })}
|
|
1338
|
+
placeholder="e.g. flex flex-col gap-4 text-center mt-2 border"
|
|
1339
|
+
className="font-mono text-xs"
|
|
1340
|
+
rows={3}
|
|
1341
|
+
/>
|
|
1342
|
+
</div>
|
|
1343
|
+
|
|
1344
|
+
{/* Styling presets */}
|
|
1345
|
+
<div className="space-y-2 pt-2 border-t">
|
|
1346
|
+
<Label className="font-bold text-muted-foreground uppercase text-[9px] block">
|
|
1347
|
+
Quick-Styling utility Presets
|
|
1348
|
+
</Label>
|
|
1349
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
1350
|
+
<Button
|
|
1351
|
+
variant="outline"
|
|
1352
|
+
size="sm"
|
|
1353
|
+
className="text-[10px] justify-start h-7 px-2"
|
|
1354
|
+
onClick={() => {
|
|
1355
|
+
const current = selectedNode.className || "";
|
|
1356
|
+
const base = current.includes("flex") ? current : `flex flex-col gap-4 ${current}`.trim();
|
|
1357
|
+
handleUpdateSelectedNode({ className: base });
|
|
1358
|
+
}}
|
|
1359
|
+
>
|
|
1360
|
+
Flex Column
|
|
1361
|
+
</Button>
|
|
1362
|
+
<Button
|
|
1363
|
+
variant="outline"
|
|
1364
|
+
size="sm"
|
|
1365
|
+
className="text-[10px] justify-start h-7 px-2"
|
|
1366
|
+
onClick={() => {
|
|
1367
|
+
const current = selectedNode.className || "";
|
|
1368
|
+
const base = current.includes("grid") ? current : `grid gap-6 md:grid-cols-2 ${current}`.trim();
|
|
1369
|
+
handleUpdateSelectedNode({ className: base });
|
|
1370
|
+
}}
|
|
1371
|
+
>
|
|
1372
|
+
Grid (2 Cols)
|
|
1373
|
+
</Button>
|
|
1374
|
+
<Button
|
|
1375
|
+
variant="outline"
|
|
1376
|
+
size="sm"
|
|
1377
|
+
className="text-[10px] justify-start h-7 px-2"
|
|
1378
|
+
onClick={() => {
|
|
1379
|
+
const current = selectedNode.className || "";
|
|
1380
|
+
const base = current.includes("p-") ? current : `p-6 rounded-xl border bg-card shadow-sm ${current}`.trim();
|
|
1381
|
+
handleUpdateSelectedNode({ className: base });
|
|
1382
|
+
}}
|
|
1383
|
+
>
|
|
1384
|
+
Bordered Card
|
|
1385
|
+
</Button>
|
|
1386
|
+
<Button
|
|
1387
|
+
variant="outline"
|
|
1388
|
+
size="sm"
|
|
1389
|
+
className="text-[10px] justify-start h-7 px-2"
|
|
1390
|
+
onClick={() => {
|
|
1391
|
+
const current = selectedNode.className || "";
|
|
1392
|
+
const base = current.includes("items-center") ? current : `items-center justify-center text-center ${current}`.trim();
|
|
1393
|
+
handleUpdateSelectedNode({ className: base });
|
|
1394
|
+
}}
|
|
1395
|
+
>
|
|
1396
|
+
Align Center
|
|
1397
|
+
</Button>
|
|
1398
|
+
</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
</div>
|
|
1401
|
+
)}
|
|
1402
|
+
</div>
|
|
1403
|
+
</div>
|
|
1404
|
+
</div>
|
|
1405
|
+
)}
|
|
1406
|
+
</div>
|
|
1407
|
+
</div>
|
|
1408
|
+
|
|
1409
|
+
{/* Right preview pane (5 columns) */}
|
|
1410
|
+
<div className="lg:col-span-5 space-y-6 lg:sticky lg:top-24">
|
|
1411
|
+
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 text-slate-100 flex items-center justify-between shadow-md">
|
|
1412
|
+
<div className="flex items-center gap-2">
|
|
1413
|
+
<Eye className="h-4 w-4 text-sky-400" />
|
|
1414
|
+
<h3 className="text-sm font-bold tracking-wider uppercase">
|
|
1415
|
+
Dynamic Layout Editor Preview
|
|
1416
|
+
</h3>
|
|
1417
|
+
</div>
|
|
1418
|
+
<Badge className="bg-emerald-500/15 text-emerald-400 border border-emerald-500/25 font-semibold text-[10px]">
|
|
1419
|
+
Live Playground
|
|
1420
|
+
</Badge>
|
|
1421
|
+
</div>
|
|
1422
|
+
|
|
1423
|
+
{/* Renders layout engine compilation */}
|
|
1424
|
+
<div className="bg-slate-100 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 p-6 rounded-xl min-h-[220px] flex items-center justify-center overflow-x-auto">
|
|
1425
|
+
{fields.length === 0 ? (
|
|
1426
|
+
<span className="text-xs text-muted-foreground italic">Add properties fields to start layout blueprint</span>
|
|
1427
|
+
) : (
|
|
1428
|
+
<div className="w-full">
|
|
1429
|
+
<DynamicLayoutEngine
|
|
1430
|
+
fields={fields}
|
|
1431
|
+
layoutSchema={layoutSchema}
|
|
1432
|
+
data={{
|
|
1433
|
+
...previewValues,
|
|
1434
|
+
resolved_relations: mockRelationRecords,
|
|
1435
|
+
}}
|
|
1436
|
+
/>
|
|
1437
|
+
</div>
|
|
1438
|
+
)}
|
|
1439
|
+
</div>
|
|
1440
|
+
|
|
1441
|
+
{/* Preview data input values form */}
|
|
1442
|
+
{fields.length > 0 && (
|
|
1443
|
+
<Card className="border-slate-200 dark:border-slate-800 shadow-sm">
|
|
1444
|
+
<CardHeader className="py-3.5 border-b">
|
|
1445
|
+
<CardTitle className="text-xs uppercase font-bold text-slate-500 tracking-wider">
|
|
1446
|
+
Test Mock Values Playground
|
|
1447
|
+
</CardTitle>
|
|
1448
|
+
<CardDescription className="text-[10px]">
|
|
1449
|
+
Fill these custom sandbox inputs to visualize your Tailwind CSS alignment.
|
|
1450
|
+
</CardDescription>
|
|
1451
|
+
</CardHeader>
|
|
1452
|
+
<CardContent className="py-4 space-y-4 max-h-[300px] overflow-y-auto">
|
|
1453
|
+
{orderedPreviewFields.map((f) => {
|
|
1454
|
+
const fieldVal = previewValues[f.key];
|
|
1455
|
+
const fieldId = `preview-${f.key}`;
|
|
1456
|
+
|
|
1457
|
+
return (
|
|
1458
|
+
<div key={f.key} className="space-y-1.5 text-xs">
|
|
1459
|
+
<Label htmlFor={fieldId} className="font-semibold text-slate-700 dark:text-slate-300">
|
|
1460
|
+
{f.label} ({f.key}) {f.required && <span className="text-destructive">*</span>}
|
|
1461
|
+
</Label>
|
|
1462
|
+
{f.type === "rich-text" ? (
|
|
1463
|
+
<Textarea
|
|
1464
|
+
id={fieldId}
|
|
1465
|
+
value={fieldVal || ""}
|
|
1466
|
+
onChange={(e) => setPreviewValues({ ...previewValues, [f.key]: e.target.value })}
|
|
1467
|
+
className="text-xs"
|
|
1468
|
+
rows={2}
|
|
1469
|
+
/>
|
|
1470
|
+
) : f.type === "image_r2" ? (
|
|
1471
|
+
<ImageR2Picker
|
|
1472
|
+
value={fieldVal && typeof fieldVal === "object" && "url" in fieldVal ? fieldVal : null}
|
|
1473
|
+
onChange={(val) => setPreviewValues({ ...previewValues, [f.key]: val })}
|
|
1474
|
+
accept={f.accept}
|
|
1475
|
+
maxBytes={f.max_bytes}
|
|
1476
|
+
/>
|
|
1477
|
+
) : f.type === "db_relation" ? (
|
|
1478
|
+
<DBRelationSelect
|
|
1479
|
+
table={f.table}
|
|
1480
|
+
value={
|
|
1481
|
+
f.multiple
|
|
1482
|
+
? Array.isArray(fieldVal) ? fieldVal.map(String) : []
|
|
1483
|
+
: fieldVal ? String(fieldVal) : null
|
|
1484
|
+
}
|
|
1485
|
+
onChange={(val, selected) => {
|
|
1486
|
+
setPreviewValues({ ...previewValues, [f.key]: val });
|
|
1487
|
+
if (f.multiple) {
|
|
1488
|
+
setMockRelationRecords({
|
|
1489
|
+
...mockRelationRecords,
|
|
1490
|
+
[f.key]: selected || [],
|
|
1491
|
+
});
|
|
1492
|
+
} else {
|
|
1493
|
+
setMockRelationRecords({
|
|
1494
|
+
...mockRelationRecords,
|
|
1495
|
+
[f.key]: selected?.[0] || null,
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}}
|
|
1499
|
+
multiple={f.multiple}
|
|
1500
|
+
displayColumn={f.display_column}
|
|
1501
|
+
valueColumn={f.value_column}
|
|
1502
|
+
filters={f.filters}
|
|
1503
|
+
/>
|
|
1504
|
+
) : (
|
|
1505
|
+
<Input
|
|
1506
|
+
id={fieldId}
|
|
1507
|
+
value={fieldVal || ""}
|
|
1508
|
+
onChange={(e) => setPreviewValues({ ...previewValues, [f.key]: e.target.value })}
|
|
1509
|
+
className="h-8 text-xs"
|
|
1510
|
+
/>
|
|
1511
|
+
)}
|
|
1512
|
+
</div>
|
|
1513
|
+
);
|
|
1514
|
+
})}
|
|
1515
|
+
</CardContent>
|
|
1516
|
+
</Card>
|
|
1517
|
+
)}
|
|
1518
|
+
</div>
|
|
1519
|
+
</div>
|
|
1520
|
+
</div>
|
|
1521
|
+
);
|
|
1522
|
+
}
|