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,86 @@
|
|
|
1
|
+
// app/api/revalidate/route.ts
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { revalidatePath } from 'next/cache';
|
|
4
|
+
|
|
5
|
+
const REVALIDATE_SECRET = process.env.REVALIDATE_SECRET_TOKEN;
|
|
6
|
+
|
|
7
|
+
// Define the expected structure of the Supabase webhook payload
|
|
8
|
+
interface SupabaseWebhookPayload {
|
|
9
|
+
type: 'INSERT' | 'UPDATE' | 'DELETE';
|
|
10
|
+
table: string;
|
|
11
|
+
schema: string;
|
|
12
|
+
record?: { slug?: string; [key: string]: unknown }; // Record for INSERT/UPDATE
|
|
13
|
+
old_record?: { slug?: string; [key: string]: unknown }; // Old record for DELETE
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function POST(request: NextRequest) {
|
|
17
|
+
const secret = request.headers.get('x-revalidate-secret');
|
|
18
|
+
|
|
19
|
+
if (secret !== REVALIDATE_SECRET) {
|
|
20
|
+
console.warn("Revalidation attempt with invalid secret token.");
|
|
21
|
+
return NextResponse.json({ message: 'Invalid secret token' }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let payload: SupabaseWebhookPayload;
|
|
25
|
+
try {
|
|
26
|
+
payload = await request.json();
|
|
27
|
+
console.log("Received Supabase webhook payload for revalidation:", JSON.stringify(payload, null, 2));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error("Failed to parse revalidation request JSON:", e);
|
|
30
|
+
return NextResponse.json({ message: 'Bad Request: Could not parse JSON payload.' }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { type, table, record, old_record } = payload;
|
|
34
|
+
const relevantRecord = type === 'DELETE' ? old_record : record;
|
|
35
|
+
|
|
36
|
+
if (!relevantRecord || typeof relevantRecord.slug !== 'string') {
|
|
37
|
+
console.warn("Revalidation payload missing relevant record or slug.", { table, type });
|
|
38
|
+
return NextResponse.json({ message: 'Payload missing slug information.' }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let pathToRevalidate: string | null = null;
|
|
42
|
+
|
|
43
|
+
if (table === 'pages') {
|
|
44
|
+
pathToRevalidate = `/${relevantRecord.slug}`;
|
|
45
|
+
} else if (table === 'posts') {
|
|
46
|
+
pathToRevalidate = `/blog/${relevantRecord.slug}`;
|
|
47
|
+
} else {
|
|
48
|
+
console.log(`Revalidation not configured for table: ${table}`);
|
|
49
|
+
return NextResponse.json({ message: `Revalidation not configured for table: ${table}` }, { status: 200 }); // Acknowledge but don't process
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (pathToRevalidate) {
|
|
53
|
+
try {
|
|
54
|
+
// Ensure path starts with a slash (it should based on construction above)
|
|
55
|
+
const normalizedPath = pathToRevalidate.startsWith('/') ? pathToRevalidate : `/${pathToRevalidate}`;
|
|
56
|
+
|
|
57
|
+
// Revalidate the specific path.
|
|
58
|
+
// Using 'page' type for revalidation as we are revalidating individual content pages.
|
|
59
|
+
await revalidatePath(normalizedPath, 'page');
|
|
60
|
+
console.log(`Successfully revalidated path: ${normalizedPath}`);
|
|
61
|
+
|
|
62
|
+
// Additionally, if it's a blog post, you might want to revalidate the main blog listing page.
|
|
63
|
+
if (table === 'posts') {
|
|
64
|
+
// Assuming your main blog listing page is at '/blog' or similar.
|
|
65
|
+
// This path needs to be known and consistent.
|
|
66
|
+
// If your blog listing is at the root of the language segment (e.g. /en/blog),
|
|
67
|
+
// and you are NOT using [lang] in URL, then the path is just '/blog'.
|
|
68
|
+
// However, if your LanguageContext means /blog shows different content per lang,
|
|
69
|
+
// revalidating just '/blog' will rebuild its default language version.
|
|
70
|
+
// Client-side fetches would still get latest for other languages.
|
|
71
|
+
// For now, let's revalidate a generic /blog path if it exists.
|
|
72
|
+
// await revalidatePath('/blog', 'page'); // Example: revalidate main blog listing
|
|
73
|
+
// console.log("Also attempted to revalidate /blog listing page.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({ revalidated: true, revalidatedPath: normalizedPath, now: Date.now() });
|
|
77
|
+
} catch (err: unknown) {
|
|
78
|
+
console.error("Error during revalidation process:", err);
|
|
79
|
+
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
|
80
|
+
return NextResponse.json({ message: `Error revalidating: ${errorMessage}` }, { status: 500 });
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// This case should ideally not be reached if table and slug checks are done.
|
|
84
|
+
return NextResponse.json({ message: 'Could not determine path to revalidate.' }, { status: 400 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
export async function POST(request: NextRequest) {
|
|
4
|
+
try {
|
|
5
|
+
const body = await request.json();
|
|
6
|
+
const path = body.path;
|
|
7
|
+
|
|
8
|
+
if (!path) {
|
|
9
|
+
return NextResponse.json({ error: 'Path is required' }, { status: 400 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log(JSON.stringify({
|
|
13
|
+
type: 'isr_revalidate',
|
|
14
|
+
path: path,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
return NextResponse.json({ success: true, path: path });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
20
|
+
console.error('Revalidation log error:', errorMessage);
|
|
21
|
+
return NextResponse.json({ error: 'Invalid request body', details: errorMessage }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// app/api/upload/presigned-url/route.ts
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server"; // Server client for auth
|
|
4
|
+
import { getS3Client } from "@nextblock-cms/utils/server";
|
|
5
|
+
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
|
6
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
7
|
+
|
|
8
|
+
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
9
|
+
|
|
10
|
+
export async function POST(request: NextRequest) {
|
|
11
|
+
const supabase = createClient();
|
|
12
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
13
|
+
|
|
14
|
+
if (authError || !user) {
|
|
15
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check user role - only WRITER or ADMIN can upload
|
|
19
|
+
const { data: profile } = await supabase
|
|
20
|
+
.from("profiles")
|
|
21
|
+
.select("role")
|
|
22
|
+
.eq("id", user.id)
|
|
23
|
+
.single();
|
|
24
|
+
|
|
25
|
+
if (!profile || !["ADMIN", "WRITER"].includes(profile.role)) {
|
|
26
|
+
return NextResponse.json({ error: "Forbidden: Insufficient permissions" }, { status: 403 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!R2_BUCKET_NAME) {
|
|
30
|
+
console.error("R2_BUCKET_NAME is not set.");
|
|
31
|
+
return NextResponse.json({ error: "Server configuration error for file uploads." }, { status: 500 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const s3Client = await getS3Client();
|
|
36
|
+
if (!s3Client) {
|
|
37
|
+
console.error('R2 client is not configured. Check your R2 environment variables.');
|
|
38
|
+
return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { filename, contentType, size, folder: rawFolder } = await request.json();
|
|
42
|
+
|
|
43
|
+
if (!filename || !contentType || !size) {
|
|
44
|
+
return NextResponse.json({ error: "Missing filename, contentType, or size" }, { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Basic validation (you can enhance this)
|
|
48
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
49
|
+
if (size > MAX_FILE_SIZE) {
|
|
50
|
+
return NextResponse.json({ error: `File size exceeds limit of ${MAX_FILE_SIZE / (1024*1024)}MB.` }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
// Add content type validation if needed
|
|
53
|
+
|
|
54
|
+
const fileExtension = filename.split('.').pop() || '';
|
|
55
|
+
const baseFilename = fileExtension ? filename.substring(0, filename.length - (fileExtension.length + 1)) : filename;
|
|
56
|
+
|
|
57
|
+
// Sanitize baseFilename to remove characters not suitable for URLs/filenames, replace spaces with hyphens
|
|
58
|
+
const sanitizedBaseFilename = baseFilename
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
61
|
+
.replace(/[^\w.-]+/g, ''); // Remove non-alphanumeric characters except hyphen and period
|
|
62
|
+
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const timestamp = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`;
|
|
65
|
+
|
|
66
|
+
// Sanitize and normalize folder path
|
|
67
|
+
const sanitizeFolder = (input?: string | null) => {
|
|
68
|
+
const f = (input ?? '').toString().trim();
|
|
69
|
+
if (!f) return 'uploads/';
|
|
70
|
+
// remove leading slashes, collapse .. and illegal chars
|
|
71
|
+
let cleaned = f.replace(/^\/+/, '');
|
|
72
|
+
cleaned = cleaned.replace(/\\/g, '/');
|
|
73
|
+
cleaned = cleaned.replace(/\.{2,}/g, '');
|
|
74
|
+
cleaned = cleaned.replace(/[^a-zA-Z0-9_\-/]+/g, '-');
|
|
75
|
+
if (cleaned && !cleaned.endsWith('/')) cleaned += '/';
|
|
76
|
+
return cleaned || 'uploads/';
|
|
77
|
+
};
|
|
78
|
+
const folder = sanitizeFolder(rawFolder);
|
|
79
|
+
|
|
80
|
+
const uniqueKey = `${folder}${sanitizedBaseFilename}_${timestamp}${fileExtension ? '.' + fileExtension : ''}`;
|
|
81
|
+
|
|
82
|
+
const command = new PutObjectCommand({
|
|
83
|
+
Bucket: R2_BUCKET_NAME,
|
|
84
|
+
Key: uniqueKey,
|
|
85
|
+
ContentType: contentType,
|
|
86
|
+
// ACL: 'public-read', // R2 objects are private by default unless bucket is public or presigned URL for GET is used
|
|
87
|
+
// For direct PUT, ACL is not typically set this way with R2. Permissions are on bucket/token.
|
|
88
|
+
Metadata: { // Optional: add any metadata
|
|
89
|
+
'uploader-user-id': user.id,
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const expiresIn = 300; // 5 minutes
|
|
94
|
+
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn });
|
|
95
|
+
|
|
96
|
+
return NextResponse.json({
|
|
97
|
+
presignedUrl,
|
|
98
|
+
objectKey: `${uniqueKey}`, // Send back the key prefixed with bucket name
|
|
99
|
+
method: "PUT",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error("Error generating pre-signed URL:", error);
|
|
104
|
+
return NextResponse.json({ error: "Failed to generate upload URL." }, { status: 500 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// apps/nextblock/app/api/upload/proxy/route.ts
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
+
import { getS3Client } from '@nextblock-cms/utils/server';
|
|
5
|
+
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
6
|
+
|
|
7
|
+
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
8
|
+
|
|
9
|
+
export async function POST(request: NextRequest) {
|
|
10
|
+
const supabase = createClient();
|
|
11
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
12
|
+
|
|
13
|
+
if (authError || !user) {
|
|
14
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!R2_BUCKET_NAME) {
|
|
18
|
+
console.error('R2_BUCKET_NAME is not set.');
|
|
19
|
+
return NextResponse.json({ error: 'Server configuration error for file uploads.' }, { status: 500 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const s3Client = await getS3Client();
|
|
24
|
+
if (!s3Client) {
|
|
25
|
+
console.error('R2 client is not configured. Check your R2 environment variables.');
|
|
26
|
+
return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const formData = await request.formData();
|
|
30
|
+
const file = formData.get('file') as File | null;
|
|
31
|
+
const rawFolder = (formData.get('folder') as string | null) ?? undefined;
|
|
32
|
+
|
|
33
|
+
if (!file) {
|
|
34
|
+
return NextResponse.json({ error: 'No file provided.' }, { status: 400 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fileExtension = file.name.split('.').pop() || '';
|
|
38
|
+
const baseFilename = fileExtension ? file.name.substring(0, file.name.length - (fileExtension.length + 1)) : file.name;
|
|
39
|
+
const sanitizedBaseFilename = baseFilename.toLowerCase().replace(/\s+/g, '-').replace(/[^\w.-]+/g, '');
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const timestamp = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`;
|
|
42
|
+
// Sanitize and normalize folder path
|
|
43
|
+
const sanitizeFolder = (input?: string | null) => {
|
|
44
|
+
const f = (input ?? '').toString().trim();
|
|
45
|
+
if (!f) return 'uploads/';
|
|
46
|
+
let cleaned = f.replace(/^\/+/, '');
|
|
47
|
+
cleaned = cleaned.replace(/\\/g, '/');
|
|
48
|
+
cleaned = cleaned.replace(/\.{2,}/g, '');
|
|
49
|
+
cleaned = cleaned.replace(/[^a-zA-Z0-9_\-/]+/g, '-');
|
|
50
|
+
if (cleaned && !cleaned.endsWith('/')) cleaned += '/';
|
|
51
|
+
return cleaned || 'uploads/';
|
|
52
|
+
};
|
|
53
|
+
const folder = sanitizeFolder(rawFolder);
|
|
54
|
+
|
|
55
|
+
const uniqueKey = `${folder}${sanitizedBaseFilename}_${timestamp}${fileExtension ? '.' + fileExtension : ''}`;
|
|
56
|
+
|
|
57
|
+
// Convert file to buffer
|
|
58
|
+
const bytes = await file.arrayBuffer();
|
|
59
|
+
const buffer = Buffer.from(bytes);
|
|
60
|
+
|
|
61
|
+
const command = new PutObjectCommand({
|
|
62
|
+
Bucket: R2_BUCKET_NAME,
|
|
63
|
+
Key: uniqueKey,
|
|
64
|
+
Body: buffer,
|
|
65
|
+
ContentType: file.type,
|
|
66
|
+
ContentLength: file.size,
|
|
67
|
+
Metadata: {
|
|
68
|
+
'uploader-user-id': user.id,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await s3Client.send(command);
|
|
73
|
+
|
|
74
|
+
return NextResponse.json({
|
|
75
|
+
objectKey: uniqueKey,
|
|
76
|
+
message: 'File uploaded successfully.',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error proxying file upload:', error);
|
|
81
|
+
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
|
|
82
|
+
return NextResponse.json({ error: `Upload failed on server: ${errorMessage}` }, { status: 500 });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createClient, getProfileWithRoleServerSide } from "@nextblock-cms/db/server";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import type { Database } from "@nextblock-cms/db";
|
|
4
|
+
|
|
5
|
+
export type UserRole = Database['public']['Enums']['user_role'];
|
|
6
|
+
|
|
7
|
+
export async function GET(request: Request) {
|
|
8
|
+
const requestUrl = new URL(request.url);
|
|
9
|
+
const code = requestUrl.searchParams.get("code");
|
|
10
|
+
const origin = requestUrl.origin;
|
|
11
|
+
const initialRedirectTo = requestUrl.searchParams.get("redirect_to")?.toString();
|
|
12
|
+
const dashboardPath = "/cms/dashboard";
|
|
13
|
+
const homePath = "/";
|
|
14
|
+
|
|
15
|
+
if (code) {
|
|
16
|
+
const supabase = await createClient();
|
|
17
|
+
const { data: { user } , error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
|
|
18
|
+
|
|
19
|
+
if (exchangeError) {
|
|
20
|
+
console.error("Auth callback: Error exchanging code for session:", exchangeError.message);
|
|
21
|
+
// Fallback: if initialRedirectTo is valid (not dashboard) use it, else home
|
|
22
|
+
if (initialRedirectTo && initialRedirectTo !== dashboardPath && !initialRedirectTo.startsWith(dashboardPath + '/')) {
|
|
23
|
+
return NextResponse.redirect(`${origin}${initialRedirectTo}`);
|
|
24
|
+
}
|
|
25
|
+
return NextResponse.redirect(`${origin}${homePath}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (user) {
|
|
29
|
+
const profile = await getProfileWithRoleServerSide(user.id);
|
|
30
|
+
const role = profile?.role as UserRole | null;
|
|
31
|
+
|
|
32
|
+
if (role === 'ADMIN' || role === 'WRITER') {
|
|
33
|
+
return NextResponse.redirect(`${origin}${dashboardPath}`);
|
|
34
|
+
} else {
|
|
35
|
+
// For USER, null, or other roles:
|
|
36
|
+
// Redirect to initialRedirectTo if it's present and NOT the dashboard.
|
|
37
|
+
// Otherwise, redirect to the homepage.
|
|
38
|
+
if (initialRedirectTo && initialRedirectTo !== dashboardPath && !initialRedirectTo.startsWith(dashboardPath + '/')) {
|
|
39
|
+
return NextResponse.redirect(`${origin}${initialRedirectTo}`);
|
|
40
|
+
}
|
|
41
|
+
return NextResponse.redirect(`${origin}${homePath}`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
// User object is null after successful exchange
|
|
45
|
+
console.warn("Auth callback: User object is null after code exchange.");
|
|
46
|
+
if (initialRedirectTo && initialRedirectTo !== dashboardPath && !initialRedirectTo.startsWith(dashboardPath + '/')) {
|
|
47
|
+
return NextResponse.redirect(`${origin}${initialRedirectTo}`);
|
|
48
|
+
}
|
|
49
|
+
return NextResponse.redirect(`${origin}${homePath}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If no code parameter:
|
|
54
|
+
if (initialRedirectTo && initialRedirectTo !== dashboardPath && !initialRedirectTo.startsWith(dashboardPath + '/')) {
|
|
55
|
+
return NextResponse.redirect(`${origin}${initialRedirectTo}`);
|
|
56
|
+
}
|
|
57
|
+
return NextResponse.redirect(`${origin}${homePath}`);
|
|
58
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// app/blog/[slug]/PostClientContent.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import Image from 'next/image';
|
|
7
|
+
import type { Database } from "@nextblock-cms/db";
|
|
8
|
+
import { useLanguage } from '@/context/LanguageContext';
|
|
9
|
+
|
|
10
|
+
type PostType = Database['public']['Tables']['posts']['Row'];
|
|
11
|
+
type BlockType = Database['public']['Tables']['blocks']['Row'];
|
|
12
|
+
|
|
13
|
+
export type ImageBlockContent = {
|
|
14
|
+
media_id: string | null;
|
|
15
|
+
object_key?: string;
|
|
16
|
+
};
|
|
17
|
+
import { useCurrentContent } from '@/context/CurrentContentContext';
|
|
18
|
+
import Link from 'next/link';
|
|
19
|
+
|
|
20
|
+
interface PostClientContentProps {
|
|
21
|
+
initialPostData: (PostType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; feature_image_url?: string | null; }) | null;
|
|
22
|
+
currentSlug: string; // The slug of the currently viewed page/post
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
translatedSlugs?: { [key: string]: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function PostClientContent({ initialPostData, currentSlug, children, translatedSlugs }: PostClientContentProps) {
|
|
28
|
+
const { currentLocale, isLoadingLanguages } = useLanguage();
|
|
29
|
+
const { currentContent, setCurrentContent } = useCurrentContent();
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
|
|
32
|
+
// currentPostData is always for the slug in the URL.
|
|
33
|
+
// It's initially set by the server. It only changes if the URL itself changes (which happens on language switch).
|
|
34
|
+
const [currentPostData, setCurrentPostData] = useState(initialPostData);
|
|
35
|
+
const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false); // For feedback during navigation
|
|
36
|
+
|
|
37
|
+
// Memoize postId and postSlug
|
|
38
|
+
const postId = useMemo(() => currentPostData?.id, [currentPostData?.id]);
|
|
39
|
+
const postSlug = useMemo(() => currentPostData?.slug, [currentPostData?.slug]);
|
|
40
|
+
|
|
41
|
+
// This effect handles navigation when the language context changes
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!isLoadingLanguages && currentLocale && initialPostData && initialPostData.language_code !== currentLocale && translatedSlugs) {
|
|
44
|
+
// The current page's language (from initialPostData.language_code)
|
|
45
|
+
// does not match the user's selected language (currentLocale).
|
|
46
|
+
// We need to find the slug for the currentLocale version of this post and navigate.
|
|
47
|
+
setIsLoadingTargetLang(true);
|
|
48
|
+
const targetSlug = translatedSlugs[currentLocale];
|
|
49
|
+
|
|
50
|
+
if (targetSlug && targetSlug !== currentSlug) {
|
|
51
|
+
router.push(`/blog/${targetSlug}`); // Navigate to the translated slug's URL
|
|
52
|
+
} else if (!targetSlug) {
|
|
53
|
+
console.warn(`No published translation found for post group ${initialPostData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
|
|
54
|
+
// Optionally, provide user feedback here (e.g., a toast message)
|
|
55
|
+
// For now, the user remains on the current page.
|
|
56
|
+
}
|
|
57
|
+
// If targetSlug === currentSlug, we are already on the correct page for the selected language.
|
|
58
|
+
setIsLoadingTargetLang(false);
|
|
59
|
+
}
|
|
60
|
+
}, [currentLocale, isLoadingLanguages, initialPostData, currentSlug, router, translatedSlugs]);
|
|
61
|
+
|
|
62
|
+
// This effect updates the document based on the currently displayed data
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (currentPostData?.language_code) {
|
|
65
|
+
document.documentElement.lang = currentPostData.language_code;
|
|
66
|
+
if (currentPostData.meta_title || currentPostData.title) {
|
|
67
|
+
document.title = currentPostData.meta_title || currentPostData.title;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, [currentPostData]);
|
|
71
|
+
|
|
72
|
+
// Update currentPostData if initialPostData changes (e.g., after ISR revalidation of the current slug)
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setCurrentPostData(initialPostData);
|
|
75
|
+
}, [initialPostData]);
|
|
76
|
+
|
|
77
|
+
// Effect for setting or updating the context
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const newType = 'post' as const;
|
|
80
|
+
const slugToSet = postSlug ?? null; // Ensures slug is string or null
|
|
81
|
+
|
|
82
|
+
const needsUpdate = postId &&
|
|
83
|
+
(currentContent.id !== postId ||
|
|
84
|
+
currentContent.type !== newType ||
|
|
85
|
+
currentContent.slug !== slugToSet);
|
|
86
|
+
|
|
87
|
+
const needsClearing = !postId &&
|
|
88
|
+
(currentContent.id !== null ||
|
|
89
|
+
currentContent.type !== null ||
|
|
90
|
+
currentContent.slug !== null);
|
|
91
|
+
|
|
92
|
+
if (needsUpdate) {
|
|
93
|
+
setCurrentContent({ id: postId, type: newType, slug: slugToSet });
|
|
94
|
+
} else if (needsClearing) {
|
|
95
|
+
setCurrentContent({ id: null, type: null, slug: null });
|
|
96
|
+
}
|
|
97
|
+
}, [postId, postSlug, setCurrentContent, currentContent.id, currentContent.type, currentContent.slug]);
|
|
98
|
+
|
|
99
|
+
// Separate useEffect for cleanup
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const idToClean = postId; // Capture the postId when this effect runs
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
// Cleanup logic: only clear context if the current context ID matches the ID this instance was managing
|
|
105
|
+
if (idToClean && currentContent.id === idToClean) {
|
|
106
|
+
setCurrentContent({ id: null, type: null, slug: null });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}, [postId, setCurrentContent, currentContent.id]);
|
|
110
|
+
|
|
111
|
+
if (!currentPostData && !isLoadingLanguages && !isLoadingTargetLang) {
|
|
112
|
+
// This state means the initial slug from the URL didn't resolve to any data.
|
|
113
|
+
// The server component (page.tsx) would have already called notFound().
|
|
114
|
+
// This is a fallback or could indicate an issue if reached.
|
|
115
|
+
return (
|
|
116
|
+
<div className="container mx-auto px-4 py-8 text-center">
|
|
117
|
+
<h1 className="text-2xl font-bold mb-4">Post Not Found</h1>
|
|
118
|
+
<p className="text-muted-foreground">The post for slug "{currentSlug}" could not be loaded.</p>
|
|
119
|
+
<p className="mt-4">
|
|
120
|
+
<Link href="/blog" className="text-primary hover:underline">Back to Blog</Link>
|
|
121
|
+
<span className="mx-2">|</span>
|
|
122
|
+
<Link href="/" className="text-primary hover:underline">Go to Homepage</Link>
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// If initialPostData was null but we are still loading language context or trying to navigate
|
|
129
|
+
if (!currentPostData && (isLoadingLanguages || isLoadingTargetLang)) {
|
|
130
|
+
return <div className="container mx-auto px-4 py-20 text-center"><p>Loading post content...</p></div>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If after all attempts, currentPostData is still null (should be caught by notFound in server component ideally)
|
|
134
|
+
if (!currentPostData) {
|
|
135
|
+
return <div className="container mx-auto px-4 py-20 text-center"><p>Could not load post content for "{currentSlug}".</p></div>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<article className="w-full mx-auto">
|
|
140
|
+
{isLoadingTargetLang && <div className="text-center py-2 text-sm text-muted-foreground">Switching language...</div>}
|
|
141
|
+
|
|
142
|
+
{currentPostData?.feature_image_url && (
|
|
143
|
+
<div className="mb-8 relative"> {/* Adjust negative margins for full-bleed effect if container has padding */}
|
|
144
|
+
<Image
|
|
145
|
+
src={currentPostData.feature_image_url}
|
|
146
|
+
alt={`Hero image for ${currentPostData.title}`}
|
|
147
|
+
width={800}
|
|
148
|
+
height={400}
|
|
149
|
+
className="w-full h-auto max-h-[400px] md:max-h-[500px] object-cover shadow-lg" // Adjust max-h as needed, add rounded corners/shadow
|
|
150
|
+
priority
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
<header className="mb-8 text-center border-b pb-6 dark:border-slate-700">
|
|
155
|
+
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-2 text-slate-900 dark:text-slate-100">{currentPostData.title}</h1>
|
|
156
|
+
{currentPostData.published_at && (
|
|
157
|
+
<p className="text-sm text-slate-500 dark:text-slate-400">
|
|
158
|
+
Published on {new Date(currentPostData.published_at).toLocaleDateString(currentPostData.language_code, { year: 'numeric', month: 'long', day: 'numeric' })}
|
|
159
|
+
</p>
|
|
160
|
+
)}
|
|
161
|
+
{currentPostData.excerpt && <p className="mt-4 text-lg text-slate-600 dark:text-slate-300 max-w-2xl mx-auto">{currentPostData.excerpt}</p>}
|
|
162
|
+
</header>
|
|
163
|
+
|
|
164
|
+
<div className="prose dark:prose-invert lg:prose-xl max-w-none mx-auto">
|
|
165
|
+
{children}
|
|
166
|
+
</div>
|
|
167
|
+
</article>
|
|
168
|
+
);
|
|
169
|
+
}
|