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,434 @@
|
|
|
1
|
+
// app/cms/blocks/actions.ts
|
|
2
|
+
"use server";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
+
import { revalidatePath } from "next/cache";
|
|
6
|
+
import type { Database, Json } from "@nextblock-cms/db";
|
|
7
|
+
import { getInitialContent, isValidBlockType } from "../../../lib/blocks/blockRegistry";
|
|
8
|
+
import { getFullPageContent, getFullPostContent } from "../revisions/utils";
|
|
9
|
+
import { createPageRevision, createPostRevision } from "../revisions/service";
|
|
10
|
+
|
|
11
|
+
type Block = Database['public']['Tables']['blocks']['Row'];
|
|
12
|
+
type BlockType = Database['public']['Tables']['blocks']['Row']['block_type'];
|
|
13
|
+
|
|
14
|
+
// Helper to verify user can edit the parent (page/post)
|
|
15
|
+
async function canEditParent(
|
|
16
|
+
supabase: ReturnType<typeof createClient>,
|
|
17
|
+
userId: string,
|
|
18
|
+
pageId?: number | null,
|
|
19
|
+
postId?: number | null
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
void pageId;
|
|
22
|
+
void postId;
|
|
23
|
+
const { data: profile } = await supabase
|
|
24
|
+
.from("profiles")
|
|
25
|
+
.select("role")
|
|
26
|
+
.eq("id", userId)
|
|
27
|
+
.single();
|
|
28
|
+
|
|
29
|
+
if (!profile || !["ADMIN", "WRITER"].includes(profile.role)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// Further checks could be added here to see if a WRITER owns the page/post
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CreateBlockPayload {
|
|
37
|
+
page_id?: number | null;
|
|
38
|
+
post_id?: number | null;
|
|
39
|
+
language_id: number;
|
|
40
|
+
block_type: BlockType;
|
|
41
|
+
content: object; // Content structure defined by block registry
|
|
42
|
+
order: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function createBlockForPage(pageId: number, languageId: number, blockType: BlockType, order: number) {
|
|
46
|
+
const supabase = createClient();
|
|
47
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
48
|
+
|
|
49
|
+
if (!user) return { error: "User not authenticated." };
|
|
50
|
+
if (!(await canEditParent(supabase, user.id, pageId, null))) {
|
|
51
|
+
return { error: "Unauthorized to add blocks to this page." };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate block type using registry
|
|
55
|
+
if (!isValidBlockType(blockType)) {
|
|
56
|
+
return { error: "Unknown block type." };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get initial content from registry
|
|
60
|
+
const initialContent = getInitialContent(blockType);
|
|
61
|
+
if (!initialContent) {
|
|
62
|
+
return { error: "Failed to get initial content for block type." };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const payload: CreateBlockPayload = {
|
|
66
|
+
page_id: pageId,
|
|
67
|
+
language_id: languageId,
|
|
68
|
+
block_type: blockType,
|
|
69
|
+
content: initialContent,
|
|
70
|
+
order: order,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// capture previous state for revision (before insert)
|
|
74
|
+
const previousContent = await getFullPageContent(pageId);
|
|
75
|
+
|
|
76
|
+
const { data, error } = await supabase.from("blocks").insert(payload).select().single();
|
|
77
|
+
|
|
78
|
+
if (error) {
|
|
79
|
+
console.error("Error creating block:", error);
|
|
80
|
+
return { error: `Failed to create block: ${error.message}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// create revision (after successful insert)
|
|
84
|
+
if (previousContent && user) {
|
|
85
|
+
const newContent = await getFullPageContent(pageId);
|
|
86
|
+
if (newContent) {
|
|
87
|
+
await createPageRevision(pageId, user.id, previousContent, newContent);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
revalidatePath(`/cms/pages/${pageId}/edit`);
|
|
92
|
+
return { success: true, newBlock: data as Block };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function createBlockForPost(postId: number, languageId: number, blockType: BlockType, order: number) {
|
|
96
|
+
const supabase = createClient();
|
|
97
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
98
|
+
|
|
99
|
+
if (!user) return { error: "User not authenticated." };
|
|
100
|
+
if (!(await canEditParent(supabase, user.id, null, postId))) {
|
|
101
|
+
return { error: "Unauthorized to add blocks to this post." };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate block type using registry
|
|
105
|
+
if (!isValidBlockType(blockType)) {
|
|
106
|
+
return { error: "Unknown block type." };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get initial content from registry
|
|
110
|
+
const initialContent = getInitialContent(blockType);
|
|
111
|
+
if (!initialContent) {
|
|
112
|
+
return { error: "Failed to get initial content for block type." };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const payload: CreateBlockPayload = {
|
|
116
|
+
post_id: postId,
|
|
117
|
+
language_id: languageId,
|
|
118
|
+
block_type: blockType,
|
|
119
|
+
content: initialContent,
|
|
120
|
+
order: order,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// capture previous content
|
|
124
|
+
const previousContent = await getFullPostContent(postId);
|
|
125
|
+
|
|
126
|
+
const { data, error } = await supabase.from("blocks").insert(payload).select().single();
|
|
127
|
+
|
|
128
|
+
if (error) {
|
|
129
|
+
console.error("Error creating block:", error);
|
|
130
|
+
return { error: `Failed to create block: ${error.message}` };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (previousContent && user) {
|
|
134
|
+
const newContent = await getFullPostContent(postId);
|
|
135
|
+
if (newContent) {
|
|
136
|
+
await createPostRevision(postId, user.id, previousContent, newContent);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
revalidatePath(`/cms/posts/${postId}/edit`);
|
|
141
|
+
return { success: true, newBlock: data as Block };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function updateBlock(blockId: number, newContent: unknown, pageId?: number | null, postId?: number | null) {
|
|
145
|
+
const supabase = createClient();
|
|
146
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
147
|
+
|
|
148
|
+
if (!user) return { error: "User not authenticated." };
|
|
149
|
+
if (!(await canEditParent(supabase, user.id, pageId, postId))) {
|
|
150
|
+
return { error: "Unauthorized to update this block." };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// fetch current block to identify parent and previous state
|
|
154
|
+
const { data: existingBlock, error: fetchError } = await supabase
|
|
155
|
+
.from('blocks')
|
|
156
|
+
.select('id, page_id, post_id, content')
|
|
157
|
+
.eq('id', blockId)
|
|
158
|
+
.single();
|
|
159
|
+
if (fetchError || !existingBlock) {
|
|
160
|
+
return { error: "Block not found." };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let prevContentAggregate: Awaited<ReturnType<typeof getFullPageContent>> | Awaited<ReturnType<typeof getFullPostContent>> | null = null;
|
|
164
|
+
if (existingBlock.page_id) {
|
|
165
|
+
prevContentAggregate = await getFullPageContent(existingBlock.page_id);
|
|
166
|
+
} else if (existingBlock.post_id) {
|
|
167
|
+
prevContentAggregate = await getFullPostContent(existingBlock.post_id);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { data, error } = await supabase
|
|
171
|
+
.from("blocks")
|
|
172
|
+
.update({ content: newContent, updated_at: new Date().toISOString() })
|
|
173
|
+
.eq("id", blockId)
|
|
174
|
+
.select()
|
|
175
|
+
.single();
|
|
176
|
+
|
|
177
|
+
if (error) {
|
|
178
|
+
console.error("Error updating block:", error);
|
|
179
|
+
return { error: `Failed to update block: ${error.message}` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// create revision after successful update
|
|
183
|
+
if (user && prevContentAggregate) {
|
|
184
|
+
if (existingBlock.page_id) {
|
|
185
|
+
const nextContentAggregate = await getFullPageContent(existingBlock.page_id, { overrideBlockId: blockId, overrideBlockContent: newContent });
|
|
186
|
+
if (nextContentAggregate) {
|
|
187
|
+
await createPageRevision(existingBlock.page_id, user.id, prevContentAggregate, nextContentAggregate);
|
|
188
|
+
}
|
|
189
|
+
} else if (existingBlock.post_id) {
|
|
190
|
+
const nextContentAggregate = await getFullPostContent(existingBlock.post_id, { overrideBlockId: blockId, overrideBlockContent: newContent });
|
|
191
|
+
if (nextContentAggregate) {
|
|
192
|
+
await createPostRevision(existingBlock.post_id, user.id, prevContentAggregate as any, nextContentAggregate as any);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { success: true, updatedBlock: data as Block };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function updateMultipleBlockOrders(
|
|
201
|
+
updates: Array<{ id: number; order: number }>,
|
|
202
|
+
pageId?: number | null,
|
|
203
|
+
postId?: number | null
|
|
204
|
+
) {
|
|
205
|
+
const supabase = createClient();
|
|
206
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
207
|
+
|
|
208
|
+
if (!user) return { error: "User not authenticated." };
|
|
209
|
+
if (!(await canEditParent(supabase, user.id, pageId, postId))) {
|
|
210
|
+
return { error: "Unauthorized to reorder blocks." };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Supabase upsert can be used for batch updates if primary key `id` is included.
|
|
214
|
+
// Or loop through updates (less efficient for many updates but simpler to write without complex SQL).
|
|
215
|
+
const updatePromises = updates.map(update =>
|
|
216
|
+
supabase.from('blocks').update({ order: update.order, updated_at: new Date().toISOString() }).eq('id', update.id)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const results = await Promise.all(updatePromises);
|
|
220
|
+
const errors = results.filter(result => result.error);
|
|
221
|
+
|
|
222
|
+
if (errors.length > 0) {
|
|
223
|
+
console.error("Error updating block orders:", errors.map(e => e.error?.message).join(", "));
|
|
224
|
+
return { error: `Failed to update some block orders: ${errors.map(e => e.error?.message).join(", ")}` };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
|
|
228
|
+
if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
|
|
229
|
+
|
|
230
|
+
return { success: true };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
export async function deleteBlock(blockId: number, pageId?: number | null, postId?: number | null) {
|
|
235
|
+
const supabase = createClient();
|
|
236
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
237
|
+
|
|
238
|
+
if (!user) return { error: "User not authenticated." };
|
|
239
|
+
if (!(await canEditParent(supabase, user.id, pageId, postId))) {
|
|
240
|
+
return { error: "Unauthorized to delete this block." };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// fetch parent and capture previous aggregate
|
|
244
|
+
const { data: existingBlock, error: fetchError } = await supabase
|
|
245
|
+
.from('blocks')
|
|
246
|
+
.select('id, page_id, post_id')
|
|
247
|
+
.eq('id', blockId)
|
|
248
|
+
.single();
|
|
249
|
+
if (fetchError || !existingBlock) {
|
|
250
|
+
return { error: "Block not found." };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let previousAggregate: Awaited<ReturnType<typeof getFullPageContent>> | Awaited<ReturnType<typeof getFullPostContent>> | null = null;
|
|
254
|
+
if (existingBlock.page_id) {
|
|
255
|
+
previousAggregate = await getFullPageContent(existingBlock.page_id);
|
|
256
|
+
} else if (existingBlock.post_id) {
|
|
257
|
+
previousAggregate = await getFullPostContent(existingBlock.post_id);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { error } = await supabase.from("blocks").delete().eq("id", blockId);
|
|
261
|
+
|
|
262
|
+
if (error) {
|
|
263
|
+
console.error("Error deleting block:", error);
|
|
264
|
+
return { error: `Failed to delete block: ${error.message}` };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// create revision after delete
|
|
268
|
+
if (user && previousAggregate) {
|
|
269
|
+
if (existingBlock.page_id) {
|
|
270
|
+
const nextAggregate = await getFullPageContent(existingBlock.page_id, { excludeDeletedBlockId: blockId });
|
|
271
|
+
if (nextAggregate) {
|
|
272
|
+
await createPageRevision(existingBlock.page_id, user.id, previousAggregate, nextAggregate);
|
|
273
|
+
}
|
|
274
|
+
} else if (existingBlock.post_id) {
|
|
275
|
+
const nextAggregate = await getFullPostContent(existingBlock.post_id, { excludeDeletedBlockId: blockId });
|
|
276
|
+
if (nextAggregate) {
|
|
277
|
+
await createPostRevision(existingBlock.post_id, user.id, previousAggregate as any, nextAggregate as any);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
|
|
283
|
+
if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
|
|
284
|
+
|
|
285
|
+
return { success: true };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function copyBlocksFromLanguage(
|
|
289
|
+
parentId: number, // ID of the page or post being edited
|
|
290
|
+
parentType: "page" | "post",
|
|
291
|
+
sourceLanguageId: number,
|
|
292
|
+
targetLanguageId: number, // Language of the current page/post being edited
|
|
293
|
+
targetTranslationGroupId: string
|
|
294
|
+
) {
|
|
295
|
+
"use server";
|
|
296
|
+
const supabase = createClient();
|
|
297
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
298
|
+
|
|
299
|
+
if (!user) {
|
|
300
|
+
return { error: "User not authenticated." };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!(await canEditParent(supabase, user.id, parentType === "page" ? parentId : null, parentType === "post" ? parentId : null))) {
|
|
304
|
+
return { error: "Unauthorized to modify blocks for this target." };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let sourceParentId: number | null = null;
|
|
308
|
+
|
|
309
|
+
// 1. Fetch Source Page/Post ID
|
|
310
|
+
try {
|
|
311
|
+
if (parentType === "page") {
|
|
312
|
+
const { data: sourcePage, error: sourcePageError } = await supabase
|
|
313
|
+
.from("pages")
|
|
314
|
+
.select("id")
|
|
315
|
+
.eq("translation_group_id", targetTranslationGroupId)
|
|
316
|
+
.eq("language_id", sourceLanguageId)
|
|
317
|
+
.single();
|
|
318
|
+
|
|
319
|
+
if (sourcePageError || !sourcePage) {
|
|
320
|
+
console.error("Error fetching source page:", sourcePageError);
|
|
321
|
+
return { error: "Source page not found or error fetching it." };
|
|
322
|
+
}
|
|
323
|
+
sourceParentId = sourcePage.id;
|
|
324
|
+
} else if (parentType === "post") {
|
|
325
|
+
const { data: sourcePost, error: sourcePostError } = await supabase
|
|
326
|
+
.from("posts")
|
|
327
|
+
.select("id")
|
|
328
|
+
.eq("translation_group_id", targetTranslationGroupId)
|
|
329
|
+
.eq("language_id", sourceLanguageId)
|
|
330
|
+
.single();
|
|
331
|
+
|
|
332
|
+
if (sourcePostError || !sourcePost) {
|
|
333
|
+
console.error("Error fetching source post:", sourcePostError);
|
|
334
|
+
return { error: "Source post not found or error fetching it." };
|
|
335
|
+
}
|
|
336
|
+
sourceParentId = sourcePost.id;
|
|
337
|
+
} else {
|
|
338
|
+
return { error: "Invalid parent type specified." };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!sourceParentId) {
|
|
342
|
+
return { error: "Could not determine source parent ID." };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 2. Fetch Blocks from Source
|
|
346
|
+
const { data: sourceBlocks, error: sourceBlocksError } = await supabase
|
|
347
|
+
.from("blocks")
|
|
348
|
+
.select("page_id, post_id, language_id, block_type, content, order") // Select only existing columns
|
|
349
|
+
.eq(parentType === "page" ? "page_id" : "post_id", sourceParentId)
|
|
350
|
+
.order("order", { ascending: true });
|
|
351
|
+
|
|
352
|
+
if (sourceBlocksError) {
|
|
353
|
+
console.error("Error fetching source blocks:", sourceBlocksError);
|
|
354
|
+
return { error: `Failed to fetch blocks from source: ${sourceBlocksError.message}` };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 3. Delete Existing Blocks from Target
|
|
358
|
+
const { error: deleteError } = await supabase
|
|
359
|
+
.from("blocks")
|
|
360
|
+
.delete()
|
|
361
|
+
.eq(parentType === "page" ? "page_id" : "post_id", parentId)
|
|
362
|
+
.eq("language_id", targetLanguageId); // Ensure we only delete for the target language
|
|
363
|
+
|
|
364
|
+
if (deleteError) {
|
|
365
|
+
console.error("Error deleting existing blocks:", deleteError);
|
|
366
|
+
return { error: `Failed to delete existing blocks: ${deleteError.message}` };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 4. Re-create Blocks for Target
|
|
370
|
+
if (sourceBlocks && sourceBlocks.length > 0) {
|
|
371
|
+
const newBlocksToInsert = sourceBlocks.map((block: {
|
|
372
|
+
page_id: number | null;
|
|
373
|
+
post_id: number | null;
|
|
374
|
+
language_id: number;
|
|
375
|
+
block_type: BlockType;
|
|
376
|
+
content: Json;
|
|
377
|
+
order: number;
|
|
378
|
+
}) => ({
|
|
379
|
+
// id, created_at, updated_at will be set by DB
|
|
380
|
+
page_id: parentType === "page" ? parentId : null,
|
|
381
|
+
post_id: parentType === "post" ? parentId : null,
|
|
382
|
+
language_id: targetLanguageId,
|
|
383
|
+
block_type: block.block_type,
|
|
384
|
+
content: block.content, // Directly copy content, which includes any type-specific configs like cols_config
|
|
385
|
+
order: block.order,
|
|
386
|
+
}));
|
|
387
|
+
|
|
388
|
+
const { error: insertError } = await supabase.from("blocks").insert(newBlocksToInsert);
|
|
389
|
+
|
|
390
|
+
if (insertError) {
|
|
391
|
+
console.error("Error re-creating blocks:", insertError);
|
|
392
|
+
return { error: `Failed to re-create blocks: ${insertError.message}` };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 5. Revalidation
|
|
397
|
+
let targetSlug: string | null = null;
|
|
398
|
+
if (parentType === "page") {
|
|
399
|
+
const { data: pageData, error: pageError } = await supabase
|
|
400
|
+
.from("pages")
|
|
401
|
+
.select("slug")
|
|
402
|
+
.eq("id", parentId)
|
|
403
|
+
.single();
|
|
404
|
+
if (pageError || !pageData) {
|
|
405
|
+
console.warn("Could not fetch target page slug for revalidation:", pageError);
|
|
406
|
+
} else {
|
|
407
|
+
targetSlug = pageData.slug;
|
|
408
|
+
if (targetSlug) revalidatePath(`/${targetSlug}`);
|
|
409
|
+
}
|
|
410
|
+
revalidatePath(`/cms/pages/${parentId}/edit`); // Revalidate edit page
|
|
411
|
+
} else if (parentType === "post") {
|
|
412
|
+
const { data: postData, error: postError } = await supabase
|
|
413
|
+
.from("posts")
|
|
414
|
+
.select("slug")
|
|
415
|
+
.eq("id", parentId)
|
|
416
|
+
.single();
|
|
417
|
+
if (postError || !postData) {
|
|
418
|
+
console.warn("Could not fetch target post slug for revalidation:", postError);
|
|
419
|
+
} else {
|
|
420
|
+
targetSlug = postData.slug;
|
|
421
|
+
if (targetSlug) revalidatePath(`/blog/${targetSlug}`);
|
|
422
|
+
}
|
|
423
|
+
revalidatePath(`/cms/posts/${parentId}/edit`); // Revalidate edit page
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
return { success: true, message: "Blocks copied successfully." };
|
|
428
|
+
|
|
429
|
+
} catch (e: unknown) {
|
|
430
|
+
console.error("Unexpected error in copyBlocksFromLanguage:", e);
|
|
431
|
+
const errorMessage = e instanceof Error ? e.message : "An unknown error occurred";
|
|
432
|
+
return { error: `An unexpected error occurred: ${errorMessage}` };
|
|
433
|
+
}
|
|
434
|
+
}
|