create-nextblock 0.16.4 → 0.17.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.
Files changed (203) hide show
  1. package/CLAUDE.md +22 -0
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/package.json +1 -1
  8. package/templates/nextblock-template/.browserslistrc +11 -11
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/CLAUDE.md +26 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  14. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  15. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  16. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  17. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  18. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  19. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  20. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  21. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  22. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  23. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +272 -205
  24. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10473 -8467
  25. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  26. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  27. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  28. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  29. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  30. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  31. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  32. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
  33. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
  34. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  36. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  37. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  38. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  39. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  40. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
  41. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  42. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
  43. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  44. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  45. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  46. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  47. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  48. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  49. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  50. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  51. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  52. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  53. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  54. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  55. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  57. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  58. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
  59. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
  60. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  62. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  63. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  64. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  65. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  66. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  67. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  68. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  69. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  70. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  71. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  72. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  73. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  74. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  75. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  76. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  77. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  78. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  79. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  80. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  81. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  82. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  83. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  84. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  85. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  86. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  87. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  88. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  89. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  90. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  91. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  92. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  93. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  94. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  95. package/templates/nextblock-template/components/Header.tsx +38 -38
  96. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  97. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  98. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  99. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  100. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  101. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  102. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  103. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
  104. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  105. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  106. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  107. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  108. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  109. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  110. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  111. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  112. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  113. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  114. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  115. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  116. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  117. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  118. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  119. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  120. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  121. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +1 -1
  122. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +402 -282
  123. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +15 -8
  124. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  125. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +23 -4
  126. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  127. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +4 -1
  128. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +8 -0
  129. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +2 -2
  130. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +95 -104
  131. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  132. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  133. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  134. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  135. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  136. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  137. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  138. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  139. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  140. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  141. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  142. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  143. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  144. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  145. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  146. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  147. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  148. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  149. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  150. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  151. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  152. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  153. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  154. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  155. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  156. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  157. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  158. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  159. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  160. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  161. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  162. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  163. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  164. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  165. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  166. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  167. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  168. package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
  169. package/templates/nextblock-template/lib/seo/redirect-store.ts +3 -2
  170. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  171. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  172. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  173. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +15 -175
  174. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  175. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  176. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  177. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  178. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  179. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  180. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  181. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  182. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  183. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  184. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  185. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  186. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  187. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  188. package/templates/nextblock-template/next-env.d.ts +1 -0
  189. package/templates/nextblock-template/package.json +1 -1
  190. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  191. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  192. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  193. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  194. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  195. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  196. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  197. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  198. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  199. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  200. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  201. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  202. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  203. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
