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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +45 -27
  3. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
  4. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +13 -3
  5. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +11 -4
  6. package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
  7. package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
  8. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
  9. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
  10. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
  11. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
  12. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
  13. package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
  14. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
  15. package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
  16. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
  17. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
  18. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
  19. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
  20. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
  21. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
  22. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
  23. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
  24. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
  25. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
  26. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
  27. package/templates/nextblock-template/components/BlockRenderer.tsx +14 -1
  28. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +126 -0
  29. package/templates/nextblock-template/docs/How to Create a Custom Block.md +149 -0
  30. package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
  31. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +196 -603
  32. package/templates/nextblock-template/next-env.d.ts +1 -1
  33. package/templates/nextblock-template/package.json +1 -1
  34. package/templates/nextblock-template/tsconfig.json +3 -0
  35. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
- switch (blockType) {
147
- case 'text':
148
- SelectedEditor = DynamicTextBlockEditor;
149
- break;
150
- case 'heading':
151
- SelectedEditor = DynamicHeadingBlockEditor;
152
- break;
153
- case 'image':
154
- SelectedEditor = DynamicImageBlockEditor;
155
- break;
156
- case 'button':
157
- SelectedEditor = DynamicButtonBlockEditor;
158
- break;
159
- case 'posts_grid':
160
- SelectedEditor = DynamicPostsGridBlockEditor;
161
- break;
162
- case 'video_embed':
163
- SelectedEditor = DynamicVideoEmbedBlockEditor;
164
- break;
165
- case 'section':
166
- SelectedEditor = DynamicSectionBlockEditor;
167
- break;
168
- default:
169
- console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
170
- alert(`Error: Editor not configured for ${blockType}.`);
171
- setEditingNestedBlockInfo(null);
172
- return;
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 && blockDef.editorComponentFilename) {
173
- const Editor = lazy(() => import(`../editors/${blockDef.editorComponentFilename.replace(/\.tsx$/, '')}`));
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 editorFilename = blockRegistry[block.block_type as BlockType]?.editorComponentFilename;
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 (editorFilename) {
68
- const Editor = lazy(() => import(`../editors/${editorFilename}`));
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, ArrowUpRight, ArrowDownRight, Eye } from "lucide-react"
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">24</div>
23
+ <div className="text-2xl font-bold">{stats.totalPages}</div>
21
24
  <p className="text-xs text-muted-foreground mt-1">
22
- <span className="text-emerald-500 font-medium inline-flex items-center">
23
- <ArrowUpRight className="h-3 w-3 mr-1" />
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">142</div>
38
- <p className="text-xs text-muted-foreground mt-1">
39
- <span className="text-emerald-500 font-medium inline-flex items-center">
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">8,623</div>
50
+ <div className="text-2xl font-bold">--</div> {/* Placeholder */}
55
51
  <p className="text-xs text-muted-foreground mt-1">
56
- <span className="text-emerald-500 font-medium inline-flex items-center">
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">Active Users</CardTitle>
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">573</div>
72
- <p className="text-xs text-muted-foreground mt-1">
73
- <span className="text-red-500 font-medium inline-flex items-center">
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.map((item, index) => (
93
- <div key={index} className="flex items-start gap-4 pb-4 border-b last:border-0 last:pb-0">
94
- <div
95
- 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`}
96
- >
97
- {item.type === "page" ? <FileText className="h-4 w-4" /> : <PenTool className="h-4 w-4" />}
98
- </div>
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.status}
112
- </span>
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
- </div>
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.map((item, index) => (
128
- <div key={index} className="flex items-start gap-4 pb-4 border-b last:border-0 last:pb-0">
129
- <div className="w-10 h-10 rounded bg-slate-100 dark:bg-slate-800 flex flex-col items-center justify-center text-center">
130
- <span className="text-xs font-medium">{item.month}</span>
131
- <span className="text-sm font-bold">{item.day}</span>
132
- </div>
133
- <div className="flex-1">
134
- <h4 className="text-sm font-medium">{item.title}</h4>
135
- <div className="flex items-center gap-2 mt-1">
136
- <Calendar className="h-3 w-3 text-muted-foreground" />
137
- <p className="text-xs text-muted-foreground">{item.time}</p>
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
- <div className="flex items-center">
141
- <span className="text-xs px-2 py-1 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300">
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
- {analyticsData.map((item, index) => (
161
- <div key={index} className="flex-1 flex flex-col items-center">
162
- <div
163
- className="w-full bg-primary/10 dark:bg-primary/20 rounded-sm"
164
- style={{ height: `${item.value}%` }}
165
- ></div>
166
- <span className="text-xs text-muted-foreground mt-2">{item.label}</span>
167
- </div>
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
- <div
107
- className={`p-3 rounded-md text-sm ${
108
- formMessage.type === 'success'
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 ? "Saving..." : "Update Media Info"}
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>