create-nextblock 0.14.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-template/.dockerignore +23 -23
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +1 -1
- package/project.json +19 -19
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.dockerignore +23 -23
- package/templates/nextblock-template/README.md +34 -34
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/postActions.ts +146 -146
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5749
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +4 -2
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/layout.tsx +73 -73
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
- package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +688 -688
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +23 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +9 -7
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/sitemap.ts +130 -130
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
- package/templates/nextblock-template/docs/README.md +39 -39
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -87
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/next-env.d.ts +1 -0
- package/templates/nextblock-template/package.json +6 -6
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
import { Editor } from '@tiptap/react';
|
|
5
5
|
import Image from 'next/image';
|
|
6
6
|
import { Image as ImageIconLucide, Search, CheckCircle } from 'lucide-react';
|
|
@@ -14,113 +14,113 @@ import {
|
|
|
14
14
|
DialogFooter,
|
|
15
15
|
DialogClose,
|
|
16
16
|
} from '@nextblock-cms/ui';
|
|
17
|
-
import { Input } from '@nextblock-cms/ui';
|
|
18
|
-
import type { Database } from '@nextblock-cms/db';
|
|
19
|
-
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
20
|
-
|
|
21
|
-
type Media = Database['public']['Tables']['media']['Row'];
|
|
22
|
-
|
|
23
|
-
const MEDIA_REQUEST_TIMEOUT_MS = 8000;
|
|
24
|
-
const MEDIA_LIBRARY_LIMIT = 20;
|
|
25
|
-
|
|
26
|
-
function resolveMediaPreviewPath(media: Media) {
|
|
27
|
-
return media.file_path || media.object_key || null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function resolveMediaPreviewSrc(path: string) {
|
|
31
|
-
return resolveMediaUrl(path);
|
|
32
|
-
}
|
|
17
|
+
import { Input } from '@nextblock-cms/ui';
|
|
18
|
+
import type { Database } from '@nextblock-cms/db';
|
|
19
|
+
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
20
|
+
|
|
21
|
+
type Media = Database['public']['Tables']['media']['Row'];
|
|
22
|
+
|
|
23
|
+
const MEDIA_REQUEST_TIMEOUT_MS = 8000;
|
|
24
|
+
const MEDIA_LIBRARY_LIMIT = 20;
|
|
25
|
+
|
|
26
|
+
function resolveMediaPreviewPath(media: Media) {
|
|
27
|
+
return media.file_path || media.object_key || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveMediaPreviewSrc(path: string) {
|
|
31
|
+
return resolveMediaUrl(path);
|
|
32
|
+
}
|
|
33
33
|
|
|
34
34
|
interface MediaLibraryModalProps {
|
|
35
35
|
editor: Editor | null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
39
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
40
|
-
const [mediaLibrary, setMediaLibrary] = useState<Media[]>([]);
|
|
41
|
-
const [isLoadingMedia, setIsLoadingMedia] = useState(false);
|
|
42
|
-
const [loadError, setLoadError] = useState<string | null>(null);
|
|
43
|
-
const [searchTerm, setSearchTerm] = useState("");
|
|
44
|
-
const requestIdRef = useRef(0);
|
|
45
|
-
|
|
46
|
-
const fetchLibrary = useCallback(async () => {
|
|
47
|
-
if (!isModalOpen) return;
|
|
48
|
-
const requestId = ++requestIdRef.current;
|
|
49
|
-
setIsLoadingMedia(true);
|
|
50
|
-
setLoadError(null);
|
|
51
|
-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
52
|
-
try {
|
|
53
|
-
const controller = new AbortController();
|
|
54
|
-
timeoutId = setTimeout(
|
|
55
|
-
() => controller.abort(new Error('Media library request timed out.')),
|
|
56
|
-
MEDIA_REQUEST_TIMEOUT_MS
|
|
57
|
-
);
|
|
58
|
-
const params = new URLSearchParams({
|
|
59
|
-
limit: MEDIA_LIBRARY_LIMIT.toString(),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (searchTerm.trim()) {
|
|
63
|
-
params.set('q', searchTerm.trim());
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const response = await fetch(`/api/media/library?${params.toString()}`, {
|
|
67
|
-
method: 'GET',
|
|
68
|
-
cache: 'no-store',
|
|
69
|
-
signal: controller.signal,
|
|
70
|
-
});
|
|
71
|
-
clearTimeout(timeoutId);
|
|
72
|
-
|
|
73
|
-
const payload = (await response.json()) as {
|
|
74
|
-
items?: Media[];
|
|
75
|
-
error?: string;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
if (requestId !== requestIdRef.current) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!response.ok) {
|
|
83
|
-
console.error('Error fetching media library:', payload.error);
|
|
84
|
-
setMediaLibrary([]);
|
|
85
|
-
setLoadError(payload.error || 'We could not load the media library. Please retry.');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
setMediaLibrary(payload.items || []);
|
|
90
|
-
} catch (error: any) {
|
|
91
|
-
if (requestId !== requestIdRef.current) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.error('Error fetching media library:', error);
|
|
96
|
-
setMediaLibrary([]);
|
|
97
|
-
if (error?.name === 'AbortError' || error?.message === 'Media library request timed out.') {
|
|
98
|
-
setLoadError('The media library took too long to respond. Please retry.');
|
|
99
|
-
} else {
|
|
100
|
-
setLoadError('We could not load the media library. Please retry.');
|
|
101
|
-
}
|
|
102
|
-
} finally {
|
|
103
|
-
if (timeoutId) {
|
|
104
|
-
clearTimeout(timeoutId);
|
|
105
|
-
}
|
|
106
|
-
if (requestId === requestIdRef.current) {
|
|
107
|
-
setIsLoadingMedia(false);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}, [isModalOpen, searchTerm]);
|
|
38
|
+
export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
39
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
40
|
+
const [mediaLibrary, setMediaLibrary] = useState<Media[]>([]);
|
|
41
|
+
const [isLoadingMedia, setIsLoadingMedia] = useState(false);
|
|
42
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
43
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
44
|
+
const requestIdRef = useRef(0);
|
|
45
|
+
|
|
46
|
+
const fetchLibrary = useCallback(async () => {
|
|
47
|
+
if (!isModalOpen) return;
|
|
48
|
+
const requestId = ++requestIdRef.current;
|
|
49
|
+
setIsLoadingMedia(true);
|
|
50
|
+
setLoadError(null);
|
|
51
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
52
|
+
try {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
timeoutId = setTimeout(
|
|
55
|
+
() => controller.abort(new Error('Media library request timed out.')),
|
|
56
|
+
MEDIA_REQUEST_TIMEOUT_MS
|
|
57
|
+
);
|
|
58
|
+
const params = new URLSearchParams({
|
|
59
|
+
limit: MEDIA_LIBRARY_LIMIT.toString(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (searchTerm.trim()) {
|
|
63
|
+
params.set('q', searchTerm.trim());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const response = await fetch(`/api/media/library?${params.toString()}`, {
|
|
67
|
+
method: 'GET',
|
|
68
|
+
cache: 'no-store',
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
});
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
|
|
73
|
+
const payload = (await response.json()) as {
|
|
74
|
+
items?: Media[];
|
|
75
|
+
error?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (requestId !== requestIdRef.current) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
console.error('Error fetching media library:', payload.error);
|
|
84
|
+
setMediaLibrary([]);
|
|
85
|
+
setLoadError(payload.error || 'We could not load the media library. Please retry.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setMediaLibrary(payload.items || []);
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
if (requestId !== requestIdRef.current) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.error('Error fetching media library:', error);
|
|
96
|
+
setMediaLibrary([]);
|
|
97
|
+
if (error?.name === 'AbortError' || error?.message === 'Media library request timed out.') {
|
|
98
|
+
setLoadError('The media library took too long to respond. Please retry.');
|
|
99
|
+
} else {
|
|
100
|
+
setLoadError('We could not load the media library. Please retry.');
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
if (timeoutId) {
|
|
104
|
+
clearTimeout(timeoutId);
|
|
105
|
+
}
|
|
106
|
+
if (requestId === requestIdRef.current) {
|
|
107
|
+
setIsLoadingMedia(false);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, [isModalOpen, searchTerm]);
|
|
111
111
|
|
|
112
112
|
useEffect(() => {
|
|
113
113
|
fetchLibrary();
|
|
114
114
|
}, [fetchLibrary]);
|
|
115
115
|
|
|
116
|
-
const handleSelectMedia = (mediaItem: Media) => {
|
|
117
|
-
const previewPath = resolveMediaPreviewPath(mediaItem);
|
|
118
|
-
const imageUrl = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
|
|
119
|
-
if (editor && mediaItem.file_type?.startsWith("image/") && imageUrl) {
|
|
120
|
-
editor.chain().focus().insertContent(`<img src="${imageUrl}" alt="${mediaItem.description || mediaItem.file_name}" />`).run();
|
|
121
|
-
}
|
|
122
|
-
setIsModalOpen(false);
|
|
123
|
-
};
|
|
116
|
+
const handleSelectMedia = (mediaItem: Media) => {
|
|
117
|
+
const previewPath = resolveMediaPreviewPath(mediaItem);
|
|
118
|
+
const imageUrl = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
|
|
119
|
+
if (editor && mediaItem.file_type?.startsWith("image/") && imageUrl) {
|
|
120
|
+
editor.chain().focus().insertContent(`<img src="${imageUrl}" alt="${mediaItem.description || mediaItem.file_name}" />`).run();
|
|
121
|
+
}
|
|
122
|
+
setIsModalOpen(false);
|
|
123
|
+
};
|
|
124
124
|
|
|
125
125
|
return (
|
|
126
126
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
@@ -143,54 +143,54 @@ export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
|
143
143
|
/>
|
|
144
144
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
145
145
|
</div>
|
|
146
|
-
{isLoadingMedia ? (
|
|
147
|
-
<div className="flex-grow flex items-center justify-center"><p>Loading media...</p></div>
|
|
148
|
-
) : loadError ? (
|
|
149
|
-
<div className="flex-grow flex flex-col items-center justify-center gap-3 text-center">
|
|
150
|
-
<p className="max-w-sm text-sm text-muted-foreground">{loadError}</p>
|
|
151
|
-
<Button type="button" variant="outline" size="sm" onClick={() => void fetchLibrary()}>
|
|
152
|
-
Retry
|
|
153
|
-
</Button>
|
|
154
|
-
</div>
|
|
155
|
-
) : mediaLibrary.length === 0 ? (
|
|
156
|
-
<div className="flex-grow flex items-center justify-center"><p>No media found.</p></div>
|
|
157
|
-
) : (
|
|
158
|
-
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 overflow-y-auto flex-grow pr-2">
|
|
159
|
-
{mediaLibrary.filter(m => m.file_type?.startsWith("image/")).map((media) => {
|
|
160
|
-
const previewPath = resolveMediaPreviewPath(media);
|
|
161
|
-
const previewSrc = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<button
|
|
165
|
-
key={media.id}
|
|
166
|
-
type="button"
|
|
167
|
-
className="relative aspect-square border rounded-md overflow-hidden group focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
|
168
|
-
onClick={() => handleSelectMedia(media)}
|
|
169
|
-
>
|
|
170
|
-
{previewSrc ? (
|
|
171
|
-
<Image
|
|
172
|
-
src={previewSrc}
|
|
173
|
-
alt={media.description || media.file_name}
|
|
174
|
-
width={200}
|
|
175
|
-
height={200}
|
|
176
|
-
className="h-full w-full object-cover"
|
|
177
|
-
/>
|
|
178
|
-
) : (
|
|
179
|
-
<div className="flex h-full w-full items-center justify-center bg-muted text-xs text-muted-foreground">
|
|
180
|
-
Preview unavailable
|
|
181
|
-
</div>
|
|
182
|
-
)}
|
|
183
|
-
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity flex items-center justify-center">
|
|
184
|
-
<CheckCircle className="h-8 w-8 text-white" />
|
|
185
|
-
</div>
|
|
186
|
-
<p className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 truncate text-center">
|
|
187
|
-
{media.file_name}
|
|
188
|
-
</p>
|
|
189
|
-
</button>
|
|
190
|
-
);
|
|
191
|
-
})}
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
146
|
+
{isLoadingMedia ? (
|
|
147
|
+
<div className="flex-grow flex items-center justify-center"><p>Loading media...</p></div>
|
|
148
|
+
) : loadError ? (
|
|
149
|
+
<div className="flex-grow flex flex-col items-center justify-center gap-3 text-center">
|
|
150
|
+
<p className="max-w-sm text-sm text-muted-foreground">{loadError}</p>
|
|
151
|
+
<Button type="button" variant="outline" size="sm" onClick={() => void fetchLibrary()}>
|
|
152
|
+
Retry
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
) : mediaLibrary.length === 0 ? (
|
|
156
|
+
<div className="flex-grow flex items-center justify-center"><p>No media found.</p></div>
|
|
157
|
+
) : (
|
|
158
|
+
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 overflow-y-auto flex-grow pr-2">
|
|
159
|
+
{mediaLibrary.filter(m => m.file_type?.startsWith("image/")).map((media) => {
|
|
160
|
+
const previewPath = resolveMediaPreviewPath(media);
|
|
161
|
+
const previewSrc = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<button
|
|
165
|
+
key={media.id}
|
|
166
|
+
type="button"
|
|
167
|
+
className="relative aspect-square border rounded-md overflow-hidden group focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
|
168
|
+
onClick={() => handleSelectMedia(media)}
|
|
169
|
+
>
|
|
170
|
+
{previewSrc ? (
|
|
171
|
+
<Image
|
|
172
|
+
src={previewSrc}
|
|
173
|
+
alt={media.description || media.file_name}
|
|
174
|
+
width={200}
|
|
175
|
+
height={200}
|
|
176
|
+
className="h-full w-full object-cover"
|
|
177
|
+
/>
|
|
178
|
+
) : (
|
|
179
|
+
<div className="flex h-full w-full items-center justify-center bg-muted text-xs text-muted-foreground">
|
|
180
|
+
Preview unavailable
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity flex items-center justify-center">
|
|
184
|
+
<CheckCircle className="h-8 w-8 text-white" />
|
|
185
|
+
</div>
|
|
186
|
+
<p className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 truncate text-center">
|
|
187
|
+
{media.file_name}
|
|
188
|
+
</p>
|
|
189
|
+
</button>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
194
|
<DialogFooter className="mt-4">
|
|
195
195
|
<DialogClose asChild>
|
|
196
196
|
<Button type="button" variant="outline">Cancel</Button>
|
|
@@ -199,4 +199,4 @@ export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
|
199
199
|
</DialogContent>
|
|
200
200
|
</Dialog>
|
|
201
201
|
);
|
|
202
|
-
};
|
|
202
|
+
};
|
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
// app/cms/blocks/editors/DynamicCustomBlockEditor.tsx
|
|
2
|
-
"use client";
|
|
3
|
-
|
|
4
|
-
import React, { useEffect, useState } from "react";
|
|
5
|
-
import { Label } from "@nextblock-cms/ui";
|
|
6
|
-
import { Input } from "@nextblock-cms/ui";
|
|
7
|
-
import { Textarea } from "@nextblock-cms/ui";
|
|
8
|
-
import { BlockEditorProps } from "../components/BlockEditorModal";
|
|
9
|
-
import { DBRelationSelect } from "../../custom-blocks/components/DBRelationSelect";
|
|
10
|
-
import { ImageR2Picker } from "../../custom-blocks/components/ImageR2Picker";
|
|
11
|
-
import type { CustomBlockField } from "@nextblock-cms/utils";
|
|
12
|
-
|
|
13
|
-
type CustomBlockDefinitionResponse = {
|
|
14
|
-
id: string;
|
|
15
|
-
slug: string;
|
|
16
|
-
name: string;
|
|
17
|
-
fields: CustomBlockField[];
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// Fallback raw-JSON editor for block types with no custom definition.
|
|
21
|
-
function JsonFallbackEditor({
|
|
22
|
-
content,
|
|
23
|
-
onChange,
|
|
24
|
-
}: {
|
|
25
|
-
content: Record<string, any>;
|
|
26
|
-
onChange: (next: any) => void;
|
|
27
|
-
}) {
|
|
28
|
-
const [value, setValue] = useState(() => JSON.stringify(content ?? {}, null, 2));
|
|
29
|
-
const [error, setError] = useState<string | null>(null);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
setValue(JSON.stringify(content ?? {}, null, 2));
|
|
33
|
-
setError(null);
|
|
34
|
-
}, [content]);
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="space-y-2 p-4 border rounded-xl bg-card/50">
|
|
38
|
-
<Label htmlFor="custom-block-json-fallback" className="text-xs font-semibold">
|
|
39
|
-
Block Content (JSON)
|
|
40
|
-
</Label>
|
|
41
|
-
<Textarea
|
|
42
|
-
id="custom-block-json-fallback"
|
|
43
|
-
value={value}
|
|
44
|
-
spellCheck={false}
|
|
45
|
-
className="min-h-[320px] font-mono text-xs"
|
|
46
|
-
onChange={(e) => {
|
|
47
|
-
const next = e.target.value;
|
|
48
|
-
setValue(next);
|
|
49
|
-
try {
|
|
50
|
-
onChange(JSON.parse(next));
|
|
51
|
-
setError(null);
|
|
52
|
-
} catch (parseError) {
|
|
53
|
-
setError(parseError instanceof Error ? parseError.message : "Invalid JSON");
|
|
54
|
-
}
|
|
55
|
-
}}
|
|
56
|
-
/>
|
|
57
|
-
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default function DynamicCustomBlockEditor({
|
|
63
|
-
block,
|
|
64
|
-
content,
|
|
65
|
-
onChange,
|
|
66
|
-
}: BlockEditorProps<Record<string, any>>) {
|
|
67
|
-
const [definition, setDefinition] = useState<CustomBlockDefinitionResponse | null>(null);
|
|
68
|
-
const [loading, setLoading] = useState(true);
|
|
69
|
-
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
async function loadDefinition() {
|
|
72
|
-
try {
|
|
73
|
-
const response = await fetch("/api/custom-blocks/editor-definitions");
|
|
74
|
-
if (response.ok) {
|
|
75
|
-
const payload = await response.json();
|
|
76
|
-
const blockType = block.block_type || (block as any).type;
|
|
77
|
-
const def = payload.definitions?.find((d: any) => d.slug === blockType);
|
|
78
|
-
if (def) {
|
|
79
|
-
setDefinition(def);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.error("Failed to load custom block definition for editor:", err);
|
|
84
|
-
} finally {
|
|
85
|
-
setLoading(false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
loadDefinition();
|
|
89
|
-
}, [block.block_type, (block as any).type]);
|
|
90
|
-
|
|
91
|
-
if (loading) {
|
|
92
|
-
return <div className="p-4 text-center text-sm text-muted-foreground">Loading custom block settings...</div>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// No matching custom block definition (e.g. a non-custom block type routed
|
|
96
|
-
// here as a fallback). Degrade gracefully to a raw JSON editor instead of an
|
|
97
|
-
// error so content stays editable.
|
|
98
|
-
if (!definition) {
|
|
99
|
-
return <JsonFallbackEditor content={content} onChange={onChange} />;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const handleFieldChange = (key: string, val: any) => {
|
|
103
|
-
onChange({ ...content, [key]: val });
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className="space-y-4 p-4 border rounded-xl bg-card/50">
|
|
108
|
-
<div className="pb-2 border-b">
|
|
109
|
-
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200">{definition.name}</h4>
|
|
110
|
-
<p className="text-xs text-muted-foreground">Custom Block Properties</p>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
{definition.fields.map((field) => {
|
|
114
|
-
const fieldVal = content[field.key];
|
|
115
|
-
const fieldId = `custom-block-editor-${definition.slug}-${field.key}`;
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div key={field.key} className="space-y-1 pt-2">
|
|
119
|
-
<Label htmlFor={fieldId} className="text-xs font-semibold text-slate-700 dark:text-slate-300">
|
|
120
|
-
{field.label} {field.required && <span className="text-destructive">*</span>}
|
|
121
|
-
</Label>
|
|
122
|
-
|
|
123
|
-
{field.type === "rich-text" ? (
|
|
124
|
-
<Textarea
|
|
125
|
-
id={fieldId}
|
|
126
|
-
value={fieldVal || ""}
|
|
127
|
-
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
128
|
-
placeholder={field.placeholder}
|
|
129
|
-
className="min-h-24 text-sm"
|
|
130
|
-
/>
|
|
131
|
-
) : field.type === "image_r2" ? (
|
|
132
|
-
<ImageR2Picker
|
|
133
|
-
value={fieldVal && typeof fieldVal === "object" && "url" in fieldVal ? fieldVal : null}
|
|
134
|
-
onChange={(val) => handleFieldChange(field.key, val)}
|
|
135
|
-
accept={field.accept}
|
|
136
|
-
maxBytes={field.max_bytes}
|
|
137
|
-
/>
|
|
138
|
-
) : field.type === "db_relation" ? (
|
|
139
|
-
<DBRelationSelect
|
|
140
|
-
table={field.table}
|
|
141
|
-
value={
|
|
142
|
-
field.multiple
|
|
143
|
-
? Array.isArray(fieldVal) ? fieldVal.map(String) : []
|
|
144
|
-
: fieldVal ? String(fieldVal) : null
|
|
145
|
-
}
|
|
146
|
-
onChange={(val) => handleFieldChange(field.key, val)}
|
|
147
|
-
multiple={field.multiple}
|
|
148
|
-
displayColumn={field.display_column}
|
|
149
|
-
valueColumn={field.value_column}
|
|
150
|
-
filters={field.filters}
|
|
151
|
-
/>
|
|
152
|
-
) : (
|
|
153
|
-
<Input
|
|
154
|
-
id={fieldId}
|
|
155
|
-
type="text"
|
|
156
|
-
value={fieldVal || ""}
|
|
157
|
-
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
158
|
-
placeholder={field.placeholder}
|
|
159
|
-
className="h-9"
|
|
160
|
-
/>
|
|
161
|
-
)}
|
|
162
|
-
</div>
|
|
163
|
-
);
|
|
164
|
-
})}
|
|
165
|
-
</div>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
1
|
+
// app/cms/blocks/editors/DynamicCustomBlockEditor.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useEffect, useState } from "react";
|
|
5
|
+
import { Label } from "@nextblock-cms/ui";
|
|
6
|
+
import { Input } from "@nextblock-cms/ui";
|
|
7
|
+
import { Textarea } from "@nextblock-cms/ui";
|
|
8
|
+
import { BlockEditorProps } from "../components/BlockEditorModal";
|
|
9
|
+
import { DBRelationSelect } from "../../custom-blocks/components/DBRelationSelect";
|
|
10
|
+
import { ImageR2Picker } from "../../custom-blocks/components/ImageR2Picker";
|
|
11
|
+
import type { CustomBlockField } from "@nextblock-cms/utils";
|
|
12
|
+
|
|
13
|
+
type CustomBlockDefinitionResponse = {
|
|
14
|
+
id: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
name: string;
|
|
17
|
+
fields: CustomBlockField[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Fallback raw-JSON editor for block types with no custom definition.
|
|
21
|
+
function JsonFallbackEditor({
|
|
22
|
+
content,
|
|
23
|
+
onChange,
|
|
24
|
+
}: {
|
|
25
|
+
content: Record<string, any>;
|
|
26
|
+
onChange: (next: any) => void;
|
|
27
|
+
}) {
|
|
28
|
+
const [value, setValue] = useState(() => JSON.stringify(content ?? {}, null, 2));
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setValue(JSON.stringify(content ?? {}, null, 2));
|
|
33
|
+
setError(null);
|
|
34
|
+
}, [content]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="space-y-2 p-4 border rounded-xl bg-card/50">
|
|
38
|
+
<Label htmlFor="custom-block-json-fallback" className="text-xs font-semibold">
|
|
39
|
+
Block Content (JSON)
|
|
40
|
+
</Label>
|
|
41
|
+
<Textarea
|
|
42
|
+
id="custom-block-json-fallback"
|
|
43
|
+
value={value}
|
|
44
|
+
spellCheck={false}
|
|
45
|
+
className="min-h-[320px] font-mono text-xs"
|
|
46
|
+
onChange={(e) => {
|
|
47
|
+
const next = e.target.value;
|
|
48
|
+
setValue(next);
|
|
49
|
+
try {
|
|
50
|
+
onChange(JSON.parse(next));
|
|
51
|
+
setError(null);
|
|
52
|
+
} catch (parseError) {
|
|
53
|
+
setError(parseError instanceof Error ? parseError.message : "Invalid JSON");
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default function DynamicCustomBlockEditor({
|
|
63
|
+
block,
|
|
64
|
+
content,
|
|
65
|
+
onChange,
|
|
66
|
+
}: BlockEditorProps<Record<string, any>>) {
|
|
67
|
+
const [definition, setDefinition] = useState<CustomBlockDefinitionResponse | null>(null);
|
|
68
|
+
const [loading, setLoading] = useState(true);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
async function loadDefinition() {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch("/api/custom-blocks/editor-definitions");
|
|
74
|
+
if (response.ok) {
|
|
75
|
+
const payload = await response.json();
|
|
76
|
+
const blockType = block.block_type || (block as any).type;
|
|
77
|
+
const def = payload.definitions?.find((d: any) => d.slug === blockType);
|
|
78
|
+
if (def) {
|
|
79
|
+
setDefinition(def);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error("Failed to load custom block definition for editor:", err);
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
loadDefinition();
|
|
89
|
+
}, [block.block_type, (block as any).type]);
|
|
90
|
+
|
|
91
|
+
if (loading) {
|
|
92
|
+
return <div className="p-4 text-center text-sm text-muted-foreground">Loading custom block settings...</div>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// No matching custom block definition (e.g. a non-custom block type routed
|
|
96
|
+
// here as a fallback). Degrade gracefully to a raw JSON editor instead of an
|
|
97
|
+
// error so content stays editable.
|
|
98
|
+
if (!definition) {
|
|
99
|
+
return <JsonFallbackEditor content={content} onChange={onChange} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleFieldChange = (key: string, val: any) => {
|
|
103
|
+
onChange({ ...content, [key]: val });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="space-y-4 p-4 border rounded-xl bg-card/50">
|
|
108
|
+
<div className="pb-2 border-b">
|
|
109
|
+
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200">{definition.name}</h4>
|
|
110
|
+
<p className="text-xs text-muted-foreground">Custom Block Properties</p>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{definition.fields.map((field) => {
|
|
114
|
+
const fieldVal = content[field.key];
|
|
115
|
+
const fieldId = `custom-block-editor-${definition.slug}-${field.key}`;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div key={field.key} className="space-y-1 pt-2">
|
|
119
|
+
<Label htmlFor={fieldId} className="text-xs font-semibold text-slate-700 dark:text-slate-300">
|
|
120
|
+
{field.label} {field.required && <span className="text-destructive">*</span>}
|
|
121
|
+
</Label>
|
|
122
|
+
|
|
123
|
+
{field.type === "rich-text" ? (
|
|
124
|
+
<Textarea
|
|
125
|
+
id={fieldId}
|
|
126
|
+
value={fieldVal || ""}
|
|
127
|
+
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
128
|
+
placeholder={field.placeholder}
|
|
129
|
+
className="min-h-24 text-sm"
|
|
130
|
+
/>
|
|
131
|
+
) : field.type === "image_r2" ? (
|
|
132
|
+
<ImageR2Picker
|
|
133
|
+
value={fieldVal && typeof fieldVal === "object" && "url" in fieldVal ? fieldVal : null}
|
|
134
|
+
onChange={(val) => handleFieldChange(field.key, val)}
|
|
135
|
+
accept={field.accept}
|
|
136
|
+
maxBytes={field.max_bytes}
|
|
137
|
+
/>
|
|
138
|
+
) : field.type === "db_relation" ? (
|
|
139
|
+
<DBRelationSelect
|
|
140
|
+
table={field.table}
|
|
141
|
+
value={
|
|
142
|
+
field.multiple
|
|
143
|
+
? Array.isArray(fieldVal) ? fieldVal.map(String) : []
|
|
144
|
+
: fieldVal ? String(fieldVal) : null
|
|
145
|
+
}
|
|
146
|
+
onChange={(val) => handleFieldChange(field.key, val)}
|
|
147
|
+
multiple={field.multiple}
|
|
148
|
+
displayColumn={field.display_column}
|
|
149
|
+
valueColumn={field.value_column}
|
|
150
|
+
filters={field.filters}
|
|
151
|
+
/>
|
|
152
|
+
) : (
|
|
153
|
+
<Input
|
|
154
|
+
id={fieldId}
|
|
155
|
+
type="text"
|
|
156
|
+
value={fieldVal || ""}
|
|
157
|
+
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
158
|
+
placeholder={field.placeholder}
|
|
159
|
+
className="h-9"
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|