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.
- package/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- package/templates/nextblock-template/tsconfig.json +62 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition, type ChangeEvent } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import Image from 'next/image'
|
|
6
|
+
import { Input } from '@nextblock-cms/ui'
|
|
7
|
+
import { Label } from '@nextblock-cms/ui'
|
|
8
|
+
import { Button } from '@nextblock-cms/ui'
|
|
9
|
+
import type { Database } from '@nextblock-cms/db'
|
|
10
|
+
import { ImageIcon, X as XIcon } from 'lucide-react'
|
|
11
|
+
import MediaPickerDialog from '@/app/cms/media/components/MediaPickerDialog'
|
|
12
|
+
type Media = Database['public']['Tables']['media']['Row'];
|
|
13
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || ''
|
|
14
|
+
|
|
15
|
+
interface LogoDetails {
|
|
16
|
+
id?: string
|
|
17
|
+
name: string
|
|
18
|
+
media_id: string | null
|
|
19
|
+
object_key: string | null
|
|
20
|
+
width: number | null
|
|
21
|
+
height: number | null
|
|
22
|
+
blur_data_url: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LogoFormProps { logo?: Database["public"]["Tables"]["logos"]["Row"] & { media: Media | null }
|
|
26
|
+
action: (
|
|
27
|
+
payload:
|
|
28
|
+
| { name: string; media_id: string }
|
|
29
|
+
| { id: string; name: string; media_id: string },
|
|
30
|
+
) => Promise<{ success: boolean; error?: string }>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function LogoForm({ logo, action }: LogoFormProps) {
|
|
34
|
+
const router = useRouter()
|
|
35
|
+
const [logoDetails, setLogoDetails] = useState<LogoDetails>({
|
|
36
|
+
id: logo?.id,
|
|
37
|
+
name: logo?.name || '',
|
|
38
|
+
media_id: logo?.media_id || null,
|
|
39
|
+
object_key: logo?.media?.object_key || null,
|
|
40
|
+
width: logo?.media?.width || null,
|
|
41
|
+
height: logo?.media?.height || null,
|
|
42
|
+
blur_data_url: logo?.media?.blur_data_url || null,
|
|
43
|
+
})
|
|
44
|
+
const [formError, setFormError] = useState<string | null>(null)
|
|
45
|
+
const [isPending, startTransition] = useTransition()
|
|
46
|
+
|
|
47
|
+
// Removed unused media library state and effect
|
|
48
|
+
|
|
49
|
+
const handleMediaSelect = (media: Media) => {
|
|
50
|
+
setLogoDetails(prev => ({
|
|
51
|
+
...prev,
|
|
52
|
+
media_id: media.id,
|
|
53
|
+
object_key: media.object_key,
|
|
54
|
+
width: media.width ?? null,
|
|
55
|
+
height: media.height ?? null,
|
|
56
|
+
blur_data_url: media.blur_data_url ?? null,
|
|
57
|
+
}))
|
|
58
|
+
// MediaPickerDialog closes itself after selection
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleRemoveImage = () => {
|
|
62
|
+
setLogoDetails(prev => ({
|
|
63
|
+
...prev,
|
|
64
|
+
media_id: null,
|
|
65
|
+
object_key: null,
|
|
66
|
+
width: null,
|
|
67
|
+
height: null,
|
|
68
|
+
blur_data_url: null,
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleSave = async () => {
|
|
73
|
+
if (!logoDetails.name || !logoDetails.media_id) {
|
|
74
|
+
setFormError('Please provide a name and select an image.')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setFormError(null)
|
|
79
|
+
|
|
80
|
+
startTransition(async () => {
|
|
81
|
+
const payload = {
|
|
82
|
+
name: logoDetails.name,
|
|
83
|
+
media_id: logoDetails.media_id as string,
|
|
84
|
+
...(logoDetails.id && { id: logoDetails.id }),
|
|
85
|
+
}
|
|
86
|
+
const result = await action(payload)
|
|
87
|
+
|
|
88
|
+
if (result?.error) {
|
|
89
|
+
setFormError(result.error)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (result?.success) {
|
|
93
|
+
router.push('/cms/settings/logos')
|
|
94
|
+
router.refresh()
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="space-y-6">
|
|
101
|
+
<div>
|
|
102
|
+
<Label htmlFor="name">Logo Name</Label>
|
|
103
|
+
<Input
|
|
104
|
+
id="name"
|
|
105
|
+
name="name"
|
|
106
|
+
value={logoDetails.name}
|
|
107
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
108
|
+
setLogoDetails(prev => ({ ...prev, name: e.target.value }))
|
|
109
|
+
}
|
|
110
|
+
required
|
|
111
|
+
className="mt-1"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div>
|
|
116
|
+
<Label>Logo Image</Label>
|
|
117
|
+
<div className="mt-1 p-3 border rounded-md bg-muted/30 min-h-[120px] flex flex-col items-center justify-center">
|
|
118
|
+
{logoDetails.object_key &&
|
|
119
|
+
logoDetails.width &&
|
|
120
|
+
logoDetails.height ? (
|
|
121
|
+
<div
|
|
122
|
+
className="relative group inline-block"
|
|
123
|
+
style={{ maxWidth: logoDetails.width, maxHeight: 200 }}
|
|
124
|
+
>
|
|
125
|
+
<Image
|
|
126
|
+
src={`${R2_BASE_URL}/${logoDetails.object_key}`}
|
|
127
|
+
alt={logoDetails.name || 'Selected logo'}
|
|
128
|
+
width={logoDetails.width}
|
|
129
|
+
height={logoDetails.height}
|
|
130
|
+
className="rounded-md object-contain"
|
|
131
|
+
style={{ maxHeight: '200px' }}
|
|
132
|
+
placeholder={logoDetails.blur_data_url ? 'blur' : 'empty'}
|
|
133
|
+
blurDataURL={logoDetails.blur_data_url || undefined}
|
|
134
|
+
/>
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
variant="destructive"
|
|
138
|
+
size="icon"
|
|
139
|
+
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6"
|
|
140
|
+
onClick={handleRemoveImage}
|
|
141
|
+
title="Remove Image"
|
|
142
|
+
>
|
|
143
|
+
<XIcon className="h-3 w-3" />
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<ImageIcon className="h-16 w-16 text-muted-foreground" />
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
<MediaPickerDialog
|
|
151
|
+
triggerLabel={logoDetails.object_key ? 'Change Image' : 'Select from Library'}
|
|
152
|
+
onSelect={handleMediaSelect}
|
|
153
|
+
accept={(m: Media) => !!m.file_type?.startsWith('image/')}
|
|
154
|
+
title="Select or Upload Logo"
|
|
155
|
+
defaultFolder="logos/"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="flex items-center gap-4">
|
|
161
|
+
<Button
|
|
162
|
+
onClick={handleSave}
|
|
163
|
+
disabled={isPending || !logoDetails.name || !logoDetails.media_id}
|
|
164
|
+
>
|
|
165
|
+
{isPending ? 'Saving...' : `${logo ? 'Update' : 'Create'} Logo`}
|
|
166
|
+
</Button>
|
|
167
|
+
{formError && (
|
|
168
|
+
<div className="text-red-500 text-sm">{formError}</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import LogoForm from '../components/LogoForm'
|
|
2
|
+
import { createLogo } from '../actions'
|
|
3
|
+
|
|
4
|
+
export default function NewLogoPage() {
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<h1 className="text-2xl font-semibold mb-6">Create New Logo</h1>
|
|
8
|
+
<LogoForm action={createLogo} />
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Link from 'next/link'
|
|
3
|
+
import { Button } from '@nextblock-cms/ui'
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from '@nextblock-cms/ui'
|
|
12
|
+
import { MoreHorizontal, PlusCircle, Edit3, Image as ImageIcon } from 'lucide-react'
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
} from '@nextblock-cms/ui'
|
|
20
|
+
import { deleteLogo, getLogos } from './actions'
|
|
21
|
+
import MediaImage from '@/app/cms/media/components/MediaImage'
|
|
22
|
+
|
|
23
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || ''
|
|
24
|
+
|
|
25
|
+
export default async function CmsLogosListPage() {
|
|
26
|
+
const logos = await getLogos()
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="w-full">
|
|
30
|
+
<div className="flex justify-between items-center mb-6">
|
|
31
|
+
<h1 className="text-2xl font-semibold">Manage Logos</h1>
|
|
32
|
+
<Button variant="default" asChild>
|
|
33
|
+
<Link href="/cms/settings/logos/new">
|
|
34
|
+
<PlusCircle className="mr-2 h-4 w-4" /> New Logo
|
|
35
|
+
</Link>
|
|
36
|
+
</Button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{logos.length === 0 ? (
|
|
40
|
+
<div className="text-center py-10 border rounded-lg">
|
|
41
|
+
<ImageIcon className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
42
|
+
<h3 className="mt-2 text-sm font-medium">No logos found.</h3>
|
|
43
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
44
|
+
Get started by creating a new logo.
|
|
45
|
+
</p>
|
|
46
|
+
<div className="mt-6">
|
|
47
|
+
<Button asChild>
|
|
48
|
+
<Link href="/cms/settings/logos/new">
|
|
49
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Create Logo
|
|
50
|
+
</Link>
|
|
51
|
+
</Button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
) : (
|
|
55
|
+
<div className="rounded-lg border overflow-hidden">
|
|
56
|
+
<Table>
|
|
57
|
+
<TableHeader>
|
|
58
|
+
<TableRow>
|
|
59
|
+
<TableHead className="w-[80px]">Image</TableHead>
|
|
60
|
+
<TableHead>Name</TableHead>
|
|
61
|
+
<TableHead className="hidden md:table-cell">Created At</TableHead>
|
|
62
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
63
|
+
</TableRow>
|
|
64
|
+
</TableHeader>
|
|
65
|
+
<TableBody>
|
|
66
|
+
{logos.map(logo => (
|
|
67
|
+
<TableRow key={logo.id}>
|
|
68
|
+
<TableCell>
|
|
69
|
+
{logo.media ? (
|
|
70
|
+
<MediaImage
|
|
71
|
+
src={`${R2_BASE_URL}/${logo.media.object_key}`}
|
|
72
|
+
alt={logo.media.alt_text || logo.name}
|
|
73
|
+
width={logo.media.width || 100}
|
|
74
|
+
height={logo.media.height || 100}
|
|
75
|
+
className="max-w-16 max-h-16 object-contain"
|
|
76
|
+
/>
|
|
77
|
+
) : (
|
|
78
|
+
<div className="w-16 h-16 bg-muted rounded-sm flex items-center justify-center">
|
|
79
|
+
<ImageIcon className="h-6 w-6 text-muted-foreground" />
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</TableCell>
|
|
83
|
+
<TableCell className="font-medium">{logo.name}</TableCell>
|
|
84
|
+
<TableCell className="hidden md:table-cell">
|
|
85
|
+
{new Date(logo.created_at).toLocaleDateString()}
|
|
86
|
+
</TableCell>
|
|
87
|
+
<TableCell className="text-right">
|
|
88
|
+
<DropdownMenu>
|
|
89
|
+
<DropdownMenuTrigger asChild>
|
|
90
|
+
<Button variant="ghost" size="icon">
|
|
91
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
92
|
+
</Button>
|
|
93
|
+
</DropdownMenuTrigger>
|
|
94
|
+
<DropdownMenuContent align="end">
|
|
95
|
+
<DropdownMenuItem asChild>
|
|
96
|
+
<Link href={`/cms/settings/logos/${logo.id}/edit`}>
|
|
97
|
+
<Edit3 className="mr-2 h-4 w-4" />
|
|
98
|
+
Edit
|
|
99
|
+
</Link>
|
|
100
|
+
</DropdownMenuItem>
|
|
101
|
+
<DropdownMenuSeparator />
|
|
102
|
+
<form action={deleteLogo.bind(null, logo.id)}>
|
|
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>
|
|
107
|
+
</DropdownMenuContent>
|
|
108
|
+
</DropdownMenu>
|
|
109
|
+
</TableCell>
|
|
110
|
+
</TableRow>
|
|
111
|
+
))}
|
|
112
|
+
</TableBody>
|
|
113
|
+
</Table>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// app/cms/users/[id]/edit/page.tsx
|
|
2
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
3
|
+
import UserForm from "../../components/UserForm";
|
|
4
|
+
import { updateUserProfile } from "../../actions";
|
|
5
|
+
import type { Database } from "@nextblock-cms/db";
|
|
6
|
+
import { notFound } from "next/navigation";
|
|
7
|
+
|
|
8
|
+
type Profile = Database['public']['Tables']['profiles']['Row'];
|
|
9
|
+
type AuthUser = {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string | undefined;
|
|
12
|
+
created_at: string | undefined;
|
|
13
|
+
last_sign_in_at: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async function getUserAndProfileData(userId: string): Promise<{ authUser: AuthUser; profile: Profile | null } | null> {
|
|
17
|
+
const supabase = createClient();
|
|
18
|
+
|
|
19
|
+
// Fetch user from auth.users
|
|
20
|
+
// For admin operations, you might need a service_role client to fetch any user.
|
|
21
|
+
// However, for just getting user details by ID, this might work if RLS allows admin to read.
|
|
22
|
+
// A more robust way for admin panel is to use supabase.auth.admin.getUserById(userId)
|
|
23
|
+
// This requires a client initialized with SERVICE_ROLE_KEY.
|
|
24
|
+
|
|
25
|
+
// Let's use the admin API for fetching the auth user.
|
|
26
|
+
const { createClient: createServiceRoleClient } = await import('@supabase/supabase-js');
|
|
27
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
28
|
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
29
|
+
|
|
30
|
+
if (!supabaseUrl || !serviceRoleKey) {
|
|
31
|
+
throw new Error('Missing required environment variables');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const serviceSupabase = createServiceRoleClient(supabaseUrl, serviceRoleKey);
|
|
35
|
+
|
|
36
|
+
const { data: { user: authUserData }, error: authUserError } = await serviceSupabase.auth.admin.getUserById(userId);
|
|
37
|
+
|
|
38
|
+
if (authUserError || !authUserData) {
|
|
39
|
+
console.error("Error fetching auth user for edit:", authUserError);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fetch profile from public.profiles
|
|
44
|
+
const { data: profileData, error: profileError } = await supabase
|
|
45
|
+
.from("profiles")
|
|
46
|
+
.select("*")
|
|
47
|
+
.eq("id", userId)
|
|
48
|
+
.single();
|
|
49
|
+
|
|
50
|
+
if (profileError && profileError.code !== 'PGRST116') { // PGRST116: single row not found, which is okay if profile not created yet
|
|
51
|
+
console.error("Error fetching profile for edit:", profileError);
|
|
52
|
+
// Decide if this is a critical error. A user might exist in auth but not profiles if trigger failed.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const simplifiedAuthUser: AuthUser = {
|
|
56
|
+
id: authUserData.id,
|
|
57
|
+
email: authUserData.email,
|
|
58
|
+
created_at: authUserData.created_at,
|
|
59
|
+
last_sign_in_at: authUserData.last_sign_in_at,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return { authUser: simplifiedAuthUser, profile: profileData as Profile | null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default async function EditUserPage(props: { params: Promise<{ id: string }> }) {
|
|
66
|
+
const params = await props.params;
|
|
67
|
+
const userId = params.id;
|
|
68
|
+
if (!userId) {
|
|
69
|
+
return notFound();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const userData = await getUserAndProfileData(userId);
|
|
73
|
+
|
|
74
|
+
if (!userData || !userData.authUser) {
|
|
75
|
+
return notFound(); // Or a more specific "User not found" component
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const updateUserActionWithId = updateUserProfile.bind(null, userId);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="max-w-xl mx-auto">
|
|
82
|
+
<h1 className="text-2xl font-bold mb-6">Edit User: {userData.authUser.email}</h1>
|
|
83
|
+
<UserForm
|
|
84
|
+
userToEditAuth={userData.authUser}
|
|
85
|
+
userToEditProfile={userData.profile}
|
|
86
|
+
formAction={updateUserActionWithId}
|
|
87
|
+
actionButtonText="Update User Profile"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// app/cms/users/actions.ts
|
|
2
|
+
"use server";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
+
import { revalidatePath } from "next/cache";
|
|
6
|
+
import { redirect } from "next/navigation";
|
|
7
|
+
import type { Database } from "@nextblock-cms/db";
|
|
8
|
+
|
|
9
|
+
type UserRole = Database['public']['Enums']['user_role'];
|
|
10
|
+
|
|
11
|
+
// Helper to check admin role using the server client
|
|
12
|
+
async function verifyAdmin(supabase: ReturnType<typeof createClient>): Promise<{ isAdmin: boolean; error?: string; userId?: string }> {
|
|
13
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
14
|
+
if (authError || !user) {
|
|
15
|
+
return { isAdmin: false, error: "Authentication required." };
|
|
16
|
+
}
|
|
17
|
+
const { data: profile, error: profileError } = await supabase
|
|
18
|
+
.from("profiles")
|
|
19
|
+
.select("role")
|
|
20
|
+
.eq("id", user.id)
|
|
21
|
+
.single();
|
|
22
|
+
|
|
23
|
+
if (profileError || !profile) {
|
|
24
|
+
return { isAdmin: false, error: "Profile not found or error fetching profile." };
|
|
25
|
+
}
|
|
26
|
+
if (profile.role !== "ADMIN") {
|
|
27
|
+
return { isAdmin: false, error: "Admin privileges required." };
|
|
28
|
+
}
|
|
29
|
+
return { isAdmin: true, userId: user.id };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type UpdateUserProfilePayload = {
|
|
33
|
+
role: UserRole;
|
|
34
|
+
username?: string | null;
|
|
35
|
+
full_name?: string | null;
|
|
36
|
+
// Add other editable profile fields here if needed
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export async function updateUserProfile(userIdToUpdate: string, formData: FormData) {
|
|
40
|
+
const supabase = createClient();
|
|
41
|
+
const adminCheck = await verifyAdmin(supabase);
|
|
42
|
+
if (!adminCheck.isAdmin) {
|
|
43
|
+
return { error: adminCheck.error || "Unauthorized" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rawFormData = {
|
|
47
|
+
role: formData.get("role") as UserRole,
|
|
48
|
+
username: formData.get("username") as string || null,
|
|
49
|
+
full_name: formData.get("full_name") as string || null,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (!rawFormData.role) {
|
|
53
|
+
return { error: "Role is a required field." };
|
|
54
|
+
}
|
|
55
|
+
if (!['ADMIN', 'WRITER', 'USER'].includes(rawFormData.role)) {
|
|
56
|
+
return { error: "Invalid role specified." };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Prevent an admin from accidentally removing their own admin role if they are the only admin
|
|
60
|
+
// This is a basic check; a more robust system might count admins.
|
|
61
|
+
if (userIdToUpdate === adminCheck.userId && rawFormData.role !== 'ADMIN') {
|
|
62
|
+
const { count } = await supabase
|
|
63
|
+
.from('profiles')
|
|
64
|
+
.select('*', { count: 'exact', head: true })
|
|
65
|
+
.eq('role', 'ADMIN');
|
|
66
|
+
if (count === 1) {
|
|
67
|
+
return { error: "Cannot remove the last admin's role." };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const profileData: UpdateUserProfilePayload = {
|
|
73
|
+
role: rawFormData.role,
|
|
74
|
+
username: rawFormData.username,
|
|
75
|
+
full_name: rawFormData.full_name,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const { error } = await supabase
|
|
79
|
+
.from("profiles")
|
|
80
|
+
.update(profileData)
|
|
81
|
+
.eq("id", userIdToUpdate);
|
|
82
|
+
|
|
83
|
+
if (error) {
|
|
84
|
+
console.error("Error updating user profile:", error);
|
|
85
|
+
return { error: `Failed to update profile: ${error.message}` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
revalidatePath("/cms/users");
|
|
89
|
+
revalidatePath(`/cms/users/${userIdToUpdate}/edit`);
|
|
90
|
+
redirect(`/cms/users/${userIdToUpdate}/edit?success=User profile updated successfully`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function deleteUserAndProfile(userIdToDelete: string) {
|
|
94
|
+
|
|
95
|
+
// For deleting a user, we need to use the Supabase Admin API,
|
|
96
|
+
// which requires a client initialized with the SERVICE_ROLE_KEY.
|
|
97
|
+
// This ensures the operation has the necessary privileges.
|
|
98
|
+
// IMPORTANT: Ensure SUPABASE_SERVICE_ROLE_KEY is set in your .env.local and Vercel env vars.
|
|
99
|
+
const supabaseAdmin = createClient(
|
|
100
|
+
// Re-create client with service role. This is a common pattern.
|
|
101
|
+
// Ensure your createClient function can be called without args to use env vars,
|
|
102
|
+
// or pass them explicitly if needed. The one from the template should work.
|
|
103
|
+
// If your createClient is specific to user context (cookies), you might need a separate
|
|
104
|
+
// admin client factory. For now, assuming `createClient()` can make a service client
|
|
105
|
+
// if called in a server action without user cookie context, or if it defaults to service key.
|
|
106
|
+
// A safer way:
|
|
107
|
+
// const supabaseAdmin = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
|
|
108
|
+
// However, the template's createClient for server components/actions should handle this by not having cookie access.
|
|
109
|
+
// Let's assume for now createClient() is sufficient if it can use service role.
|
|
110
|
+
// A more explicit way for admin actions:
|
|
111
|
+
// import { createClient as createAdminClient } from '@supabase/supabase-js';
|
|
112
|
+
// const supabaseAdmin = createAdminClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
const adminCheck = await verifyAdmin(supabaseAdmin); // Verify current user is admin
|
|
117
|
+
if (!adminCheck.isAdmin) {
|
|
118
|
+
return { error: adminCheck.error || "Unauthorized" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (userIdToDelete === adminCheck.userId) {
|
|
122
|
+
return { error: "Admins cannot delete their own account through this panel." };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Use the Supabase Auth Admin API to delete the user
|
|
126
|
+
// This requires the `SERVICE_ROLE_KEY` to be configured for the Supabase client.
|
|
127
|
+
// The standard `createClient` from `utils/supabase/server` might not use the service role by default.
|
|
128
|
+
// You might need a dedicated admin client instance.
|
|
129
|
+
// For this example, we'll assume `supabase.auth.admin.deleteUser` is available and configured.
|
|
130
|
+
// If not, this part needs adjustment to use a service_role client.
|
|
131
|
+
|
|
132
|
+
// The `createClient()` from `@supabase/ssr` for server context doesn't directly expose `auth.admin`.
|
|
133
|
+
// We need to create a standard Supabase client with the service role key.
|
|
134
|
+
const { createClient: createServiceRoleClient } = await import('@supabase/supabase-js');
|
|
135
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
136
|
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
137
|
+
|
|
138
|
+
if (!supabaseUrl || !serviceRoleKey) {
|
|
139
|
+
throw new Error('Missing required environment variables');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const serviceSupabase = createServiceRoleClient(supabaseUrl, serviceRoleKey);
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const { error: deletionError } = await serviceSupabase.auth.admin.deleteUser(userIdToDelete);
|
|
146
|
+
|
|
147
|
+
if (deletionError) {
|
|
148
|
+
console.error("Error deleting user:", deletionError);
|
|
149
|
+
// If the profile was deleted by cascade but auth user deletion failed, this is an inconsistent state.
|
|
150
|
+
return { error: `Failed to delete user: ${deletionError.message}` };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The `profiles` table has ON DELETE CASCADE for the user ID, so it should be deleted automatically.
|
|
154
|
+
revalidatePath("/cms/users");
|
|
155
|
+
redirect("/cms/users?success=User deleted successfully");
|
|
156
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// app/cms/users/components/DeleteUserButton.tsx
|
|
2
|
+
"use client"; // This is crucial
|
|
3
|
+
|
|
4
|
+
import React, { useState, useTransition } from "react";
|
|
5
|
+
import { DropdownMenuItem } from "@nextblock-cms/ui";
|
|
6
|
+
import { Trash2, ShieldAlert } from "lucide-react";
|
|
7
|
+
import { deleteUserAndProfile } from "../actions";
|
|
8
|
+
import { ConfirmationModal } from "@/app/cms/components/ConfirmationModal";
|
|
9
|
+
|
|
10
|
+
export function DeleteUserButtonClient({
|
|
11
|
+
userId,
|
|
12
|
+
userEmail,
|
|
13
|
+
currentAdminId,
|
|
14
|
+
}: {
|
|
15
|
+
userId: string;
|
|
16
|
+
userEmail?: string;
|
|
17
|
+
currentAdminId?: string;
|
|
18
|
+
}) {
|
|
19
|
+
void userEmail;
|
|
20
|
+
const [isPending, startTransition] = useTransition();
|
|
21
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
22
|
+
|
|
23
|
+
const handleDelete = () => {
|
|
24
|
+
if (userId === currentAdminId) {
|
|
25
|
+
alert("Admins cannot delete their own account through this panel.");
|
|
26
|
+
setIsModalOpen(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
startTransition(async () => {
|
|
30
|
+
const result = await deleteUserAndProfile(userId);
|
|
31
|
+
if (result?.error) {
|
|
32
|
+
alert(`Error: ${result.error}`);
|
|
33
|
+
}
|
|
34
|
+
setIsModalOpen(false);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<DropdownMenuItem
|
|
41
|
+
className={`text-red-600 hover:!text-red-600 hover:!bg-red-50 dark:hover:!bg-red-700/20 ${
|
|
42
|
+
userId === currentAdminId ? "opacity-50 cursor-not-allowed" : ""
|
|
43
|
+
}`}
|
|
44
|
+
onSelect={(e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
if (userId !== currentAdminId) setIsModalOpen(true);
|
|
47
|
+
}}
|
|
48
|
+
disabled={userId === currentAdminId}
|
|
49
|
+
>
|
|
50
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
51
|
+
Delete User
|
|
52
|
+
{userId === currentAdminId && (
|
|
53
|
+
<span title="You cannot delete your own account.">
|
|
54
|
+
<ShieldAlert
|
|
55
|
+
className="ml-auto h-4 w-4 text-yellow-500"
|
|
56
|
+
aria-label="You cannot delete your own account."
|
|
57
|
+
/>
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</DropdownMenuItem>
|
|
61
|
+
<ConfirmationModal
|
|
62
|
+
isOpen={isModalOpen}
|
|
63
|
+
onClose={() => setIsModalOpen(false)}
|
|
64
|
+
onConfirm={handleDelete}
|
|
65
|
+
title="Are you sure?"
|
|
66
|
+
description="This will permanently delete the user. This action cannot be undone."
|
|
67
|
+
confirmText={isPending ? "Deleting..." : "Confirm"}
|
|
68
|
+
/>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|