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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
  3. package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
  4. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
  5. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
  6. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
  7. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
  8. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
  9. package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
  10. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
  11. package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
  12. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
  13. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
  14. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
  15. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
  16. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
  17. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
  18. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
  19. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
  20. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
  21. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
  22. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
  23. package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
  24. package/templates/nextblock-template/next-env.d.ts +1 -1
  25. package/templates/nextblock-template/package.json +1 -1
@@ -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>
@@ -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, AlertTitle, AlertDescription } from "@nextblock-cms/ui";
15
16
  import { MoreHorizontal, PlusCircle, Edit3, Languages as LanguagesIcon, ShieldAlert } from "lucide-react";
16
17
  import {
17
18
  DropdownMenu,
@@ -66,9 +67,12 @@ export default async function CmsLanguagesListPage() {
66
67
  </div>
67
68
 
68
69
  {successMessage && (
69
- <div className="mb-4 p-3 rounded-md text-sm bg-green-100 text-green-700 border border-green-200">
70
- {decodeURIComponent(successMessage)}
71
- </div>
70
+ <Alert variant="success" className="mb-4">
71
+ <AlertTitle>Success</AlertTitle>
72
+ <AlertDescription>
73
+ {decodeURIComponent(successMessage)}
74
+ </AlertDescription>
75
+ </Alert>
72
76
  )}
73
77
 
74
78
  {languages.length === 0 ? (
@@ -140,16 +144,14 @@ export default async function CmsLanguagesListPage() {
140
144
  </Table>
141
145
  </div>
142
146
  )}
143
- <div className="mt-6 p-4 border border-amber-300 bg-amber-50 dark:bg-amber-900/30 rounded-lg">
144
- <div className="flex items-start">
145
- <ShieldAlert className="h-5 w-5 text-amber-600 dark:text-amber-400 mr-3 flex-shrink-0 mt-0.5" />
146
- <div>
147
- <h4 className="text-sm font-semibold text-amber-700 dark:text-amber-300">Important Note on Deleting Languages</h4>
148
- <p className="text-xs text-amber-600 dark:text-amber-400 mt-1">
149
- Deleting a language is a destructive action. All content (pages, posts, blocks, navigation items) specifically associated with that language will be permanently deleted due to database cascade rules. Please ensure this is intended before proceeding. You cannot delete the current default language if it is the only one.
150
- </p>
151
- </div>
152
- </div>
147
+ <div className="mt-6">
148
+ <Alert variant="warning">
149
+ <ShieldAlert className="h-4 w-4" />
150
+ <AlertTitle>Important Note on Deleting Languages</AlertTitle>
151
+ <AlertDescription>
152
+ Deleting a language is a destructive action. All content (pages, posts, blocks, navigation items) specifically associated with that language will be permanently deleted due to database cascade rules. Please ensure this is intended before proceeding. You cannot delete the current default language if it is the only one.
153
+ </AlertDescription>
154
+ </Alert>
153
155
  </div>
154
156
  </div>
155
157
  );
@@ -51,12 +51,11 @@ export async function deleteLogo(id: string) {
51
51
 
52
52
  if (error) {
53
53
  console.error('Error deleting logo:', error)
54
- // Optionally, handle the error more gracefully
55
- // redirect('/error?message=Could not delete logo')
56
- return
54
+ return { success: false, error: error.message }
57
55
  }
58
56
 
59
57
  revalidatePath('/cms/settings/logos')
58
+ return { success: true }
60
59
  }
61
60
 
62
61
  export async function getLogos() {
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { DropdownMenuItem } from "@nextblock-cms/ui";
4
+ import { Trash2 } from "lucide-react";
5
+ import { deleteLogo } from "../actions";
6
+ import { useTransition, useState } from 'react';
7
+ import { ConfirmationModal } from "@/app/cms/components/ConfirmationModal";
8
+ import { toast } from 'react-hot-toast';
9
+
10
+ interface DeleteLogoButtonProps {
11
+ logoId: string;
12
+ }
13
+
14
+ export default function DeleteLogoButton({ logoId }: DeleteLogoButtonProps) {
15
+ const [isPending, startTransition] = useTransition();
16
+ const [isModalOpen, setIsModalOpen] = useState(false);
17
+
18
+ const handleDeleteConfirm = () => {
19
+ startTransition(async () => {
20
+ const result = await deleteLogo(logoId);
21
+ if (result?.error) {
22
+ toast.error(`Error: ${result.error}`);
23
+ } else {
24
+ toast.success("Logo deleted successfully");
25
+ }
26
+ setIsModalOpen(false);
27
+ });
28
+ };
29
+
30
+ return (
31
+ <>
32
+ <DropdownMenuItem
33
+ className="text-red-600 hover:!text-red-600 cursor-pointer hover:!bg-red-50 dark:hover:!bg-red-700/20"
34
+ onSelect={(e) => e.preventDefault()}
35
+ onClick={() => !isPending && setIsModalOpen(true)}
36
+ disabled={isPending}
37
+ >
38
+ <Trash2 className="mr-2 h-4 w-4" />
39
+ {isPending ? "Deleting..." : "Delete"}
40
+ </DropdownMenuItem>
41
+ <ConfirmationModal
42
+ isOpen={isModalOpen}
43
+ onClose={() => setIsModalOpen(false)}
44
+ onConfirm={handleDeleteConfirm}
45
+ title="Delete Logo?"
46
+ description="This will permanently delete the logo. This action cannot be undone."
47
+ />
48
+ </>
49
+ );
50
+ }
@@ -6,9 +6,11 @@ import Image from 'next/image'
6
6
  import { Input } from '@nextblock-cms/ui'
7
7
  import { Label } from '@nextblock-cms/ui'
8
8
  import { Button } from '@nextblock-cms/ui'
9
+ import { Alert, AlertDescription, Spinner } from '@nextblock-cms/ui'
9
10
  import type { Database } from '@nextblock-cms/db'
10
11
  import { ImageIcon, X as XIcon } from 'lucide-react'
11
12
  import MediaPickerDialog from '@/app/cms/media/components/MediaPickerDialog'
13
+ import { useHotkeys } from '@/hooks/use-hotkeys'
12
14
  type Media = Database['public']['Tables']['media']['Row'];
13
15
  const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || ''
14
16
 
@@ -96,6 +98,8 @@ export default function LogoForm({ logo, action }: LogoFormProps) {
96
98
  })
97
99
  }
98
100
 
101
+ useHotkeys('ctrl+s', handleSave, [handleSave]);
102
+
99
103
  return (
100
104
  <div className="space-y-6">
101
105
  <div>
@@ -162,10 +166,18 @@ export default function LogoForm({ logo, action }: LogoFormProps) {
162
166
  onClick={handleSave}
163
167
  disabled={isPending || !logoDetails.name || !logoDetails.media_id}
164
168
  >
165
- {isPending ? 'Saving...' : `${logo ? 'Update' : 'Create'} Logo`}
169
+ {isPending ? (
170
+ <>
171
+ <Spinner className="mr-2 h-4 w-4" /> Saving...
172
+ </>
173
+ ) : (
174
+ `${logo ? 'Update' : 'Create'} Logo`
175
+ )}
166
176
  </Button>
167
177
  {formError && (
168
- <div className="text-red-500 text-sm">{formError}</div>
178
+ <Alert variant="destructive" className="py-2 px-4 w-auto inline-flex items-center">
179
+ <AlertDescription>{formError}</AlertDescription>
180
+ </Alert>
169
181
  )}
170
182
  </div>
171
183
  </div>