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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// app/cms/users/components/UserForm.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { useEffect, useState, useTransition } from "react";
|
|
4
|
+
import { useEffect, useState, useTransition, useRef } from "react";
|
|
5
|
+
import { useHotkeys } from "@/hooks/use-hotkeys";
|
|
5
6
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
6
7
|
import { Button } from "@nextblock-cms/ui";
|
|
8
|
+
import { Spinner, Alert, AlertDescription, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@nextblock-cms/ui";
|
|
9
|
+
import { Info } from "lucide-react";
|
|
7
10
|
import { Input } from "@nextblock-cms/ui";
|
|
8
11
|
import { Label } from "@nextblock-cms/ui";
|
|
9
12
|
import {
|
|
@@ -85,18 +88,15 @@ export default function UserForm({
|
|
|
85
88
|
|
|
86
89
|
const userRoles: UserRole[] = ['USER', 'WRITER', 'ADMIN'];
|
|
87
90
|
|
|
91
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
92
|
+
useHotkeys('ctrl+s', () => formRef.current?.requestSubmit());
|
|
93
|
+
|
|
88
94
|
return (
|
|
89
|
-
<form onSubmit={handleSubmit} className="space-y-6">
|
|
95
|
+
<form ref={formRef} onSubmit={handleSubmit} className="space-y-6">
|
|
90
96
|
{formMessage && (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
? 'bg-green-100 text-green-700 border border-green-200'
|
|
95
|
-
: 'bg-red-100 text-red-700 border border-red-200'
|
|
96
|
-
}`}
|
|
97
|
-
>
|
|
98
|
-
{formMessage.text}
|
|
99
|
-
</div>
|
|
97
|
+
<Alert variant={formMessage.type === 'success' ? 'success' : 'destructive'}>
|
|
98
|
+
<AlertDescription>{formMessage.text}</AlertDescription>
|
|
99
|
+
</Alert>
|
|
100
100
|
)}
|
|
101
101
|
<div>
|
|
102
102
|
<Label htmlFor="email">Email (Read-only)</Label>
|
|
@@ -114,7 +114,21 @@ export default function UserForm({
|
|
|
114
114
|
</div>
|
|
115
115
|
|
|
116
116
|
<div>
|
|
117
|
-
<
|
|
117
|
+
<div className="flex items-center gap-2 mb-2">
|
|
118
|
+
<Label htmlFor="role">Role</Label>
|
|
119
|
+
<TooltipProvider>
|
|
120
|
+
<Tooltip>
|
|
121
|
+
<TooltipTrigger asChild>
|
|
122
|
+
<Info className="h-4 w-4 text-muted-foreground opacity-70 cursor-pointer" />
|
|
123
|
+
</TooltipTrigger>
|
|
124
|
+
<TooltipContent className="max-w-xs">
|
|
125
|
+
<p><strong>ADMIN:</strong> Full access to settings and content.</p>
|
|
126
|
+
<p><strong>WRITER:</strong> Can create/edit content, no settings access.</p>
|
|
127
|
+
<p><strong>USER:</strong> Read-only access.</p>
|
|
128
|
+
</TooltipContent>
|
|
129
|
+
</Tooltip>
|
|
130
|
+
</TooltipProvider>
|
|
131
|
+
</div>
|
|
118
132
|
<Select name="role" value={role} onValueChange={(value) => setRole(value as UserRole)} required>
|
|
119
133
|
<SelectTrigger className="mt-1"><SelectValue placeholder="Select role" /></SelectTrigger>
|
|
120
134
|
<SelectContent>
|
|
@@ -130,7 +144,13 @@ export default function UserForm({
|
|
|
130
144
|
Cancel
|
|
131
145
|
</Button>
|
|
132
146
|
<Button type="submit" disabled={isPending || authLoading}>
|
|
133
|
-
{isPending ?
|
|
147
|
+
{isPending ? (
|
|
148
|
+
<>
|
|
149
|
+
<Spinner className="mr-2 h-4 w-4" /> Saving...
|
|
150
|
+
</>
|
|
151
|
+
) : (
|
|
152
|
+
actionButtonText
|
|
153
|
+
)}
|
|
134
154
|
</Button>
|
|
135
155
|
</div>
|
|
136
156
|
</form>
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import Link from "next/link";
|
|
3
|
+
import { Button } from "@nextblock-cms/ui";
|
|
4
|
+
import { cn } from "@nextblock-cms/utils";
|
|
5
|
+
|
|
3
6
|
export type ButtonBlockContent = {
|
|
4
7
|
text?: string;
|
|
5
8
|
url?: string;
|
|
6
9
|
variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
7
|
-
size?: 'default' | 'sm' | 'lg';
|
|
10
|
+
size?: 'default' | 'sm' | 'lg' | 'full';
|
|
11
|
+
position?: 'left' | 'center' | 'right';
|
|
8
12
|
};
|
|
9
13
|
|
|
10
14
|
interface ButtonBlockRendererProps {
|
|
@@ -14,25 +18,7 @@ interface ButtonBlockRendererProps {
|
|
|
14
18
|
|
|
15
19
|
const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
|
|
16
20
|
content,
|
|
17
|
-
// languageId, // Unused
|
|
18
21
|
}) => {
|
|
19
|
-
const baseClasses =
|
|
20
|
-
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
|
|
21
|
-
const variantClasses: Record<string, string> = {
|
|
22
|
-
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
23
|
-
outline:
|
|
24
|
-
"border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
25
|
-
secondary:
|
|
26
|
-
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
27
|
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
28
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
29
|
-
};
|
|
30
|
-
const sizeClasses: Record<string, string> = {
|
|
31
|
-
default: "h-10 px-4 py-2",
|
|
32
|
-
sm: "h-9 rounded-md px-3",
|
|
33
|
-
lg: "h-11 rounded-md px-8",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
22
|
const isExternal =
|
|
37
23
|
content.url?.startsWith("http") ||
|
|
38
24
|
content.url?.startsWith("mailto:") ||
|
|
@@ -42,48 +28,54 @@ const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
|
|
|
42
28
|
const buttonText = content.text || "Button";
|
|
43
29
|
const buttonVariant = content.variant || "default";
|
|
44
30
|
const buttonSize = content.size || "default";
|
|
31
|
+
const buttonPosition = content.position || "left";
|
|
32
|
+
|
|
33
|
+
const alignmentClasses = {
|
|
34
|
+
left: "justify-start text-left",
|
|
35
|
+
center: "justify-center text-center",
|
|
36
|
+
right: "justify-end text-right",
|
|
37
|
+
};
|
|
45
38
|
|
|
46
39
|
return (
|
|
47
|
-
<div className="my-6
|
|
40
|
+
<div className={cn("my-6 flex w-full", alignmentClasses[buttonPosition])}>
|
|
48
41
|
{/* Case 1: Internal link (not external, not anchor, has URL) */}
|
|
49
42
|
{!isExternal && !isAnchor && !!content.url ? (
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
sizeClasses[buttonSize],
|
|
56
|
-
].join(" ")}
|
|
43
|
+
<Button
|
|
44
|
+
asChild
|
|
45
|
+
variant={buttonVariant}
|
|
46
|
+
size={buttonSize}
|
|
47
|
+
className={cn(content.variant === 'outline' && "text-foreground")}
|
|
57
48
|
>
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
<Link href={content.url}>
|
|
50
|
+
{buttonText}
|
|
51
|
+
</Link>
|
|
52
|
+
</Button>
|
|
60
53
|
) : /* Case 2: External or Anchor link (has URL) */
|
|
61
54
|
(isExternal || isAnchor) && !!content.url ? (
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
baseClasses,
|
|
68
|
-
variantClasses[buttonVariant],
|
|
69
|
-
sizeClasses[buttonSize],
|
|
70
|
-
].join(" ")}
|
|
55
|
+
<Button
|
|
56
|
+
asChild
|
|
57
|
+
variant={buttonVariant}
|
|
58
|
+
size={buttonSize}
|
|
59
|
+
className={cn(content.variant === 'outline' && "text-foreground")}
|
|
71
60
|
>
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
<a
|
|
62
|
+
href={content.url}
|
|
63
|
+
target={isExternal ? "_blank" : undefined}
|
|
64
|
+
rel={isExternal ? "noopener noreferrer" : undefined}
|
|
65
|
+
>
|
|
66
|
+
{buttonText}
|
|
67
|
+
</a>
|
|
68
|
+
</Button>
|
|
74
69
|
) : (
|
|
75
70
|
/* Case 3: No URL or other edge cases - render a plain or disabled button */
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
sizeClasses[buttonSize],
|
|
82
|
-
].join(" ")}
|
|
83
|
-
disabled={!content.url}
|
|
71
|
+
<Button
|
|
72
|
+
variant={buttonVariant}
|
|
73
|
+
size={buttonSize}
|
|
74
|
+
disabled={!content.url}
|
|
75
|
+
className={cn(content.variant === 'outline' && "text-foreground")}
|
|
84
76
|
>
|
|
85
77
|
{buttonText}
|
|
86
|
-
</
|
|
78
|
+
</Button>
|
|
87
79
|
)}
|
|
88
80
|
</div>
|
|
89
81
|
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to handle keyboard shortcuts.
|
|
5
|
+
* Currently optimized for 'ctrl+s' / 'meta+s'.
|
|
6
|
+
*
|
|
7
|
+
* @param key The key combination to listen for (e.g. 'ctrl+s')
|
|
8
|
+
* @param callback The function to call when the key combination is pressed
|
|
9
|
+
* @param deps Dependencies array for the effect
|
|
10
|
+
*/
|
|
11
|
+
export function useHotkeys(key: string, callback: (event: KeyboardEvent) => void, deps: any[] = []) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
14
|
+
const isCtrl = event.ctrlKey || event.metaKey; // cmd on mac, ctrl on windows
|
|
15
|
+
const keyLower = event.key.toLowerCase();
|
|
16
|
+
|
|
17
|
+
// Check for ctrl+s / cmd+s
|
|
18
|
+
if ((key === 'ctrl+s' || key === 'meta+s') && isCtrl && keyLower === 's') {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
callback(event);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
25
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
26
|
+
}, [key, ...deps]); // callback should be stable or included in deps if handled by caller
|
|
27
|
+
}
|
|
@@ -44,7 +44,8 @@ export const ButtonBlockSchema = z.object({
|
|
|
44
44
|
text: z.string().describe('The text displayed on the button'),
|
|
45
45
|
url: z.string().describe('The URL the button links to'),
|
|
46
46
|
variant: z.enum(['default', 'outline', 'secondary', 'ghost', 'link']).optional().describe('Visual style variant'),
|
|
47
|
-
size: z.enum(['default', 'sm', 'lg']).optional().describe('Size of the button'),
|
|
47
|
+
size: z.enum(['default', 'sm', 'lg', 'full']).optional().describe('Size of the button'),
|
|
48
|
+
position: z.enum(['left', 'center', 'right']).optional().describe('Button alignment'),
|
|
48
49
|
});
|
|
49
50
|
export type ButtonBlockContent = z.infer<typeof ButtonBlockSchema>;
|
|
50
51
|
|
|
@@ -281,7 +282,7 @@ export const blockRegistry: Record<BlockType, BlockDefinition> = {
|
|
|
281
282
|
type: "button",
|
|
282
283
|
label: "Button",
|
|
283
284
|
icon: "SquareMousePointer",
|
|
284
|
-
initialContent: { text: "Click Me", url: "#", variant: "default", size: "default" } as ButtonBlockContent,
|
|
285
|
+
initialContent: { text: "Click Me", url: "#", variant: "default", size: "default", position: "left" } as ButtonBlockContent,
|
|
285
286
|
editorComponentFilename: "ButtonBlockEditor.tsx",
|
|
286
287
|
rendererComponentFilename: "ButtonBlockRenderer.tsx",
|
|
287
288
|
schema: ButtonBlockSchema,
|