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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +73 -34
  3. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +309 -53
  4. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +175 -25
  5. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +44 -26
  6. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +74 -16
  7. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +2 -0
  8. package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
  9. package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
  10. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
  11. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
  12. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
  13. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
  14. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
  15. package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
  16. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
  17. package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
  18. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
  19. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
  20. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
  21. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
  22. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
  23. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
  24. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
  25. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
  26. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
  27. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
  28. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
  29. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +41 -49
  30. package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
  31. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +3 -2
  32. 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
- <div
92
- className={`p-3 rounded-md text-sm ${
93
- formMessage.type === 'success'
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
- <Label htmlFor="role">Role</Label>
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 ? "Saving..." : actionButtonText}
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 text-center">
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
- <Link
51
- href={content.url}
52
- className={[
53
- baseClasses,
54
- variantClasses[buttonVariant],
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
- {buttonText}
59
- </Link>
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
- <a
63
- href={content.url} // content.url is guaranteed by the condition
64
- target={isExternal ? "_blank" : undefined}
65
- rel={isExternal ? "noopener noreferrer" : undefined}
66
- className={[
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
- {buttonText}
73
- </a>
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
- <button
77
- type="button"
78
- className={[
79
- baseClasses,
80
- variantClasses[buttonVariant],
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
- </button>
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/template",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev",