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,13 +1,13 @@
1
1
  // components/blocks/PostsGridClient.tsx
2
2
  'use client';
3
3
 
4
- import React, { useState, useEffect } from 'react';
5
- import Link from 'next/link';
6
- import { useLanguage } from '../../context/LanguageContext';
7
- import type { PostWithMediaDimensions } from './types';
8
- import Image from 'next/image';
9
- import { Button } from '@nextblock-cms/ui';
10
- import PostCardSkeleton from './PostCardSkeleton'; // Added import
4
+ import React, { useState, useEffect } from 'react';
5
+ import Link from 'next/link';
6
+ import { useLanguage } from '../../context/LanguageContext';
7
+ import type { PostWithMediaDimensions } from './types';
8
+ import Image from 'next/image';
9
+ import { Button } from '@nextblock-cms/ui';
10
+ import PostCardSkeleton from './PostCardSkeleton'; // Added import
11
11
 
12
12
  interface PostsGridClientProps {
13
13
  initialPosts: PostWithMediaDimensions[];
@@ -20,10 +20,10 @@ interface PostsGridClientProps {
20
20
  fetchAction: (languageId: number, page: number, limit: number) => Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string }>;
21
21
  }
22
22
 
23
- const DEFAULT_FEATURE_IMAGE_WIDTH = 1600;
24
- const DEFAULT_FEATURE_IMAGE_HEIGHT = 900;
25
-
26
- const PostsGridClient: React.FC<PostsGridClientProps> = ({
23
+ const DEFAULT_FEATURE_IMAGE_WIDTH = 1600;
24
+ const DEFAULT_FEATURE_IMAGE_HEIGHT = 900;
25
+
26
+ const PostsGridClient: React.FC<PostsGridClientProps> = ({
27
27
  initialPosts,
28
28
  initialPage,
29
29
  postsPerPage,
@@ -32,10 +32,10 @@ const PostsGridClient: React.FC<PostsGridClientProps> = ({
32
32
  languageId,
33
33
  showPagination,
34
34
  fetchAction,
35
- }) => {
36
- const { currentLocale } = useLanguage();
37
- const [currentPage, setCurrentPage] = useState(initialPage);
38
- const [posts, setPosts] = useState<PostWithMediaDimensions[]>(initialPosts);
35
+ }) => {
36
+ const { currentLocale } = useLanguage();
37
+ const [currentPage, setCurrentPage] = useState(initialPage);
38
+ const [posts, setPosts] = useState<PostWithMediaDimensions[]>(initialPosts);
39
39
  const [isLoading, setIsLoading] = useState(false);
40
40
  const [error, setError] = useState<string | null>(null);
41
41
  // Initialize skeletonCount to postsPerPage, or a sensible minimum if initialPosts is empty.
@@ -112,40 +112,40 @@ const PostsGridClient: React.FC<PostsGridClientProps> = ({
112
112
  <PostCardSkeleton key={`skeleton-${index}`} />
113
113
  ))
114
114
  ) : posts.length > 0 ? (
115
- posts.map((post) => (
116
- <Link href={`/article/${post.slug}`} key={post.id} className="block group h-full">
115
+ posts.map((post) => (
116
+ <Link href={`/article/${post.slug}`} key={post.id} className="block group h-full">
117
117
  <div className="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow bg-card text-card-foreground h-full flex flex-col">
118
118
  {/* Basic Post Card Structure - Enhanced with Feature Image */}
119
- {post.feature_image_url ? (
120
- <div className="aspect-video overflow-hidden">
121
- <Image
122
- src={post.feature_image_url}
123
- alt={`Feature image for ${post.title}`}
124
- width={post.feature_image_width && post.feature_image_width > 0 ? post.feature_image_width : DEFAULT_FEATURE_IMAGE_WIDTH}
125
- height={post.feature_image_height && post.feature_image_height > 0 ? post.feature_image_height : DEFAULT_FEATURE_IMAGE_HEIGHT}
126
- sizes={imageSizes}
127
- loading="lazy"
128
- placeholder={post.blur_data_url ? 'blur' : 'empty'}
129
- blurDataURL={post.blur_data_url ?? undefined}
130
- quality={60}
131
- className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
132
- />
133
- </div>
134
- ) : null}
135
- <div className="p-4 flex flex-1 flex-col">
136
- <div className="mb-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
137
- <span className="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-semibold text-slate-700">
138
- {post.label?.trim() || 'Article'}
139
- </span>
140
- <span>
141
- {post.estimated_read_time_minutes} {currentLocale === 'fr' ? 'min de lecture' : 'min read'}
142
- </span>
143
- </div>
144
- <h3 className="text-lg font-semibold mb-2 group-hover:text-primary">{post.title}</h3>
145
- {post.excerpt && <p className="text-sm text-muted-foreground mb-3 line-clamp-3">{post.excerpt}</p>}
146
- <div className="mt-auto pt-2">
147
- <span className="text-xs text-primary group-hover:underline">Read more</span>
148
- </div>
119
+ {post.feature_image_url ? (
120
+ <div className="aspect-video overflow-hidden">
121
+ <Image
122
+ src={post.feature_image_url}
123
+ alt={`Feature image for ${post.title}`}
124
+ width={post.feature_image_width && post.feature_image_width > 0 ? post.feature_image_width : DEFAULT_FEATURE_IMAGE_WIDTH}
125
+ height={post.feature_image_height && post.feature_image_height > 0 ? post.feature_image_height : DEFAULT_FEATURE_IMAGE_HEIGHT}
126
+ sizes={imageSizes}
127
+ loading="lazy"
128
+ placeholder={post.blur_data_url ? 'blur' : 'empty'}
129
+ blurDataURL={post.blur_data_url ?? undefined}
130
+ quality={60}
131
+ className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
132
+ />
133
+ </div>
134
+ ) : null}
135
+ <div className="p-4 flex flex-1 flex-col">
136
+ <div className="mb-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
137
+ <span className="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-semibold text-slate-700">
138
+ {post.label?.trim() || 'Article'}
139
+ </span>
140
+ <span>
141
+ {post.estimated_read_time_minutes} {currentLocale === 'fr' ? 'min de lecture' : 'min read'}
142
+ </span>
143
+ </div>
144
+ <h3 className="text-lg font-semibold mb-2 group-hover:text-primary">{post.title}</h3>
145
+ {post.excerpt && <p className="text-sm text-muted-foreground mb-3 line-clamp-3">{post.excerpt}</p>}
146
+ <div className="mt-auto pt-2">
147
+ <span className="text-xs text-primary group-hover:underline">Read more</span>
148
+ </div>
149
149
  </div>
150
150
  </div>
151
151
  </Link>
@@ -181,4 +181,4 @@ const PostsGridClient: React.FC<PostsGridClientProps> = ({
181
181
  );
182
182
  };
183
183
 
184
- export default PostsGridClient;
184
+ export default PostsGridClient;
@@ -1,12 +1,12 @@
1
- import React from 'react';
2
- import { z } from '../../lib/zod-config';
3
- import { BlockConfig, BlockProps, BlockEditorProps } from '@nextblock-cms/sdk';
4
- import { Avatar, AvatarFallback, AvatarImage } from '@nextblock-cms/ui';
5
- import { Card, CardContent } from '@nextblock-cms/ui';
6
- import { Input } from '@nextblock-cms/ui';
7
- import { Label } from '@nextblock-cms/ui';
8
- import { Textarea } from '@nextblock-cms/ui';
9
- import { MessageSquareQuote } from 'lucide-react';
1
+ import React from 'react';
2
+ import { z } from '../../lib/zod-config';
3
+ import { BlockConfig, BlockProps, BlockEditorProps } from '@nextblock-cms/sdk';
4
+ import { Avatar, AvatarFallback, AvatarImage } from '@nextblock-cms/ui';
5
+ import { Card, CardContent } from '@nextblock-cms/ui';
6
+ import { Input } from '@nextblock-cms/ui';
7
+ import { Label } from '@nextblock-cms/ui';
8
+ import { Textarea } from '@nextblock-cms/ui';
9
+ import { MessageSquareQuote } from 'lucide-react';
10
10
 
11
11
  // 1. Define the Schema
12
12
  export const TestimonialSchema = z.object({
@@ -1,25 +1,25 @@
1
- import type { ComponentType } from "react";
2
- import type { BlockType } from "../../lib/blocks/blockRegistry";
3
-
4
- type PublicBlockRendererLoader = () => Promise<{
5
- default: ComponentType<any>;
6
- }>;
7
-
8
- const publicBlockRendererLoaders: Partial<
9
- Record<BlockType | "hero", PublicBlockRendererLoader>
10
- > = {
11
- text: () => import("./renderers/TextBlockRenderer"),
12
- heading: () => import("./renderers/HeadingBlockRenderer"),
13
- image: () => import("./renderers/ImageBlockRenderer"),
14
- button: () => import("./renderers/ButtonBlockRenderer"),
15
- posts_grid: () => import("./renderers/PostsGridBlockRenderer"),
16
- video_embed: () => import("./renderers/VideoEmbedBlockRenderer"),
17
- section: () => import("./renderers/SectionBlockRenderer"),
18
- hero: () => import("./renderers/SectionBlockRenderer"),
19
- form: () => import("./renderers/FormBlockRenderer"),
20
- testimonial: () => import("./renderers/TestimonialBlockRenderer"),
21
- };
22
-
23
- export function getPublicBlockRendererLoader(blockType: string) {
24
- return publicBlockRendererLoaders[blockType as BlockType | "hero"];
25
- }
1
+ import type { ComponentType } from "react";
2
+ import type { BlockType } from "../../lib/blocks/blockRegistry";
3
+
4
+ type PublicBlockRendererLoader = () => Promise<{
5
+ default: ComponentType<any>;
6
+ }>;
7
+
8
+ const publicBlockRendererLoaders: Partial<
9
+ Record<BlockType | "hero", PublicBlockRendererLoader>
10
+ > = {
11
+ text: () => import("./renderers/TextBlockRenderer"),
12
+ heading: () => import("./renderers/HeadingBlockRenderer"),
13
+ image: () => import("./renderers/ImageBlockRenderer"),
14
+ button: () => import("./renderers/ButtonBlockRenderer"),
15
+ posts_grid: () => import("./renderers/PostsGridBlockRenderer"),
16
+ video_embed: () => import("./renderers/VideoEmbedBlockRenderer"),
17
+ section: () => import("./renderers/SectionBlockRenderer"),
18
+ hero: () => import("./renderers/SectionBlockRenderer"),
19
+ form: () => import("./renderers/FormBlockRenderer"),
20
+ testimonial: () => import("./renderers/TestimonialBlockRenderer"),
21
+ };
22
+
23
+ export function getPublicBlockRendererLoader(blockType: string) {
24
+ return publicBlockRendererLoaders[blockType as BlockType | "hero"];
25
+ }
@@ -1,92 +1,92 @@
1
- "use client";
2
- import React from "react";
3
- import Link from "next/link";
4
- import { buttonVariants } from "@nextblock-cms/ui";
5
- import { cn } from "@nextblock-cms/utils";
6
- import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
7
-
8
- export type ButtonBlockContent = {
9
- text?: string;
10
- url?: string;
11
- variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
12
- size?: 'default' | 'sm' | 'lg' | 'full';
13
- position?: 'left' | 'center' | 'right';
14
- };
15
-
16
- interface ButtonBlockRendererProps {
17
- content: ButtonBlockContent;
18
- languageId: number; // This prop seems unused
19
- visualEditAttributes?: VisualEditAttributes;
20
- }
21
-
22
- const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
23
- content,
24
- visualEditAttributes,
25
- }) => {
26
- const isExternal =
27
- content.url?.startsWith("http") ||
28
- content.url?.startsWith("mailto:") ||
29
- content.url?.startsWith("tel:");
30
- const isAnchor = content.url?.startsWith("#");
31
-
32
- const buttonText = content.text || "Button";
33
- // Map variant name if needed or pass directly if it matches
34
- // The migration might have uppercase 'DEFAULT' or 'OUTLINE', ensure lowercase
35
- const variantRaw = content.variant?.toLowerCase() || "default";
36
- // Cast to specific allowed variants for TS safety if strictly typed, else as any
37
- const variant = (variantRaw === 'default' || variantRaw === 'outline' || variantRaw === 'secondary' || variantRaw === 'ghost' || variantRaw === 'link' || variantRaw === 'destructive')
38
- ? variantRaw
39
- : 'default';
40
-
41
- const sizeRaw = content.size?.toLowerCase() || "default";
42
- const size = (sizeRaw === 'default' || sizeRaw === 'sm' || sizeRaw === 'lg' || sizeRaw === 'icon')
43
- ? sizeRaw
44
- : 'default';
45
-
46
- const buttonPosition = content.position || "left";
47
-
48
- const alignmentClasses = {
49
- left: "justify-start text-left",
50
- center: "justify-center text-center",
51
- right: "justify-end text-right",
52
- };
53
-
54
- // We use buttonVariants directly to avoid 'asChild' composition issues with Radix Slot
55
- // This ensures we just render a clean Link or Anchor with the correct classes.
56
- const classes = cn(
57
- buttonVariants({ variant, size }),
58
- variant === 'outline' && "text-foreground", // Ensure outline text is visible
59
- "no-underline" // Force no-underline for links
60
- );
61
-
62
- return (
63
- <div
64
- className={cn("my-6 flex w-full", alignmentClasses[buttonPosition])}
65
- {...visualEditAttributes}
66
- >
67
- {/* Case 1: Internal link */}
68
- {!isExternal && !isAnchor && !!content.url ? (
69
- <Link href={content.url} className={classes}>
70
- {buttonText}
71
- </Link>
72
- ) : /* Case 2: External or Anchor link */
73
- (isExternal || isAnchor) && !!content.url ? (
74
- <a
75
- href={content.url}
76
- target={isExternal ? "_blank" : undefined}
77
- rel={isExternal ? "noopener noreferrer" : undefined}
78
- className={classes}
79
- >
80
- {buttonText}
81
- </a>
82
- ) : (
83
- /* Case 3: No URL - render a disabled fake button */
84
- <button disabled className={cn(classes, "opacity-50 cursor-not-allowed")}>
85
- {buttonText}
86
- </button>
87
- )}
88
- </div>
89
- );
90
- };
91
-
92
- export default ButtonBlockRenderer;
1
+ "use client";
2
+ import React from "react";
3
+ import Link from "next/link";
4
+ import { buttonVariants } from "@nextblock-cms/ui";
5
+ import { cn } from "@nextblock-cms/utils";
6
+ import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
7
+
8
+ export type ButtonBlockContent = {
9
+ text?: string;
10
+ url?: string;
11
+ variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
12
+ size?: 'default' | 'sm' | 'lg' | 'full';
13
+ position?: 'left' | 'center' | 'right';
14
+ };
15
+
16
+ interface ButtonBlockRendererProps {
17
+ content: ButtonBlockContent;
18
+ languageId: number; // This prop seems unused
19
+ visualEditAttributes?: VisualEditAttributes;
20
+ }
21
+
22
+ const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
23
+ content,
24
+ visualEditAttributes,
25
+ }) => {
26
+ const isExternal =
27
+ content.url?.startsWith("http") ||
28
+ content.url?.startsWith("mailto:") ||
29
+ content.url?.startsWith("tel:");
30
+ const isAnchor = content.url?.startsWith("#");
31
+
32
+ const buttonText = content.text || "Button";
33
+ // Map variant name if needed or pass directly if it matches
34
+ // The migration might have uppercase 'DEFAULT' or 'OUTLINE', ensure lowercase
35
+ const variantRaw = content.variant?.toLowerCase() || "default";
36
+ // Cast to specific allowed variants for TS safety if strictly typed, else as any
37
+ const variant = (variantRaw === 'default' || variantRaw === 'outline' || variantRaw === 'secondary' || variantRaw === 'ghost' || variantRaw === 'link' || variantRaw === 'destructive')
38
+ ? variantRaw
39
+ : 'default';
40
+
41
+ const sizeRaw = content.size?.toLowerCase() || "default";
42
+ const size = (sizeRaw === 'default' || sizeRaw === 'sm' || sizeRaw === 'lg' || sizeRaw === 'icon')
43
+ ? sizeRaw
44
+ : 'default';
45
+
46
+ const buttonPosition = content.position || "left";
47
+
48
+ const alignmentClasses = {
49
+ left: "justify-start text-left",
50
+ center: "justify-center text-center",
51
+ right: "justify-end text-right",
52
+ };
53
+
54
+ // We use buttonVariants directly to avoid 'asChild' composition issues with Radix Slot
55
+ // This ensures we just render a clean Link or Anchor with the correct classes.
56
+ const classes = cn(
57
+ buttonVariants({ variant, size }),
58
+ variant === 'outline' && "text-foreground", // Ensure outline text is visible
59
+ "no-underline" // Force no-underline for links
60
+ );
61
+
62
+ return (
63
+ <div
64
+ className={cn("my-6 flex w-full", alignmentClasses[buttonPosition])}
65
+ {...visualEditAttributes}
66
+ >
67
+ {/* Case 1: Internal link */}
68
+ {!isExternal && !isAnchor && !!content.url ? (
69
+ <Link href={content.url} className={classes}>
70
+ {buttonText}
71
+ </Link>
72
+ ) : /* Case 2: External or Anchor link */
73
+ (isExternal || isAnchor) && !!content.url ? (
74
+ <a
75
+ href={content.url}
76
+ target={isExternal ? "_blank" : undefined}
77
+ rel={isExternal ? "noopener noreferrer" : undefined}
78
+ className={classes}
79
+ >
80
+ {buttonText}
81
+ </a>
82
+ ) : (
83
+ /* Case 3: No URL - render a disabled fake button */
84
+ <button disabled className={cn(classes, "opacity-50 cursor-not-allowed")}>
85
+ {buttonText}
86
+ </button>
87
+ )}
88
+ </div>
89
+ );
90
+ };
91
+
92
+ export default ButtonBlockRenderer;
@@ -51,6 +51,16 @@ type CmsImageMetadata = {
51
51
  height: number;
52
52
  sizes: string;
53
53
  priority?: boolean;
54
+ /**
55
+ * Serve the bundled file as-is instead of through the image optimizer.
56
+ *
57
+ * The optimizer re-encodes at `quality` 60 and prefers AVIF, which is the right
58
+ * trade for photos but smears the fine text and 1px strokes of a rendered
59
+ * diagram, and its `sizes`-driven variant is then upscaled to the figure width.
60
+ * A diagram that is already an optimized webp at 2x the widest figure is
61
+ * sharper and not much heavier served untouched.
62
+ */
63
+ unoptimized?: boolean;
54
64
  };
55
65
 
56
66
  type HtmlImageProps = {
@@ -215,6 +225,7 @@ function renderOptimizedCmsImage(attribs: Record<string, string>) {
215
225
  height={image.height}
216
226
  sizes={sizes}
217
227
  quality={60}
228
+ unoptimized={image.unoptimized === true}
218
229
  priority={priority}
219
230
  fetchPriority={priority ? 'high' : undefined}
220
231
  loading={priority ? undefined : 'lazy'}
@@ -1,7 +1,7 @@
1
- import React from "react";
2
- import PostsGridBlock from "../../blocks/PostsGridBlock";
3
- import type { Database } from "@nextblock-cms/db";
4
- import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
1
+ import React from "react";
2
+ import PostsGridBlock from "../../blocks/PostsGridBlock";
3
+ import type { Database } from "@nextblock-cms/db";
4
+ import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
5
5
 
6
6
  type Block = Database['public']['Tables']['blocks']['Row'];
7
7
  type PostsGridBlockContent = {
@@ -12,27 +12,27 @@ type PostsGridBlockContent = {
12
12
  };
13
13
 
14
14
  interface PostsGridBlockRendererProps {
15
- content: PostsGridBlockContent;
16
- languageId: number;
17
- block: Block;
18
- visualEditAttributes?: VisualEditAttributes;
19
- }
15
+ content: PostsGridBlockContent;
16
+ languageId: number;
17
+ block: Block;
18
+ visualEditAttributes?: VisualEditAttributes;
19
+ }
20
20
 
21
21
  const PostsGridBlockRenderer: React.FC<PostsGridBlockRendererProps> = ({
22
- content,
23
- languageId,
24
- block,
25
- visualEditAttributes,
26
- }) => {
27
- void content;
28
- return (
29
- <div {...visualEditAttributes}>
30
- <PostsGridBlock
31
- block={block}
32
- languageId={languageId}
33
- />
34
- </div>
35
- );
36
- };
22
+ content,
23
+ languageId,
24
+ block,
25
+ visualEditAttributes,
26
+ }) => {
27
+ void content;
28
+ return (
29
+ <div {...visualEditAttributes}>
30
+ <PostsGridBlock
31
+ block={block}
32
+ languageId={languageId}
33
+ />
34
+ </div>
35
+ );
36
+ };
37
37
 
38
38
  export default PostsGridBlockRenderer;
@@ -1,57 +1,57 @@
1
- import React from "react";
2
- import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
3
- import { Card, CardContent } from "@nextblock-cms/ui";
4
- import { MessageSquareQuote } from "lucide-react";
5
- import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
6
-
7
- type TestimonialBlockContent = {
8
- quote?: string;
9
- author_name?: string;
10
- author_title?: string;
11
- image_url?: string;
12
- };
13
-
14
- interface TestimonialBlockRendererProps {
15
- content: TestimonialBlockContent;
16
- visualEditAttributes?: VisualEditAttributes;
17
- }
18
-
19
- const TestimonialBlockRenderer: React.FC<TestimonialBlockRendererProps> = ({
20
- content,
21
- visualEditAttributes,
22
- }) => {
23
- const authorName = content.author_name || "Customer";
24
-
25
- return (
26
- <div className="container m-8" {...visualEditAttributes}>
27
- <Card className="h-full">
28
- <CardContent className="pt-6 flex flex-col gap-4 h-full">
29
- <MessageSquareQuote className="w-8 h-8 text-primary/40" />
30
- {content.quote && (
31
- <blockquote className="flex-grow text-lg italic text-muted-foreground">
32
- &quot;{content.quote}&quot;
33
- </blockquote>
34
- )}
35
- <div className="flex items-center gap-3 mt-4">
36
- <Avatar>
37
- {content.image_url && (
38
- <AvatarImage src={content.image_url} alt={authorName} />
39
- )}
40
- <AvatarFallback>{authorName.slice(0, 2).toUpperCase()}</AvatarFallback>
41
- </Avatar>
42
- <div>
43
- <div className="font-semibold">{authorName}</div>
44
- {content.author_title && (
45
- <div className="text-sm text-muted-foreground">
46
- {content.author_title}
47
- </div>
48
- )}
49
- </div>
50
- </div>
51
- </CardContent>
52
- </Card>
53
- </div>
54
- );
55
- };
56
-
57
- export default TestimonialBlockRenderer;
1
+ import React from "react";
2
+ import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
3
+ import { Card, CardContent } from "@nextblock-cms/ui";
4
+ import { MessageSquareQuote } from "lucide-react";
5
+ import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
6
+
7
+ type TestimonialBlockContent = {
8
+ quote?: string;
9
+ author_name?: string;
10
+ author_title?: string;
11
+ image_url?: string;
12
+ };
13
+
14
+ interface TestimonialBlockRendererProps {
15
+ content: TestimonialBlockContent;
16
+ visualEditAttributes?: VisualEditAttributes;
17
+ }
18
+
19
+ const TestimonialBlockRenderer: React.FC<TestimonialBlockRendererProps> = ({
20
+ content,
21
+ visualEditAttributes,
22
+ }) => {
23
+ const authorName = content.author_name || "Customer";
24
+
25
+ return (
26
+ <div className="container m-8" {...visualEditAttributes}>
27
+ <Card className="h-full">
28
+ <CardContent className="pt-6 flex flex-col gap-4 h-full">
29
+ <MessageSquareQuote className="w-8 h-8 text-primary/40" />
30
+ {content.quote && (
31
+ <blockquote className="flex-grow text-lg italic text-muted-foreground">
32
+ &quot;{content.quote}&quot;
33
+ </blockquote>
34
+ )}
35
+ <div className="flex items-center gap-3 mt-4">
36
+ <Avatar>
37
+ {content.image_url && (
38
+ <AvatarImage src={content.image_url} alt={authorName} />
39
+ )}
40
+ <AvatarFallback>{authorName.slice(0, 2).toUpperCase()}</AvatarFallback>
41
+ </Avatar>
42
+ <div>
43
+ <div className="font-semibold">{authorName}</div>
44
+ {content.author_title && (
45
+ <div className="text-sm text-muted-foreground">
46
+ {content.author_title}
47
+ </div>
48
+ )}
49
+ </div>
50
+ </div>
51
+ </CardContent>
52
+ </Card>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export default TestimonialBlockRenderer;
@@ -1,4 +1,4 @@
1
- interface AlertWidgetRendererProps {
1
+ interface AlertWidgetRendererProps {
2
2
  type: 'info' | 'warning' | 'notification' | 'danger';
3
3
  title: string;
4
4
  message: string;
@@ -46,4 +46,4 @@ const AlertWidgetRenderer = ({ type, title, message, align, size, textAlign }: A
46
46
  );
47
47
  };
48
48
 
49
- export default AlertWidgetRenderer;
49
+ export default AlertWidgetRenderer;
@@ -1,4 +1,4 @@
1
- import Link from 'next/link';
1
+ import Link from 'next/link';
2
2
 
3
3
  interface CtaWidgetRendererProps {
4
4
  text: string;
@@ -35,4 +35,4 @@ const CtaWidgetRenderer = ({ text, url, style, size, textAlign }: CtaWidgetRende
35
35
  );
36
36
  };
37
37
 
38
- export default CtaWidgetRenderer;
38
+ export default CtaWidgetRenderer;