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,344 @@
|
|
|
1
|
+
// apps/nextblock/app/cms/revisions/service.ts
|
|
2
|
+
"use server";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
+
import type { Json } from "@nextblock-cms/db";
|
|
6
|
+
import { compare, applyPatch } from 'fast-json-patch';
|
|
7
|
+
import type { FullPageContent, FullPostContent } from './utils';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function shouldCreateSnapshot(currentVersion: number): boolean {
|
|
11
|
+
// Create a snapshot every 20 revisions
|
|
12
|
+
return currentVersion % 20 === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function createPageRevision(
|
|
16
|
+
pageId: number,
|
|
17
|
+
authorId: string,
|
|
18
|
+
previousContent: FullPageContent,
|
|
19
|
+
newContent: FullPageContent
|
|
20
|
+
) {
|
|
21
|
+
const supabase = createClient();
|
|
22
|
+
|
|
23
|
+
// Get current version
|
|
24
|
+
const { data: page, error: pageError } = await supabase
|
|
25
|
+
.from('pages')
|
|
26
|
+
.select('version')
|
|
27
|
+
.eq('id', pageId)
|
|
28
|
+
.single();
|
|
29
|
+
if (pageError || !page) return { error: 'Page not found' } as const;
|
|
30
|
+
|
|
31
|
+
const nextVersion = (page.version ?? 1) + 1;
|
|
32
|
+
const makeSnapshot = shouldCreateSnapshot(page.version ?? 1) || nextVersion === 2; // ensure early snapshot cadence
|
|
33
|
+
|
|
34
|
+
const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
|
|
35
|
+
const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
|
|
36
|
+
|
|
37
|
+
const { error: insertError } = await supabase.from('page_revisions').insert({
|
|
38
|
+
page_id: pageId,
|
|
39
|
+
author_id: authorId,
|
|
40
|
+
version: nextVersion,
|
|
41
|
+
revision_type: revisionType,
|
|
42
|
+
content,
|
|
43
|
+
});
|
|
44
|
+
if (insertError) return { error: `Failed to insert page revision: ${insertError.message}` } as const;
|
|
45
|
+
|
|
46
|
+
const { error: updateVersionError } = await supabase
|
|
47
|
+
.from('pages')
|
|
48
|
+
.update({ version: nextVersion })
|
|
49
|
+
.eq('id', pageId);
|
|
50
|
+
if (updateVersionError) return { error: `Failed to bump page version: ${updateVersionError.message}` } as const;
|
|
51
|
+
|
|
52
|
+
return { success: true as const, version: nextVersion };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function createPostRevision(
|
|
56
|
+
postId: number,
|
|
57
|
+
authorId: string,
|
|
58
|
+
previousContent: FullPostContent,
|
|
59
|
+
newContent: FullPostContent
|
|
60
|
+
) {
|
|
61
|
+
const supabase = createClient();
|
|
62
|
+
|
|
63
|
+
const { data: post, error: postError } = await supabase
|
|
64
|
+
.from('posts')
|
|
65
|
+
.select('version')
|
|
66
|
+
.eq('id', postId)
|
|
67
|
+
.single();
|
|
68
|
+
if (postError || !post) return { error: 'Post not found' } as const;
|
|
69
|
+
|
|
70
|
+
const nextVersion = (post.version ?? 1) + 1;
|
|
71
|
+
const makeSnapshot = shouldCreateSnapshot(post.version ?? 1) || nextVersion === 2;
|
|
72
|
+
|
|
73
|
+
const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
|
|
74
|
+
const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
|
|
75
|
+
|
|
76
|
+
const { error: insertError } = await supabase.from('post_revisions').insert({
|
|
77
|
+
post_id: postId,
|
|
78
|
+
author_id: authorId,
|
|
79
|
+
version: nextVersion,
|
|
80
|
+
revision_type: revisionType,
|
|
81
|
+
content,
|
|
82
|
+
});
|
|
83
|
+
if (insertError) return { error: `Failed to insert post revision: ${insertError.message}` } as const;
|
|
84
|
+
|
|
85
|
+
const { error: updateVersionError } = await supabase
|
|
86
|
+
.from('posts')
|
|
87
|
+
.update({ version: nextVersion })
|
|
88
|
+
.eq('id', postId);
|
|
89
|
+
if (updateVersionError) return { error: `Failed to bump post version: ${updateVersionError.message}` } as const;
|
|
90
|
+
|
|
91
|
+
return { success: true as const, version: nextVersion };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function restorePageToVersion(pageId: number, targetVersion: number, authorId: string) {
|
|
95
|
+
const supabase = createClient();
|
|
96
|
+
|
|
97
|
+
// 1. Find latest snapshot at or before target
|
|
98
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
99
|
+
.from('page_revisions')
|
|
100
|
+
.select('version, content, revision_type')
|
|
101
|
+
.eq('page_id', pageId)
|
|
102
|
+
.lte('version', targetVersion)
|
|
103
|
+
.eq('revision_type', 'snapshot')
|
|
104
|
+
.order('version', { ascending: false })
|
|
105
|
+
.limit(1)
|
|
106
|
+
.maybeSingle();
|
|
107
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
108
|
+
|
|
109
|
+
let content = snapshot.content as unknown as FullPageContent;
|
|
110
|
+
|
|
111
|
+
// 2. Fetch diffs up to target and apply
|
|
112
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
113
|
+
.from('page_revisions')
|
|
114
|
+
.select('version, content, revision_type')
|
|
115
|
+
.eq('page_id', pageId)
|
|
116
|
+
.gt('version', snapshot.version)
|
|
117
|
+
.lte('version', targetVersion)
|
|
118
|
+
.order('version', { ascending: true });
|
|
119
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
120
|
+
|
|
121
|
+
for (const r of diffs || []) {
|
|
122
|
+
if (r.revision_type === 'diff') {
|
|
123
|
+
const ops = r.content as any[];
|
|
124
|
+
const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
|
|
125
|
+
content = result.newDocument as unknown as FullPageContent;
|
|
126
|
+
} else {
|
|
127
|
+
content = r.content as unknown as FullPageContent;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Determine next version number (append a new revision for restored state)
|
|
132
|
+
const { data: pageRow } = await supabase
|
|
133
|
+
.from('pages')
|
|
134
|
+
.select('version')
|
|
135
|
+
.eq('id', pageId)
|
|
136
|
+
.single();
|
|
137
|
+
const newVersion = ((pageRow?.version as number | null) ?? 1) + 1;
|
|
138
|
+
|
|
139
|
+
// 3. Apply to DB: update page meta and replace blocks; bump to newVersion
|
|
140
|
+
const { error: updatePageError } = await supabase
|
|
141
|
+
.from('pages')
|
|
142
|
+
.update({
|
|
143
|
+
title: content.meta.title,
|
|
144
|
+
slug: content.meta.slug,
|
|
145
|
+
language_id: content.meta.language_id,
|
|
146
|
+
status: content.meta.status,
|
|
147
|
+
meta_title: content.meta.meta_title,
|
|
148
|
+
meta_description: content.meta.meta_description,
|
|
149
|
+
version: newVersion,
|
|
150
|
+
})
|
|
151
|
+
.eq('id', pageId);
|
|
152
|
+
if (updatePageError) return { error: `Failed to update page: ${updatePageError.message}` } as const;
|
|
153
|
+
|
|
154
|
+
// delete all existing blocks for this page then reinsert
|
|
155
|
+
const { error: deleteError } = await supabase.from('blocks').delete().eq('page_id', pageId);
|
|
156
|
+
if (deleteError) return { error: `Failed to clear blocks: ${deleteError.message}` } as const;
|
|
157
|
+
|
|
158
|
+
if (content.blocks.length > 0) {
|
|
159
|
+
const toInsert = content.blocks.map(b => ({
|
|
160
|
+
page_id: pageId,
|
|
161
|
+
post_id: null,
|
|
162
|
+
language_id: b.language_id,
|
|
163
|
+
block_type: b.block_type,
|
|
164
|
+
content: b.content,
|
|
165
|
+
order: b.order,
|
|
166
|
+
}));
|
|
167
|
+
const { error: insertError } = await supabase.from('blocks').insert(toInsert);
|
|
168
|
+
if (insertError) return { error: `Failed to insert blocks: ${insertError.message}` } as const;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 4. Record a new snapshot revision representing the restored state at newVersion
|
|
172
|
+
const { error: revErr } = await supabase.from('page_revisions').insert({
|
|
173
|
+
page_id: pageId,
|
|
174
|
+
author_id: authorId,
|
|
175
|
+
version: newVersion,
|
|
176
|
+
revision_type: 'snapshot',
|
|
177
|
+
content: content as unknown as Json,
|
|
178
|
+
});
|
|
179
|
+
if (revErr) return { error: `Failed to write restored revision: ${revErr.message}` } as const;
|
|
180
|
+
|
|
181
|
+
return { success: true as const };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function restorePostToVersion(postId: number, targetVersion: number, authorId: string) {
|
|
185
|
+
const supabase = createClient();
|
|
186
|
+
|
|
187
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
188
|
+
.from('post_revisions')
|
|
189
|
+
.select('version, content, revision_type')
|
|
190
|
+
.eq('post_id', postId)
|
|
191
|
+
.lte('version', targetVersion)
|
|
192
|
+
.eq('revision_type', 'snapshot')
|
|
193
|
+
.order('version', { ascending: false })
|
|
194
|
+
.limit(1)
|
|
195
|
+
.maybeSingle();
|
|
196
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
197
|
+
|
|
198
|
+
let content = snapshot.content as unknown as FullPostContent;
|
|
199
|
+
|
|
200
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
201
|
+
.from('post_revisions')
|
|
202
|
+
.select('version, content, revision_type')
|
|
203
|
+
.eq('post_id', postId)
|
|
204
|
+
.gt('version', snapshot.version)
|
|
205
|
+
.lte('version', targetVersion)
|
|
206
|
+
.order('version', { ascending: true });
|
|
207
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
208
|
+
|
|
209
|
+
for (const r of diffs || []) {
|
|
210
|
+
if (r.revision_type === 'diff') {
|
|
211
|
+
const ops = r.content as any[];
|
|
212
|
+
const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
|
|
213
|
+
content = result.newDocument as unknown as FullPostContent;
|
|
214
|
+
} else {
|
|
215
|
+
content = r.content as unknown as FullPostContent;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Determine next version for post
|
|
220
|
+
const { data: postRow } = await supabase
|
|
221
|
+
.from('posts')
|
|
222
|
+
.select('version')
|
|
223
|
+
.eq('id', postId)
|
|
224
|
+
.single();
|
|
225
|
+
const newVersion = ((postRow?.version as number | null) ?? 1) + 1;
|
|
226
|
+
|
|
227
|
+
const { error: updatePostError } = await supabase
|
|
228
|
+
.from('posts')
|
|
229
|
+
.update({
|
|
230
|
+
title: content.meta.title,
|
|
231
|
+
slug: content.meta.slug,
|
|
232
|
+
language_id: content.meta.language_id,
|
|
233
|
+
status: content.meta.status,
|
|
234
|
+
meta_title: content.meta.meta_title,
|
|
235
|
+
meta_description: content.meta.meta_description,
|
|
236
|
+
excerpt: content.meta.excerpt,
|
|
237
|
+
published_at: content.meta.published_at,
|
|
238
|
+
feature_image_id: content.meta.feature_image_id,
|
|
239
|
+
version: newVersion,
|
|
240
|
+
})
|
|
241
|
+
.eq('id', postId);
|
|
242
|
+
if (updatePostError) return { error: `Failed to update post: ${updatePostError.message}` } as const;
|
|
243
|
+
|
|
244
|
+
const { error: deleteError } = await supabase.from('blocks').delete().eq('post_id', postId);
|
|
245
|
+
if (deleteError) return { error: `Failed to clear blocks: ${deleteError.message}` } as const;
|
|
246
|
+
|
|
247
|
+
if (content.blocks.length > 0) {
|
|
248
|
+
const toInsert = content.blocks.map(b => ({
|
|
249
|
+
page_id: null,
|
|
250
|
+
post_id: postId,
|
|
251
|
+
language_id: b.language_id,
|
|
252
|
+
block_type: b.block_type,
|
|
253
|
+
content: b.content,
|
|
254
|
+
order: b.order,
|
|
255
|
+
}));
|
|
256
|
+
const { error: insertError } = await supabase.from('blocks').insert(toInsert);
|
|
257
|
+
if (insertError) return { error: `Failed to insert blocks: ${insertError.message}` } as const;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { error: revErr } = await supabase.from('post_revisions').insert({
|
|
261
|
+
post_id: postId,
|
|
262
|
+
author_id: authorId,
|
|
263
|
+
version: newVersion,
|
|
264
|
+
revision_type: 'snapshot',
|
|
265
|
+
content: content as unknown as Json,
|
|
266
|
+
});
|
|
267
|
+
if (revErr) return { error: `Failed to write restored revision: ${revErr.message}` } as const;
|
|
268
|
+
|
|
269
|
+
return { success: true as const };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function reconstructPageVersionContent(pageId: number, targetVersion: number) {
|
|
273
|
+
const supabase = createClient();
|
|
274
|
+
|
|
275
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
276
|
+
.from('page_revisions')
|
|
277
|
+
.select('version, content, revision_type')
|
|
278
|
+
.eq('page_id', pageId)
|
|
279
|
+
.lte('version', targetVersion)
|
|
280
|
+
.eq('revision_type', 'snapshot')
|
|
281
|
+
.order('version', { ascending: false })
|
|
282
|
+
.limit(1)
|
|
283
|
+
.maybeSingle();
|
|
284
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
285
|
+
|
|
286
|
+
let content = snapshot.content as unknown as FullPageContent;
|
|
287
|
+
|
|
288
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
289
|
+
.from('page_revisions')
|
|
290
|
+
.select('version, content, revision_type')
|
|
291
|
+
.eq('page_id', pageId)
|
|
292
|
+
.gt('version', snapshot.version)
|
|
293
|
+
.lte('version', targetVersion)
|
|
294
|
+
.order('version', { ascending: true });
|
|
295
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
296
|
+
|
|
297
|
+
for (const r of diffs || []) {
|
|
298
|
+
if (r.revision_type === 'diff') {
|
|
299
|
+
const ops = r.content as any[];
|
|
300
|
+
const result = applyPatch(content as any, ops, false, true);
|
|
301
|
+
content = result.newDocument as unknown as FullPageContent;
|
|
302
|
+
} else {
|
|
303
|
+
content = r.content as unknown as FullPageContent;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { success: true as const, content };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function reconstructPostVersionContent(postId: number, targetVersion: number) {
|
|
310
|
+
const supabase = createClient();
|
|
311
|
+
|
|
312
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
313
|
+
.from('post_revisions')
|
|
314
|
+
.select('version, content, revision_type')
|
|
315
|
+
.eq('post_id', postId)
|
|
316
|
+
.lte('version', targetVersion)
|
|
317
|
+
.eq('revision_type', 'snapshot')
|
|
318
|
+
.order('version', { ascending: false })
|
|
319
|
+
.limit(1)
|
|
320
|
+
.maybeSingle();
|
|
321
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
322
|
+
|
|
323
|
+
let content = snapshot.content as unknown as FullPostContent;
|
|
324
|
+
|
|
325
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
326
|
+
.from('post_revisions')
|
|
327
|
+
.select('version, content, revision_type')
|
|
328
|
+
.eq('post_id', postId)
|
|
329
|
+
.gt('version', snapshot.version)
|
|
330
|
+
.lte('version', targetVersion)
|
|
331
|
+
.order('version', { ascending: true });
|
|
332
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
333
|
+
|
|
334
|
+
for (const r of diffs || []) {
|
|
335
|
+
if (r.revision_type === 'diff') {
|
|
336
|
+
const ops = r.content as any[];
|
|
337
|
+
const result = applyPatch(content as any, ops, false, true);
|
|
338
|
+
content = result.newDocument as unknown as FullPostContent;
|
|
339
|
+
} else {
|
|
340
|
+
content = r.content as unknown as FullPostContent;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { success: true as const, content };
|
|
344
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// apps/nextblock/app/cms/revisions/utils.ts
|
|
2
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
3
|
+
import type { Database, Json } from "@nextblock-cms/db";
|
|
4
|
+
|
|
5
|
+
type BlockRow = Database['public']['Tables']['blocks']['Row'];
|
|
6
|
+
|
|
7
|
+
export interface PageMetaContent {
|
|
8
|
+
title: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
language_id: number;
|
|
11
|
+
status: Database['public']['Enums']['page_status'];
|
|
12
|
+
meta_title: string | null;
|
|
13
|
+
meta_description: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PostMetaContent extends PageMetaContent {
|
|
17
|
+
excerpt: string | null;
|
|
18
|
+
published_at: string | null;
|
|
19
|
+
feature_image_id: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SimpleBlockContent {
|
|
23
|
+
language_id: number;
|
|
24
|
+
block_type: BlockRow['block_type'];
|
|
25
|
+
content: Json;
|
|
26
|
+
order: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FullPageContent {
|
|
30
|
+
meta: PageMetaContent;
|
|
31
|
+
blocks: SimpleBlockContent[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FullPostContent {
|
|
35
|
+
meta: PostMetaContent;
|
|
36
|
+
blocks: SimpleBlockContent[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getFullPageContent(
|
|
40
|
+
pageId: number,
|
|
41
|
+
opts?: { overrideBlockId?: number; overrideBlockContent?: unknown; excludeDeletedBlockId?: number }
|
|
42
|
+
): Promise<FullPageContent | null> {
|
|
43
|
+
const supabase = createClient();
|
|
44
|
+
const { data: page, error: pageError } = await supabase
|
|
45
|
+
.from('pages')
|
|
46
|
+
.select('id, title, slug, language_id, status, meta_title, meta_description')
|
|
47
|
+
.eq('id', pageId)
|
|
48
|
+
.single();
|
|
49
|
+
if (pageError || !page) return null;
|
|
50
|
+
|
|
51
|
+
const { data: blocks, error: blocksError } = await supabase
|
|
52
|
+
.from('blocks')
|
|
53
|
+
.select('id, language_id, block_type, content, order, page_id, post_id')
|
|
54
|
+
.eq('page_id', pageId)
|
|
55
|
+
.order('order', { ascending: true });
|
|
56
|
+
if (blocksError) return null;
|
|
57
|
+
|
|
58
|
+
const processed = (blocks || [])
|
|
59
|
+
.filter(b => opts?.excludeDeletedBlockId ? b.id !== opts.excludeDeletedBlockId : true)
|
|
60
|
+
.map(b => ({
|
|
61
|
+
language_id: b.language_id,
|
|
62
|
+
block_type: b.block_type,
|
|
63
|
+
content: (opts?.overrideBlockId && b.id === opts.overrideBlockId)
|
|
64
|
+
? (opts.overrideBlockContent as Json)
|
|
65
|
+
: (b.content as Json),
|
|
66
|
+
order: b.order,
|
|
67
|
+
} satisfies SimpleBlockContent));
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
meta: {
|
|
71
|
+
title: page.title,
|
|
72
|
+
slug: page.slug,
|
|
73
|
+
language_id: page.language_id,
|
|
74
|
+
status: page.status,
|
|
75
|
+
meta_title: page.meta_title,
|
|
76
|
+
meta_description: page.meta_description,
|
|
77
|
+
},
|
|
78
|
+
blocks: processed,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function getFullPostContent(
|
|
83
|
+
postId: number,
|
|
84
|
+
opts?: { overrideBlockId?: number; overrideBlockContent?: unknown; excludeDeletedBlockId?: number }
|
|
85
|
+
): Promise<FullPostContent | null> {
|
|
86
|
+
const supabase = createClient();
|
|
87
|
+
const { data: post, error: postError } = await supabase
|
|
88
|
+
.from('posts')
|
|
89
|
+
.select('id, title, slug, language_id, status, meta_title, meta_description, excerpt, published_at, feature_image_id')
|
|
90
|
+
.eq('id', postId)
|
|
91
|
+
.single();
|
|
92
|
+
if (postError || !post) return null;
|
|
93
|
+
|
|
94
|
+
const { data: blocks, error: blocksError } = await supabase
|
|
95
|
+
.from('blocks')
|
|
96
|
+
.select('id, language_id, block_type, content, order, page_id, post_id')
|
|
97
|
+
.eq('post_id', postId)
|
|
98
|
+
.order('order', { ascending: true });
|
|
99
|
+
if (blocksError) return null;
|
|
100
|
+
|
|
101
|
+
const processed = (blocks || [])
|
|
102
|
+
.filter(b => opts?.excludeDeletedBlockId ? b.id !== opts.excludeDeletedBlockId : true)
|
|
103
|
+
.map(b => ({
|
|
104
|
+
language_id: b.language_id,
|
|
105
|
+
block_type: b.block_type,
|
|
106
|
+
content: (opts?.overrideBlockId && b.id === opts.overrideBlockId)
|
|
107
|
+
? (opts.overrideBlockContent as Json)
|
|
108
|
+
: (b.content as Json),
|
|
109
|
+
order: b.order,
|
|
110
|
+
} satisfies SimpleBlockContent));
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
meta: {
|
|
114
|
+
title: post.title,
|
|
115
|
+
slug: post.slug,
|
|
116
|
+
language_id: post.language_id,
|
|
117
|
+
status: post.status,
|
|
118
|
+
meta_title: post.meta_title,
|
|
119
|
+
meta_description: post.meta_description,
|
|
120
|
+
excerpt: post.excerpt,
|
|
121
|
+
published_at: post.published_at ? new Date(post.published_at).toISOString() : null,
|
|
122
|
+
feature_image_id: post.feature_image_id ?? null,
|
|
123
|
+
},
|
|
124
|
+
blocks: processed,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// app/cms/settings/copyright/actions.ts
|
|
2
|
+
'use server';
|
|
3
|
+
|
|
4
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
5
|
+
import { revalidatePath } from 'next/cache';
|
|
6
|
+
|
|
7
|
+
export type CopyrightSettings = {
|
|
8
|
+
[key: string]: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function getCopyrightSettings(): Promise<CopyrightSettings> {
|
|
12
|
+
const supabase = createClient();
|
|
13
|
+
const { data, error } = await supabase
|
|
14
|
+
.from('site_settings')
|
|
15
|
+
.select('value')
|
|
16
|
+
.eq('key', 'footer_copyright')
|
|
17
|
+
.single();
|
|
18
|
+
|
|
19
|
+
if (error || !data) {
|
|
20
|
+
// If you want to handle the "not found" case gracefully,
|
|
21
|
+
// you could return a default value instead of throwing an error.
|
|
22
|
+
console.error('Copyright settings not found:', error);
|
|
23
|
+
return { en: '© {year} Default Copyright. All rights reserved.' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return data.value as CopyrightSettings;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function updateCopyrightSettings(formData: FormData) {
|
|
30
|
+
const supabase = createClient();
|
|
31
|
+
|
|
32
|
+
// Check if user is an admin
|
|
33
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
34
|
+
if (!user) {
|
|
35
|
+
throw new Error('You must be logged in to update settings.');
|
|
36
|
+
}
|
|
37
|
+
const { data: profile, error: profileError } = await supabase
|
|
38
|
+
.from('profiles')
|
|
39
|
+
.select('role')
|
|
40
|
+
.eq('id', user.id)
|
|
41
|
+
.single();
|
|
42
|
+
|
|
43
|
+
if (profileError || !profile || !['ADMIN', 'WRITER'].includes(profile.role)) {
|
|
44
|
+
throw new Error('You do not have permission to perform this action.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const newSettings: CopyrightSettings = {};
|
|
48
|
+
formData.forEach((value, key) => {
|
|
49
|
+
if (key.startsWith('copyright_')) {
|
|
50
|
+
const langCode = key.replace('copyright_', '');
|
|
51
|
+
newSettings[langCode] = value as string;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { error } = await supabase
|
|
56
|
+
.from('site_settings')
|
|
57
|
+
.upsert({ key: 'footer_copyright', value: newSettings });
|
|
58
|
+
|
|
59
|
+
if (error) {
|
|
60
|
+
console.error('Error updating copyright settings:', error);
|
|
61
|
+
throw new Error('Failed to update copyright settings.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Revalidate the root layout to reflect changes immediately across the site.
|
|
65
|
+
revalidatePath('/', 'layout');
|
|
66
|
+
|
|
67
|
+
return { success: true, message: 'Copyright settings updated successfully.' };
|
|
68
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
import type { Database } from '@nextblock-cms/db';
|
|
5
|
+
|
|
6
|
+
type Language = Database['public']['Tables']['languages']['Row'];
|
|
7
|
+
import { CopyrightSettings, updateCopyrightSettings } from '../actions';
|
|
8
|
+
import { Label } from '@nextblock-cms/ui';
|
|
9
|
+
import { Input } from '@nextblock-cms/ui';
|
|
10
|
+
import { Button } from '@nextblock-cms/ui';
|
|
11
|
+
import { FormMessage, type Message } from '@/components/form-message';
|
|
12
|
+
|
|
13
|
+
interface CopyrightFormProps {
|
|
14
|
+
languages: Language[];
|
|
15
|
+
initialSettings: CopyrightSettings;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function CopyrightForm({ languages, initialSettings }: CopyrightFormProps) {
|
|
19
|
+
const [isPending, startTransition] = useTransition();
|
|
20
|
+
const [message, setMessage] = useState<Message | null>(null);
|
|
21
|
+
const [settings, setSettings] = useState<CopyrightSettings>(initialSettings);
|
|
22
|
+
|
|
23
|
+
const handleInputChange = (langCode: string, value: string) => {
|
|
24
|
+
setSettings(prev => ({ ...prev, [langCode]: value }));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
setMessage(null);
|
|
30
|
+
|
|
31
|
+
const formData = new FormData();
|
|
32
|
+
for (const lang of languages) {
|
|
33
|
+
const value = settings[lang.code] || '';
|
|
34
|
+
formData.append(`copyright_${lang.code}`, value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
startTransition(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await updateCopyrightSettings(formData);
|
|
40
|
+
if (result.success) {
|
|
41
|
+
setMessage({ success: result.message });
|
|
42
|
+
} else {
|
|
43
|
+
setMessage({ error: 'An unexpected error occurred.' });
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
setMessage({ error: error instanceof Error ? error.message : 'An unknown error occurred.' });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
53
|
+
<div className="space-y-4">
|
|
54
|
+
{languages.map(lang => (
|
|
55
|
+
<div key={lang.id} className="space-y-2">
|
|
56
|
+
<Label htmlFor={`copyright_${lang.code}`}>
|
|
57
|
+
{lang.name} ({lang.code})
|
|
58
|
+
</Label>
|
|
59
|
+
<Input
|
|
60
|
+
id={`copyright_${lang.code}`}
|
|
61
|
+
name={`copyright_${lang.code}`}
|
|
62
|
+
value={settings[lang.code] || ''}
|
|
63
|
+
onChange={(e) => handleInputChange(lang.code, e.target.value)}
|
|
64
|
+
placeholder="e.g., © {year} Copyright"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="flex items-center gap-4">
|
|
71
|
+
<Button type="submit" disabled={isPending}>
|
|
72
|
+
{isPending ? 'Saving...' : 'Save Settings'}
|
|
73
|
+
</Button>
|
|
74
|
+
{message && <FormMessage message={message} />}
|
|
75
|
+
</div>
|
|
76
|
+
</form>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// app/cms/settings/copyright/page.tsx
|
|
2
|
+
import { getActiveLanguagesServerSide } from '@/app/cms/settings/languages/actions';
|
|
3
|
+
import { getCopyrightSettings } from './actions';
|
|
4
|
+
import CopyrightForm from './components/CopyrightForm';
|
|
5
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextblock-cms/ui';
|
|
6
|
+
|
|
7
|
+
export default async function CopyrightSettingsPage() {
|
|
8
|
+
const languages = await getActiveLanguagesServerSide();
|
|
9
|
+
const copyrightSettings = await getCopyrightSettings();
|
|
10
|
+
|
|
11
|
+
const year = new Date().getFullYear();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="max-w-4xl mx-auto">
|
|
15
|
+
<Card>
|
|
16
|
+
<CardHeader>
|
|
17
|
+
<CardTitle>Footer Copyright Settings</CardTitle>
|
|
18
|
+
<CardDescription>
|
|
19
|
+
Manage the copyright text displayed in the site footer for each language.
|
|
20
|
+
Use "{year}" as a placeholder for the current year.
|
|
21
|
+
</CardDescription>
|
|
22
|
+
</CardHeader>
|
|
23
|
+
<CardContent>
|
|
24
|
+
<CopyrightForm
|
|
25
|
+
languages={languages}
|
|
26
|
+
initialSettings={copyrightSettings}
|
|
27
|
+
/>
|
|
28
|
+
</CardContent>
|
|
29
|
+
</Card>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|