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.
- package/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- 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
|
+
}
|