create-nextblock 0.2.45 → 0.2.47
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/package.json +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +45 -27
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +13 -3
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +11 -4
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
- package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
- package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
- package/templates/nextblock-template/components/BlockRenderer.tsx +14 -1
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +126 -0
- package/templates/nextblock-template/docs/How to Create a Custom Block.md +149 -0
- package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +196 -603
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/tsconfig.json +3 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -0
package/package.json
CHANGED
|
@@ -143,33 +143,51 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
143
143
|
let SelectedEditor: React.ComponentType<any> | null = null;
|
|
144
144
|
|
|
145
145
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
// Check block registry for editor component
|
|
147
|
+
const blockDef = getBlockDefinition(blockType);
|
|
148
|
+
|
|
149
|
+
if (blockDef?.EditorComponent) {
|
|
150
|
+
SelectedEditor = blockDef.EditorComponent;
|
|
151
|
+
} else if (blockDef?.editorComponentFilename) {
|
|
152
|
+
// We can't easily do dynamic imports with variable paths inside this useEffect
|
|
153
|
+
// without potentially breaking webpack analysis or needing a different strategy.
|
|
154
|
+
// However, for the core blocks, we have the pre-defined dynamic imports below.
|
|
155
|
+
|
|
156
|
+
switch (blockType) {
|
|
157
|
+
case 'text':
|
|
158
|
+
SelectedEditor = DynamicTextBlockEditor;
|
|
159
|
+
break;
|
|
160
|
+
case 'heading':
|
|
161
|
+
SelectedEditor = DynamicHeadingBlockEditor;
|
|
162
|
+
break;
|
|
163
|
+
case 'image':
|
|
164
|
+
SelectedEditor = DynamicImageBlockEditor;
|
|
165
|
+
break;
|
|
166
|
+
case 'button':
|
|
167
|
+
SelectedEditor = DynamicButtonBlockEditor;
|
|
168
|
+
break;
|
|
169
|
+
case 'posts_grid':
|
|
170
|
+
SelectedEditor = DynamicPostsGridBlockEditor;
|
|
171
|
+
break;
|
|
172
|
+
case 'video_embed':
|
|
173
|
+
SelectedEditor = DynamicVideoEmbedBlockEditor;
|
|
174
|
+
break;
|
|
175
|
+
case 'section':
|
|
176
|
+
SelectedEditor = DynamicSectionBlockEditor;
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
// Fallback for custom blocks that might use file-based routing but aren't in the switch
|
|
180
|
+
// This might still fail if webpack hasn't bundled them, but it's worth a try or we need to explicitly add them.
|
|
181
|
+
// For the PoC Testimonial block, it has EditorComponent so it hits the first if.
|
|
182
|
+
console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
|
|
183
|
+
alert(`Error: Editor not configured for ${blockType}.`);
|
|
184
|
+
setEditingNestedBlockInfo(null);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
console.warn(`No definition found for nested block type: ${blockType}`);
|
|
189
|
+
setEditingNestedBlockInfo(null);
|
|
190
|
+
return;
|
|
173
191
|
}
|
|
174
192
|
setNestedBlockEditorComponent(() => SelectedEditor);
|
|
175
193
|
setTempNestedBlockContent(JSON.parse(JSON.stringify(editingNestedBlockInfo.blockData.content)));
|
|
@@ -33,7 +33,7 @@ type BlockEditorModalProps = {
|
|
|
33
33
|
isOpen: boolean;
|
|
34
34
|
onClose: () => void;
|
|
35
35
|
onSave: (updatedContent: unknown) => void;
|
|
36
|
-
EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown
|
|
36
|
+
EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>> | ComponentType<BlockEditorProps<unknown>>;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
export function BlockEditorModal({
|
|
@@ -121,7 +121,7 @@ type EditingBlock = ColumnBlock & { index: number };
|
|
|
121
121
|
export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
|
|
122
122
|
const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
|
|
123
123
|
const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
|
|
124
|
-
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | null>(null);
|
|
124
|
+
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | React.ComponentType<any> | null>(null);
|
|
125
125
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
|
126
126
|
const [blockToDeleteIndex, setBlockToDeleteIndex] = useState<number | null>(null);
|
|
127
127
|
|
|
@@ -169,8 +169,18 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
169
169
|
|
|
170
170
|
const handleStartEdit = (block: ColumnBlock, index: number) => {
|
|
171
171
|
const blockDef = getBlockDefinition(block.block_type);
|
|
172
|
-
if (blockDef
|
|
173
|
-
|
|
172
|
+
if (!blockDef) {
|
|
173
|
+
console.error(`No definition found for block type: ${block.block_type}`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (blockDef.EditorComponent) {
|
|
178
|
+
const Component = blockDef.EditorComponent;
|
|
179
|
+
setLazyEditor(() => Component);
|
|
180
|
+
setEditingBlock({ ...block, index });
|
|
181
|
+
} else if (blockDef.editorComponentFilename) {
|
|
182
|
+
const filename = blockDef.editorComponentFilename;
|
|
183
|
+
const Editor = lazy(() => import(`../editors/${filename.replace(/\.tsx$/, '')}`));
|
|
174
184
|
setLazyEditor(Editor);
|
|
175
185
|
setEditingBlock({ ...block, index });
|
|
176
186
|
} else {
|
|
@@ -34,7 +34,7 @@ export default function EditableBlock({
|
|
|
34
34
|
// Move all hooks to the top before any conditional returns
|
|
35
35
|
const [isConfigPanelOpen, setIsConfigPanelOpen] = useState(false);
|
|
36
36
|
const [editingBlock, setEditingBlock] = useState<Block | null>(null);
|
|
37
|
-
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<any>> | null>(null);
|
|
37
|
+
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<any>> | ComponentType<any> | null>(null);
|
|
38
38
|
|
|
39
39
|
const SectionEditor = useMemo(() => {
|
|
40
40
|
if (block?.block_type === 'section' || block?.block_type === 'hero') {
|
|
@@ -58,14 +58,21 @@ export default function EditableBlock({
|
|
|
58
58
|
if (block.block_type === 'section' || block.block_type === 'hero') {
|
|
59
59
|
setIsConfigPanelOpen(prev => !prev);
|
|
60
60
|
} else {
|
|
61
|
-
const
|
|
61
|
+
const blockDef = getBlockDefinition(block.block_type as BlockType);
|
|
62
|
+
|
|
62
63
|
if (block.block_type === 'posts_grid') {
|
|
63
64
|
const LazifiedPostsGridEditor = lazy(() => Promise.resolve({ default: PostsGridBlockEditor }));
|
|
64
65
|
setLazyEditor(LazifiedPostsGridEditor);
|
|
65
66
|
setEditingBlock(block);
|
|
66
67
|
}
|
|
67
|
-
else if (
|
|
68
|
-
const
|
|
68
|
+
else if (blockDef?.EditorComponent) {
|
|
69
|
+
const Component = blockDef.EditorComponent;
|
|
70
|
+
setLazyEditor(() => Component);
|
|
71
|
+
setEditingBlock(block);
|
|
72
|
+
}
|
|
73
|
+
else if (blockDef?.editorComponentFilename) {
|
|
74
|
+
const filename = blockDef.editorComponentFilename;
|
|
75
|
+
const Editor = lazy(() => import(`../editors/${filename.replace(/\.tsx$/, '')}`));
|
|
69
76
|
setLazyEditor(Editor);
|
|
70
77
|
setEditingBlock(block);
|
|
71
78
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
5
|
+
|
|
6
|
+
export type DashboardStats = {
|
|
7
|
+
totalPages: number;
|
|
8
|
+
totalPosts: number;
|
|
9
|
+
totalUsers: number;
|
|
10
|
+
recentContent: {
|
|
11
|
+
type: 'post' | 'page';
|
|
12
|
+
title: string;
|
|
13
|
+
author: string;
|
|
14
|
+
date: string;
|
|
15
|
+
status: string;
|
|
16
|
+
}[];
|
|
17
|
+
scheduledContent: {
|
|
18
|
+
title: string;
|
|
19
|
+
date: string;
|
|
20
|
+
type: string;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function getDashboardStats(): Promise<DashboardStats> {
|
|
25
|
+
const supabase = createClient();
|
|
26
|
+
const now = new Date().toISOString();
|
|
27
|
+
|
|
28
|
+
// Parallelize queries
|
|
29
|
+
const [
|
|
30
|
+
{ count: totalPages },
|
|
31
|
+
{ count: totalPosts },
|
|
32
|
+
{ count: totalUsers },
|
|
33
|
+
{ data: recentPosts },
|
|
34
|
+
{ data: recentPages },
|
|
35
|
+
{ data: scheduledPosts }
|
|
36
|
+
] = await Promise.all([
|
|
37
|
+
supabase.from('pages').select('*', { count: 'exact', head: true }),
|
|
38
|
+
supabase.from('posts').select('*', { count: 'exact', head: true }),
|
|
39
|
+
supabase.from('profiles').select('*', { count: 'exact', head: true }),
|
|
40
|
+
|
|
41
|
+
// Recent Posts
|
|
42
|
+
supabase.from('posts')
|
|
43
|
+
.select('title, status, updated_at, created_at, profiles(full_name)')
|
|
44
|
+
.order('updated_at', { ascending: false })
|
|
45
|
+
.limit(5),
|
|
46
|
+
|
|
47
|
+
// Recent Pages
|
|
48
|
+
supabase.from('pages')
|
|
49
|
+
.select('title, status, updated_at, created_at')
|
|
50
|
+
.order('updated_at', { ascending: false })
|
|
51
|
+
.limit(5),
|
|
52
|
+
|
|
53
|
+
// Scheduled Posts (published_at > now)
|
|
54
|
+
supabase.from('posts')
|
|
55
|
+
.select('title, published_at')
|
|
56
|
+
.gt('published_at', now)
|
|
57
|
+
.order('published_at', { ascending: true })
|
|
58
|
+
.limit(5)
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// Process Recent Content
|
|
62
|
+
const combinedRecent = [
|
|
63
|
+
...(recentPosts?.map((p: any) => ({
|
|
64
|
+
type: 'post' as const,
|
|
65
|
+
title: p.title,
|
|
66
|
+
author: p.profiles?.full_name || 'Unknown',
|
|
67
|
+
date: p.updated_at || p.created_at,
|
|
68
|
+
status: p.status
|
|
69
|
+
})) || []),
|
|
70
|
+
...(recentPages?.map((p: any) => ({
|
|
71
|
+
type: 'page' as const,
|
|
72
|
+
title: p.title,
|
|
73
|
+
author: 'System', // Pages don't always track author in this schema
|
|
74
|
+
date: p.updated_at || p.created_at,
|
|
75
|
+
status: p.status
|
|
76
|
+
})) || [])
|
|
77
|
+
].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
78
|
+
.slice(0, 5)
|
|
79
|
+
.map(item => ({
|
|
80
|
+
...item,
|
|
81
|
+
date: formatDistanceToNow(new Date(item.date), { addSuffix: true })
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Process Scheduled Content
|
|
85
|
+
const processedScheduled = (scheduledPosts || []).map((p: any) => ({
|
|
86
|
+
title: p.title,
|
|
87
|
+
date: new Date(p.published_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }),
|
|
88
|
+
type: 'Post'
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
totalPages: totalPages || 0,
|
|
93
|
+
totalPosts: totalPosts || 0,
|
|
94
|
+
totalUsers: totalUsers || 0,
|
|
95
|
+
recentContent: combinedRecent,
|
|
96
|
+
scheduledContent: processedScheduled
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@nextblock-cms/ui"
|
|
2
|
-
import { Calendar, FileText, PenTool, Users,
|
|
2
|
+
import { Calendar, FileText, PenTool, Users, Eye } from "lucide-react"
|
|
3
|
+
import { getDashboardStats } from "./actions"
|
|
4
|
+
|
|
5
|
+
export default async function CmsDashboardPage() {
|
|
6
|
+
const stats = await getDashboardStats();
|
|
3
7
|
|
|
4
|
-
export default function CmsDashboardPage() {
|
|
5
8
|
return (
|
|
6
9
|
<div className="w-full space-y-6">
|
|
7
10
|
<div>
|
|
@@ -17,13 +20,10 @@ export default function CmsDashboardPage() {
|
|
|
17
20
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
|
18
21
|
</CardHeader>
|
|
19
22
|
<CardContent>
|
|
20
|
-
<div className="text-2xl font-bold">
|
|
23
|
+
<div className="text-2xl font-bold">{stats.totalPages}</div>
|
|
21
24
|
<p className="text-xs text-muted-foreground mt-1">
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
12%
|
|
25
|
-
</span>{" "}
|
|
26
|
-
from last month
|
|
25
|
+
{/* Trend data would require historical data, keeping static or hiding for now */}
|
|
26
|
+
<span className="text-muted-foreground opacity-50">Total pages on site</span>
|
|
27
27
|
</p>
|
|
28
28
|
</CardContent>
|
|
29
29
|
</Card>
|
|
@@ -34,13 +34,9 @@ export default function CmsDashboardPage() {
|
|
|
34
34
|
<PenTool className="h-4 w-4 text-muted-foreground" />
|
|
35
35
|
</CardHeader>
|
|
36
36
|
<CardContent>
|
|
37
|
-
<div className="text-2xl font-bold">
|
|
38
|
-
|
|
39
|
-
<span className="text-
|
|
40
|
-
<ArrowUpRight className="h-3 w-3 mr-1" />
|
|
41
|
-
8%
|
|
42
|
-
</span>{" "}
|
|
43
|
-
from last month
|
|
37
|
+
<div className="text-2xl font-bold">{stats.totalPosts}</div>
|
|
38
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
39
|
+
<span className="text-muted-foreground opacity-50">Total blog posts</span>
|
|
44
40
|
</p>
|
|
45
41
|
</CardContent>
|
|
46
42
|
</Card>
|
|
@@ -51,30 +47,22 @@ export default function CmsDashboardPage() {
|
|
|
51
47
|
<Eye className="h-4 w-4 text-muted-foreground" />
|
|
52
48
|
</CardHeader>
|
|
53
49
|
<CardContent>
|
|
54
|
-
<div className="text-2xl font-bold"
|
|
50
|
+
<div className="text-2xl font-bold">--</div> {/* Placeholder */}
|
|
55
51
|
<p className="text-xs text-muted-foreground mt-1">
|
|
56
|
-
|
|
57
|
-
<ArrowUpRight className="h-3 w-3 mr-1" />
|
|
58
|
-
24%
|
|
59
|
-
</span>{" "}
|
|
60
|
-
from last month
|
|
52
|
+
Analytics Integration Coming Soon
|
|
61
53
|
</p>
|
|
62
54
|
</CardContent>
|
|
63
55
|
</Card>
|
|
64
56
|
|
|
65
57
|
<Card>
|
|
66
58
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
67
|
-
<CardTitle className="text-sm font-medium">
|
|
59
|
+
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
|
|
68
60
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
69
61
|
</CardHeader>
|
|
70
62
|
<CardContent>
|
|
71
|
-
<div className="text-2xl font-bold">
|
|
72
|
-
|
|
73
|
-
<span className="text-
|
|
74
|
-
<ArrowDownRight className="h-3 w-3 mr-1" />
|
|
75
|
-
3%
|
|
76
|
-
</span>{" "}
|
|
77
|
-
from last month
|
|
63
|
+
<div className="text-2xl font-bold">{stats.totalUsers}</div>
|
|
64
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
65
|
+
<span className="text-muted-foreground opacity-50">Registered profiles</span>
|
|
78
66
|
</p>
|
|
79
67
|
</CardContent>
|
|
80
68
|
</Card>
|
|
@@ -89,30 +77,34 @@ export default function CmsDashboardPage() {
|
|
|
89
77
|
</CardHeader>
|
|
90
78
|
<CardContent>
|
|
91
79
|
<div className="space-y-4">
|
|
92
|
-
{recentContent.
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<div className="flex-1">
|
|
100
|
-
<h4 className="text-sm font-medium">{item.title}</h4>
|
|
101
|
-
<div className="flex items-center gap-2 mt-1">
|
|
102
|
-
<p className="text-xs text-muted-foreground">{item.author}</p>
|
|
103
|
-
<span className="text-xs text-muted-foreground">•</span>
|
|
104
|
-
<p className="text-xs text-muted-foreground">{item.date}</p>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
<div className="flex items-center">
|
|
108
|
-
<span
|
|
109
|
-
className={`text-xs px-2 py-1 rounded-full ${item.status === "Published" ? "bg-emerald-100 text-emerald-700" : "bg-amber-100 text-amber-700"} dark:bg-opacity-20`}
|
|
80
|
+
{stats.recentContent.length === 0 ? (
|
|
81
|
+
<p className="text-sm text-muted-foreground">No recent content found.</p>
|
|
82
|
+
) : (
|
|
83
|
+
stats.recentContent.map((item, index) => (
|
|
84
|
+
<div key={index} className="flex items-start gap-4 pb-4 border-b last:border-0 last:pb-0">
|
|
85
|
+
<div
|
|
86
|
+
className={`w-8 h-8 rounded-full flex items-center justify-center ${item.type === "page" ? "bg-blue-100 text-blue-600" : "bg-amber-100 text-amber-600"} dark:bg-opacity-20`}
|
|
110
87
|
>
|
|
111
|
-
{item.
|
|
112
|
-
</
|
|
88
|
+
{item.type === "page" ? <FileText className="h-4 w-4" /> : <PenTool className="h-4 w-4" />}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex-1">
|
|
91
|
+
<h4 className="text-sm font-medium">{item.title}</h4>
|
|
92
|
+
<div className="flex items-center gap-2 mt-1">
|
|
93
|
+
<p className="text-xs text-muted-foreground">{item.author}</p>
|
|
94
|
+
<span className="text-xs text-muted-foreground">•</span>
|
|
95
|
+
<p className="text-xs text-muted-foreground">{item.date}</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex items-center">
|
|
99
|
+
<span
|
|
100
|
+
className={`text-xs px-2 py-1 rounded-full ${item.status === "published" ? "bg-emerald-100 text-emerald-700" : "bg-zinc-100 text-zinc-700"} dark:bg-opacity-20 uppercase`}
|
|
101
|
+
>
|
|
102
|
+
{item.status}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
113
105
|
</div>
|
|
114
|
-
|
|
115
|
-
)
|
|
106
|
+
))
|
|
107
|
+
)}
|
|
116
108
|
</div>
|
|
117
109
|
</CardContent>
|
|
118
110
|
</Card>
|
|
@@ -124,26 +116,31 @@ export default function CmsDashboardPage() {
|
|
|
124
116
|
</CardHeader>
|
|
125
117
|
<CardContent>
|
|
126
118
|
<div className="space-y-4">
|
|
127
|
-
{scheduledContent.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<div className="flex items-center
|
|
136
|
-
<Calendar className="h-
|
|
137
|
-
|
|
119
|
+
{stats.scheduledContent.length === 0 ? (
|
|
120
|
+
<div className="flex flex-col items-center justify-center py-8 text-center text-muted-foreground">
|
|
121
|
+
<Calendar className="h-8 w-8 mb-2 opacity-20" />
|
|
122
|
+
<p>No content scheduled.</p>
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
stats.scheduledContent.map((item, index) => (
|
|
126
|
+
<div key={index} className="flex items-start gap-4 pb-4 border-b last:border-0 last:pb-0">
|
|
127
|
+
<div className="w-10 h-10 rounded bg-slate-100 dark:bg-slate-800 flex flex-col items-center justify-center text-center">
|
|
128
|
+
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex-1">
|
|
131
|
+
<h4 className="text-sm font-medium">{item.title}</h4>
|
|
132
|
+
<div className="flex items-center gap-2 mt-1">
|
|
133
|
+
<p className="text-xs text-muted-foreground">{item.date}</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="flex items-center">
|
|
137
|
+
<span className="text-xs px-2 py-1 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300">
|
|
138
|
+
{item.type}
|
|
139
|
+
</span>
|
|
138
140
|
</div>
|
|
139
141
|
</div>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
{item.type}
|
|
143
|
-
</span>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
))}
|
|
142
|
+
))
|
|
143
|
+
)}
|
|
147
144
|
</div>
|
|
148
145
|
</CardContent>
|
|
149
146
|
</Card>
|
|
@@ -153,95 +150,21 @@ export default function CmsDashboardPage() {
|
|
|
153
150
|
<Card>
|
|
154
151
|
<CardHeader>
|
|
155
152
|
<CardTitle>Traffic Overview</CardTitle>
|
|
156
|
-
<CardDescription>Page views over the last 30 days</CardDescription>
|
|
153
|
+
<CardDescription>Page views over the last 30 days (Mock Data)</CardDescription>
|
|
157
154
|
</CardHeader>
|
|
158
155
|
<CardContent>
|
|
159
|
-
<div className="h-[200px] flex items-end gap-2">
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<div
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
))}
|
|
156
|
+
<div className="h-[200px] flex items-end gap-2 align-bottom">
|
|
157
|
+
{/* Mock chart bars */}
|
|
158
|
+
{[20, 45, 30, 80, 55, 40, 60, 30, 70, 45, 25, 65, 50, 40].map((h, i) => (
|
|
159
|
+
<div key={i} className="flex-1 bg-primary/20 hover:bg-primary/40 transition-colors rounded-t-sm relative group" style={{ height: `${h}%` }}>
|
|
160
|
+
<div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-black text-white text-[10px] py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity">
|
|
161
|
+
{h * 10} views
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
169
165
|
</div>
|
|
170
166
|
</CardContent>
|
|
171
167
|
</Card>
|
|
172
168
|
</div>
|
|
173
169
|
)
|
|
174
170
|
}
|
|
175
|
-
|
|
176
|
-
// Sample data
|
|
177
|
-
const recentContent = [
|
|
178
|
-
{
|
|
179
|
-
title: "Homepage Redesign",
|
|
180
|
-
type: "page",
|
|
181
|
-
author: "John Smith",
|
|
182
|
-
date: "Today, 2:30 PM",
|
|
183
|
-
status: "Published",
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
title: "New Product Launch",
|
|
187
|
-
type: "post",
|
|
188
|
-
author: "Sarah Johnson",
|
|
189
|
-
date: "Yesterday, 10:15 AM",
|
|
190
|
-
status: "Published",
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
title: "Q2 Marketing Strategy",
|
|
194
|
-
type: "post",
|
|
195
|
-
author: "Michael Brown",
|
|
196
|
-
date: "May 12, 2023",
|
|
197
|
-
status: "Draft",
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
title: "About Us Page",
|
|
201
|
-
type: "page",
|
|
202
|
-
author: "John Smith",
|
|
203
|
-
date: "May 10, 2023",
|
|
204
|
-
status: "Published",
|
|
205
|
-
},
|
|
206
|
-
]
|
|
207
|
-
|
|
208
|
-
const scheduledContent = [
|
|
209
|
-
{
|
|
210
|
-
title: "Summer Sale Announcement",
|
|
211
|
-
month: "May",
|
|
212
|
-
day: "20",
|
|
213
|
-
time: "9:00 AM",
|
|
214
|
-
type: "Post",
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
title: "New Feature Release",
|
|
218
|
-
month: "May",
|
|
219
|
-
day: "22",
|
|
220
|
-
time: "12:00 PM",
|
|
221
|
-
type: "Page",
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
title: "Customer Testimonials",
|
|
225
|
-
month: "May",
|
|
226
|
-
day: "25",
|
|
227
|
-
time: "3:30 PM",
|
|
228
|
-
type: "Post",
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
title: "Team Page Update",
|
|
232
|
-
month: "May",
|
|
233
|
-
day: "28",
|
|
234
|
-
time: "10:00 AM",
|
|
235
|
-
type: "Page",
|
|
236
|
-
},
|
|
237
|
-
]
|
|
238
|
-
|
|
239
|
-
const analyticsData = [
|
|
240
|
-
{ label: "1", value: 20 },
|
|
241
|
-
{ label: "5", value: 40 },
|
|
242
|
-
{ label: "10", value: 35 },
|
|
243
|
-
{ label: "15", value: 50 },
|
|
244
|
-
{ label: "20", value: 30 },
|
|
245
|
-
{ label: "25", value: 80 },
|
|
246
|
-
{ label: "30", value: 60 },
|
|
247
|
-
]
|
|
@@ -5,11 +5,13 @@ import React, { useState, useTransition, useEffect } from 'react';
|
|
|
5
5
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
6
6
|
import Image from 'next/image';
|
|
7
7
|
import { Button } from '@nextblock-cms/ui';
|
|
8
|
+
import { Spinner, Alert, AlertDescription } from '@nextblock-cms/ui';
|
|
8
9
|
import { Input } from '@nextblock-cms/ui';
|
|
9
10
|
import { Label } from '@nextblock-cms/ui';
|
|
10
11
|
import { Textarea } from '@nextblock-cms/ui';
|
|
11
12
|
import type { Database } from '@nextblock-cms/db';
|
|
12
13
|
import { useAuth } from '@/context/AuthContext';
|
|
14
|
+
import { useHotkeys } from '@/hooks/use-hotkeys';
|
|
13
15
|
|
|
14
16
|
type Media = Database['public']['Tables']['media']['Row'];
|
|
15
17
|
import { FileText } from 'lucide-react';
|
|
@@ -74,6 +76,9 @@ export default function MediaEditForm({ mediaItem, formAction }: MediaEditFormPr
|
|
|
74
76
|
return <div>Access Denied. You do not have permission to edit media.</div>;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
const formRef = React.useRef<HTMLFormElement>(null);
|
|
80
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
81
|
+
|
|
77
82
|
return (
|
|
78
83
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
79
84
|
<div className="md:col-span-1 space-y-4">
|
|
@@ -101,17 +106,11 @@ export default function MediaEditForm({ mediaItem, formAction }: MediaEditFormPr
|
|
|
101
106
|
</div>
|
|
102
107
|
</div>
|
|
103
108
|
|
|
104
|
-
<form onSubmit={handleSubmit} className="md:col-span-2 space-y-6">
|
|
109
|
+
<form ref={formRef} onSubmit={handleSubmit} className="md:col-span-2 space-y-6">
|
|
105
110
|
{formMessage && (
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
? 'bg-green-100 text-green-700 border border-green-200'
|
|
110
|
-
: 'bg-red-100 text-red-700 border border-red-200'
|
|
111
|
-
}`}
|
|
112
|
-
>
|
|
113
|
-
{formMessage.text}
|
|
114
|
-
</div>
|
|
111
|
+
<Alert variant={formMessage.type === 'success' ? 'success' : 'destructive'} className="mb-4">
|
|
112
|
+
<AlertDescription>{formMessage.text}</AlertDescription>
|
|
113
|
+
</Alert>
|
|
115
114
|
)}
|
|
116
115
|
<div>
|
|
117
116
|
<Label htmlFor="file_name">Display Name</Label>
|
|
@@ -148,7 +147,13 @@ export default function MediaEditForm({ mediaItem, formAction }: MediaEditFormPr
|
|
|
148
147
|
Cancel
|
|
149
148
|
</Button>
|
|
150
149
|
<Button type="submit" disabled={isPending || authLoading}>
|
|
151
|
-
{isPending ?
|
|
150
|
+
{isPending ? (
|
|
151
|
+
<>
|
|
152
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
153
|
+
</>
|
|
154
|
+
) : (
|
|
155
|
+
"Update Media Info"
|
|
156
|
+
)}
|
|
152
157
|
</Button>
|
|
153
158
|
</div>
|
|
154
159
|
</form>
|