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
@@ -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
- <div className="flex items-center text-green-600">
333
- <CheckCircle2 className="h-5 w-5 mr-2" />
334
- <p>Upload successful!</p>
335
- </div>
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
- <div className="flex items-center text-red-600">
339
- <XCircle className="h-5 w-5 mr-2" />
340
- <p>Error: {errorMessage}</p>
341
- </div>
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
- <p className="text-sm text-blue-600 animate-pulse">Processing image variants...</p>
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" ? `Uploading ${uploadProgress}%...`
356
- : processingStatus === "processing" ? "Processing..."
357
- : "Upload File"}
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
- <div className={`p-3 rounded-md text-sm ${formMessage.type === 'success' ? 'bg-green-100 text-green-700 border border-green-200' : 'bg-red-100 text-red-700 border border-red-200'}`}>
142
- {formMessage.text}
143
- </div>
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
- <Label htmlFor="menu_key">Menu Location</Label>
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 ? "Saving..." : actionButtonText}
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
- <div
132
- className={`p-3 rounded-md text-sm ${
133
- formMessage.type === 'success'
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 ? "Saving..." : actionButtonText}
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
- <div className="mb-4 p-3 rounded-md text-sm bg-green-100 text-green-700 border border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700">
114
- {decodeURIComponent(successMessage)}
115
- </div>
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
- <div
226
- className={`p-3 rounded-md text-sm ${
227
- formMessage.type === 'success'
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 ? "Loading..." : "Load More"}
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 ? "Saving..." : actionButtonText}
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
- <div className="mb-4 p-3 rounded-md text-sm bg-green-100 text-green-700 border border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700">
99
- {decodeURIComponent(successMessage)}
100
- </div>
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 && <div className="text-sm text-muted-foreground">Loading revisions…</div>}
143
- {error && <div className="text-sm text-red-600">{error}</div>}
144
- {message && <div className="text-sm text-green-600">{message}</div>}
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 ? 'Loading…' : 'Compare'}
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 ? 'Restoring…' : 'Restore'}
185
+ {isPending ? <><Spinner className="mr-2 h-3 w-3" /> Restoring</> : 'Restore'}
173
186
  </Button>
174
187
  </div>
175
188
  </div>
@@ -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 { FormMessage, type Message } from '@/components/form-message';
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 ? 'Saving...' : 'Save Settings'}
78
+ {isPending ? (
79
+ <>
80
+ <Spinner className="mr-2 h-4 w-4" /> Saving...
81
+ </>
82
+ ) : (
83
+ 'Save Settings'
84
+ )}
73
85
  </Button>
74
- {message && <FormMessage message={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 <div>Loading...</div>;
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 && <p className="text-red-500 text-sm">{state.error}</p>}
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 ? 'Saving...' : 'Save'}
225
+ {isSubmitting ? (
226
+ <>
227
+ <Spinner className="mr-2 h-4 w-4" /> Saving...
228
+ </>
229
+ ) : (
230
+ 'Save'
231
+ )}
210
232
  </Button>
211
- {error && <p className="text-red-500 text-xs mt-1">{error}</p>}
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>
@@ -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
- alert("Cannot delete the default language. Please set another language as default first, or ensure this is not the only language.");
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
- alert(`Error: ${result.error}`);
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'; // Assuming shadcn/ui Checkbox
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
- <div
85
- className={`p-3 rounded-md text-sm ${
86
- formMessage.type === 'success'
87
- ? 'bg-green-100 text-green-700 border border-green-200'
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
- <Label htmlFor="code">Language Code (e.g., en, fr-CA)</Label>
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 ? "Saving..." : actionButtonText}
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>