create-nextblock 0.2.33 → 0.2.35

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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
  3. package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
  4. package/templates/nextblock-template/app/cms/blocks/actions.ts +5 -5
  5. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -350
  6. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +13 -16
  7. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
  8. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +24 -42
  9. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +6 -6
  10. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +35 -56
  11. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
  12. package/templates/nextblock-template/app/cms/media/actions.ts +12 -12
  13. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
  14. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
  15. package/templates/nextblock-template/app/cms/media/page.tsx +120 -120
  16. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -87
  17. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +10 -16
  18. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
  19. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
  20. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +0 -1
  21. package/templates/nextblock-template/app/providers.tsx +2 -2
  22. package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
  23. package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
  24. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  25. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
  26. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
  27. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
  28. package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
  29. package/templates/nextblock-template/eslint.config.mjs +35 -37
  30. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +10 -10
  31. package/templates/nextblock-template/next-env.d.ts +6 -6
  32. package/templates/nextblock-template/package.json +1 -1
@@ -1,344 +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, type Operation } 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 unknown as Operation[];
124
- const result = applyPatch(content as unknown as Record<string, unknown>, 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 unknown as Operation[];
212
- const result = applyPatch(content as unknown as Record<string, unknown>, 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 unknown as Operation[];
300
- const result = applyPatch(content as unknown as Record<string, unknown>, 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 unknown as Operation[];
337
- const result = applyPatch(content as unknown as Record<string, unknown>, 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
- }
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
+ }