create-nextblock 0.0.1

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 (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1,138 @@
1
+ // app/cms/users/components/UserForm.tsx
2
+ "use client";
3
+
4
+ import { useEffect, useState, useTransition } from "react";
5
+ import { useRouter, useSearchParams } from "next/navigation";
6
+ import { Button } from "@nextblock-cms/ui";
7
+ import { Input } from "@nextblock-cms/ui";
8
+ import { Label } from "@nextblock-cms/ui";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@nextblock-cms/ui";
16
+ import type { Database } from "@nextblock-cms/db";
17
+ import { useAuth } from "@/context/AuthContext";
18
+
19
+ type Profile = Database['public']['Tables']['profiles']['Row'];
20
+ type UserRole = Database['public']['Enums']['user_role'];
21
+ type AuthUser = {
22
+ id: string;
23
+ email: string | undefined;
24
+ created_at: string | undefined;
25
+ last_sign_in_at: string | undefined;
26
+ };
27
+
28
+ interface UserFormProps {
29
+ userToEditAuth: AuthUser; // Auth details (email, id) - email usually not editable here
30
+ userToEditProfile: Profile | null; // Profile details (role, username, etc.)
31
+ formAction: (formData: FormData) => Promise<{ error?: string } | void>;
32
+ actionButtonText?: string;
33
+ }
34
+
35
+ export default function UserForm({
36
+ userToEditAuth,
37
+ userToEditProfile,
38
+ formAction,
39
+ actionButtonText = "Save Changes",
40
+ }: UserFormProps) {
41
+ const router = useRouter();
42
+ const searchParams = useSearchParams();
43
+ const [isPending, startTransition] = useTransition();
44
+ const { isAdmin, isLoading: authLoading } = useAuth(); // For client-side guard
45
+
46
+ const [role, setRole] = useState<UserRole>(userToEditProfile?.role || "USER");
47
+ const [username, setUsername] = useState(userToEditProfile?.username || "");
48
+ const [fullName, setFullName] = useState(userToEditProfile?.full_name || "");
49
+ // Email is typically not changed here by an admin, it's part of auth.users managed by user or super-admin
50
+ const email = userToEditAuth.email || "N/A";
51
+
52
+ const [formMessage, setFormMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
53
+
54
+ useEffect(() => {
55
+ const successMessage = searchParams.get('success');
56
+ const errorMessage = searchParams.get('error');
57
+ if (successMessage) {
58
+ setFormMessage({ type: 'success', text: successMessage });
59
+ // Optionally clear the query param from URL
60
+ // router.replace(pathname, undefined, { shallow: true }); // if using next/router
61
+ } else if (errorMessage) {
62
+ setFormMessage({ type: 'error', text: errorMessage });
63
+ }
64
+ }, [searchParams, router]);
65
+
66
+
67
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
68
+ event.preventDefault();
69
+ setFormMessage(null);
70
+ const formData = new FormData(event.currentTarget);
71
+ // Add user ID to form data if needed by action, or pass it directly
72
+ // formData.append("userId", userToEditAuth.id);
73
+
74
+ startTransition(async () => {
75
+ const result = await formAction(formData); // The action is already bound with userId
76
+ if (result?.error) {
77
+ setFormMessage({ type: 'error', text: result.error });
78
+ }
79
+ // Success is handled by redirect with query param in server action
80
+ });
81
+ };
82
+
83
+ if (authLoading) return <div>Loading...</div>;
84
+ if (!isAdmin) return <div>Access Denied. Admin role required.</div>;
85
+
86
+ const userRoles: UserRole[] = ['USER', 'WRITER', 'ADMIN'];
87
+
88
+ return (
89
+ <form onSubmit={handleSubmit} className="space-y-6">
90
+ {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>
100
+ )}
101
+ <div>
102
+ <Label htmlFor="email">Email (Read-only)</Label>
103
+ <Input id="email" name="email" value={email} readOnly disabled className="mt-1 bg-muted/50" />
104
+ </div>
105
+
106
+ <div>
107
+ <Label htmlFor="username">Username</Label>
108
+ <Input id="username" name="username" value={username} onChange={(e) => setUsername(e.target.value)} className="mt-1" />
109
+ </div>
110
+
111
+ <div>
112
+ <Label htmlFor="full_name">Full Name</Label>
113
+ <Input id="full_name" name="full_name" value={fullName} onChange={(e) => setFullName(e.target.value)} className="mt-1" />
114
+ </div>
115
+
116
+ <div>
117
+ <Label htmlFor="role">Role</Label>
118
+ <Select name="role" value={role} onValueChange={(value) => setRole(value as UserRole)} required>
119
+ <SelectTrigger className="mt-1"><SelectValue placeholder="Select role" /></SelectTrigger>
120
+ <SelectContent>
121
+ {userRoles.map((r) => (
122
+ <SelectItem key={r} value={r}>{r.charAt(0) + r.slice(1).toLowerCase()}</SelectItem>
123
+ ))}
124
+ </SelectContent>
125
+ </Select>
126
+ </div>
127
+
128
+ <div className="flex justify-end space-x-3 pt-4">
129
+ <Button type="button" variant="outline" onClick={() => router.push("/cms/users")} disabled={isPending}>
130
+ Cancel
131
+ </Button>
132
+ <Button type="submit" disabled={isPending || authLoading}>
133
+ {isPending ? "Saving..." : actionButtonText}
134
+ </Button>
135
+ </div>
136
+ </form>
137
+ );
138
+ }
@@ -0,0 +1,183 @@
1
+ import React from "react";
2
+ import { createClient } from "@nextblock-cms/db/server";
3
+ import Link from "next/link";
4
+ import { Button } from "@nextblock-cms/ui";
5
+ import {
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from "@nextblock-cms/ui";
13
+ import { Badge } from "@nextblock-cms/ui";
14
+ import { MoreHorizontal, Edit3, Users } from "lucide-react";
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuItem,
19
+ DropdownMenuSeparator,
20
+ DropdownMenuTrigger,
21
+ } from "@nextblock-cms/ui";
22
+ import type { Database } from "@nextblock-cms/db";
23
+ import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
24
+
25
+ type AuthUser = {
26
+ id: string;
27
+ email: string | undefined;
28
+ created_at: string | undefined;
29
+ last_sign_in_at: string | undefined;
30
+ };
31
+ type UserWithProfile = {
32
+ authUser: AuthUser;
33
+ profile: Database['public']['Tables']['profiles']['Row'] | null;
34
+ };
35
+ import { DeleteUserButtonClient } from "./components/DeleteUserButton";
36
+
37
+ async function getUsersData(currentAdminId: string): Promise<UserWithProfile[]> {
38
+ // This needs to use a service role client to list all users from auth.users
39
+ const { createClient: createServiceRoleClient } = await import('@supabase/supabase-js');
40
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
41
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
42
+
43
+ if (!supabaseUrl || !serviceRoleKey) {
44
+ throw new Error('Missing required environment variables');
45
+ }
46
+
47
+ const supabaseAdmin = createServiceRoleClient(supabaseUrl, serviceRoleKey);
48
+
49
+ const { data: { users: authUsers }, error: usersError } = await supabaseAdmin.auth.admin.listUsers({
50
+ page: 1,
51
+ perPage: 1000, // Adjust as needed, handle pagination for very large user bases
52
+ });
53
+
54
+ if (usersError) {
55
+ console.error("Error fetching auth users:", usersError);
56
+ return [];
57
+ }
58
+ if (!authUsers) return [];
59
+
60
+ // Fetch all profiles
61
+ const supabase = createClient(); // Standard client for profile fetching (RLS applies)
62
+ const { data: profiles, error: profilesError } = await supabase
63
+ .from("profiles")
64
+ .select("*");
65
+
66
+ if (profilesError) {
67
+ console.error("Error fetching profiles:", profilesError);
68
+ // Continue without profiles if there's an error, or handle differently
69
+ }
70
+
71
+ const profilesMap = new Map(profiles?.map(p => [p.id, p]));
72
+
73
+ return authUsers.map(authUser => {
74
+ // Simplify authUser to only include necessary fields to avoid sending too much data
75
+ const simplifiedAuthUser: AuthUser = {
76
+ id: authUser.id,
77
+ email: authUser.email,
78
+ created_at: authUser.created_at,
79
+ last_sign_in_at: authUser.last_sign_in_at,
80
+ };
81
+ return {
82
+ authUser: simplifiedAuthUser,
83
+ profile: profilesMap.get(authUser.id) || null,
84
+ };
85
+ }).filter(user => user.authUser.id !== currentAdminId); // Filter out the current admin from the list
86
+ }
87
+
88
+ export default async function CmsUsersListPage() {
89
+ const supabase = createClient();
90
+ const { data: { user: currentAdmin } } = await supabase.auth.getUser();
91
+
92
+ if (!currentAdmin) {
93
+ // This should ideally be caught by middleware or layout auth checks
94
+ return <p>Access Denied. Not authenticated.</p>;
95
+ }
96
+ // Further check if current user is admin (already done by layout, but good for direct access attempts)
97
+ const { data: adminProfile } = await supabase.from('profiles').select('role').eq('id', currentAdmin.id).single();
98
+ if (adminProfile?.role !== 'ADMIN') {
99
+ return <p>Access Denied. Admin privileges required.</p>;
100
+ }
101
+
102
+ const users = await getUsersData(currentAdmin.id);
103
+
104
+ return (
105
+ <div className="w-full">
106
+ <div className="flex justify-between items-center mb-6">
107
+ <h1 className="text-2xl font-semibold">Manage Users</h1>
108
+ {/* No "Create New User" button as users are created via sign-up flow. Admins manage roles. */}
109
+ </div>
110
+
111
+ {users.length === 0 ? (
112
+ <div className="text-center py-10 border rounded-lg">
113
+ <Users className="mx-auto h-12 w-12 text-muted-foreground" />
114
+ <h3 className="mt-2 text-sm font-medium text-foreground">No other users found</h3>
115
+ <p className="mt-1 text-sm text-muted-foreground">
116
+ New users will appear here after they sign up.
117
+ </p>
118
+ </div>
119
+ ) : (
120
+ <div className="rounded-lg border overflow-hidden">
121
+ <Table>
122
+ <TableHeader>
123
+ <TableRow>
124
+ <TableHead className="w-[80px]">Avatar</TableHead>
125
+ <TableHead>Email</TableHead>
126
+ <TableHead>Username</TableHead>
127
+ <TableHead>Full Name</TableHead>
128
+ <TableHead>Role</TableHead>
129
+ <TableHead>Joined</TableHead>
130
+ <TableHead className="text-right w-[80px]">Actions</TableHead>
131
+ </TableRow>
132
+ </TableHeader>
133
+ <TableBody>
134
+ {users.map(({ authUser, profile }) => (
135
+ <TableRow key={authUser.id}>
136
+ <TableCell>
137
+ <Avatar className="h-9 w-9">
138
+ <AvatarImage src={profile?.avatar_url || undefined} alt={profile?.username || authUser.email} />
139
+ <AvatarFallback>{authUser.email?.charAt(0).toUpperCase() || 'U'}</AvatarFallback>
140
+ </Avatar>
141
+ </TableCell>
142
+ <TableCell className="font-medium">{authUser.email}</TableCell>
143
+ <TableCell className="text-muted-foreground">{profile?.username || "N/A"}</TableCell>
144
+ <TableCell className="text-muted-foreground">{profile?.full_name || "N/A"}</TableCell>
145
+ <TableCell>
146
+ <Badge variant={
147
+ profile?.role === "ADMIN" ? "destructive" :
148
+ profile?.role === "WRITER" ? "secondary" : "outline"
149
+ }>
150
+ {profile?.role || "N/A"}
151
+ </Badge>
152
+ </TableCell>
153
+ <TableCell className="text-xs text-muted-foreground">
154
+ {authUser.created_at ? new Date(authUser.created_at).toLocaleDateString() : "N/A"}
155
+ </TableCell>
156
+ <TableCell className="text-right">
157
+ <DropdownMenu>
158
+ <DropdownMenuTrigger asChild>
159
+ <Button variant="ghost" size="icon" disabled={authUser.id === currentAdmin.id}>
160
+ <MoreHorizontal className="h-4 w-4" />
161
+ <span className="sr-only">User actions</span>
162
+ </Button>
163
+ </DropdownMenuTrigger>
164
+ <DropdownMenuContent align="end">
165
+ <DropdownMenuItem asChild>
166
+ <Link href={`/cms/users/${authUser.id}/edit`} className="flex items-center">
167
+ <Edit3 className="mr-2 h-4 w-4" /> Edit Role/Profile
168
+ </Link>
169
+ </DropdownMenuItem>
170
+ <DropdownMenuSeparator />
171
+ <DeleteUserButtonClient userId={authUser.id} userEmail={authUser.email} currentAdminId={currentAdmin.id} />
172
+ </DropdownMenuContent>
173
+ </DropdownMenu>
174
+ </TableCell>
175
+ </TableRow>
176
+ ))}
177
+ </TableBody>
178
+ </Table>
179
+ </div>
180
+ )}
181
+ </div>
182
+ );
183
+ }