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,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { signInAction } from "../../actions";
|
|
4
|
+
import { FormMessage, Message } from "../../../components/form-message";
|
|
5
|
+
import { SubmitButton } from "../../../components/submit-button";
|
|
6
|
+
import { Input } from "@nextblock-cms/ui";
|
|
7
|
+
import { Label } from "@nextblock-cms/ui";
|
|
8
|
+
import Link from "next/link";
|
|
9
|
+
import { useTranslations } from "@nextblock-cms/utils";
|
|
10
|
+
import { useSearchParams } from "next/navigation";
|
|
11
|
+
|
|
12
|
+
function getMessage(searchParams: URLSearchParams): Message | undefined {
|
|
13
|
+
if (searchParams.has('error')) {
|
|
14
|
+
const error = searchParams.get('error');
|
|
15
|
+
if (error) return { error };
|
|
16
|
+
}
|
|
17
|
+
if (searchParams.has('success')) {
|
|
18
|
+
const success = searchParams.get('success');
|
|
19
|
+
if (success) return { success };
|
|
20
|
+
}
|
|
21
|
+
if (searchParams.has('message')) {
|
|
22
|
+
const message = searchParams.get('message');
|
|
23
|
+
if (message) return { message };
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function Login() {
|
|
29
|
+
const { t } = useTranslations();
|
|
30
|
+
const searchParams = useSearchParams();
|
|
31
|
+
const formMessage = getMessage(searchParams);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<form className="flex-1 flex flex-col min-w-64">
|
|
35
|
+
<h1 className="text-2xl font-medium">{t('sign_in')}</h1>
|
|
36
|
+
<p className="text-sm text-foreground">
|
|
37
|
+
{t('dont_have_account')}{" "}
|
|
38
|
+
<Link className="text-foreground font-medium underline" href="/sign-up">
|
|
39
|
+
{t('sign_up')}
|
|
40
|
+
</Link>
|
|
41
|
+
</p>
|
|
42
|
+
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
|
|
43
|
+
<Label htmlFor="email">{t('email')}</Label>
|
|
44
|
+
<Input name="email" placeholder={t('you_at_example_com')} required />
|
|
45
|
+
<div className="flex justify-between items-center">
|
|
46
|
+
<Label htmlFor="password">{t('password')}</Label>
|
|
47
|
+
<Link
|
|
48
|
+
className="text-xs text-foreground underline"
|
|
49
|
+
href="/forgot-password"
|
|
50
|
+
>
|
|
51
|
+
{t('forgot_password')}
|
|
52
|
+
</Link>
|
|
53
|
+
</div>
|
|
54
|
+
<Input
|
|
55
|
+
type="password"
|
|
56
|
+
name="password"
|
|
57
|
+
placeholder={t('your_password')}
|
|
58
|
+
required
|
|
59
|
+
/>
|
|
60
|
+
<SubmitButton pendingText={t('signing_in_pending')} formAction={signInAction}>
|
|
61
|
+
{t('sign_in')}
|
|
62
|
+
</SubmitButton>
|
|
63
|
+
<FormMessage message={formMessage} />
|
|
64
|
+
</div>
|
|
65
|
+
</form>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { signUpAction } from "../../actions";
|
|
4
|
+
import { FormMessage, Message } from "../../../components/form-message";
|
|
5
|
+
import { SubmitButton } from "../../../components/submit-button";
|
|
6
|
+
import { Input } from "@nextblock-cms/ui";
|
|
7
|
+
import { Label } from "@nextblock-cms/ui";
|
|
8
|
+
import Link from "next/link";
|
|
9
|
+
import { useTranslations } from "@nextblock-cms/utils";
|
|
10
|
+
import { useSearchParams } from "next/navigation";
|
|
11
|
+
|
|
12
|
+
function getMessage(searchParams: URLSearchParams): Message | undefined {
|
|
13
|
+
if (searchParams.has('error')) {
|
|
14
|
+
const error = searchParams.get('error');
|
|
15
|
+
if (error) return { error };
|
|
16
|
+
}
|
|
17
|
+
if (searchParams.has('success')) {
|
|
18
|
+
const success = searchParams.get('success');
|
|
19
|
+
if (success) return { success };
|
|
20
|
+
}
|
|
21
|
+
if (searchParams.has('message')) {
|
|
22
|
+
const message = searchParams.get('message');
|
|
23
|
+
if (message) return { message };
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function Signup() {
|
|
29
|
+
const { t } = useTranslations();
|
|
30
|
+
const searchParams = useSearchParams();
|
|
31
|
+
const formMessage = getMessage(searchParams);
|
|
32
|
+
|
|
33
|
+
if (formMessage && 'message' in formMessage) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="w-full flex-1 flex items-center h-screen sm:max-w-md justify-center gap-2 p-4">
|
|
36
|
+
<FormMessage message={formMessage} />
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<form className="flex flex-col min-w-64 max-w-64 mx-auto">
|
|
44
|
+
<h1 className="text-2xl font-medium">{t('sign_up')}</h1>
|
|
45
|
+
<p className="text-sm text text-foreground">
|
|
46
|
+
{t('already_have_account')}{" "}
|
|
47
|
+
<Link className="text-primary font-medium underline" href="/sign-in">
|
|
48
|
+
{t('sign_in')}
|
|
49
|
+
</Link>
|
|
50
|
+
</p>
|
|
51
|
+
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
|
|
52
|
+
<Label htmlFor="email">{t('email')}</Label>
|
|
53
|
+
<Input name="email" placeholder={t('you_at_example_com')} required />
|
|
54
|
+
<Label htmlFor="password">{t('password')}</Label>
|
|
55
|
+
<Input
|
|
56
|
+
type="password"
|
|
57
|
+
name="password"
|
|
58
|
+
placeholder={t('your_password')}
|
|
59
|
+
minLength={6}
|
|
60
|
+
required
|
|
61
|
+
/>
|
|
62
|
+
<SubmitButton formAction={signUpAction} pendingText={t('signing_up_pending')}>
|
|
63
|
+
{t('sign_up')}
|
|
64
|
+
</SubmitButton>
|
|
65
|
+
<FormMessage message={formMessage} />
|
|
66
|
+
</div>
|
|
67
|
+
</form>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Toaster } from "react-hot-toast";
|
|
4
|
+
|
|
5
|
+
export function ToasterProvider() {
|
|
6
|
+
return (
|
|
7
|
+
<Toaster
|
|
8
|
+
position="top-right"
|
|
9
|
+
toastOptions={{
|
|
10
|
+
style: { fontSize: 14 },
|
|
11
|
+
success: { iconTheme: { primary: '#16a34a', secondary: 'white' } },
|
|
12
|
+
error: { iconTheme: { primary: '#dc2626', secondary: 'white' } },
|
|
13
|
+
}}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// app/[slug]/PageClientContent.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
5
|
+
import { useRouter } from 'next/navigation'; // For navigation on lang switch
|
|
6
|
+
import type { Database } from "@nextblock-cms/db";
|
|
7
|
+
import { useLanguage } from '@/context/LanguageContext';
|
|
8
|
+
import { useCurrentContent } from '@/context/CurrentContentContext';
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
|
|
11
|
+
type PageType = Database['public']['Tables']['pages']['Row'];
|
|
12
|
+
type BlockType = Database['public']['Tables']['blocks']['Row'];
|
|
13
|
+
|
|
14
|
+
interface PageClientContentProps {
|
|
15
|
+
initialPageData: (PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; }) | null;
|
|
16
|
+
currentSlug: string; // The slug of the currently viewed page
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
translatedSlugs?: { [key: string]: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fetches the slug for a given translation_group_id and target language_code
|
|
22
|
+
// This function is no longer needed here as slugs are pre-fetched.
|
|
23
|
+
// async function getSlugForTranslatedPage(
|
|
24
|
+
// translationGroupId: string,
|
|
25
|
+
// targetLanguageCode: string,
|
|
26
|
+
// supabase: ReturnType<typeof createClient>
|
|
27
|
+
// ): Promise<string | null> {
|
|
28
|
+
// const { data: langInfo, error: langErr } = await supabase
|
|
29
|
+
// .from("languages").select("id").eq("code", targetLanguageCode).single();
|
|
30
|
+
// if (langErr || !langInfo) return null;
|
|
31
|
+
|
|
32
|
+
// const { data: page, error: pageErr } = await supabase
|
|
33
|
+
// .from("pages")
|
|
34
|
+
// .select("slug")
|
|
35
|
+
// .eq("translation_group_id", translationGroupId)
|
|
36
|
+
// .eq("language_id", langInfo.id)
|
|
37
|
+
// .eq("status", "published")
|
|
38
|
+
// .single();
|
|
39
|
+
|
|
40
|
+
// if (pageErr || !page) return null;
|
|
41
|
+
// return page.slug;
|
|
42
|
+
// }
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
export default function PageClientContent({ initialPageData, currentSlug, children, translatedSlugs }: PageClientContentProps) {
|
|
46
|
+
const { currentLocale, isLoadingLanguages } = useLanguage();
|
|
47
|
+
const { currentContent, setCurrentContent } = useCurrentContent();
|
|
48
|
+
const router = useRouter();
|
|
49
|
+
// currentPageData is the data for the slug currently in the URL.
|
|
50
|
+
// It's initially set by the server for the slug it resolved.
|
|
51
|
+
const [currentPageData] = useState(initialPageData);
|
|
52
|
+
const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false);
|
|
53
|
+
|
|
54
|
+
// Memoize pageId and pageSlug
|
|
55
|
+
const pageId = useMemo(() => currentPageData?.id, [currentPageData?.id]);
|
|
56
|
+
const pageSlug = useMemo(() => currentPageData?.slug, [currentPageData?.slug]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (currentLocale && currentPageData && currentPageData.language_code !== currentLocale && translatedSlugs) {
|
|
60
|
+
// Current page's language doesn't match context, try to navigate to translated version
|
|
61
|
+
setIsLoadingTargetLang(true);
|
|
62
|
+
const targetSlug = translatedSlugs[currentLocale];
|
|
63
|
+
|
|
64
|
+
if (targetSlug && targetSlug !== currentSlug) {
|
|
65
|
+
router.push(`/${targetSlug}`); // Navigate to the translated slug's URL
|
|
66
|
+
} else if (targetSlug && targetSlug === currentSlug) {
|
|
67
|
+
// Already on the correct page for the selected language, do nothing or refresh data if needed
|
|
68
|
+
} else {
|
|
69
|
+
console.warn(`No published translation found for group ${currentPageData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
|
|
70
|
+
// Optionally, provide feedback to the user that translation is not available
|
|
71
|
+
}
|
|
72
|
+
setIsLoadingTargetLang(false);
|
|
73
|
+
}
|
|
74
|
+
}, [currentLocale, currentPageData, currentSlug, router, initialPageData, translatedSlugs]); // Rerun if initialPageData changes (e.g. after revalidation)
|
|
75
|
+
|
|
76
|
+
// Update HTML lang attribute based on the *actually displayed* content's language
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (currentPageData?.language_code) {
|
|
79
|
+
document.documentElement.lang = currentPageData.language_code;
|
|
80
|
+
if (currentPageData.meta_title || currentPageData.title) {
|
|
81
|
+
document.title = currentPageData.meta_title || currentPageData.title;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, [currentPageData]);
|
|
85
|
+
|
|
86
|
+
// Effect for setting or updating the context
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const newType = 'page' as const;
|
|
89
|
+
const slugToSet = pageSlug ?? null; // Ensures slug is string or null
|
|
90
|
+
|
|
91
|
+
const needsUpdate = pageId &&
|
|
92
|
+
(currentContent.id !== pageId ||
|
|
93
|
+
currentContent.type !== newType ||
|
|
94
|
+
currentContent.slug !== slugToSet);
|
|
95
|
+
|
|
96
|
+
const needsClearing = !pageId &&
|
|
97
|
+
(currentContent.id !== null ||
|
|
98
|
+
currentContent.type !== null ||
|
|
99
|
+
currentContent.slug !== null);
|
|
100
|
+
|
|
101
|
+
if (needsUpdate) {
|
|
102
|
+
setCurrentContent({ id: pageId, type: newType, slug: slugToSet });
|
|
103
|
+
} else if (needsClearing) {
|
|
104
|
+
setCurrentContent({ id: null, type: null, slug: null });
|
|
105
|
+
}
|
|
106
|
+
}, [pageId, pageSlug, setCurrentContent, currentContent.id, currentContent.type, currentContent.slug]);
|
|
107
|
+
|
|
108
|
+
// Separate useEffect for cleanup
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const idToClean = pageId; // Capture the pageId when this effect runs
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
// Cleanup logic: only clear context if the current context ID matches the ID this instance was managing
|
|
114
|
+
if (idToClean && currentContent.id === idToClean) {
|
|
115
|
+
setCurrentContent({ id: null, type: null, slug: null });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}, [pageId, setCurrentContent, currentContent.id]);
|
|
119
|
+
|
|
120
|
+
if (!currentPageData && !isLoadingLanguages && !isLoadingTargetLang) { // If initial data was null and no target lang found
|
|
121
|
+
return (
|
|
122
|
+
<div className="container mx-auto px-4 py-8 text-center">
|
|
123
|
+
<h1 className="text-2xl font-bold mb-4">Page Not Found</h1>
|
|
124
|
+
<p className="text-muted-foreground">The page for slug "{currentSlug}" could not be loaded or is not available in any language.</p>
|
|
125
|
+
<p className="mt-4"><Link href="/" className="text-primary hover:underline">Go to Homepage</Link></p>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!currentPageData && (isLoadingLanguages || isLoadingTargetLang)) {
|
|
131
|
+
return <div className="container mx-auto px-4 py-20 text-center"><p>Loading page content...</p></div>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!currentPageData) { // Fallback if still no data after loading attempts
|
|
135
|
+
return <div className="container mx-auto px-4 py-20 text-center"><p>Could not load page content.</p></div>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<article className="w-full mx-auto">
|
|
141
|
+
{isLoadingTargetLang && <div className="text-center py-2 text-sm text-muted-foreground">Switching language...</div>}
|
|
142
|
+
|
|
143
|
+
{/* Render blocks passed as children */}
|
|
144
|
+
{children}
|
|
145
|
+
</article>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// app/[slug]/page.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { getSsgSupabaseClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
import type { Metadata } from 'next';
|
|
6
|
+
import PageClientContent from "./PageClientContent";
|
|
7
|
+
import { getPageDataBySlug } from "./page.utils";
|
|
8
|
+
import BlockRenderer from "../../components/BlockRenderer";
|
|
9
|
+
import type { HeroBlockContent } from '../../lib/blocks/blockRegistry';
|
|
10
|
+
|
|
11
|
+
export const dynamicParams = true;
|
|
12
|
+
export const revalidate = 3600;
|
|
13
|
+
|
|
14
|
+
interface ResolvedPageParams {
|
|
15
|
+
slug: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PageProps {
|
|
19
|
+
params: Promise<ResolvedPageParams>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PageTranslation {
|
|
23
|
+
slug: string;
|
|
24
|
+
languages: {
|
|
25
|
+
code: string;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function generateStaticParams(): Promise<ResolvedPageParams[]> {
|
|
30
|
+
const supabase = getSsgSupabaseClient();
|
|
31
|
+
const { data: pages, error } = await supabase
|
|
32
|
+
.from("pages")
|
|
33
|
+
.select("slug")
|
|
34
|
+
.eq("status", "published");
|
|
35
|
+
|
|
36
|
+
if (error || !pages) {
|
|
37
|
+
console.error("SSG: Error fetching page slugs for static params:", error);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return pages.map((page) => ({ slug: page.slug }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function generateMetadata(
|
|
44
|
+
{ params: paramsPromise }: PageProps,
|
|
45
|
+
): Promise<Metadata> {
|
|
46
|
+
const params = await paramsPromise;
|
|
47
|
+
const pageData = await getPageDataBySlug(params.slug);
|
|
48
|
+
|
|
49
|
+
if (!pageData) {
|
|
50
|
+
return { title: "Page Not Found" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
|
|
54
|
+
const supabase = getSsgSupabaseClient();
|
|
55
|
+
|
|
56
|
+
// Parallel queries for better performance
|
|
57
|
+
const [languagesResult, pageTranslationsResult] = await Promise.all([
|
|
58
|
+
supabase.from('languages').select('id, code'),
|
|
59
|
+
supabase
|
|
60
|
+
.from('pages')
|
|
61
|
+
.select('language_id, slug')
|
|
62
|
+
.eq('translation_group_id', pageData.translation_group_id)
|
|
63
|
+
.eq('status', 'published')
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const { data: languages } = languagesResult;
|
|
67
|
+
const { data: pageTranslations } = pageTranslationsResult;
|
|
68
|
+
|
|
69
|
+
const alternates: { [key: string]: string } = {};
|
|
70
|
+
if (languages && pageTranslations) {
|
|
71
|
+
pageTranslations.forEach(pt => {
|
|
72
|
+
const langInfo = languages.find(l => l.id === pt.language_id);
|
|
73
|
+
if (langInfo) {
|
|
74
|
+
alternates[langInfo.code] = `${siteUrl}/${pt.slug}`;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
title: pageData.meta_title || pageData.title,
|
|
81
|
+
description: pageData.meta_description || "",
|
|
82
|
+
alternates: {
|
|
83
|
+
canonical: `${siteUrl}/${params.slug}`,
|
|
84
|
+
languages: Object.keys(alternates).length > 0 ? alternates : undefined,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default async function DynamicPage({ params: paramsPromise }: PageProps) {
|
|
90
|
+
const params = await paramsPromise;
|
|
91
|
+
const pageData = await getPageDataBySlug(params.slug);
|
|
92
|
+
|
|
93
|
+
if (!pageData) {
|
|
94
|
+
notFound();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const translatedSlugs: { [key: string]: string } = {};
|
|
98
|
+
if (pageData.translation_group_id) {
|
|
99
|
+
const supabase = getSsgSupabaseClient();
|
|
100
|
+
const { data: translations } = await supabase
|
|
101
|
+
.from("pages")
|
|
102
|
+
.select("slug, languages!inner(code)")
|
|
103
|
+
.eq("translation_group_id", pageData.translation_group_id)
|
|
104
|
+
.eq("status", "published");
|
|
105
|
+
|
|
106
|
+
if (translations) {
|
|
107
|
+
translations.forEach((translation: PageTranslation) => {
|
|
108
|
+
if (translation.languages && translation.languages.length > 0 && translation.slug) {
|
|
109
|
+
translatedSlugs[translation.languages[0].code] = translation.slug;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let lcpImageUrl: string | null = null;
|
|
116
|
+
const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
117
|
+
|
|
118
|
+
if (pageData && pageData.blocks && r2BaseUrl) {
|
|
119
|
+
const heroBlock = pageData.blocks.find(block => block.block_type === 'hero');
|
|
120
|
+
if (heroBlock) {
|
|
121
|
+
const heroContent = heroBlock.content as unknown as HeroBlockContent;
|
|
122
|
+
if (
|
|
123
|
+
heroContent.background &&
|
|
124
|
+
heroContent.background.type === "image" &&
|
|
125
|
+
heroContent.background.image &&
|
|
126
|
+
heroContent.background.image.object_key
|
|
127
|
+
) {
|
|
128
|
+
lcpImageUrl = `${r2BaseUrl}/${heroContent.background.image.object_key}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pageBlocks = pageData ? <BlockRenderer blocks={pageData.blocks} languageId={pageData.language_id} /> : null;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
{lcpImageUrl && (
|
|
138
|
+
<link rel="preload" as="image" href={lcpImageUrl} />
|
|
139
|
+
)}
|
|
140
|
+
<PageClientContent initialPageData={pageData} currentSlug={params.slug} translatedSlugs={translatedSlugs}>
|
|
141
|
+
{pageBlocks}
|
|
142
|
+
</PageClientContent>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// app/[slug]/page.utils.ts
|
|
2
|
+
import { getSsgSupabaseClient } from "@nextblock-cms/db/server";
|
|
3
|
+
import type { Database } from "@nextblock-cms/db";
|
|
4
|
+
|
|
5
|
+
type PageType = Database['public']['Tables']['pages']['Row'];
|
|
6
|
+
type BlockType = Database['public']['Tables']['blocks']['Row'];
|
|
7
|
+
|
|
8
|
+
// Define a more specific type for the content of an Image Block
|
|
9
|
+
export type ImageBlockContent = {
|
|
10
|
+
media_id: string | null;
|
|
11
|
+
object_key?: string; // Optional because it's added later
|
|
12
|
+
blur_data_url?: string | null; // Optional because it's added later
|
|
13
|
+
};
|
|
14
|
+
interface SectionOrHeroBlockContent {
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
background?: {
|
|
17
|
+
type?: 'image' | 'color';
|
|
18
|
+
image?: {
|
|
19
|
+
media_id?: string;
|
|
20
|
+
object_key?: string;
|
|
21
|
+
blur_data_url?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Interface to represent a page object after the initial database query and selection
|
|
27
|
+
interface SelectedPageType extends PageType { // Assumes PageType includes fields like id, slug, status, language_id, translation_group_id
|
|
28
|
+
language_details: { id: number; code: string } | null; // From the join; kept nullable due to original code's caution
|
|
29
|
+
blocks: BlockType[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getPageDataBySlug(slug: string): Promise<(PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string | null; }) | null> {
|
|
33
|
+
const supabase = getSsgSupabaseClient();
|
|
34
|
+
|
|
35
|
+
// Optimized query with specific field selection instead of *
|
|
36
|
+
const { data: candidatePagesData, error: pageError } = await supabase
|
|
37
|
+
.from("pages")
|
|
38
|
+
.select(`
|
|
39
|
+
id, slug, title, meta_title, meta_description, status, language_id, translation_group_id, author_id, created_at, updated_at,
|
|
40
|
+
language_details:languages!inner(id, code),
|
|
41
|
+
blocks (id, page_id, block_type, content, order)
|
|
42
|
+
`)
|
|
43
|
+
.eq("slug", slug)
|
|
44
|
+
.eq("status", "published")
|
|
45
|
+
.order('order', { foreignTable: 'blocks', ascending: true });
|
|
46
|
+
|
|
47
|
+
if (pageError) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const candidatePages: SelectedPageType[] = (candidatePagesData || []).map(page => ({
|
|
52
|
+
...page,
|
|
53
|
+
language_details: Array.isArray(page.language_details) ? page.language_details[0] : page.language_details
|
|
54
|
+
})) as SelectedPageType[];
|
|
55
|
+
|
|
56
|
+
if (candidatePages.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let selectedPage: SelectedPageType | null = null;
|
|
61
|
+
|
|
62
|
+
if (candidatePages.length === 1) {
|
|
63
|
+
selectedPage = candidatePages[0];
|
|
64
|
+
} else {
|
|
65
|
+
const enPage = candidatePages.find(p => p.language_details && p.language_details.code === 'en');
|
|
66
|
+
if (enPage) {
|
|
67
|
+
selectedPage = enPage;
|
|
68
|
+
} else {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!selectedPage) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let languageCode: string | undefined = selectedPage.language_details?.code;
|
|
78
|
+
let languageId: number | undefined = selectedPage.language_details?.id;
|
|
79
|
+
|
|
80
|
+
// Optimize fallback language query with specific fields
|
|
81
|
+
if (!languageCode || typeof languageId !== 'number') {
|
|
82
|
+
if (typeof selectedPage.language_id === 'number') {
|
|
83
|
+
const { data: fallbackLang, error: langFetchError } = await supabase
|
|
84
|
+
.from("languages")
|
|
85
|
+
.select("id, code")
|
|
86
|
+
.eq("id", selectedPage.language_id)
|
|
87
|
+
.single();
|
|
88
|
+
|
|
89
|
+
if (langFetchError) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (fallbackLang) {
|
|
94
|
+
languageCode = fallbackLang.code;
|
|
95
|
+
languageId = fallbackLang.id;
|
|
96
|
+
} else {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof languageCode !== 'string' || typeof languageId !== 'number') {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let blocksWithMediaData: BlockType[] = selectedPage.blocks || [];
|
|
109
|
+
if (blocksWithMediaData.length > 0) {
|
|
110
|
+
const mediaIds = blocksWithMediaData
|
|
111
|
+
.map(block => {
|
|
112
|
+
if (block.block_type === 'image') {
|
|
113
|
+
return (block.content as ImageBlockContent)?.media_id;
|
|
114
|
+
}
|
|
115
|
+
if (block.block_type === 'section' || block.block_type === 'hero') {
|
|
116
|
+
const content = block.content as SectionOrHeroBlockContent;
|
|
117
|
+
if (content.background?.type === 'image' && content.background?.image?.media_id) {
|
|
118
|
+
return content.background.image.media_id;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
})
|
|
123
|
+
.filter((id): id is string => id !== null && typeof id === 'string');
|
|
124
|
+
|
|
125
|
+
if (mediaIds.length > 0) {
|
|
126
|
+
// Optimized media query with specific fields only
|
|
127
|
+
const { data: mediaItems, error: mediaError } = await supabase
|
|
128
|
+
.from('media')
|
|
129
|
+
.select('id, object_key, blur_data_url')
|
|
130
|
+
.in('id', mediaIds);
|
|
131
|
+
|
|
132
|
+
if (mediaError) {
|
|
133
|
+
console.error('Error fetching media data:', mediaError);
|
|
134
|
+
} else if (mediaItems) {
|
|
135
|
+
const mediaMap = new Map(mediaItems.map(m => [m.id, { object_key: m.object_key, blur_data_url: m.blur_data_url }]));
|
|
136
|
+
blocksWithMediaData = blocksWithMediaData.map(block => {
|
|
137
|
+
if (block.block_type === 'image') {
|
|
138
|
+
const content = block.content as ImageBlockContent;
|
|
139
|
+
if (content.media_id) {
|
|
140
|
+
const mediaData = mediaMap.get(content.media_id);
|
|
141
|
+
if (mediaData) {
|
|
142
|
+
return { ...block, content: { ...content, object_key: mediaData.object_key, blur_data_url: mediaData.blur_data_url } };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (block.block_type === 'section' || block.block_type === 'hero') {
|
|
147
|
+
const content = block.content as SectionOrHeroBlockContent;
|
|
148
|
+
if (content.background?.type === 'image' && content.background?.image?.media_id) {
|
|
149
|
+
const mediaData = mediaMap.get(content.background.image.media_id);
|
|
150
|
+
if (mediaData) {
|
|
151
|
+
const newContent = {
|
|
152
|
+
...content,
|
|
153
|
+
background: {
|
|
154
|
+
...content.background,
|
|
155
|
+
image: {
|
|
156
|
+
...content.background.image,
|
|
157
|
+
object_key: mediaData.object_key,
|
|
158
|
+
blur_data_url: mediaData.blur_data_url,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
return { ...block, content: newContent };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return block;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const { language_details, blocks, ...basePageData } = selectedPage;
|
|
173
|
+
void language_details;
|
|
174
|
+
void blocks;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...(basePageData as PageType),
|
|
178
|
+
blocks: blocksWithMediaData,
|
|
179
|
+
language_code: languageCode,
|
|
180
|
+
language_id: languageId,
|
|
181
|
+
translation_group_id: selectedPage.translation_group_id,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { getEmailServerConfig } from '@nextblock-cms/utils/server';
|
|
4
|
+
import nodemailer from 'nodemailer';
|
|
5
|
+
|
|
6
|
+
interface EmailParams {
|
|
7
|
+
to: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
text: string;
|
|
10
|
+
html: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function sendEmail({ to, subject, text, html }: EmailParams) {
|
|
14
|
+
const emailConfig = await getEmailServerConfig();
|
|
15
|
+
|
|
16
|
+
if (!emailConfig) {
|
|
17
|
+
throw new Error("Email server is not configured. Please check environment variables.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const transporter = nodemailer.createTransport(emailConfig);
|
|
21
|
+
|
|
22
|
+
const options = {
|
|
23
|
+
from: emailConfig.from,
|
|
24
|
+
to,
|
|
25
|
+
subject,
|
|
26
|
+
text,
|
|
27
|
+
html,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return transporter.sendMail(options);
|
|
31
|
+
}
|