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
|
@@ -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>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { 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 } from "@nextblock-cms/ui";
|
|
7
8
|
import { Input } from "@nextblock-cms/ui";
|
|
8
9
|
import { Label } from "@nextblock-cms/ui";
|
|
9
10
|
import {
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
import { Textarea } from "@nextblock-cms/ui";
|
|
17
18
|
import type { Database } from "@nextblock-cms/db";
|
|
18
19
|
import { useAuth } from "@/context/AuthContext";
|
|
20
|
+
import { useRef } from "react";
|
|
21
|
+
import { useHotkeys } from "@/hooks/use-hotkeys";
|
|
19
22
|
|
|
20
23
|
type Page = Database['public']['Tables']['pages']['Row'];
|
|
21
24
|
type PageStatus = Database['public']['Enums']['page_status'];
|
|
@@ -124,19 +127,16 @@ export default function PageForm({
|
|
|
124
127
|
return <div>Please log in to manage pages.</div>;
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
131
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
132
|
+
|
|
127
133
|
return (
|
|
128
|
-
<form onSubmit={handleSubmit} className="space-y-6 w-full mx-auto px-6">
|
|
134
|
+
<form ref={formRef} onSubmit={handleSubmit} className="space-y-6 w-full mx-auto px-6">
|
|
129
135
|
{/* ... (rest of the form remains the same, but `availableLanguages` is now populated by the prop) ... */}
|
|
130
136
|
{formMessage && (
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
? 'bg-green-100 text-green-700 border border-green-200'
|
|
135
|
-
: 'bg-red-100 text-red-700 border border-red-200'
|
|
136
|
-
}`}
|
|
137
|
-
>
|
|
138
|
-
{formMessage.text}
|
|
139
|
-
</div>
|
|
137
|
+
<Alert variant={formMessage.type === 'success' ? 'success' : 'destructive'} className="mb-4">
|
|
138
|
+
<AlertDescription>{formMessage.text}</AlertDescription>
|
|
139
|
+
</Alert>
|
|
140
140
|
)}
|
|
141
141
|
{translationGroupId && (
|
|
142
142
|
<input type="hidden" name="translation_group_id" value={translationGroupId} />
|
|
@@ -245,7 +245,13 @@ export default function PageForm({
|
|
|
245
245
|
</Button>
|
|
246
246
|
{/* Ensure button is not disabled due to removed languagesLoading */}
|
|
247
247
|
<Button type="submit" disabled={isPending || authLoading || availableLanguages.length === 0}>
|
|
248
|
-
{isPending ?
|
|
248
|
+
{isPending ? (
|
|
249
|
+
<>
|
|
250
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
251
|
+
</>
|
|
252
|
+
) : (
|
|
253
|
+
actionButtonText
|
|
254
|
+
)}
|
|
249
255
|
</Button>
|
|
250
256
|
</div>
|
|
251
257
|
</form>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
TableRow,
|
|
13
13
|
} from "@nextblock-cms/ui";
|
|
14
14
|
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
+
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
15
16
|
import {
|
|
16
17
|
MoreHorizontal,
|
|
17
18
|
PlusCircle,
|
|
@@ -110,9 +111,11 @@ export default async function CmsPagesListPage(props: CmsPagesListPageProps) {
|
|
|
110
111
|
</div>
|
|
111
112
|
|
|
112
113
|
{successMessage && (
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
<Alert variant="success" className="mb-4">
|
|
115
|
+
<AlertDescription>
|
|
116
|
+
{decodeURIComponent(successMessage)}
|
|
117
|
+
</AlertDescription>
|
|
118
|
+
</Alert>
|
|
116
119
|
)}
|
|
117
120
|
|
|
118
121
|
{pagesWithDetails.length === 0 ? (
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
SelectTrigger,
|
|
15
15
|
SelectValue,
|
|
16
16
|
} from "@nextblock-cms/ui";
|
|
17
|
+
import { Spinner, Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
17
18
|
import { Textarea } from "@nextblock-cms/ui";
|
|
18
19
|
import {
|
|
19
20
|
Dialog,
|
|
@@ -36,6 +37,8 @@ import MediaImage from "@/app/cms/media/components/MediaImage"; // For displayin
|
|
|
36
37
|
import { getMediaItems } from "@/app/cms/media/actions";
|
|
37
38
|
import MediaUploadForm from "@/app/cms/media/components/MediaUploadForm";
|
|
38
39
|
import { Separator } from "@nextblock-cms/ui";
|
|
40
|
+
import { useRef } from "react";
|
|
41
|
+
import { useHotkeys } from "@/hooks/use-hotkeys";
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
interface PostFormProps {
|
|
@@ -219,18 +222,15 @@ export default function PostForm({
|
|
|
219
222
|
return <div>Please log in to manage posts.</div>;
|
|
220
223
|
}
|
|
221
224
|
|
|
225
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
226
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
227
|
+
|
|
222
228
|
return (
|
|
223
|
-
<form onSubmit={handleSubmit} className="space-y-6 w-full mx-auto px-6">
|
|
229
|
+
<form ref={formRef} onSubmit={handleSubmit} className="space-y-6 w-full mx-auto px-6">
|
|
224
230
|
{formMessage && (
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
? 'bg-green-100 text-green-700 border border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700'
|
|
229
|
-
: 'bg-red-100 text-red-700 border border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-700'
|
|
230
|
-
}`}
|
|
231
|
-
>
|
|
232
|
-
{formMessage.text}
|
|
233
|
-
</div>
|
|
231
|
+
<Alert variant={formMessage.type === 'success' ? 'success' : 'destructive'} className="mb-4">
|
|
232
|
+
<AlertDescription>{formMessage.text}</AlertDescription>
|
|
233
|
+
</Alert>
|
|
234
234
|
)}
|
|
235
235
|
<div>
|
|
236
236
|
<Label htmlFor="title">Title</Label>
|
|
@@ -393,7 +393,7 @@ export default function PostForm({
|
|
|
393
393
|
{!mediaLoading && hasMoreMedia && mediaItems.length > 0 && (
|
|
394
394
|
<div className="text-center mt-6">
|
|
395
395
|
<Button onClick={() => loadMedia(mediaPage + 1, true)} variant="outline" disabled={mediaLoading}>
|
|
396
|
-
{mediaLoading ? "
|
|
396
|
+
{mediaLoading ? <><Spinner className="mr-2 h-4 w-4" /> Loading...</> : "Load More"}
|
|
397
397
|
</Button>
|
|
398
398
|
</div>
|
|
399
399
|
)}
|
|
@@ -411,7 +411,13 @@ export default function PostForm({
|
|
|
411
411
|
<div className="flex justify-end space-x-3 pt-6"> {/* Increased pt for spacing */}
|
|
412
412
|
<Button type="button" variant="outline" onClick={() => router.push("/cms/posts")} disabled={isPending}>Cancel</Button>
|
|
413
413
|
<Button type="submit" disabled={isPending || authLoading || availableLanguages.length === 0}>
|
|
414
|
-
{isPending ?
|
|
414
|
+
{isPending ? (
|
|
415
|
+
<>
|
|
416
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
417
|
+
</>
|
|
418
|
+
) : (
|
|
419
|
+
actionButtonText
|
|
420
|
+
)}
|
|
415
421
|
</Button>
|
|
416
422
|
</div>
|
|
417
423
|
</form>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
TableRow,
|
|
13
13
|
} from "@nextblock-cms/ui";
|
|
14
14
|
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
+
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
15
16
|
import { MoreHorizontal, PlusCircle, Edit3, PenTool } from "lucide-react"; // Removed Trash2 as it's in the client component
|
|
16
17
|
import {
|
|
17
18
|
DropdownMenu,
|
|
@@ -95,9 +96,11 @@ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
|
|
|
95
96
|
</div>
|
|
96
97
|
|
|
97
98
|
{successMessage && (
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
<Alert variant="success" className="mb-4">
|
|
100
|
+
<AlertDescription>
|
|
101
|
+
{decodeURIComponent(successMessage)}
|
|
102
|
+
</AlertDescription>
|
|
103
|
+
</Alert>
|
|
101
104
|
)}
|
|
102
105
|
|
|
103
106
|
{postsWithDetails.length === 0 ? (
|
|
@@ -158,7 +161,7 @@ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
|
|
|
158
161
|
</Badge>
|
|
159
162
|
</TableCell>
|
|
160
163
|
<TableCell><Badge variant="outline" className="dark:border-slate-600">{languageCode}</Badge></TableCell>
|
|
161
|
-
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">/article/{post.slug}</TableCell>
|
|
164
|
+
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">/article/{post.slug}</TableCell>
|
|
162
165
|
<TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
|
|
163
166
|
{post.published_at ? new Date(post.published_at).toLocaleDateString() : "Not yet"}
|
|
164
167
|
</TableCell>
|
|
@@ -189,4 +192,4 @@ export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
|
|
|
189
192
|
)}
|
|
190
193
|
</div>
|
|
191
194
|
);
|
|
192
|
-
}
|
|
195
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { useEffect, useState, useTransition } from 'react';
|
|
5
5
|
import { Button } from "@nextblock-cms/ui";
|
|
6
|
+
import { Spinner, Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
6
7
|
import {
|
|
7
8
|
Dialog,
|
|
8
9
|
DialogContent,
|
|
@@ -139,9 +140,21 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
|
|
|
139
140
|
</DialogDescription>
|
|
140
141
|
</DialogHeader>
|
|
141
142
|
<div className="space-y-3">
|
|
142
|
-
{loading &&
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
{loading && (
|
|
144
|
+
<div className="flex items-center justify-center py-4">
|
|
145
|
+
<Spinner size="lg" />
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
{error && (
|
|
149
|
+
<Alert variant="destructive">
|
|
150
|
+
<AlertDescription>{error}</AlertDescription>
|
|
151
|
+
</Alert>
|
|
152
|
+
)}
|
|
153
|
+
{message && (
|
|
154
|
+
<Alert variant="success">
|
|
155
|
+
<AlertDescription>{message}</AlertDescription>
|
|
156
|
+
</Alert>
|
|
157
|
+
)}
|
|
145
158
|
|
|
146
159
|
{(!loading && revisions && revisions.length === 0) && (
|
|
147
160
|
<div className="text-sm text-muted-foreground">No revisions yet.</div>
|
|
@@ -166,10 +179,10 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
|
|
|
166
179
|
</div>
|
|
167
180
|
<div className="flex gap-2">
|
|
168
181
|
<Button variant="secondary" size="sm" onClick={() => handleCompare(rev.version)} disabled={compareLoading && activeCompareVersion === rev.version}>
|
|
169
|
-
{compareLoading && activeCompareVersion === rev.version ?
|
|
182
|
+
{compareLoading && activeCompareVersion === rev.version ? <><Spinner className="mr-2 h-3 w-3" /> Loading</> : 'Compare'}
|
|
170
183
|
</Button>
|
|
171
184
|
<Button size="sm" onClick={() => handleRestore(rev.version)} disabled={isPending}>
|
|
172
|
-
{isPending ?
|
|
185
|
+
{isPending ? <><Spinner className="mr-2 h-3 w-3" /> Restoring</> : 'Restore'}
|
|
173
186
|
</Button>
|
|
174
187
|
</div>
|
|
175
188
|
</div>
|
package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx
CHANGED
|
@@ -8,7 +8,10 @@ import { CopyrightSettings, updateCopyrightSettings } from '../actions';
|
|
|
8
8
|
import { Label } from '@nextblock-cms/ui';
|
|
9
9
|
import { Input } from '@nextblock-cms/ui';
|
|
10
10
|
import { Button } from '@nextblock-cms/ui';
|
|
11
|
-
import {
|
|
11
|
+
import { Alert, AlertDescription, Spinner } from '@nextblock-cms/ui';
|
|
12
|
+
import { type Message } from '@/components/form-message';
|
|
13
|
+
import { useRef } from 'react';
|
|
14
|
+
import { useHotkeys } from '@/hooks/use-hotkeys';
|
|
12
15
|
|
|
13
16
|
interface CopyrightFormProps {
|
|
14
17
|
languages: Language[];
|
|
@@ -48,8 +51,11 @@ export default function CopyrightForm({ languages, initialSettings }: CopyrightF
|
|
|
48
51
|
});
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
55
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
56
|
+
|
|
51
57
|
return (
|
|
52
|
-
<form onSubmit={handleSubmit} className="space-y-6">
|
|
58
|
+
<form ref={formRef} onSubmit={handleSubmit} className="space-y-6">
|
|
53
59
|
<div className="space-y-4">
|
|
54
60
|
{languages.map(lang => (
|
|
55
61
|
<div key={lang.id} className="space-y-2">
|
|
@@ -69,9 +75,19 @@ export default function CopyrightForm({ languages, initialSettings }: CopyrightF
|
|
|
69
75
|
|
|
70
76
|
<div className="flex items-center gap-4">
|
|
71
77
|
<Button type="submit" disabled={isPending}>
|
|
72
|
-
{isPending ?
|
|
78
|
+
{isPending ? (
|
|
79
|
+
<>
|
|
80
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
81
|
+
</>
|
|
82
|
+
) : (
|
|
83
|
+
'Save Settings'
|
|
84
|
+
)}
|
|
73
85
|
</Button>
|
|
74
|
-
{message &&
|
|
86
|
+
{message && (
|
|
87
|
+
<Alert variant={'success' in message ? 'success' : 'destructive'} className="py-2 px-4 w-auto inline-flex items-center">
|
|
88
|
+
<AlertDescription>{'success' in message ? (message as any).success : (message as any).error}</AlertDescription>
|
|
89
|
+
</Alert>
|
|
90
|
+
)}
|
|
75
91
|
</div>
|
|
76
92
|
</form>
|
|
77
93
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useTransition } from 'react';
|
|
3
|
+
import { useEffect, useState, useTransition, useRef } from 'react';
|
|
4
4
|
import { useActionState } from 'react';
|
|
5
|
+
import { useHotkeys } from '@/hooks/use-hotkeys';
|
|
5
6
|
import { getTranslations, createTranslation, updateTranslation } from './actions';
|
|
6
7
|
import { getLanguages } from '@/app/cms/settings/languages/actions';
|
|
7
8
|
import { Button } from '@nextblock-cms/ui';
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
TableHeader,
|
|
24
25
|
TableRow,
|
|
25
26
|
} from '@nextblock-cms/ui';
|
|
27
|
+
import { Spinner, Alert, AlertDescription } from '@nextblock-cms/ui';
|
|
26
28
|
import { SubmitButton } from '@/components/submit-button';
|
|
27
29
|
|
|
28
30
|
type Translation = Awaited<ReturnType<typeof getTranslations>>[number];
|
|
@@ -49,7 +51,11 @@ export default function ExtraTranslationsPage() {
|
|
|
49
51
|
}, []);
|
|
50
52
|
|
|
51
53
|
if (isPending && translations.length === 0) {
|
|
52
|
-
return
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex justify-center items-center p-12">
|
|
56
|
+
<Spinner size="lg" />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
return (
|
|
@@ -74,6 +80,9 @@ function CreateTranslationForm({ onSuccess }: { onSuccess: () => void }) {
|
|
|
74
80
|
}
|
|
75
81
|
}, [state, onSuccess]);
|
|
76
82
|
|
|
83
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
84
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
85
|
+
|
|
77
86
|
return (
|
|
78
87
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
79
88
|
<DialogTrigger asChild>
|
|
@@ -83,7 +92,7 @@ function CreateTranslationForm({ onSuccess }: { onSuccess: () => void }) {
|
|
|
83
92
|
<DialogHeader>
|
|
84
93
|
<DialogTitle>Create New Translation</DialogTitle>
|
|
85
94
|
</DialogHeader>
|
|
86
|
-
<form action={formAction} className="space-y-4">
|
|
95
|
+
<form ref={formRef} action={formAction} className="space-y-4">
|
|
87
96
|
<div>
|
|
88
97
|
<Label htmlFor="key">Key</Label>
|
|
89
98
|
<Input id="key" name="key" placeholder="e.g., sign_in_button" required />
|
|
@@ -94,7 +103,11 @@ function CreateTranslationForm({ onSuccess }: { onSuccess: () => void }) {
|
|
|
94
103
|
<Input id="en" name="en" placeholder="e.g., Sign In" required />
|
|
95
104
|
{state?.errors?.en && <p className="text-red-500 text-sm mt-1">{state.errors.en[0]}</p>}
|
|
96
105
|
</div>
|
|
97
|
-
{state?.error &&
|
|
106
|
+
{state?.error && (
|
|
107
|
+
<Alert variant="destructive">
|
|
108
|
+
<AlertDescription>{state.error}</AlertDescription>
|
|
109
|
+
</Alert>
|
|
110
|
+
)}
|
|
98
111
|
<DialogFooter>
|
|
99
112
|
<SubmitButton>Create</SubmitButton>
|
|
100
113
|
</DialogFooter>
|
|
@@ -190,6 +203,9 @@ function EditableTranslationRow({ translation, languages, onSuccess }: EditableR
|
|
|
190
203
|
}
|
|
191
204
|
};
|
|
192
205
|
|
|
206
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
207
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
208
|
+
|
|
193
209
|
return (
|
|
194
210
|
<TableRow>
|
|
195
211
|
<TableCell className="font-medium">{translation.key}</TableCell>
|
|
@@ -204,11 +220,21 @@ function EditableTranslationRow({ translation, languages, onSuccess }: EditableR
|
|
|
204
220
|
</TableCell>
|
|
205
221
|
))}
|
|
206
222
|
<TableCell className="text-right">
|
|
207
|
-
<form onSubmit={handleSubmit}>
|
|
223
|
+
<form ref={formRef} onSubmit={handleSubmit} className="flex flex-col items-end gap-2">
|
|
208
224
|
<Button type="submit" disabled={!isDirty || isSubmitting}>
|
|
209
|
-
{isSubmitting ?
|
|
225
|
+
{isSubmitting ? (
|
|
226
|
+
<>
|
|
227
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
228
|
+
</>
|
|
229
|
+
) : (
|
|
230
|
+
'Save'
|
|
231
|
+
)}
|
|
210
232
|
</Button>
|
|
211
|
-
{error &&
|
|
233
|
+
{error && (
|
|
234
|
+
<Alert variant="destructive" className="py-1 px-2 text-xs w-auto">
|
|
235
|
+
<AlertDescription>{error}</AlertDescription>
|
|
236
|
+
</Alert>
|
|
237
|
+
)}
|
|
212
238
|
</form>
|
|
213
239
|
</TableCell>
|
|
214
240
|
</TableRow>
|
package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { deleteLanguage } from "../actions"; // Server action
|
|
|
6
6
|
import type { Database } from "@nextblock-cms/db";
|
|
7
7
|
import { useTransition, useState } from 'react';
|
|
8
8
|
import { ConfirmationModal } from "@/app/cms/components/ConfirmationModal";
|
|
9
|
+
import { toast } from 'react-hot-toast';
|
|
9
10
|
|
|
10
11
|
type Language = Database['public']['Tables']['languages']['Row'];
|
|
11
12
|
|
|
@@ -28,7 +29,7 @@ export default function DeleteLanguageClientButton({ language }: DeleteLanguageC
|
|
|
28
29
|
if (isDefaultLanguage) {
|
|
29
30
|
// The server action has a more robust check for "only default"
|
|
30
31
|
// For now, a simple alert for any default language.
|
|
31
|
-
|
|
32
|
+
toast.error("Cannot delete the default language. Please set another language as default first, or ensure this is not the only language.");
|
|
32
33
|
setIsModalOpen(false);
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
@@ -36,8 +37,7 @@ export default function DeleteLanguageClientButton({ language }: DeleteLanguageC
|
|
|
36
37
|
startTransition(async () => {
|
|
37
38
|
const result = await deleteLanguage(language.id); // Call the server action
|
|
38
39
|
if (result?.error) {
|
|
39
|
-
|
|
40
|
-
// In a real app, use a toast or a more integrated notification system
|
|
40
|
+
toast.error(`Error: ${result.error}`);
|
|
41
41
|
}
|
|
42
42
|
// Revalidation and redirection are handled by the server action itself.
|
|
43
43
|
setIsModalOpen(false);
|
|
@@ -6,11 +6,14 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
|
|
6
6
|
import { Button } from '@nextblock-cms/ui';
|
|
7
7
|
import { Input } from '@nextblock-cms/ui';
|
|
8
8
|
import { Label } from '@nextblock-cms/ui';
|
|
9
|
-
import { Checkbox } from '@nextblock-cms/ui';
|
|
9
|
+
import { Checkbox } from '@nextblock-cms/ui';
|
|
10
|
+
import { Alert, AlertTitle, AlertDescription, Spinner, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@nextblock-cms/ui';
|
|
11
|
+
import { Info } from 'lucide-react';
|
|
10
12
|
import type { Database } from "@nextblock-cms/db";
|
|
11
13
|
|
|
12
14
|
type Language = Database["public"]["Tables"]["languages"]["Row"];
|
|
13
15
|
import { useAuth } from '@/context/AuthContext';
|
|
16
|
+
import { useHotkeys } from '@/hooks/use-hotkeys';
|
|
14
17
|
|
|
15
18
|
interface LanguageFormProps {
|
|
16
19
|
language?: Language | null;
|
|
@@ -77,22 +80,31 @@ export default function LanguageForm({
|
|
|
77
80
|
|
|
78
81
|
const isTheOnlyDefaultLanguage = isEditing && language?.is_default && allLanguages.filter(l => l.is_default).length === 1;
|
|
79
82
|
|
|
83
|
+
const formRef = React.useRef<HTMLFormElement>(null);
|
|
84
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
80
85
|
|
|
81
86
|
return (
|
|
82
|
-
<form onSubmit={handleSubmit} className="space-y-6">
|
|
87
|
+
<form ref={formRef} onSubmit={handleSubmit} className="space-y-6">
|
|
83
88
|
{formMessage && (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
: 'bg-red-100 text-red-700 border border-red-200'
|
|
89
|
-
}`}
|
|
90
|
-
>
|
|
91
|
-
{formMessage.text}
|
|
92
|
-
</div>
|
|
89
|
+
<Alert variant={formMessage.type === 'success' ? 'success' : 'destructive'}>
|
|
90
|
+
<AlertTitle>{formMessage.type === 'success' ? 'Success' : 'Error'}</AlertTitle>
|
|
91
|
+
<AlertDescription>{formMessage.text}</AlertDescription>
|
|
92
|
+
</Alert>
|
|
93
93
|
)}
|
|
94
94
|
<div>
|
|
95
|
-
<
|
|
95
|
+
<div className="flex items-center gap-2 mb-2">
|
|
96
|
+
<Label htmlFor="code">Language Code</Label>
|
|
97
|
+
<TooltipProvider>
|
|
98
|
+
<Tooltip>
|
|
99
|
+
<TooltipTrigger asChild>
|
|
100
|
+
<Info className="h-4 w-4 text-muted-foreground opacity-70 cursor-pointer" />
|
|
101
|
+
</TooltipTrigger>
|
|
102
|
+
<TooltipContent>
|
|
103
|
+
<p>ISO 639-1 code (e.g., 'en' for English, 'fr' for French).</p>
|
|
104
|
+
</TooltipContent>
|
|
105
|
+
</Tooltip>
|
|
106
|
+
</TooltipProvider>
|
|
107
|
+
</div>
|
|
96
108
|
<Input
|
|
97
109
|
id="code"
|
|
98
110
|
name="code"
|
|
@@ -130,6 +142,16 @@ export default function LanguageForm({
|
|
|
130
142
|
<Label htmlFor="is_default" className="font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
131
143
|
Set as Default Language
|
|
132
144
|
</Label>
|
|
145
|
+
<TooltipProvider>
|
|
146
|
+
<Tooltip>
|
|
147
|
+
<TooltipTrigger asChild>
|
|
148
|
+
<Info className="h-4 w-4 text-muted-foreground opacity-70 cursor-pointer" />
|
|
149
|
+
</TooltipTrigger>
|
|
150
|
+
<TooltipContent>
|
|
151
|
+
<p>The default language for the site. Only one language can be default.</p>
|
|
152
|
+
</TooltipContent>
|
|
153
|
+
</Tooltip>
|
|
154
|
+
</TooltipProvider>
|
|
133
155
|
</div>
|
|
134
156
|
{isTheOnlyDefaultLanguage && isDefault && (
|
|
135
157
|
<p className="text-xs text-amber-600">This is the only default language. To change, set another language as default.</p>
|
|
@@ -159,7 +181,13 @@ export default function LanguageForm({
|
|
|
159
181
|
Cancel
|
|
160
182
|
</Button>
|
|
161
183
|
<Button type="submit" disabled={isPending || authLoading}>
|
|
162
|
-
{isPending ?
|
|
184
|
+
{isPending ? (
|
|
185
|
+
<>
|
|
186
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
187
|
+
</>
|
|
188
|
+
) : (
|
|
189
|
+
actionButtonText
|
|
190
|
+
)}
|
|
163
191
|
</Button>
|
|
164
192
|
</div>
|
|
165
193
|
</form>
|