create-nextblock 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- package/templates/nextblock-template/tsconfig.json +62 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// apps/nextblock/app/cms/revisions/JsonDiffView.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
import { compare, type Operation } from 'fast-json-patch';
|
|
6
|
+
|
|
7
|
+
interface JsonDiffViewProps {
|
|
8
|
+
oldValue: string;
|
|
9
|
+
newValue: string;
|
|
10
|
+
leftTitle?: string;
|
|
11
|
+
rightTitle?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getByPointer(obj: any, pointer: string): any {
|
|
15
|
+
if (!pointer || pointer === '/') return obj;
|
|
16
|
+
const parts = pointer.split('/').slice(1).map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
17
|
+
let cur = obj;
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (cur == null) return undefined;
|
|
20
|
+
cur = cur[part];
|
|
21
|
+
}
|
|
22
|
+
return cur;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safeStringify(v: any): string {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.stringify(v, null, 2);
|
|
28
|
+
} catch {
|
|
29
|
+
return String(v);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function JsonDiffView({ oldValue, newValue, leftTitle = 'Current', rightTitle = 'Selected' }: JsonDiffViewProps) {
|
|
34
|
+
const { ops, oldObj } = useMemo(() => {
|
|
35
|
+
let a: any = null, b: any = null;
|
|
36
|
+
try { a = JSON.parse(oldValue); } catch { a = oldValue; }
|
|
37
|
+
try { b = JSON.parse(newValue); } catch { b = newValue; }
|
|
38
|
+
const operations: Operation[] = compare(a, b);
|
|
39
|
+
return { ops: operations, oldObj: a };
|
|
40
|
+
}, [oldValue, newValue]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="border rounded">
|
|
44
|
+
<div className="flex items-center justify-between px-3 py-2 border-b text-sm text-muted-foreground">
|
|
45
|
+
<div>{leftTitle}</div>
|
|
46
|
+
<div>{rightTitle}</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="p-3 text-sm space-y-2">
|
|
49
|
+
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
50
|
+
<span className="inline-flex items-center gap-1"><span className="inline-block w-3 h-3 rounded bg-green-200 border border-green-300" /> Current</span>
|
|
51
|
+
<span className="inline-flex items-center gap-1"><span className="inline-block w-3 h-3 rounded bg-red-200 border border-red-300" /> Selected Version</span>
|
|
52
|
+
</div>
|
|
53
|
+
{ops.length === 0 && (
|
|
54
|
+
<div className="text-muted-foreground">No differences.</div>
|
|
55
|
+
)}
|
|
56
|
+
{ops.map((op, idx) => {
|
|
57
|
+
const oldAtPath = op.op !== 'add' ? getByPointer(oldObj, op.path) : undefined;
|
|
58
|
+
const oldStr = op.op !== 'add' ? safeStringify(oldAtPath) : '';
|
|
59
|
+
const newStr = op.op !== 'remove' ? safeStringify((op as any).value) : '';
|
|
60
|
+
return (
|
|
61
|
+
<div key={idx} className="rounded border">
|
|
62
|
+
<div className="px-2 py-1 border-b flex items-center gap-2 text-xs">
|
|
63
|
+
<span className="uppercase tracking-wide font-semibold">{op.op}</span>
|
|
64
|
+
<code className="text-muted-foreground break-all">{op.path || '/'}</code>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="grid grid-cols-1 md:grid-cols-2">
|
|
67
|
+
{op.op !== 'add' && (
|
|
68
|
+
<div className="p-2 border-r md:border-r">
|
|
69
|
+
<div className="text-xs text-muted-foreground mb-1">Current</div>
|
|
70
|
+
<pre className="whitespace-pre-wrap break-words text-green-800 bg-green-50 rounded p-2 m-0">{oldStr}</pre>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
{op.op !== 'remove' && (
|
|
74
|
+
<div className="p-2">
|
|
75
|
+
<div className="text-xs text-muted-foreground mb-1">Selected Version</div>
|
|
76
|
+
<pre className="whitespace-pre-wrap break-words text-red-800 bg-red-50 rounded p-2 m-0">{newStr}</pre>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// apps/nextblock/app/cms/revisions/RevisionHistoryButton.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useEffect, useState, useTransition } from 'react';
|
|
5
|
+
import { Button } from "@nextblock-cms/ui";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@nextblock-cms/ui";
|
|
13
|
+
import { listPageRevisions, listPostRevisions, restorePageVersion, restorePostVersion, comparePageVersion, comparePostVersion } from './actions';
|
|
14
|
+
import { useRouter } from 'next/navigation';
|
|
15
|
+
import JsonDiffView from './JsonDiffView';
|
|
16
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
17
|
+
import { toast } from 'react-hot-toast';
|
|
18
|
+
|
|
19
|
+
type ParentType = 'page' | 'post';
|
|
20
|
+
|
|
21
|
+
interface RevisionHistoryButtonProps {
|
|
22
|
+
parentType: ParentType;
|
|
23
|
+
parentId: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RevisionItem = {
|
|
27
|
+
id: number;
|
|
28
|
+
version: number;
|
|
29
|
+
revision_type: 'snapshot' | 'diff';
|
|
30
|
+
created_at: string;
|
|
31
|
+
author_id: string | null;
|
|
32
|
+
author?: { full_name?: string | null; username?: string | null } | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default function RevisionHistoryButton({ parentType, parentId }: RevisionHistoryButtonProps) {
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
const [revisions, setRevisions] = useState<RevisionItem[] | null>(null);
|
|
39
|
+
const [currentVersion, setCurrentVersion] = useState<number | null>(null);
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
42
|
+
const [isPending, startTransition] = useTransition();
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
|
|
45
|
+
const [compareLoading, setCompareLoading] = useState(false);
|
|
46
|
+
const [activeCompareVersion, setActiveCompareVersion] = useState<number | null>(null);
|
|
47
|
+
const [leftText, setLeftText] = useState<string | null>(null);
|
|
48
|
+
const [rightText, setRightText] = useState<string | null>(null);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!open) return;
|
|
51
|
+
setLoading(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
(async () => {
|
|
54
|
+
try {
|
|
55
|
+
if (parentType === 'page') {
|
|
56
|
+
const res = await listPageRevisions(parentId);
|
|
57
|
+
if ('error' in res) {
|
|
58
|
+
setError(res.error ?? 'Unknown error');
|
|
59
|
+
setRevisions(null);
|
|
60
|
+
setCurrentVersion(null);
|
|
61
|
+
} else {
|
|
62
|
+
setRevisions(res.revisions as unknown as RevisionItem[]);
|
|
63
|
+
setCurrentVersion((res as any).currentVersion ?? null);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const res = await listPostRevisions(parentId);
|
|
67
|
+
if ('error' in res) { setError(res.error ?? 'Unknown error'); setRevisions(null); setCurrentVersion(null); }
|
|
68
|
+
else { setRevisions(res.revisions as unknown as RevisionItem[]); setCurrentVersion((res as any).currentVersion ?? null); }
|
|
69
|
+
}
|
|
70
|
+
} catch (e: unknown) {
|
|
71
|
+
setError(e instanceof Error ? e.message : 'Failed to load revisions');
|
|
72
|
+
} finally {
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
}, [open, parentId, parentType]);
|
|
77
|
+
|
|
78
|
+
const handleRestore = (version: number) => {
|
|
79
|
+
setMessage(null);
|
|
80
|
+
setError(null);
|
|
81
|
+
startTransition(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const res = parentType === 'page'
|
|
84
|
+
? await restorePageVersion(parentId, version)
|
|
85
|
+
: await restorePostVersion(parentId, version);
|
|
86
|
+
if ('error' in res) {
|
|
87
|
+
setError(res.error ?? 'Unknown error');
|
|
88
|
+
toast.error(res.error ?? 'Failed to restore version');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setMessage('Version restored successfully.');
|
|
92
|
+
toast.success('Version restored successfully');
|
|
93
|
+
// refresh current page to fetch restored content
|
|
94
|
+
router.refresh();
|
|
95
|
+
// Close the dialog after a short delay
|
|
96
|
+
setTimeout(() => setOpen(false), 800);
|
|
97
|
+
} catch (e: unknown) {
|
|
98
|
+
setError(e instanceof Error ? e.message : 'Failed to restore version');
|
|
99
|
+
toast.error(e instanceof Error ? e.message : 'Failed to restore version');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type CompareResult = { success: true; current: unknown; target: unknown } | { error: string };
|
|
105
|
+
const handleCompare = async (version: number) => {
|
|
106
|
+
setError(null);
|
|
107
|
+
setMessage(null);
|
|
108
|
+
setActiveCompareVersion(version);
|
|
109
|
+
setCompareLoading(true);
|
|
110
|
+
try {
|
|
111
|
+
const res: CompareResult = parentType === 'page'
|
|
112
|
+
? await comparePageVersion(parentId, version)
|
|
113
|
+
: await comparePostVersion(parentId, version);
|
|
114
|
+
if ('error' in res) {
|
|
115
|
+
setError(res.error);
|
|
116
|
+
setLeftText(null);
|
|
117
|
+
setRightText(null);
|
|
118
|
+
} else {
|
|
119
|
+
const left = JSON.stringify(res.current, null, 2);
|
|
120
|
+
const right = JSON.stringify(res.target, null, 2);
|
|
121
|
+
setLeftText(left);
|
|
122
|
+
setRightText(right);
|
|
123
|
+
}
|
|
124
|
+
} catch (e: unknown) {
|
|
125
|
+
setError(e instanceof Error ? e.message : 'Failed to load comparison');
|
|
126
|
+
} finally {
|
|
127
|
+
setCompareLoading(false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
133
|
+
<Button variant="outline" onClick={() => setOpen(true)}>Revision History</Button>
|
|
134
|
+
<DialogContent className="max-w-2xl h-[95vh] overflow-y-auto">
|
|
135
|
+
<DialogHeader>
|
|
136
|
+
<DialogTitle>Revision History</DialogTitle>
|
|
137
|
+
<DialogDescription>
|
|
138
|
+
Browse and restore previous versions.
|
|
139
|
+
</DialogDescription>
|
|
140
|
+
</DialogHeader>
|
|
141
|
+
<div className="space-y-3">
|
|
142
|
+
{loading && <div className="text-sm text-muted-foreground">Loading revisions…</div>}
|
|
143
|
+
{error && <div className="text-sm text-red-600">{error}</div>}
|
|
144
|
+
{message && <div className="text-sm text-green-600">{message}</div>}
|
|
145
|
+
|
|
146
|
+
{(!loading && revisions && revisions.length === 0) && (
|
|
147
|
+
<div className="text-sm text-muted-foreground">No revisions yet.</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{revisions && revisions.length > 0 && (
|
|
151
|
+
<div className="divide-y rounded border">
|
|
152
|
+
{revisions.map((rev: RevisionItem) => {
|
|
153
|
+
const when = rev.created_at ? formatDistanceToNow(new Date(rev.created_at), { addSuffix: true }) : '';
|
|
154
|
+
const who = rev.author?.full_name || rev.author?.username || rev.author_id || 'Unknown';
|
|
155
|
+
return (
|
|
156
|
+
<div key={rev.id} className="flex items-center justify-between gap-4 p-3">
|
|
157
|
+
<div className="min-w-0">
|
|
158
|
+
<div className="font-medium flex items-center gap-2">
|
|
159
|
+
<span>Version {rev.version}</span>
|
|
160
|
+
<span className="text-xs text-muted-foreground">({rev.revision_type})</span>
|
|
161
|
+
{currentVersion != null && rev.version === currentVersion && (
|
|
162
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-200">Current</span>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
<div className="text-xs text-muted-foreground truncate">{when} • {who}</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="flex gap-2">
|
|
168
|
+
<Button variant="secondary" size="sm" onClick={() => handleCompare(rev.version)} disabled={compareLoading && activeCompareVersion === rev.version}>
|
|
169
|
+
{compareLoading && activeCompareVersion === rev.version ? 'Loading…' : 'Compare'}
|
|
170
|
+
</Button>
|
|
171
|
+
<Button size="sm" onClick={() => handleRestore(rev.version)} disabled={isPending}>
|
|
172
|
+
{isPending ? 'Restoring…' : 'Restore'}
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
{(activeCompareVersion !== null) && (
|
|
181
|
+
<div className="mt-4">
|
|
182
|
+
<div className="mb-2 flex items-center justify-between">
|
|
183
|
+
<div className="font-semibold">Comparing Version {activeCompareVersion} to Current</div>
|
|
184
|
+
<Button variant="outline" size="sm" onClick={() => { setActiveCompareVersion(null); setLeftText(null); setRightText(null); }}>Close Compare</Button>
|
|
185
|
+
</div>
|
|
186
|
+
{compareLoading && <div className="text-sm text-muted-foreground">Preparing diff…</div>}
|
|
187
|
+
{!compareLoading && leftText && rightText && (
|
|
188
|
+
<JsonDiffView
|
|
189
|
+
oldValue={leftText}
|
|
190
|
+
newValue={rightText}
|
|
191
|
+
leftTitle="Current (Now)"
|
|
192
|
+
rightTitle={`Version ${activeCompareVersion}`}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</DialogContent>
|
|
199
|
+
</Dialog>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// apps/nextblock/app/cms/revisions/actions.ts
|
|
2
|
+
"use server";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
+
import { restorePageToVersion, restorePostToVersion, reconstructPageVersionContent, reconstructPostVersionContent } from './service';
|
|
6
|
+
import { getFullPageContent, getFullPostContent } from './utils';
|
|
7
|
+
|
|
8
|
+
type RevisionListItem = {
|
|
9
|
+
id: number;
|
|
10
|
+
version: number;
|
|
11
|
+
revision_type: 'snapshot' | 'diff';
|
|
12
|
+
created_at: string;
|
|
13
|
+
author_id: string | null;
|
|
14
|
+
author?: { full_name?: string | null; username?: string | null } | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function listPageRevisions(pageId: number) {
|
|
18
|
+
const supabase = createClient();
|
|
19
|
+
const { data, error } = await supabase
|
|
20
|
+
.from('page_revisions')
|
|
21
|
+
.select('id, page_id, author_id, version, revision_type, created_at, author:profiles(full_name, username)')
|
|
22
|
+
.eq('page_id', pageId)
|
|
23
|
+
.order('version', { ascending: false });
|
|
24
|
+
if (error) return { error: error.message } as const;
|
|
25
|
+
const { data: pageRow } = await supabase.from('pages').select('version').eq('id', pageId).single();
|
|
26
|
+
const currentVersion = pageRow?.version ?? null;
|
|
27
|
+
return { success: true as const, revisions: data as RevisionListItem[], currentVersion };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function listPostRevisions(postId: number) {
|
|
31
|
+
const supabase = createClient();
|
|
32
|
+
const { data, error } = await supabase
|
|
33
|
+
.from('post_revisions')
|
|
34
|
+
.select('id, post_id, author_id, version, revision_type, created_at, author:profiles(full_name, username)')
|
|
35
|
+
.eq('post_id', postId)
|
|
36
|
+
.order('version', { ascending: false });
|
|
37
|
+
if (error) return { error: error.message } as const;
|
|
38
|
+
const { data: postRow } = await supabase.from('posts').select('version').eq('id', postId).single();
|
|
39
|
+
const currentVersion = postRow?.version ?? null;
|
|
40
|
+
return { success: true as const, revisions: data as RevisionListItem[], currentVersion };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function restorePageVersion(pageId: number, targetVersion: number) {
|
|
44
|
+
const supabase = createClient();
|
|
45
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
46
|
+
if (!user) return { error: 'User not authenticated.' } as const;
|
|
47
|
+
// Role checks are enforced by RLS; we can still short-circuit if needed
|
|
48
|
+
return await restorePageToVersion(pageId, targetVersion, user.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function restorePostVersion(postId: number, targetVersion: number) {
|
|
52
|
+
const supabase = createClient();
|
|
53
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
54
|
+
if (!user) return { error: 'User not authenticated.' } as const;
|
|
55
|
+
return await restorePostToVersion(postId, targetVersion, user.id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
import type { FullPageContent, FullPostContent } from './utils';
|
|
59
|
+
|
|
60
|
+
type CompareResponse<T> = { success: true; current: T; target: T } | { error: string };
|
|
61
|
+
|
|
62
|
+
export async function comparePageVersion(pageId: number, targetVersion: number): Promise<CompareResponse<FullPageContent>> {
|
|
63
|
+
const supabase = createClient();
|
|
64
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
65
|
+
if (!user) return { error: 'User not authenticated.' } as const;
|
|
66
|
+
|
|
67
|
+
const current = await getFullPageContent(pageId);
|
|
68
|
+
if (!current) return { error: 'Failed to fetch current content.' } as const;
|
|
69
|
+
const reconstructed = await reconstructPageVersionContent(pageId, targetVersion);
|
|
70
|
+
if ('error' in reconstructed) return { error: reconstructed.error ?? 'Unknown error occurred while reconstructing page version' } as const;
|
|
71
|
+
return { success: true as const, current, target: reconstructed.content };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function comparePostVersion(postId: number, targetVersion: number): Promise<CompareResponse<FullPostContent>> {
|
|
75
|
+
const supabase = createClient();
|
|
76
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
77
|
+
if (!user) return { error: 'User not authenticated.' } as const;
|
|
78
|
+
|
|
79
|
+
const current = await getFullPostContent(postId);
|
|
80
|
+
if (!current) return { error: 'Failed to fetch current content.' } as const;
|
|
81
|
+
const reconstructed = await reconstructPostVersionContent(postId, targetVersion);
|
|
82
|
+
if ('error' in reconstructed) return { error: reconstructed.error ?? 'Unknown error occurred while reconstructing post version' } as const;
|
|
83
|
+
return { success: true as const, current, target: reconstructed.content };
|
|
84
|
+
}
|