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,65 @@
1
+ // app/actions/formActions.ts
2
+ "use server";
3
+
4
+ import { sendEmail } from './email';
5
+
6
+ interface FormSubmissionResult {
7
+ success: boolean;
8
+ message: string;
9
+ }
10
+
11
+ export async function handleFormSubmission(
12
+ recipient: string,
13
+ prevState: unknown,
14
+ formData: FormData
15
+ ): Promise<FormSubmissionResult> {
16
+
17
+ const data: Record<string, string | File> = {};
18
+ let submitterEmail = 'a user'; // Default value
19
+
20
+ formData.forEach((value, key) => {
21
+ if (typeof value === 'string' && !key.startsWith('$')) {
22
+ data[key] = value;
23
+ // Attempt to find a field that looks like an email address to use in the subject
24
+ if (key.toLowerCase().includes('email')) {
25
+ submitterEmail = value;
26
+ }
27
+ }
28
+ });
29
+
30
+ // Create a more readable HTML body for the email
31
+ const htmlBody = `
32
+ <h2>New Form Submission</h2>
33
+ <p>You have received a new submission from your website form.</p>
34
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
35
+ <tbody>
36
+ ${Object.entries(data)
37
+ .map(([key, value]) => `
38
+ <tr>
39
+ <td style="padding: 8px;"><strong>${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</strong></td>
40
+ <td style="padding: 8px;">${value}</td>
41
+ </tr>
42
+ `)
43
+ .join('')}
44
+ </tbody>
45
+ </table>
46
+ `;
47
+
48
+ const textBody = `
49
+ New Form Submission:
50
+ ${Object.entries(data).map(([key, value]) => `${key}: ${value}`).join('\n')}
51
+ `;
52
+
53
+ try {
54
+ await sendEmail({
55
+ to: recipient,
56
+ subject: `New Form Submission from ${submitterEmail}`,
57
+ text: textBody,
58
+ html: htmlBody,
59
+ });
60
+ return { success: true, message: "Submission successful!" };
61
+ } catch (error) {
62
+ console.error("Email sending failed:", error);
63
+ return { success: false, message: "Sorry, there was an error sending your message. Please try again later." };
64
+ }
65
+ }
@@ -0,0 +1,130 @@
1
+ 'use server';
2
+
3
+ import { createClient } from '@nextblock-cms/db';
4
+ import { cookies } from 'next/headers';
5
+ import { redirect } from 'next/navigation';
6
+ import { getLanguageByCode } from '@/app/cms/settings/languages/actions';
7
+
8
+ export interface Language {
9
+ id: number;
10
+ name: string;
11
+ code: string;
12
+ is_default: boolean;
13
+ created_at?: string;
14
+ }
15
+
16
+ export async function getAvailableLanguages(): Promise<Language[]> {
17
+ const supabase = createClient();
18
+ const { data, error } = await supabase.from('languages').select('id, code, name, is_default, is_active, created_at, updated_at').order('name');
19
+ if (error) {
20
+ console.error('Error fetching languages:', error);
21
+ return [];
22
+ }
23
+ return data as Language[];
24
+ }
25
+
26
+ export async function getCurrentLocale(defaultLocale = 'en'): Promise<string> {
27
+ const cookieStore = await cookies();
28
+ return cookieStore.get('NEXT_LOCALE')?.value || defaultLocale;
29
+ }
30
+
31
+ export async function setCurrentLocaleCookie(locale: string) {
32
+ const cookieStore = await cookies();
33
+ cookieStore.set('NEXT_LOCALE', locale, { path: '/' });
34
+ }
35
+
36
+ export async function getPageTranslations(translationGroupId: string): Promise<{ slug: string, language_code: string }[]> {
37
+ if (!translationGroupId) {
38
+ console.warn('getPageTranslations called without translationGroupId');
39
+ return [];
40
+ }
41
+ const supabase = createClient();
42
+
43
+ const { data, error } = await supabase
44
+ .from('pages')
45
+ .select('slug, status, languages(code)') // Use actual table name for join
46
+ .eq('translation_group_id', translationGroupId)
47
+ .eq('status', 'published');
48
+
49
+ if (error) {
50
+ console.error('Error fetching page translations:', error);
51
+ return [];
52
+ }
53
+
54
+ interface PageWithLanguage {
55
+ slug: string;
56
+ status: string; // Or your actual status type
57
+ languages: { code: string } | { code: string }[] | null; // Can be object, array of objects, or null
58
+ }
59
+
60
+ // Map the data to the expected format { slug: string, language_code: string }
61
+ const formattedTranslations = data
62
+ ? (data as PageWithLanguage[]).map(page => {
63
+ let langCode = '';
64
+ if (page.languages) {
65
+ if (Array.isArray(page.languages)) {
66
+ langCode = page.languages[0]?.code || '';
67
+ } else { // It's an object
68
+ langCode = page.languages.code || '';
69
+ }
70
+ }
71
+ return {
72
+ slug: page.slug,
73
+ language_code: langCode,
74
+ };
75
+ }).filter(t => t.language_code)
76
+ : [];
77
+
78
+ return formattedTranslations;
79
+ }
80
+
81
+ // Helper to get language details by code, potentially used by LanguageSwitcher or other components
82
+ export async function getLanguageDetails(localeCode: string): Promise<Language | null> {
83
+ const { data, error } = await getLanguageByCode(localeCode);
84
+ if (error || !data) {
85
+ // Optionally log the error or handle it more gracefully
86
+ console.warn(`Could not fetch language details for ${localeCode}: ${error}`);
87
+ return null;
88
+ }
89
+ return data;
90
+ }
91
+
92
+ export async function getPageMetadataBySlugAndLocale(slug: string, localeCode: string): Promise<{ slug: string; translation_group_id: string | null } | null> {
93
+ if (!slug || !localeCode) {
94
+ console.warn('getPageMetadataBySlugAndLocale called without slug or localeCode');
95
+ return null;
96
+ }
97
+ const supabase = createClient();
98
+ const { data: languageData, error: langError } = await getLanguageByCode(localeCode);
99
+
100
+ if (langError || !languageData) {
101
+ console.warn(`Language with code ${localeCode} not found or error fetching: ${langError}`);
102
+ return null;
103
+ }
104
+
105
+ const { data: page, error } = await supabase
106
+ .from('pages')
107
+ .select('slug, translation_group_id')
108
+ .eq('slug', slug)
109
+ .eq('language_id', languageData.id)
110
+ .maybeSingle();
111
+
112
+ if (error) {
113
+ console.error(`Error fetching page metadata for slug ${slug} and locale ${localeCode}:`, error);
114
+ return null;
115
+ }
116
+ if (!page) {
117
+ // It's possible the slug is for a different content type (e.g. blog post) or doesn't exist.
118
+ // For now, we only search 'pages'. This might need to be expanded or handled gracefully.
119
+ console.warn(`No page found for slug ${slug} and locale ${localeCode}`);
120
+ return null;
121
+ }
122
+ return page;
123
+ }
124
+
125
+ export async function changeLanguage(newLocale: string, currentPath: string) {
126
+ await setCurrentLocaleCookie(newLocale);
127
+ // This is a basic redirect, LanguageSwitcher will have more complex logic
128
+ // For finding translated slugs.
129
+ redirect(currentPath); // Or redirect to a translated path if available
130
+ }
@@ -0,0 +1,80 @@
1
+ 'use server';
2
+
3
+ import { cache } from 'react';
4
+ import { createClient } from '@nextblock-cms/db/server';
5
+ import { revalidatePath } from 'next/cache';
6
+ import type { PostWithMediaDimensions } from '../../components/blocks/types';
7
+
8
+ export async function fetchPaginatedPublishedPosts(languageId: number, page: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string }> {
9
+ const supabase = createClient();
10
+ const offset = (page - 1) * limit;
11
+
12
+ const { data: posts, error, count } = await supabase
13
+ .from('posts')
14
+ .select('*, media:media!feature_image_id(*)', { count: 'exact' })
15
+ .eq('status', 'published')
16
+ .eq('language_id', languageId)
17
+ .order('published_at', { ascending: false })
18
+ .range(offset, offset + limit - 1);
19
+
20
+ if (error) {
21
+ console.error("Error fetching paginated posts:", error);
22
+ return { posts: [], totalCount: 0, error: error.message };
23
+ }
24
+
25
+ return { posts: posts as PostWithMediaDimensions[], totalCount: count || 0, error: undefined }; // Return error: undefined on success
26
+ }
27
+
28
+ // You could also move fetchInitialPublishedPosts here if it makes sense for organization
29
+ export const fetchInitialPublishedPosts = cache(async (languageId: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string | null }> => {
30
+ const supabase = createClient(); // This createClient is from utils/supabase/server
31
+ const { data: posts, error, count } = await supabase
32
+ .from('posts')
33
+ .select('*, media:media!feature_image_id(*)', { count: 'exact' })
34
+ .eq('status', 'published')
35
+ .eq('language_id', languageId)
36
+ .order('published_at', { ascending: false })
37
+ .limit(limit);
38
+
39
+ if (error) {
40
+ console.error("Error fetching initial posts:", error);
41
+ return { posts: [], totalCount: 0, error: error.message };
42
+ }
43
+
44
+ return { posts: posts as PostWithMediaDimensions[], totalCount: count || 0, error: null };
45
+ });
46
+ export async function revalidateAndLog(path: string): Promise<{ success: boolean; error?: string }> {
47
+ try {
48
+ // Step 1: Revalidate the path
49
+ revalidatePath(path);
50
+
51
+ // Step 2: Log the revalidation by calling the API route
52
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
53
+ if (!baseUrl) {
54
+ throw new Error('NEXT_PUBLIC_BASE_URL is not set in environment variables.');
55
+ }
56
+
57
+ const logUrl = new URL('/api/revalidate-log', baseUrl);
58
+
59
+ const response = await fetch(logUrl.toString(), {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ },
64
+ body: JSON.stringify({ path }),
65
+ });
66
+
67
+ if (!response.ok) {
68
+ const errorBody = await response.json();
69
+ throw new Error(`Failed to log revalidation: ${response.status} ${response.statusText} - ${errorBody.error}`);
70
+ }
71
+
72
+ console.log(`Successfully revalidated and logged path: ${path}`);
73
+ return { success: true };
74
+
75
+ } catch (error) {
76
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
77
+ console.error(`Error in revalidateAndLog for path "${path}":`, errorMessage);
78
+ return { success: false, error: errorMessage };
79
+ }
80
+ }
@@ -0,0 +1,146 @@
1
+ "use server";
2
+
3
+ import { encodedRedirect } from "@nextblock-cms/utils/server";
4
+ import { createClient } from "@nextblock-cms/db/server";
5
+ import { headers } from "next/headers";
6
+ import { redirect } from "next/navigation";
7
+
8
+ export const signUpAction = async (formData: FormData) => {
9
+ const email = formData.get("email")?.toString();
10
+ const password = formData.get("password")?.toString();
11
+ const supabase = await createClient();
12
+ const origin = (await headers()).get("origin");
13
+
14
+ if (!email || !password) {
15
+ return encodedRedirect(
16
+ "error",
17
+ "/sign-up",
18
+ "Email and password are required",
19
+ );
20
+ }
21
+
22
+ const { error } = await supabase.auth.signUp({
23
+ email,
24
+ password,
25
+ options: {
26
+ emailRedirectTo: `${origin}/auth/callback`,
27
+ },
28
+ });
29
+
30
+ if (error) {
31
+ console.error(error.code + " " + error.message);
32
+ return encodedRedirect("error", "/sign-up", error.message);
33
+ } else {
34
+ return encodedRedirect(
35
+ "success",
36
+ "/sign-up",
37
+ "Thanks for signing up! Please check your email for a verification link.",
38
+ );
39
+ }
40
+ };
41
+
42
+ export const signInAction = async (formData: FormData) => {
43
+ const email = formData.get("email") as string;
44
+ const password = formData.get("password") as string;
45
+ const supabase = await createClient();
46
+
47
+ const { data, error } = await supabase.auth.signInWithPassword({
48
+ email,
49
+ password,
50
+ });
51
+
52
+ if (error) {
53
+ return encodedRedirect("error", "/sign-in", error.message);
54
+ }
55
+
56
+ if (data.user) {
57
+ const { data: profile } = await supabase
58
+ .from('profiles')
59
+ .select('role')
60
+ .eq('id', data.user.id)
61
+ .single();
62
+
63
+ if (profile && (profile.role === 'ADMIN' || profile.role === 'WRITER')) {
64
+ return redirect("/post-sign-in?redirect_to=/cms/dashboard");
65
+ }
66
+ }
67
+
68
+ return redirect("/post-sign-in");
69
+ };
70
+
71
+ export const forgotPasswordAction = async (formData: FormData) => {
72
+ const email = formData.get("email")?.toString();
73
+ const supabase = await createClient();
74
+ const origin = (await headers()).get("origin");
75
+ const callbackUrl = formData.get("callbackUrl")?.toString();
76
+
77
+ if (!email) {
78
+ return encodedRedirect("error", "/forgot-password", "Email is required");
79
+ }
80
+
81
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
82
+ redirectTo: `${origin}/auth/callback?redirect_to=/reset-password`,
83
+ });
84
+
85
+ if (error) {
86
+ console.error(error.message);
87
+ return encodedRedirect(
88
+ "error",
89
+ "/forgot-password",
90
+ "Could not reset password",
91
+ );
92
+ }
93
+
94
+ if (callbackUrl) {
95
+ return redirect(callbackUrl);
96
+ }
97
+
98
+ return encodedRedirect(
99
+ "success",
100
+ "/forgot-password",
101
+ "Check your email for a link to reset your password.",
102
+ );
103
+ };
104
+
105
+ export const resetPasswordAction = async (formData: FormData) => {
106
+ const supabase = await createClient();
107
+
108
+ const password = formData.get("password") as string;
109
+ const confirmPassword = formData.get("confirmPassword") as string;
110
+
111
+ if (!password || !confirmPassword) {
112
+ encodedRedirect(
113
+ "error",
114
+ "/reset-password",
115
+ "Password and confirm password are required",
116
+ );
117
+ }
118
+
119
+ if (password !== confirmPassword) {
120
+ encodedRedirect(
121
+ "error",
122
+ "/reset-password",
123
+ "Passwords do not match",
124
+ );
125
+ }
126
+
127
+ const { error } = await supabase.auth.updateUser({
128
+ password: password,
129
+ });
130
+
131
+ if (error) {
132
+ encodedRedirect(
133
+ "error",
134
+ "/reset-password",
135
+ "Password update failed",
136
+ );
137
+ }
138
+
139
+ encodedRedirect("success", "/reset-password", "Password updated");
140
+ };
141
+
142
+ export const signOutAction = async () => {
143
+ const supabase = await createClient();
144
+ await supabase.auth.signOut();
145
+ return redirect("/");
146
+ };
@@ -0,0 +1,210 @@
1
+ // app/api/process-image/route.ts
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { getS3Client } from '@nextblock-cms/utils/server';
4
+ import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
5
+ import sharp from 'sharp';
6
+ import { Readable } from 'stream';
7
+ import { getPlaiceholder } from 'plaiceholder';
8
+
9
+ // Helper to convert stream to buffer
10
+ async function streamToBuffer(stream: Readable): Promise<Buffer> {
11
+ return new Promise((resolve, reject) => {
12
+ const chunks: Buffer[] = [];
13
+ stream.on('data', (chunk) => chunks.push(chunk as Buffer));
14
+ stream.on('error', reject);
15
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
16
+ });
17
+ }
18
+
19
+ const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
20
+ // Construct the public URL base. Assumes your R2 bucket is set up for public access.
21
+ // Example: https://<your-bucket>.<account-id>.r2.cloudflarestorage.com
22
+ // Or if you use a custom domain: https://your.custom.domain
23
+ const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL;
24
+
25
+ interface ProcessedImageVariant {
26
+ objectKey: string;
27
+ url: string;
28
+ width: number;
29
+ height: number;
30
+ fileType: string; // e.g., 'image/avif'
31
+ sizeBytes: number;
32
+ variantLabel: string; // e.g., 'large_avif', 'medium_avif', 'thumbnail_avif', 'original_avif'
33
+ }
34
+
35
+ // Define target sizes (widths) and the AVIF format
36
+ const TARGET_SIZES = [
37
+ { width: 1920, label: 'xlarge_avif' },
38
+ { width: 1280, label: 'large_avif' },
39
+ { width: 768, label: 'medium_avif' },
40
+ { width: 384, label: 'small_avif' },
41
+ { width: 128, label: 'thumbnail_avif' }, // For very small previews or blur placeholders
42
+ ];
43
+ const TARGET_FORMAT = 'avif';
44
+ const TARGET_MIME_TYPE = 'image/avif';
45
+
46
+ export async function POST(request: NextRequest) {
47
+ if (!R2_BUCKET_NAME) {
48
+ return NextResponse.json({ error: 'R2 bucket name is not configured.' }, { status: 500 });
49
+ }
50
+ if (!process.env.R2_S3_ENDPOINT && !process.env.R2_ACCOUNT_ID) {
51
+ console.error("R2_S3_ENDPOINT or R2_ACCOUNT_ID must be set to construct R2_PUBLIC_URL_BASE");
52
+ return NextResponse.json({ error: 'Server configuration error for R2 public URL.' }, { status: 500 });
53
+ }
54
+
55
+
56
+ try {
57
+ const s3Client = await getS3Client();
58
+ if (!s3Client) {
59
+ console.error('R2 client is not configured. Check your R2 environment variables.');
60
+ return NextResponse.json({ error: 'Image processing is not configured on this server.' }, { status: 500 });
61
+ }
62
+
63
+ const { objectKey: originalObjectKey, contentType: originalContentType } = await request.json();
64
+
65
+ if (!originalObjectKey || !originalContentType) {
66
+ return NextResponse.json({ error: 'Missing objectKey or contentType in request body.' }, { status: 400 });
67
+ }
68
+
69
+ if (!originalContentType.startsWith('image/')) {
70
+ // For now, we only process images. Could be extended for other file types if needed.
71
+ return NextResponse.json({
72
+ message: 'File is not an image. Skipping processing.',
73
+ originalImage: { objectKey: originalObjectKey, fileType: originalContentType, url: `${R2_PUBLIC_URL_BASE}/${originalObjectKey}` },
74
+ processedVariants: [],
75
+ blurDataURL: null // Or an empty string, depending on how you want to handle non-images
76
+ }, { status: 200 });
77
+ }
78
+
79
+ // 1. Fetch the original image from R2
80
+ const getObjectParams = {
81
+ Bucket: R2_BUCKET_NAME,
82
+ Key: originalObjectKey,
83
+ };
84
+ const getObjectCommand = new GetObjectCommand(getObjectParams);
85
+ const getObjectResponse = await s3Client.send(getObjectCommand);
86
+
87
+ if (!getObjectResponse.Body) {
88
+ throw new Error('Failed to retrieve image from R2: Empty body.');
89
+ }
90
+
91
+ let imageBuffer = await streamToBuffer(getObjectResponse.Body as Readable);
92
+ let sharpInstance = sharp(imageBuffer);
93
+ let originalMetadata = await sharpInstance.metadata();
94
+
95
+ const MAX_WIDTH = 2560; // Define max width
96
+
97
+ // Check if the image width is greater than MAX_WIDTH
98
+ if (originalMetadata.width && originalMetadata.width > MAX_WIDTH) {
99
+ // Resize the image
100
+ const resizedBuffer = await sharpInstance
101
+ .resize({
102
+ width: MAX_WIDTH,
103
+ // height is scaled automatically to maintain aspect ratio
104
+ })
105
+ .toBuffer();
106
+
107
+ // Update buffer and sharp instance for all subsequent operations
108
+ imageBuffer = resizedBuffer;
109
+ sharpInstance = sharp(imageBuffer);
110
+ // Update metadata as well
111
+ originalMetadata = await sharpInstance.metadata();
112
+ }
113
+
114
+ const processedVariants: ProcessedImageVariant[] = [];
115
+ const baseName = originalObjectKey.substring(0, originalObjectKey.lastIndexOf('.'));
116
+ // const originalExtension = originalObjectKey.substring(originalObjectKey.lastIndexOf('.') + 1);
117
+
118
+ // 2. Process and upload variants (resized AVIF)
119
+ for (const size of TARGET_SIZES) {
120
+ if (!originalMetadata.width) continue; // Skip if original width is unknown
121
+
122
+ const targetWidth = Math.min(size.width, originalMetadata.width); // Don't upscale beyond original
123
+
124
+ const processedImageBuffer = await sharpInstance
125
+ .clone() // Important: clone before each new operation
126
+ .resize({ width: targetWidth, withoutEnlargement: true })
127
+ .toFormat(TARGET_FORMAT, { quality: 75 }) // Adjust quality as needed
128
+ .toBuffer();
129
+
130
+ const newObjectKey = `${baseName}_${size.label}.${TARGET_FORMAT}`;
131
+ const newPublicUrl = `${R2_PUBLIC_URL_BASE}/${newObjectKey}`;
132
+
133
+ const putObjectParams = {
134
+ Bucket: R2_BUCKET_NAME,
135
+ Key: newObjectKey,
136
+ Body: processedImageBuffer,
137
+ ContentType: TARGET_MIME_TYPE,
138
+ };
139
+ await s3Client.send(new PutObjectCommand(putObjectParams));
140
+
141
+ const newMetadata = await sharp(processedImageBuffer).metadata();
142
+ processedVariants.push({
143
+ objectKey: newObjectKey,
144
+ url: newPublicUrl,
145
+ width: newMetadata.width || targetWidth,
146
+ height: newMetadata.height || 0, // Sharp should provide this
147
+ fileType: TARGET_MIME_TYPE,
148
+ sizeBytes: processedImageBuffer.length,
149
+ variantLabel: size.label,
150
+ });
151
+ }
152
+
153
+ // 3. Optionally, convert the original image to AVIF if it's not already (and keep original size)
154
+ // This gives an AVIF version of the original uploaded image.
155
+ if (originalContentType !== TARGET_MIME_TYPE) {
156
+ const originalAvifBuffer = await sharp(imageBuffer)
157
+ .clone()
158
+ .toFormat(TARGET_FORMAT, { quality: 80 }) // Potentially higher quality for "original" AVIF
159
+ .toBuffer();
160
+
161
+ const originalAvifObjectKey = `${baseName}_original.${TARGET_FORMAT}`;
162
+ const originalAvifPublicUrl = `${R2_PUBLIC_URL_BASE}/${originalAvifObjectKey}`;
163
+
164
+ await s3Client.send(new PutObjectCommand({
165
+ Bucket: R2_BUCKET_NAME,
166
+ Key: originalAvifObjectKey,
167
+ Body: originalAvifBuffer,
168
+ ContentType: TARGET_MIME_TYPE,
169
+ }));
170
+ const originalAvifMetadata = await sharp(originalAvifBuffer).metadata();
171
+ processedVariants.push({
172
+ objectKey: originalAvifObjectKey,
173
+ url: originalAvifPublicUrl,
174
+ width: originalAvifMetadata.width || originalMetadata.width || 0,
175
+ height: originalAvifMetadata.height || originalMetadata.height || 0,
176
+ fileType: TARGET_MIME_TYPE,
177
+ sizeBytes: originalAvifBuffer.length,
178
+ variantLabel: 'original_avif',
179
+ });
180
+ }
181
+
182
+
183
+ // Include original image details (even if not AVIF) for reference in the database
184
+ // The client already has some of this, but good to have a consistent structure.
185
+ const originalImageDetails: ProcessedImageVariant = {
186
+ objectKey: originalObjectKey,
187
+ url: `${R2_PUBLIC_URL_BASE}/${originalObjectKey}`,
188
+ width: originalMetadata.width || 0,
189
+ height: originalMetadata.height || 0,
190
+ fileType: originalContentType,
191
+ sizeBytes: imageBuffer.length,
192
+ variantLabel: 'original_uploaded',
193
+ };
194
+
195
+ // Generate blurDataURL
196
+ const { base64: blurDataURL } = await getPlaiceholder(imageBuffer, { size: 10 });
197
+
198
+ return NextResponse.json({
199
+ message: 'Image processed successfully.',
200
+ originalImage: originalImageDetails,
201
+ processedVariants,
202
+ blurDataURL
203
+ }, { status: 200 });
204
+
205
+ } catch (error: unknown) {
206
+ console.error('Error processing image:', error);
207
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
208
+ return NextResponse.json({ error: 'Failed to process image.', details: errorMessage }, { status: 500 });
209
+ }
210
+ }