@@ -1,160 +1,160 @@
1
- "use client";
2
-
3
- import React, { useEffect, useMemo, useState } from "react";
4
- import { Loader2 } from "lucide-react";
5
- import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
6
-
7
- // Module-level cache so every block preview shares a single network request for
8
- // the custom block definitions instead of re-fetching per card.
9
- type EditorDefinition = {
10
- slug: string;
11
- name: string;
12
- description?: string | null;
13
- fields: any[];
14
- layout_schema: any;
15
- };
16
-
17
- const definitionCache: { definitions: EditorDefinition[]; promise: Promise<EditorDefinition[]> | null } = {
18
- definitions: [],
19
- promise: null,
20
- };
21
-
22
- function loadCustomBlockDefinitions(): Promise<EditorDefinition[]> {
23
- if (definitionCache.definitions.length > 0) {
24
- return Promise.resolve(definitionCache.definitions);
25
- }
26
- if (!definitionCache.promise) {
27
- definitionCache.promise = fetch("/api/custom-blocks/editor-definitions")
28
- .then((res) => (res.ok ? res.json() : { definitions: [] }))
29
- .then((data) => {
30
- definitionCache.definitions = (data?.definitions ?? []) as EditorDefinition[];
31
- return definitionCache.definitions;
32
- })
33
- .catch(() => []);
34
- }
35
- return definitionCache.promise;
36
- }
37
-
38
- function useCustomBlockDefinition(slug: string) {
39
- const [definitions, setDefinitions] = useState<EditorDefinition[]>(definitionCache.definitions);
40
- const [loading, setLoading] = useState(definitionCache.definitions.length === 0);
41
-
42
- useEffect(() => {
43
- let cancelled = false;
44
- loadCustomBlockDefinitions().then((defs) => {
45
- if (!cancelled) {
46
- setDefinitions(defs);
47
- setLoading(false);
48
- }
49
- });
50
- return () => {
51
- cancelled = true;
52
- };
53
- }, []);
54
-
55
- const definition = useMemo(
56
- () => definitions.find((def) => def.slug === slug) ?? null,
57
- [definitions, slug]
58
- );
59
-
60
- return { definition, loading };
61
- }
62
-
63
- // Resolve db_relation field values into records so the preview matches the front
64
- // end (including relation images), mirroring the server-side hydration.
65
- function useResolvedRelations(definition: EditorDefinition | null, content: Record<string, any>) {
66
- const [resolvedRelations, setResolvedRelations] = useState<Record<string, any>>({});
67
-
68
- const relationSignature = useMemo(() => {
69
- if (!definition) return "";
70
- return definition.fields
71
- .filter((field) => field.type === "db_relation")
72
- .map((field) => `${field.key}:${JSON.stringify(content?.[field.key] ?? null)}`)
73
- .join("|");
74
- }, [definition, content]);
75
-
76
- useEffect(() => {
77
- if (!definition) return;
78
- const relationFields = definition.fields.filter((field) => field.type === "db_relation");
79
- if (relationFields.length === 0) {
80
- setResolvedRelations({});
81
- return;
82
- }
83
-
84
- let cancelled = false;
85
- (async () => {
86
- const next: Record<string, any> = {};
87
- await Promise.all(
88
- relationFields.map(async (field) => {
89
- const raw = content?.[field.key];
90
- const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
91
- if (values.length === 0) return;
92
-
93
- const params = new URLSearchParams({
94
- table: field.table,
95
- valueColumn: field.value_column || "id",
96
- displayColumn: field.display_column || "title",
97
- values: values.map(String).join(","),
98
- limit: String(values.length),
99
- });
100
-
101
- try {
102
- const res = await fetch(`/api/custom-blocks/db-relations?${params.toString()}`, {
103
- cache: "no-store",
104
- });
105
- if (!res.ok) return;
106
- const data = await res.json();
107
- const items = data?.items ?? [];
108
- next[field.key] = field.multiple ? items : items[0] ?? null;
109
- } catch {
110
- // Leave the relation unresolved; the engine falls back gracefully.
111
- }
112
- })
113
- );
114
-
115
- if (!cancelled) {
116
- setResolvedRelations(next);
117
- }
118
- })();
119
-
120
- return () => {
121
- cancelled = true;
122
- };
123
- }, [definition, relationSignature]);
124
-
125
- return resolvedRelations;
126
- }
127
-
128
- export interface CustomBlockEditorPreviewProps {
129
- blockType: string;
130
- content: Record<string, any>;
131
- fallback: React.ReactNode;
132
- }
133
-
134
- export function CustomBlockEditorPreview({ blockType, content, fallback }: CustomBlockEditorPreviewProps) {
135
- const { definition, loading } = useCustomBlockDefinition(blockType);
136
- const resolvedRelations = useResolvedRelations(definition, content);
137
-
138
- if (!definition) {
139
- if (loading) {
140
- return (
141
- <div className="flex items-center gap-2 py-3 text-xs text-muted-foreground">
142
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
143
- Loading block preview…
144
- </div>
145
- );
146
- }
147
- return <>{fallback}</>;
148
- }
149
-
150
- return (
151
- // pointer-events-none lets clicks fall through to the card so editing still opens.
152
- <div className="pointer-events-none w-full overflow-hidden">
153
- <DynamicLayoutEngine
154
- fields={definition.fields}
155
- layoutSchema={definition.layout_schema}
156
- data={{ ...(content || {}), resolved_relations: resolvedRelations }}
157
- />
158
- </div>
159
- );
160
- }
1
+ "use client";
2
+
3
+ import React, { useEffect, useMemo, useState } from "react";
4
+ import { Loader2 } from "lucide-react";
5
+ import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
6
+
7
+ // Module-level cache so every block preview shares a single network request for
8
+ // the custom block definitions instead of re-fetching per card.
9
+ type EditorDefinition = {
10
+ slug: string;
11
+ name: string;
12
+ description?: string | null;
13
+ fields: any[];
14
+ layout_schema: any;
15
+ };
16
+
17
+ const definitionCache: { definitions: EditorDefinition[]; promise: Promise<EditorDefinition[]> | null } = {
18
+ definitions: [],
19
+ promise: null,
20
+ };
21
+
22
+ function loadCustomBlockDefinitions(): Promise<EditorDefinition[]> {
23
+ if (definitionCache.definitions.length > 0) {
24
+ return Promise.resolve(definitionCache.definitions);
25
+ }
26
+ if (!definitionCache.promise) {
27
+ definitionCache.promise = fetch("/api/custom-blocks/editor-definitions")
28
+ .then((res) => (res.ok ? res.json() : { definitions: [] }))
29
+ .then((data) => {
30
+ definitionCache.definitions = (data?.definitions ?? []) as EditorDefinition[];
31
+ return definitionCache.definitions;
32
+ })
33
+ .catch(() => []);
34
+ }
35
+ return definitionCache.promise;
36
+ }
37
+
38
+ function useCustomBlockDefinition(slug: string) {
39
+ const [definitions, setDefinitions] = useState<EditorDefinition[]>(definitionCache.definitions);
40
+ const [loading, setLoading] = useState(definitionCache.definitions.length === 0);
41
+
42
+ useEffect(() => {
43
+ let cancelled = false;
44
+ loadCustomBlockDefinitions().then((defs) => {
45
+ if (!cancelled) {
46
+ setDefinitions(defs);
47
+ setLoading(false);
48
+ }
49
+ });
50
+ return () => {
51
+ cancelled = true;
52
+ };
53
+ }, []);
54
+
55
+ const definition = useMemo(
56
+ () => definitions.find((def) => def.slug === slug) ?? null,
57
+ [definitions, slug]
58
+ );
59
+
60
+ return { definition, loading };
61
+ }
62
+
63
+ // Resolve db_relation field values into records so the preview matches the front
64
+ // end (including relation images), mirroring the server-side hydration.
65
+ function useResolvedRelations(definition: EditorDefinition | null, content: Record<string, any>) {
66
+ const [resolvedRelations, setResolvedRelations] = useState<Record<string, any>>({});
67
+
68
+ const relationSignature = useMemo(() => {
69
+ if (!definition) return "";
70
+ return definition.fields
71
+ .filter((field) => field.type === "db_relation")
72
+ .map((field) => `${field.key}:${JSON.stringify(content?.[field.key] ?? null)}`)
73
+ .join("|");
74
+ }, [definition, content]);
75
+
76
+ useEffect(() => {
77
+ if (!definition) return;
78
+ const relationFields = definition.fields.filter((field) => field.type === "db_relation");
79
+ if (relationFields.length === 0) {
80
+ setResolvedRelations({});
81
+ return;
82
+ }
83
+
84
+ let cancelled = false;
85
+ (async () => {
86
+ const next: Record<string, any> = {};
87
+ await Promise.all(
88
+ relationFields.map(async (field) => {
89
+ const raw = content?.[field.key];
90
+ const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
91
+ if (values.length === 0) return;
92
+
93
+ const params = new URLSearchParams({
94
+ table: field.table,
95
+ valueColumn: field.value_column || "id",
96
+ displayColumn: field.display_column || "title",
97
+ values: values.map(String).join(","),
98
+ limit: String(values.length),
99
+ });
100
+
101
+ try {
102
+ const res = await fetch(`/api/custom-blocks/db-relations?${params.toString()}`, {
103
+ cache: "no-store",
104
+ });
105
+ if (!res.ok) return;
106
+ const data = await res.json();
107
+ const items = data?.items ?? [];
108
+ next[field.key] = field.multiple ? items : items[0] ?? null;
109
+ } catch {
110
+ // Leave the relation unresolved; the engine falls back gracefully.
111
+ }
112
+ })
113
+ );
114
+
115
+ if (!cancelled) {
116
+ setResolvedRelations(next);
117
+ }
118
+ })();
119
+
120
+ return () => {
121
+ cancelled = true;
122
+ };
123
+ }, [definition, relationSignature]);
124
+
125
+ return resolvedRelations;
126
+ }
127
+
128
+ export interface CustomBlockEditorPreviewProps {
129
+ blockType: string;
130
+ content: Record<string, any>;
131
+ fallback: React.ReactNode;
132
+ }
133
+
134
+ export function CustomBlockEditorPreview({ blockType, content, fallback }: CustomBlockEditorPreviewProps) {
135
+ const { definition, loading } = useCustomBlockDefinition(blockType);
136
+ const resolvedRelations = useResolvedRelations(definition, content);
137
+
138
+ if (!definition) {
139
+ if (loading) {
140
+ return (
141
+ <div className="flex items-center gap-2 py-3 text-xs text-muted-foreground">
142
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
143
+ Loading block preview…
144
+ </div>
145
+ );
146
+ }
147
+ return <>{fallback}</>;
148
+ }
149
+
150
+ return (
151
+ // pointer-events-none lets clicks fall through to the card so editing still opens.
152
+ <div className="pointer-events-none w-full overflow-hidden">
153
+ <DynamicLayoutEngine
154
+ fields={definition.fields}
155
+ layoutSchema={definition.layout_schema}
156
+ data={{ ...(content || {}), resolved_relations: resolvedRelations }}
157
+ />
158
+ </div>
159
+ );
160
+ }
@@ -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
+ }