create-nextblock 0.2.46 → 0.2.48
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/BlockEditorModal.tsx +73 -34
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +309 -53
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +175 -25
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +44 -26
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +74 -16
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +2 -0
- 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/blocks/renderers/ButtonBlockRenderer.tsx +41 -49
- package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +3 -2
- package/templates/nextblock-template/package.json +1 -1
|
@@ -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>
|
|
@@ -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
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
<
|
|
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>
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
DropdownMenuTrigger,
|
|
18
18
|
DropdownMenuSeparator,
|
|
19
19
|
} from '@nextblock-cms/ui'
|
|
20
|
-
import {
|
|
20
|
+
import { getLogos } from './actions'
|
|
21
21
|
import MediaImage from '@/app/cms/media/components/MediaImage'
|
|
22
|
+
import DeleteLogoButton from './components/DeleteLogoButton'
|
|
22
23
|
|
|
23
24
|
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || ''
|
|
24
25
|
|
|
@@ -99,11 +100,7 @@ export default async function CmsLogosListPage() {
|
|
|
99
100
|
</Link>
|
|
100
101
|
</DropdownMenuItem>
|
|
101
102
|
<DropdownMenuSeparator />
|
|
102
|
-
<
|
|
103
|
-
<button type="submit" className="w-full text-left px-2 py-1.5 text-sm text-red-500">
|
|
104
|
-
Delete
|
|
105
|
-
</button>
|
|
106
|
-
</form>
|
|
103
|
+
<DeleteLogoButton logoId={logo.id} />
|
|
107
104
|
</DropdownMenuContent>
|
|
108
105
|
</DropdownMenu>
|
|
109
106
|
</TableCell>
|