create-nextblock 0.0.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 (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { signInAction } from "../../actions";
4
+ import { FormMessage, Message } from "../../../components/form-message";
5
+ import { SubmitButton } from "../../../components/submit-button";
6
+ import { Input } from "@nextblock-cms/ui";
7
+ import { Label } from "@nextblock-cms/ui";
8
+ import Link from "next/link";
9
+ import { useTranslations } from "@nextblock-cms/utils";
10
+ import { useSearchParams } from "next/navigation";
11
+
12
+ function getMessage(searchParams: URLSearchParams): Message | undefined {
13
+ if (searchParams.has('error')) {
14
+ const error = searchParams.get('error');
15
+ if (error) return { error };
16
+ }
17
+ if (searchParams.has('success')) {
18
+ const success = searchParams.get('success');
19
+ if (success) return { success };
20
+ }
21
+ if (searchParams.has('message')) {
22
+ const message = searchParams.get('message');
23
+ if (message) return { message };
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export default function Login() {
29
+ const { t } = useTranslations();
30
+ const searchParams = useSearchParams();
31
+ const formMessage = getMessage(searchParams);
32
+
33
+ return (
34
+ <form className="flex-1 flex flex-col min-w-64">
35
+ <h1 className="text-2xl font-medium">{t('sign_in')}</h1>
36
+ <p className="text-sm text-foreground">
37
+ {t('dont_have_account')}{" "}
38
+ <Link className="text-foreground font-medium underline" href="/sign-up">
39
+ {t('sign_up')}
40
+ </Link>
41
+ </p>
42
+ <div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
43
+ <Label htmlFor="email">{t('email')}</Label>
44
+ <Input name="email" placeholder={t('you_at_example_com')} required />
45
+ <div className="flex justify-between items-center">
46
+ <Label htmlFor="password">{t('password')}</Label>
47
+ <Link
48
+ className="text-xs text-foreground underline"
49
+ href="/forgot-password"
50
+ >
51
+ {t('forgot_password')}
52
+ </Link>
53
+ </div>
54
+ <Input
55
+ type="password"
56
+ name="password"
57
+ placeholder={t('your_password')}
58
+ required
59
+ />
60
+ <SubmitButton pendingText={t('signing_in_pending')} formAction={signInAction}>
61
+ {t('sign_in')}
62
+ </SubmitButton>
63
+ <FormMessage message={formMessage} />
64
+ </div>
65
+ </form>
66
+ );
67
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { signUpAction } from "../../actions";
4
+ import { FormMessage, Message } from "../../../components/form-message";
5
+ import { SubmitButton } from "../../../components/submit-button";
6
+ import { Input } from "@nextblock-cms/ui";
7
+ import { Label } from "@nextblock-cms/ui";
8
+ import Link from "next/link";
9
+ import { useTranslations } from "@nextblock-cms/utils";
10
+ import { useSearchParams } from "next/navigation";
11
+
12
+ function getMessage(searchParams: URLSearchParams): Message | undefined {
13
+ if (searchParams.has('error')) {
14
+ const error = searchParams.get('error');
15
+ if (error) return { error };
16
+ }
17
+ if (searchParams.has('success')) {
18
+ const success = searchParams.get('success');
19
+ if (success) return { success };
20
+ }
21
+ if (searchParams.has('message')) {
22
+ const message = searchParams.get('message');
23
+ if (message) return { message };
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export default function Signup() {
29
+ const { t } = useTranslations();
30
+ const searchParams = useSearchParams();
31
+ const formMessage = getMessage(searchParams);
32
+
33
+ if (formMessage && 'message' in formMessage) {
34
+ return (
35
+ <div className="w-full flex-1 flex items-center h-screen sm:max-w-md justify-center gap-2 p-4">
36
+ <FormMessage message={formMessage} />
37
+ </div>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <>
43
+ <form className="flex flex-col min-w-64 max-w-64 mx-auto">
44
+ <h1 className="text-2xl font-medium">{t('sign_up')}</h1>
45
+ <p className="text-sm text text-foreground">
46
+ {t('already_have_account')}{" "}
47
+ <Link className="text-primary font-medium underline" href="/sign-in">
48
+ {t('sign_in')}
49
+ </Link>
50
+ </p>
51
+ <div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
52
+ <Label htmlFor="email">{t('email')}</Label>
53
+ <Input name="email" placeholder={t('you_at_example_com')} required />
54
+ <Label htmlFor="password">{t('password')}</Label>
55
+ <Input
56
+ type="password"
57
+ name="password"
58
+ placeholder={t('your_password')}
59
+ minLength={6}
60
+ required
61
+ />
62
+ <SubmitButton formAction={signUpAction} pendingText={t('signing_up_pending')}>
63
+ {t('sign_up')}
64
+ </SubmitButton>
65
+ <FormMessage message={formMessage} />
66
+ </div>
67
+ </form>
68
+ </>
69
+ );
70
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { Toaster } from "react-hot-toast";
4
+
5
+ export function ToasterProvider() {
6
+ return (
7
+ <Toaster
8
+ position="top-right"
9
+ toastOptions={{
10
+ style: { fontSize: 14 },
11
+ success: { iconTheme: { primary: '#16a34a', secondary: 'white' } },
12
+ error: { iconTheme: { primary: '#dc2626', secondary: 'white' } },
13
+ }}
14
+ />
15
+ );
16
+ }
17
+
@@ -0,0 +1,147 @@
1
+ // app/[slug]/PageClientContent.tsx
2
+ "use client";
3
+
4
+ import React, { useState, useEffect, useMemo } from 'react';
5
+ import { useRouter } from 'next/navigation'; // For navigation on lang switch
6
+ import type { Database } from "@nextblock-cms/db";
7
+ import { useLanguage } from '@/context/LanguageContext';
8
+ import { useCurrentContent } from '@/context/CurrentContentContext';
9
+ import Link from 'next/link';
10
+
11
+ type PageType = Database['public']['Tables']['pages']['Row'];
12
+ type BlockType = Database['public']['Tables']['blocks']['Row'];
13
+
14
+ interface PageClientContentProps {
15
+ initialPageData: (PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; }) | null;
16
+ currentSlug: string; // The slug of the currently viewed page
17
+ children: React.ReactNode;
18
+ translatedSlugs?: { [key: string]: string };
19
+ }
20
+
21
+ // Fetches the slug for a given translation_group_id and target language_code
22
+ // This function is no longer needed here as slugs are pre-fetched.
23
+ // async function getSlugForTranslatedPage(
24
+ // translationGroupId: string,
25
+ // targetLanguageCode: string,
26
+ // supabase: ReturnType<typeof createClient>
27
+ // ): Promise<string | null> {
28
+ // const { data: langInfo, error: langErr } = await supabase
29
+ // .from("languages").select("id").eq("code", targetLanguageCode).single();
30
+ // if (langErr || !langInfo) return null;
31
+
32
+ // const { data: page, error: pageErr } = await supabase
33
+ // .from("pages")
34
+ // .select("slug")
35
+ // .eq("translation_group_id", translationGroupId)
36
+ // .eq("language_id", langInfo.id)
37
+ // .eq("status", "published")
38
+ // .single();
39
+
40
+ // if (pageErr || !page) return null;
41
+ // return page.slug;
42
+ // }
43
+
44
+
45
+ export default function PageClientContent({ initialPageData, currentSlug, children, translatedSlugs }: PageClientContentProps) {
46
+ const { currentLocale, isLoadingLanguages } = useLanguage();
47
+ const { currentContent, setCurrentContent } = useCurrentContent();
48
+ const router = useRouter();
49
+ // currentPageData is the data for the slug currently in the URL.
50
+ // It's initially set by the server for the slug it resolved.
51
+ const [currentPageData] = useState(initialPageData);
52
+ const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false);
53
+
54
+ // Memoize pageId and pageSlug
55
+ const pageId = useMemo(() => currentPageData?.id, [currentPageData?.id]);
56
+ const pageSlug = useMemo(() => currentPageData?.slug, [currentPageData?.slug]);
57
+
58
+ useEffect(() => {
59
+ if (currentLocale && currentPageData && currentPageData.language_code !== currentLocale && translatedSlugs) {
60
+ // Current page's language doesn't match context, try to navigate to translated version
61
+ setIsLoadingTargetLang(true);
62
+ const targetSlug = translatedSlugs[currentLocale];
63
+
64
+ if (targetSlug && targetSlug !== currentSlug) {
65
+ router.push(`/${targetSlug}`); // Navigate to the translated slug's URL
66
+ } else if (targetSlug && targetSlug === currentSlug) {
67
+ // Already on the correct page for the selected language, do nothing or refresh data if needed
68
+ } else {
69
+ console.warn(`No published translation found for group ${currentPageData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
70
+ // Optionally, provide feedback to the user that translation is not available
71
+ }
72
+ setIsLoadingTargetLang(false);
73
+ }
74
+ }, [currentLocale, currentPageData, currentSlug, router, initialPageData, translatedSlugs]); // Rerun if initialPageData changes (e.g. after revalidation)
75
+
76
+ // Update HTML lang attribute based on the *actually displayed* content's language
77
+ useEffect(() => {
78
+ if (currentPageData?.language_code) {
79
+ document.documentElement.lang = currentPageData.language_code;
80
+ if (currentPageData.meta_title || currentPageData.title) {
81
+ document.title = currentPageData.meta_title || currentPageData.title;
82
+ }
83
+ }
84
+ }, [currentPageData]);
85
+
86
+ // Effect for setting or updating the context
87
+ useEffect(() => {
88
+ const newType = 'page' as const;
89
+ const slugToSet = pageSlug ?? null; // Ensures slug is string or null
90
+
91
+ const needsUpdate = pageId &&
92
+ (currentContent.id !== pageId ||
93
+ currentContent.type !== newType ||
94
+ currentContent.slug !== slugToSet);
95
+
96
+ const needsClearing = !pageId &&
97
+ (currentContent.id !== null ||
98
+ currentContent.type !== null ||
99
+ currentContent.slug !== null);
100
+
101
+ if (needsUpdate) {
102
+ setCurrentContent({ id: pageId, type: newType, slug: slugToSet });
103
+ } else if (needsClearing) {
104
+ setCurrentContent({ id: null, type: null, slug: null });
105
+ }
106
+ }, [pageId, pageSlug, setCurrentContent, currentContent.id, currentContent.type, currentContent.slug]);
107
+
108
+ // Separate useEffect for cleanup
109
+ useEffect(() => {
110
+ const idToClean = pageId; // Capture the pageId when this effect runs
111
+
112
+ return () => {
113
+ // Cleanup logic: only clear context if the current context ID matches the ID this instance was managing
114
+ if (idToClean && currentContent.id === idToClean) {
115
+ setCurrentContent({ id: null, type: null, slug: null });
116
+ }
117
+ };
118
+ }, [pageId, setCurrentContent, currentContent.id]);
119
+
120
+ if (!currentPageData && !isLoadingLanguages && !isLoadingTargetLang) { // If initial data was null and no target lang found
121
+ return (
122
+ <div className="container mx-auto px-4 py-8 text-center">
123
+ <h1 className="text-2xl font-bold mb-4">Page Not Found</h1>
124
+ <p className="text-muted-foreground">The page for slug &quot;{currentSlug}&quot; could not be loaded or is not available in any language.</p>
125
+ <p className="mt-4"><Link href="/" className="text-primary hover:underline">Go to Homepage</Link></p>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ if (!currentPageData && (isLoadingLanguages || isLoadingTargetLang)) {
131
+ return <div className="container mx-auto px-4 py-20 text-center"><p>Loading page content...</p></div>;
132
+ }
133
+
134
+ if (!currentPageData) { // Fallback if still no data after loading attempts
135
+ return <div className="container mx-auto px-4 py-20 text-center"><p>Could not load page content.</p></div>;
136
+ }
137
+
138
+
139
+ return (
140
+ <article className="w-full mx-auto">
141
+ {isLoadingTargetLang && <div className="text-center py-2 text-sm text-muted-foreground">Switching language...</div>}
142
+
143
+ {/* Render blocks passed as children */}
144
+ {children}
145
+ </article>
146
+ );
147
+ }
@@ -0,0 +1,145 @@
1
+ // app/[slug]/page.tsx
2
+ import React from 'react';
3
+ import { getSsgSupabaseClient } from "@nextblock-cms/db/server";
4
+ import { notFound } from "next/navigation";
5
+ import type { Metadata } from 'next';
6
+ import PageClientContent from "./PageClientContent";
7
+ import { getPageDataBySlug } from "./page.utils";
8
+ import BlockRenderer from "../../components/BlockRenderer";
9
+ import type { HeroBlockContent } from '../../lib/blocks/blockRegistry';
10
+
11
+ export const dynamicParams = true;
12
+ export const revalidate = 3600;
13
+
14
+ interface ResolvedPageParams {
15
+ slug: string;
16
+ }
17
+
18
+ interface PageProps {
19
+ params: Promise<ResolvedPageParams>;
20
+ }
21
+
22
+ interface PageTranslation {
23
+ slug: string;
24
+ languages: {
25
+ code: string;
26
+ }[];
27
+ }
28
+
29
+ export async function generateStaticParams(): Promise<ResolvedPageParams[]> {
30
+ const supabase = getSsgSupabaseClient();
31
+ const { data: pages, error } = await supabase
32
+ .from("pages")
33
+ .select("slug")
34
+ .eq("status", "published");
35
+
36
+ if (error || !pages) {
37
+ console.error("SSG: Error fetching page slugs for static params:", error);
38
+ return [];
39
+ }
40
+ return pages.map((page) => ({ slug: page.slug }));
41
+ }
42
+
43
+ export async function generateMetadata(
44
+ { params: paramsPromise }: PageProps,
45
+ ): Promise<Metadata> {
46
+ const params = await paramsPromise;
47
+ const pageData = await getPageDataBySlug(params.slug);
48
+
49
+ if (!pageData) {
50
+ return { title: "Page Not Found" };
51
+ }
52
+
53
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
54
+ const supabase = getSsgSupabaseClient();
55
+
56
+ // Parallel queries for better performance
57
+ const [languagesResult, pageTranslationsResult] = await Promise.all([
58
+ supabase.from('languages').select('id, code'),
59
+ supabase
60
+ .from('pages')
61
+ .select('language_id, slug')
62
+ .eq('translation_group_id', pageData.translation_group_id)
63
+ .eq('status', 'published')
64
+ ]);
65
+
66
+ const { data: languages } = languagesResult;
67
+ const { data: pageTranslations } = pageTranslationsResult;
68
+
69
+ const alternates: { [key: string]: string } = {};
70
+ if (languages && pageTranslations) {
71
+ pageTranslations.forEach(pt => {
72
+ const langInfo = languages.find(l => l.id === pt.language_id);
73
+ if (langInfo) {
74
+ alternates[langInfo.code] = `${siteUrl}/${pt.slug}`;
75
+ }
76
+ });
77
+ }
78
+
79
+ return {
80
+ title: pageData.meta_title || pageData.title,
81
+ description: pageData.meta_description || "",
82
+ alternates: {
83
+ canonical: `${siteUrl}/${params.slug}`,
84
+ languages: Object.keys(alternates).length > 0 ? alternates : undefined,
85
+ },
86
+ };
87
+ }
88
+
89
+ export default async function DynamicPage({ params: paramsPromise }: PageProps) {
90
+ const params = await paramsPromise;
91
+ const pageData = await getPageDataBySlug(params.slug);
92
+
93
+ if (!pageData) {
94
+ notFound();
95
+ }
96
+
97
+ const translatedSlugs: { [key: string]: string } = {};
98
+ if (pageData.translation_group_id) {
99
+ const supabase = getSsgSupabaseClient();
100
+ const { data: translations } = await supabase
101
+ .from("pages")
102
+ .select("slug, languages!inner(code)")
103
+ .eq("translation_group_id", pageData.translation_group_id)
104
+ .eq("status", "published");
105
+
106
+ if (translations) {
107
+ translations.forEach((translation: PageTranslation) => {
108
+ if (translation.languages && translation.languages.length > 0 && translation.slug) {
109
+ translatedSlugs[translation.languages[0].code] = translation.slug;
110
+ }
111
+ });
112
+ }
113
+ }
114
+
115
+ let lcpImageUrl: string | null = null;
116
+ const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
117
+
118
+ if (pageData && pageData.blocks && r2BaseUrl) {
119
+ const heroBlock = pageData.blocks.find(block => block.block_type === 'hero');
120
+ if (heroBlock) {
121
+ const heroContent = heroBlock.content as unknown as HeroBlockContent;
122
+ if (
123
+ heroContent.background &&
124
+ heroContent.background.type === "image" &&
125
+ heroContent.background.image &&
126
+ heroContent.background.image.object_key
127
+ ) {
128
+ lcpImageUrl = `${r2BaseUrl}/${heroContent.background.image.object_key}`;
129
+ }
130
+ }
131
+ }
132
+
133
+ const pageBlocks = pageData ? <BlockRenderer blocks={pageData.blocks} languageId={pageData.language_id} /> : null;
134
+
135
+ return (
136
+ <>
137
+ {lcpImageUrl && (
138
+ <link rel="preload" as="image" href={lcpImageUrl} />
139
+ )}
140
+ <PageClientContent initialPageData={pageData} currentSlug={params.slug} translatedSlugs={translatedSlugs}>
141
+ {pageBlocks}
142
+ </PageClientContent>
143
+ </>
144
+ );
145
+ }
@@ -0,0 +1,183 @@
1
+ // app/[slug]/page.utils.ts
2
+ import { getSsgSupabaseClient } from "@nextblock-cms/db/server";
3
+ import type { Database } from "@nextblock-cms/db";
4
+
5
+ type PageType = Database['public']['Tables']['pages']['Row'];
6
+ type BlockType = Database['public']['Tables']['blocks']['Row'];
7
+
8
+ // Define a more specific type for the content of an Image Block
9
+ export type ImageBlockContent = {
10
+ media_id: string | null;
11
+ object_key?: string; // Optional because it's added later
12
+ blur_data_url?: string | null; // Optional because it's added later
13
+ };
14
+ interface SectionOrHeroBlockContent {
15
+ [key: string]: unknown;
16
+ background?: {
17
+ type?: 'image' | 'color';
18
+ image?: {
19
+ media_id?: string;
20
+ object_key?: string;
21
+ blur_data_url?: string;
22
+ };
23
+ };
24
+ }
25
+
26
+ // Interface to represent a page object after the initial database query and selection
27
+ interface SelectedPageType extends PageType { // Assumes PageType includes fields like id, slug, status, language_id, translation_group_id
28
+ language_details: { id: number; code: string } | null; // From the join; kept nullable due to original code's caution
29
+ blocks: BlockType[];
30
+ }
31
+
32
+ export async function getPageDataBySlug(slug: string): Promise<(PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string | null; }) | null> {
33
+ const supabase = getSsgSupabaseClient();
34
+
35
+ // Optimized query with specific field selection instead of *
36
+ const { data: candidatePagesData, error: pageError } = await supabase
37
+ .from("pages")
38
+ .select(`
39
+ id, slug, title, meta_title, meta_description, status, language_id, translation_group_id, author_id, created_at, updated_at,
40
+ language_details:languages!inner(id, code),
41
+ blocks (id, page_id, block_type, content, order)
42
+ `)
43
+ .eq("slug", slug)
44
+ .eq("status", "published")
45
+ .order('order', { foreignTable: 'blocks', ascending: true });
46
+
47
+ if (pageError) {
48
+ return null;
49
+ }
50
+
51
+ const candidatePages: SelectedPageType[] = (candidatePagesData || []).map(page => ({
52
+ ...page,
53
+ language_details: Array.isArray(page.language_details) ? page.language_details[0] : page.language_details
54
+ })) as SelectedPageType[];
55
+
56
+ if (candidatePages.length === 0) {
57
+ return null;
58
+ }
59
+
60
+ let selectedPage: SelectedPageType | null = null;
61
+
62
+ if (candidatePages.length === 1) {
63
+ selectedPage = candidatePages[0];
64
+ } else {
65
+ const enPage = candidatePages.find(p => p.language_details && p.language_details.code === 'en');
66
+ if (enPage) {
67
+ selectedPage = enPage;
68
+ } else {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ if (!selectedPage) {
74
+ return null;
75
+ }
76
+
77
+ let languageCode: string | undefined = selectedPage.language_details?.code;
78
+ let languageId: number | undefined = selectedPage.language_details?.id;
79
+
80
+ // Optimize fallback language query with specific fields
81
+ if (!languageCode || typeof languageId !== 'number') {
82
+ if (typeof selectedPage.language_id === 'number') {
83
+ const { data: fallbackLang, error: langFetchError } = await supabase
84
+ .from("languages")
85
+ .select("id, code")
86
+ .eq("id", selectedPage.language_id)
87
+ .single();
88
+
89
+ if (langFetchError) {
90
+ return null;
91
+ }
92
+
93
+ if (fallbackLang) {
94
+ languageCode = fallbackLang.code;
95
+ languageId = fallbackLang.id;
96
+ } else {
97
+ return null;
98
+ }
99
+ } else {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ if (typeof languageCode !== 'string' || typeof languageId !== 'number') {
105
+ return null;
106
+ }
107
+
108
+ let blocksWithMediaData: BlockType[] = selectedPage.blocks || [];
109
+ if (blocksWithMediaData.length > 0) {
110
+ const mediaIds = blocksWithMediaData
111
+ .map(block => {
112
+ if (block.block_type === 'image') {
113
+ return (block.content as ImageBlockContent)?.media_id;
114
+ }
115
+ if (block.block_type === 'section' || block.block_type === 'hero') {
116
+ const content = block.content as SectionOrHeroBlockContent;
117
+ if (content.background?.type === 'image' && content.background?.image?.media_id) {
118
+ return content.background.image.media_id;
119
+ }
120
+ }
121
+ return null;
122
+ })
123
+ .filter((id): id is string => id !== null && typeof id === 'string');
124
+
125
+ if (mediaIds.length > 0) {
126
+ // Optimized media query with specific fields only
127
+ const { data: mediaItems, error: mediaError } = await supabase
128
+ .from('media')
129
+ .select('id, object_key, blur_data_url')
130
+ .in('id', mediaIds);
131
+
132
+ if (mediaError) {
133
+ console.error('Error fetching media data:', mediaError);
134
+ } else if (mediaItems) {
135
+ const mediaMap = new Map(mediaItems.map(m => [m.id, { object_key: m.object_key, blur_data_url: m.blur_data_url }]));
136
+ blocksWithMediaData = blocksWithMediaData.map(block => {
137
+ if (block.block_type === 'image') {
138
+ const content = block.content as ImageBlockContent;
139
+ if (content.media_id) {
140
+ const mediaData = mediaMap.get(content.media_id);
141
+ if (mediaData) {
142
+ return { ...block, content: { ...content, object_key: mediaData.object_key, blur_data_url: mediaData.blur_data_url } };
143
+ }
144
+ }
145
+ }
146
+ if (block.block_type === 'section' || block.block_type === 'hero') {
147
+ const content = block.content as SectionOrHeroBlockContent;
148
+ if (content.background?.type === 'image' && content.background?.image?.media_id) {
149
+ const mediaData = mediaMap.get(content.background.image.media_id);
150
+ if (mediaData) {
151
+ const newContent = {
152
+ ...content,
153
+ background: {
154
+ ...content.background,
155
+ image: {
156
+ ...content.background.image,
157
+ object_key: mediaData.object_key,
158
+ blur_data_url: mediaData.blur_data_url,
159
+ },
160
+ },
161
+ };
162
+ return { ...block, content: newContent };
163
+ }
164
+ }
165
+ }
166
+ return block;
167
+ });
168
+ }
169
+ }
170
+ }
171
+
172
+ const { language_details, blocks, ...basePageData } = selectedPage;
173
+ void language_details;
174
+ void blocks;
175
+
176
+ return {
177
+ ...(basePageData as PageType),
178
+ blocks: blocksWithMediaData,
179
+ language_code: languageCode,
180
+ language_id: languageId,
181
+ translation_group_id: selectedPage.translation_group_id,
182
+ };
183
+ }
@@ -0,0 +1,31 @@
1
+ "use server";
2
+
3
+ import { getEmailServerConfig } from '@nextblock-cms/utils/server';
4
+ import nodemailer from 'nodemailer';
5
+
6
+ interface EmailParams {
7
+ to: string;
8
+ subject: string;
9
+ text: string;
10
+ html: string;
11
+ }
12
+
13
+ export async function sendEmail({ to, subject, text, html }: EmailParams) {
14
+ const emailConfig = await getEmailServerConfig();
15
+
16
+ if (!emailConfig) {
17
+ throw new Error("Email server is not configured. Please check environment variables.");
18
+ }
19
+
20
+ const transporter = nodemailer.createTransport(emailConfig);
21
+
22
+ const options = {
23
+ from: emailConfig.from,
24
+ to,
25
+ subject,
26
+ text,
27
+ html,
28
+ };
29
+
30
+ return transporter.sendMail(options);
31
+ }