create-nextblock 0.2.46 → 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/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/hooks/use-hotkeys.ts +27 -0
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
package/package.json
CHANGED
|
@@ -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>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import React, { useState, useRef, useTransition, useEffect } from "react";
|
|
5
5
|
import Image from "next/image";
|
|
6
6
|
import { Button } from "@nextblock-cms/ui";
|
|
7
|
+
import { Spinner, Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
7
8
|
import { Input } from "@nextblock-cms/ui";
|
|
8
9
|
import { Label } from "@nextblock-cms/ui";
|
|
9
10
|
import { Progress } from "@nextblock-cms/ui"; // Assuming you have this shadcn/ui component
|
|
@@ -329,19 +330,21 @@ export default function MediaUploadForm({ onUploadSuccess, returnJustData, defau
|
|
|
329
330
|
<Progress value={uploadProgress} className="w-full h-2" />
|
|
330
331
|
)}
|
|
331
332
|
{uploadStatus === "success" && (
|
|
332
|
-
|
|
333
|
-
<CheckCircle2 className="h-
|
|
334
|
-
<
|
|
335
|
-
|
|
333
|
+
<Alert variant="success" className="mb-4">
|
|
334
|
+
<CheckCircle2 className="h-4 w-4" />
|
|
335
|
+
<AlertDescription>Upload successful!</AlertDescription>
|
|
336
|
+
</Alert>
|
|
336
337
|
)}
|
|
337
338
|
{uploadStatus === "error" && errorMessage && (
|
|
338
|
-
<
|
|
339
|
-
<XCircle className="h-
|
|
340
|
-
<
|
|
341
|
-
</
|
|
339
|
+
<Alert variant="destructive" className="mb-4">
|
|
340
|
+
<XCircle className="h-4 w-4" />
|
|
341
|
+
<AlertDescription>Error: {errorMessage}</AlertDescription>
|
|
342
|
+
</Alert>
|
|
342
343
|
)}
|
|
343
344
|
{processingStatus === "processing" && (
|
|
344
|
-
<
|
|
345
|
+
<div className="flex items-center text-sm text-blue-600 animate-pulse">
|
|
346
|
+
<Spinner className="mr-2 h-4 w-4" /> Processing image variants...
|
|
347
|
+
</div>
|
|
345
348
|
)}
|
|
346
349
|
{/* Message for when original uploads but variants fail, errorMessage will be set */}
|
|
347
350
|
|
|
@@ -352,9 +355,17 @@ export default function MediaUploadForm({ onUploadSuccess, returnJustData, defau
|
|
|
352
355
|
disabled={isPending || uploadStatus === "uploading" || processingStatus === "processing" || !file}
|
|
353
356
|
className="w-full sm:w-auto"
|
|
354
357
|
>
|
|
355
|
-
{uploadStatus === "uploading" ?
|
|
356
|
-
|
|
357
|
-
|
|
358
|
+
{uploadStatus === "uploading" ? (
|
|
359
|
+
<>
|
|
360
|
+
<Spinner className="mr-2 h-4 w-4" /> Uploading {uploadProgress}%...
|
|
361
|
+
</>
|
|
362
|
+
) : processingStatus === "processing" ? (
|
|
363
|
+
<>
|
|
364
|
+
<Spinner className="mr-2 h-4 w-4" /> Processing...
|
|
365
|
+
</>
|
|
366
|
+
) : (
|
|
367
|
+
"Upload File"
|
|
368
|
+
)}
|
|
358
369
|
</Button>
|
|
359
370
|
</form>
|
|
360
371
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { DropdownMenuItem } from "@nextblock-cms/ui";
|
|
5
|
+
import { toast } from "react-hot-toast";
|
|
5
6
|
import { Trash2 } from "lucide-react";
|
|
6
7
|
import { deleteNavigationItem } from "../actions";
|
|
7
8
|
import { ConfirmationModal } from "../../components/ConfirmationModal";
|
|
@@ -17,12 +18,15 @@ export default function DeleteNavItemButton({ itemId }: DeleteNavItemButtonProps
|
|
|
17
18
|
try {
|
|
18
19
|
const result = await deleteNavigationItem(itemId);
|
|
19
20
|
if (result.success) {
|
|
21
|
+
toast.success("Item deleted");
|
|
20
22
|
window.location.reload();
|
|
21
23
|
} else {
|
|
22
24
|
console.error("Delete operation failed:", result.error);
|
|
25
|
+
toast.error(`Delete failed: ${result.error}`);
|
|
23
26
|
}
|
|
24
27
|
} catch (error) {
|
|
25
28
|
console.error("Exception during delete action:", error);
|
|
29
|
+
toast.error("An unexpected error occurred.");
|
|
26
30
|
} finally {
|
|
27
31
|
setIsModalOpen(false);
|
|
28
32
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import React, { useEffect, useState, useTransition } from "react";
|
|
5
5
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
6
6
|
import { Button } from "@nextblock-cms/ui";
|
|
7
|
+
import { Spinner, Alert, AlertDescription, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@nextblock-cms/ui";
|
|
8
|
+
import { Info } from "lucide-react";
|
|
7
9
|
import { Input } from "@nextblock-cms/ui";
|
|
8
10
|
import { Label } from "@nextblock-cms/ui";
|
|
9
11
|
import {
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
} from "@nextblock-cms/ui";
|
|
16
18
|
import type { Database } from "@nextblock-cms/db";
|
|
17
19
|
import { useAuth } from "@/context/AuthContext";
|
|
20
|
+
import { useHotkeys } from "@/hooks/use-hotkeys";
|
|
18
21
|
|
|
19
22
|
type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
|
|
20
23
|
type MenuLocation = Database['public']['Enums']['menu_location'];
|
|
@@ -135,12 +138,15 @@ export default function NavigationItemForm({
|
|
|
135
138
|
|
|
136
139
|
const menuLocations: MenuLocation[] = ['HEADER', 'FOOTER', 'SIDEBAR'];
|
|
137
140
|
|
|
141
|
+
const formRef = React.useRef<HTMLFormElement>(null);
|
|
142
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
143
|
+
|
|
138
144
|
return (
|
|
139
|
-
<form onSubmit={handleSubmit} className="space-y-6">
|
|
145
|
+
<form ref={formRef} onSubmit={handleSubmit} className="space-y-6">
|
|
140
146
|
{formMessage && (
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
</
|
|
147
|
+
<Alert variant={formMessage.type === 'success' ? 'success' : 'destructive'}>
|
|
148
|
+
<AlertDescription>{formMessage.text}</AlertDescription>
|
|
149
|
+
</Alert>
|
|
144
150
|
)}
|
|
145
151
|
|
|
146
152
|
{/* Hidden input for from_translation_group_id if present in URL params and not editing */}
|
|
@@ -198,7 +204,19 @@ export default function NavigationItemForm({
|
|
|
198
204
|
</Select>
|
|
199
205
|
</div>
|
|
200
206
|
<div>
|
|
201
|
-
<
|
|
207
|
+
<div className="flex items-center gap-2 mb-2">
|
|
208
|
+
<Label htmlFor="menu_key">Menu Location</Label>
|
|
209
|
+
<TooltipProvider>
|
|
210
|
+
<Tooltip>
|
|
211
|
+
<TooltipTrigger asChild>
|
|
212
|
+
<Info className="h-4 w-4 text-muted-foreground opacity-70 cursor-pointer" />
|
|
213
|
+
</TooltipTrigger>
|
|
214
|
+
<TooltipContent>
|
|
215
|
+
<p>Where this item will appear in the site layout.</p>
|
|
216
|
+
</TooltipContent>
|
|
217
|
+
</Tooltip>
|
|
218
|
+
</TooltipProvider>
|
|
219
|
+
</div>
|
|
202
220
|
<Select
|
|
203
221
|
name="menu_key"
|
|
204
222
|
value={menuKey}
|
|
@@ -240,7 +258,13 @@ export default function NavigationItemForm({
|
|
|
240
258
|
<div className="flex justify-end space-x-3">
|
|
241
259
|
<Button type="button" variant="outline" onClick={() => router.push("/cms/navigation")} disabled={isPending}>Cancel</Button>
|
|
242
260
|
<Button type="submit" disabled={isPending || dataLoading || !languageId || !menuKey}>
|
|
243
|
-
{isPending ?
|
|
261
|
+
{isPending ? (
|
|
262
|
+
<>
|
|
263
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
264
|
+
</>
|
|
265
|
+
) : (
|
|
266
|
+
actionButtonText
|
|
267
|
+
)}
|
|
244
268
|
</Button>
|
|
245
269
|
</div>
|
|
246
270
|
</form>
|