create-ej-app 24.0.0 → 26.0.0
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/cli.js +6 -0
- package/cli.ts +8 -0
- package/package.json +2 -2
- package/template/api/package.json +2 -2
- package/template/nextjs/gitignore +1 -0
- package/template/nextjs/package.json +20 -20
- package/template/nextjs/postcss.config.mjs +3 -1
- package/template/saas-starter/.dockerignore +7 -0
- package/template/saas-starter/.env.example +25 -0
- package/template/saas-starter/.vscode/extensions.json +11 -0
- package/template/saas-starter/.vscode/settings.json +3 -0
- package/template/saas-starter/Dockerfile +15 -0
- package/template/saas-starter/README.md +3 -0
- package/template/saas-starter/components.json +25 -0
- package/template/saas-starter/drizzle.config.ts +10 -0
- package/template/saas-starter/eslint.config.mjs +23 -0
- package/template/saas-starter/gitignore +9 -0
- package/template/saas-starter/next-env.d.ts +6 -0
- package/template/saas-starter/next.config.ts +14 -0
- package/template/saas-starter/package.json +92 -0
- package/template/saas-starter/postcss.config.mjs +7 -0
- package/template/saas-starter/prettier.config.mjs +11 -0
- package/template/saas-starter/public/placeholder.svg +1 -0
- package/template/saas-starter/src/actions/uploads.ts +28 -0
- package/template/saas-starter/src/actions/uploads.validation.ts +14 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +97 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +21 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +40 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +57 -0
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +3 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +183 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +129 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +415 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +15 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +152 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +163 -0
- package/template/saas-starter/src/app/(app)/actions/organization.ts +92 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +20 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +34 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +100 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +364 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +155 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +232 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +116 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +169 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +571 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +40 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +69 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +29 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +17 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +27 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +41 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +140 -0
- package/template/saas-starter/src/app/(app)/app/layout.tsx +52 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +129 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +42 -0
- package/template/saas-starter/src/app/(app)/app/page.tsx +81 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +109 -0
- package/template/saas-starter/src/app/(auth)/login/page.tsx +87 -0
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +75 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +88 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +167 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +14 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +3 -0
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +3 -0
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +3 -0
- package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
- package/template/saas-starter/src/app/favicon.ico +0 -0
- package/template/saas-starter/src/app/globals.css +240 -0
- package/template/saas-starter/src/app/layout.tsx +53 -0
- package/template/saas-starter/src/app/not-found.tsx +27 -0
- package/template/saas-starter/src/app/proxy.ts +28 -0
- package/template/saas-starter/src/app/robots.ts +32 -0
- package/template/saas-starter/src/app/sitemap.ts +20 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +64 -0
- package/template/saas-starter/src/components/data-table/enum.ts +5 -0
- package/template/saas-starter/src/components/data-table/index.tsx +960 -0
- package/template/saas-starter/src/components/data-table/types.ts +75 -0
- package/template/saas-starter/src/components/error.tsx +26 -0
- package/template/saas-starter/src/components/file-upload.tsx +215 -0
- package/template/saas-starter/src/components/impersonation-banner.tsx +44 -0
- package/template/saas-starter/src/components/loader.tsx +25 -0
- package/template/saas-starter/src/components/navigation-search.tsx +140 -0
- package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
- package/template/saas-starter/src/components/page-heading.tsx +32 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +12 -0
- package/template/saas-starter/src/components/screen-size.tsx +40 -0
- package/template/saas-starter/src/components/site-footer.tsx +23 -0
- package/template/saas-starter/src/components/theme-provider.tsx +67 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +22 -0
- package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
- package/template/saas-starter/src/components/ui/badge.tsx +49 -0
- package/template/saas-starter/src/components/ui/button.tsx +67 -0
- package/template/saas-starter/src/components/ui/card.tsx +103 -0
- package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
- package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
- package/template/saas-starter/src/components/ui/command.tsx +192 -0
- package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
- package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/saas-starter/src/components/ui/field.tsx +238 -0
- package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
- package/template/saas-starter/src/components/ui/input.tsx +19 -0
- package/template/saas-starter/src/components/ui/label.tsx +24 -0
- package/template/saas-starter/src/components/ui/popover.tsx +89 -0
- package/template/saas-starter/src/components/ui/select.tsx +195 -0
- package/template/saas-starter/src/components/ui/separator.tsx +28 -0
- package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
- package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
- package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
- package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
- package/template/saas-starter/src/components/ui/switch.tsx +33 -0
- package/template/saas-starter/src/components/ui/table.tsx +116 -0
- package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
- package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
- package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
- package/template/saas-starter/src/components/user-menu.tsx +90 -0
- package/template/saas-starter/src/db/common.ts +31 -0
- package/template/saas-starter/src/db/enums.ts +26 -0
- package/template/saas-starter/src/db/index.ts +51 -0
- package/template/saas-starter/src/db/relations.ts +75 -0
- package/template/saas-starter/src/db/schema.ts +191 -0
- package/template/saas-starter/src/emails/components/email-layout.tsx +63 -0
- package/template/saas-starter/src/emails/invitation.tsx +56 -0
- package/template/saas-starter/src/emails/magic-link.tsx +49 -0
- package/template/saas-starter/src/emails/test.tsx +32 -0
- package/template/saas-starter/src/emails/welcome.tsx +34 -0
- package/template/saas-starter/src/env.ts +66 -0
- package/template/saas-starter/src/hooks/use-mobile.ts +19 -0
- package/template/saas-starter/src/instrumentation.ts +26 -0
- package/template/saas-starter/src/lib/actions.ts +21 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +31 -0
- package/template/saas-starter/src/lib/app-navigation.ts +59 -0
- package/template/saas-starter/src/lib/auth-client.ts +29 -0
- package/template/saas-starter/src/lib/auth-permissions.ts +22 -0
- package/template/saas-starter/src/lib/auth.ts +211 -0
- package/template/saas-starter/src/lib/billing.ts +117 -0
- package/template/saas-starter/src/lib/cn.ts +4 -0
- package/template/saas-starter/src/lib/constants.ts +22 -0
- package/template/saas-starter/src/lib/delay.ts +2 -0
- package/template/saas-starter/src/lib/email-service.ts +89 -0
- package/template/saas-starter/src/lib/format-currency.ts +27 -0
- package/template/saas-starter/src/lib/format-date.ts +12 -0
- package/template/saas-starter/src/lib/nodemailer.ts +43 -0
- package/template/saas-starter/src/lib/organization-access.ts +93 -0
- package/template/saas-starter/src/lib/organization-plan-limits.ts +38 -0
- package/template/saas-starter/src/lib/otel.ts +27 -0
- package/template/saas-starter/src/lib/plans.ts +63 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +75 -0
- package/template/saas-starter/src/lib/safe-action.ts +67 -0
- package/template/saas-starter/src/lib/siteConfig.ts +4 -0
- package/template/saas-starter/src/lib/storage.ts +181 -0
- package/template/saas-starter/src/lib/stripe.ts +6 -0
- package/template/saas-starter/src/lib/toast-message.ts +15 -0
- package/template/saas-starter/src/lib/validations.ts +12 -0
- package/template/saas-starter/src/scripts/reset-db.ts +22 -0
- package/template/saas-starter/src/scripts/seed.ts +11 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +200 -0
- package/template/saas-starter/src/scripts/test-mail.ts +35 -0
- package/template/saas-starter/tsconfig.json +51 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from 'lucide-react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { transferOrganizationOwnershipAction } from '~/app/(app)/actions/organization'
|
|
7
|
+
import { Button } from '~/components/ui/button'
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogTrigger
|
|
16
|
+
} from '~/components/ui/dialog'
|
|
17
|
+
import { Input } from '~/components/ui/input'
|
|
18
|
+
import { Label } from '~/components/ui/label'
|
|
19
|
+
import {
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue
|
|
25
|
+
} from '~/components/ui/select'
|
|
26
|
+
import { organization } from '~/lib/auth-client'
|
|
27
|
+
import { useSafeActionMutation } from '~/lib/safe-action-client'
|
|
28
|
+
import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
|
|
29
|
+
|
|
30
|
+
type TransferTarget = {
|
|
31
|
+
memberId: string
|
|
32
|
+
name: string
|
|
33
|
+
email: string
|
|
34
|
+
role: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function DangerZone({
|
|
38
|
+
orgId,
|
|
39
|
+
orgName,
|
|
40
|
+
isOwner,
|
|
41
|
+
transferTargets
|
|
42
|
+
}: {
|
|
43
|
+
orgId: string
|
|
44
|
+
orgName: string
|
|
45
|
+
isOwner: boolean
|
|
46
|
+
transferTargets: TransferTarget[]
|
|
47
|
+
}) {
|
|
48
|
+
// The sole owner can't leave: ownership must first be handed to someone else.
|
|
49
|
+
const isSoleOwner =
|
|
50
|
+
isOwner && !transferTargets.some(target => target.role === 'owner')
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<section className="border-destructive/30 space-y-6 rounded-lg border p-6">
|
|
54
|
+
<div>
|
|
55
|
+
<h2 className="text-destructive text-lg font-semibold">Danger zone</h2>
|
|
56
|
+
<p className="text-muted-foreground text-sm">
|
|
57
|
+
These actions are irreversible. Proceed with care.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{isOwner && transferTargets.length > 0 ? (
|
|
62
|
+
<TransferOwnershipRow
|
|
63
|
+
orgId={orgId}
|
|
64
|
+
isOwner={isOwner}
|
|
65
|
+
transferTargets={transferTargets}
|
|
66
|
+
/>
|
|
67
|
+
) : null}
|
|
68
|
+
|
|
69
|
+
<LeaveOrganizationRow
|
|
70
|
+
orgId={orgId}
|
|
71
|
+
orgName={orgName}
|
|
72
|
+
isSoleOwner={isSoleOwner}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
{isOwner ? (
|
|
76
|
+
<DeleteOrganizationRow orgId={orgId} orgName={orgName} />
|
|
77
|
+
) : null}
|
|
78
|
+
</section>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Row({
|
|
83
|
+
title,
|
|
84
|
+
description,
|
|
85
|
+
children
|
|
86
|
+
}: {
|
|
87
|
+
title: string
|
|
88
|
+
description: string
|
|
89
|
+
children: React.ReactNode
|
|
90
|
+
}) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex flex-col gap-3 border-t pt-6 sm:flex-row sm:items-center sm:justify-between">
|
|
93
|
+
<div className="space-y-0.5">
|
|
94
|
+
<p className="text-sm font-medium">{title}</p>
|
|
95
|
+
<p className="text-muted-foreground text-sm">{description}</p>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="shrink-0">{children}</div>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function TransferOwnershipRow({
|
|
103
|
+
orgId,
|
|
104
|
+
isOwner,
|
|
105
|
+
transferTargets
|
|
106
|
+
}: {
|
|
107
|
+
orgId: string
|
|
108
|
+
isOwner: boolean
|
|
109
|
+
transferTargets: TransferTarget[]
|
|
110
|
+
}) {
|
|
111
|
+
const router = useRouter()
|
|
112
|
+
const [open, setOpen] = useState(false)
|
|
113
|
+
const [targetId, setTargetId] = useState('')
|
|
114
|
+
|
|
115
|
+
const transferOwnership = useSafeActionMutation(
|
|
116
|
+
transferOrganizationOwnershipAction,
|
|
117
|
+
{
|
|
118
|
+
onSuccess: () => {
|
|
119
|
+
toastSuccessMessage('Ownership transferred')
|
|
120
|
+
setOpen(false)
|
|
121
|
+
router.refresh()
|
|
122
|
+
},
|
|
123
|
+
onError: error => {
|
|
124
|
+
toastErrorMessage(error.message ?? 'Failed to transfer ownership')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
function handleTransfer() {
|
|
130
|
+
if (!isOwner) {
|
|
131
|
+
toastErrorMessage('Only the organization owner can transfer ownership.')
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (!targetId) return
|
|
135
|
+
|
|
136
|
+
transferOwnership.mutate({
|
|
137
|
+
organizationId: orgId,
|
|
138
|
+
targetMemberId: targetId
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Row
|
|
144
|
+
title="Transfer ownership"
|
|
145
|
+
description="Make another member the owner. You'll become an admin."
|
|
146
|
+
>
|
|
147
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
148
|
+
<DialogTrigger asChild>
|
|
149
|
+
<Button variant="outline">Transfer</Button>
|
|
150
|
+
</DialogTrigger>
|
|
151
|
+
<DialogContent className="sm:max-w-md">
|
|
152
|
+
<DialogHeader>
|
|
153
|
+
<DialogTitle>Transfer ownership</DialogTitle>
|
|
154
|
+
<DialogDescription>
|
|
155
|
+
Select the member who should become the new owner. You'll be
|
|
156
|
+
demoted to admin.
|
|
157
|
+
</DialogDescription>
|
|
158
|
+
</DialogHeader>
|
|
159
|
+
|
|
160
|
+
<div className="space-y-2 py-1">
|
|
161
|
+
<Label>New owner</Label>
|
|
162
|
+
<Select value={targetId} onValueChange={setTargetId}>
|
|
163
|
+
<SelectTrigger className="w-full">
|
|
164
|
+
<SelectValue placeholder="Select a member" />
|
|
165
|
+
</SelectTrigger>
|
|
166
|
+
<SelectContent>
|
|
167
|
+
{transferTargets.map(target => (
|
|
168
|
+
<SelectItem key={target.memberId} value={target.memberId}>
|
|
169
|
+
{target.name} ({target.email})
|
|
170
|
+
</SelectItem>
|
|
171
|
+
))}
|
|
172
|
+
</SelectContent>
|
|
173
|
+
</Select>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<DialogFooter>
|
|
177
|
+
<Button
|
|
178
|
+
variant="outline"
|
|
179
|
+
onClick={() => setOpen(false)}
|
|
180
|
+
disabled={transferOwnership.isPending}
|
|
181
|
+
>
|
|
182
|
+
Cancel
|
|
183
|
+
</Button>
|
|
184
|
+
<Button
|
|
185
|
+
onClick={handleTransfer}
|
|
186
|
+
disabled={transferOwnership.isPending || !targetId}
|
|
187
|
+
>
|
|
188
|
+
{transferOwnership.isPending && (
|
|
189
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
190
|
+
)}
|
|
191
|
+
Transfer ownership
|
|
192
|
+
</Button>
|
|
193
|
+
</DialogFooter>
|
|
194
|
+
</DialogContent>
|
|
195
|
+
</Dialog>
|
|
196
|
+
</Row>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function LeaveOrganizationRow({
|
|
201
|
+
orgId,
|
|
202
|
+
orgName,
|
|
203
|
+
isSoleOwner
|
|
204
|
+
}: {
|
|
205
|
+
orgId: string
|
|
206
|
+
orgName: string
|
|
207
|
+
isSoleOwner: boolean
|
|
208
|
+
}) {
|
|
209
|
+
const router = useRouter()
|
|
210
|
+
const [open, setOpen] = useState(false)
|
|
211
|
+
const [submitting, setSubmitting] = useState(false)
|
|
212
|
+
|
|
213
|
+
async function handleLeave() {
|
|
214
|
+
// Guard: the only owner must transfer ownership before leaving.
|
|
215
|
+
if (isSoleOwner) {
|
|
216
|
+
toastErrorMessage(
|
|
217
|
+
'Transfer ownership to another member before leaving this organization.'
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setSubmitting(true)
|
|
223
|
+
const { error } = await organization.leave({ organizationId: orgId })
|
|
224
|
+
|
|
225
|
+
if (error) {
|
|
226
|
+
toastErrorMessage(error.message ?? 'Failed to leave organization')
|
|
227
|
+
setSubmitting(false)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
toastSuccessMessage(`You left ${orgName}`)
|
|
232
|
+
router.push('/app')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<Row
|
|
237
|
+
title="Leave organization"
|
|
238
|
+
description="Remove yourself from this organization."
|
|
239
|
+
>
|
|
240
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
241
|
+
<DialogTrigger asChild>
|
|
242
|
+
<Button variant="outline">Leave</Button>
|
|
243
|
+
</DialogTrigger>
|
|
244
|
+
<DialogContent className="sm:max-w-md">
|
|
245
|
+
<DialogHeader>
|
|
246
|
+
<DialogTitle>
|
|
247
|
+
{isSoleOwner ? 'Transfer ownership first' : `Leave ${orgName}?`}
|
|
248
|
+
</DialogTitle>
|
|
249
|
+
<DialogDescription>
|
|
250
|
+
{isSoleOwner
|
|
251
|
+
? "You're the only owner of this organization. Make another member the owner before you leave - or delete the organization instead."
|
|
252
|
+
: "You'll lose access to this organization. An owner or admin will need to invite you back."}
|
|
253
|
+
</DialogDescription>
|
|
254
|
+
</DialogHeader>
|
|
255
|
+
<DialogFooter>
|
|
256
|
+
<Button
|
|
257
|
+
variant="outline"
|
|
258
|
+
onClick={() => setOpen(false)}
|
|
259
|
+
disabled={submitting}
|
|
260
|
+
>
|
|
261
|
+
{isSoleOwner ? 'Close' : 'Cancel'}
|
|
262
|
+
</Button>
|
|
263
|
+
{!isSoleOwner ? (
|
|
264
|
+
<Button
|
|
265
|
+
variant="destructive"
|
|
266
|
+
onClick={handleLeave}
|
|
267
|
+
disabled={submitting}
|
|
268
|
+
>
|
|
269
|
+
{submitting && (
|
|
270
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
271
|
+
)}
|
|
272
|
+
Leave organization
|
|
273
|
+
</Button>
|
|
274
|
+
) : null}
|
|
275
|
+
</DialogFooter>
|
|
276
|
+
</DialogContent>
|
|
277
|
+
</Dialog>
|
|
278
|
+
</Row>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function DeleteOrganizationRow({
|
|
283
|
+
orgId,
|
|
284
|
+
orgName
|
|
285
|
+
}: {
|
|
286
|
+
orgId: string
|
|
287
|
+
orgName: string
|
|
288
|
+
}) {
|
|
289
|
+
const router = useRouter()
|
|
290
|
+
const [open, setOpen] = useState(false)
|
|
291
|
+
const [confirmText, setConfirmText] = useState('')
|
|
292
|
+
const [submitting, setSubmitting] = useState(false)
|
|
293
|
+
|
|
294
|
+
async function handleDelete() {
|
|
295
|
+
setSubmitting(true)
|
|
296
|
+
const { error } = await organization.delete({ organizationId: orgId })
|
|
297
|
+
|
|
298
|
+
if (error) {
|
|
299
|
+
toastErrorMessage(error.message ?? 'Failed to delete organization')
|
|
300
|
+
setSubmitting(false)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
toastSuccessMessage(`${orgName} deleted`)
|
|
305
|
+
router.push('/app')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<Row
|
|
310
|
+
title="Delete organization"
|
|
311
|
+
description="Permanently delete this organization and all of its data."
|
|
312
|
+
>
|
|
313
|
+
<Dialog
|
|
314
|
+
open={open}
|
|
315
|
+
onOpenChange={next => {
|
|
316
|
+
setOpen(next)
|
|
317
|
+
if (!next) setConfirmText('')
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
320
|
+
<DialogTrigger asChild>
|
|
321
|
+
<Button variant="destructive">Delete</Button>
|
|
322
|
+
</DialogTrigger>
|
|
323
|
+
<DialogContent className="sm:max-w-md">
|
|
324
|
+
<DialogHeader>
|
|
325
|
+
<DialogTitle>Delete {orgName}?</DialogTitle>
|
|
326
|
+
<DialogDescription>
|
|
327
|
+
This cannot be undone. All members, invitations and data for this
|
|
328
|
+
organization will be permanently removed.
|
|
329
|
+
</DialogDescription>
|
|
330
|
+
</DialogHeader>
|
|
331
|
+
|
|
332
|
+
<div className="space-y-2 py-1">
|
|
333
|
+
<Label>
|
|
334
|
+
Type <span className="font-semibold">{orgName}</span> to confirm
|
|
335
|
+
</Label>
|
|
336
|
+
<Input
|
|
337
|
+
value={confirmText}
|
|
338
|
+
onChange={e => setConfirmText(e.target.value)}
|
|
339
|
+
placeholder={orgName}
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<DialogFooter>
|
|
344
|
+
<Button
|
|
345
|
+
variant="outline"
|
|
346
|
+
onClick={() => setOpen(false)}
|
|
347
|
+
disabled={submitting}
|
|
348
|
+
>
|
|
349
|
+
Cancel
|
|
350
|
+
</Button>
|
|
351
|
+
<Button
|
|
352
|
+
variant="destructive"
|
|
353
|
+
onClick={handleDelete}
|
|
354
|
+
disabled={submitting || confirmText !== orgName}
|
|
355
|
+
>
|
|
356
|
+
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
357
|
+
Delete organization
|
|
358
|
+
</Button>
|
|
359
|
+
</DialogFooter>
|
|
360
|
+
</DialogContent>
|
|
361
|
+
</Dialog>
|
|
362
|
+
</Row>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
4
|
+
import { Loader2 } from 'lucide-react'
|
|
5
|
+
import { useRouter } from 'next/navigation'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import { useForm } from 'react-hook-form'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { updateOrganizationLogoAction } from '~/app/(app)/actions/organization'
|
|
10
|
+
import { FileUpload } from '~/components/file-upload'
|
|
11
|
+
import { Button } from '~/components/ui/button'
|
|
12
|
+
import {
|
|
13
|
+
Field,
|
|
14
|
+
FieldContent,
|
|
15
|
+
FieldDescription,
|
|
16
|
+
FieldError,
|
|
17
|
+
FieldTitle
|
|
18
|
+
} from '~/components/ui/field'
|
|
19
|
+
import { Input } from '~/components/ui/input'
|
|
20
|
+
import { organization } from '~/lib/auth-client'
|
|
21
|
+
import { useSafeActionMutation } from '~/lib/safe-action-client'
|
|
22
|
+
import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
|
|
23
|
+
import { stringValidation } from '~/lib/validations'
|
|
24
|
+
|
|
25
|
+
const schema = z.object({
|
|
26
|
+
name: stringValidation,
|
|
27
|
+
slug: z
|
|
28
|
+
.string()
|
|
29
|
+
.trim()
|
|
30
|
+
.min(1, 'Slug is required')
|
|
31
|
+
.max(255)
|
|
32
|
+
.regex(
|
|
33
|
+
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
|
|
34
|
+
'Use lowercase letters, numbers and hyphens only'
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
type Form = z.infer<typeof schema>
|
|
39
|
+
|
|
40
|
+
export function GeneralSettingsForm({
|
|
41
|
+
orgId,
|
|
42
|
+
defaultName,
|
|
43
|
+
defaultSlug,
|
|
44
|
+
currentLogoUrl,
|
|
45
|
+
currentLogoKey
|
|
46
|
+
}: {
|
|
47
|
+
orgId: string
|
|
48
|
+
defaultName: string
|
|
49
|
+
defaultSlug: string
|
|
50
|
+
currentLogoUrl: string | null
|
|
51
|
+
currentLogoKey: string | null
|
|
52
|
+
}) {
|
|
53
|
+
const router = useRouter()
|
|
54
|
+
const [logo, setLogo] = useState<string | null>(currentLogoKey)
|
|
55
|
+
const [submitting, setSubmitting] = useState(false)
|
|
56
|
+
const { mutateAsync: updateOrgLogo } = useSafeActionMutation(
|
|
57
|
+
updateOrganizationLogoAction
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
register,
|
|
62
|
+
handleSubmit,
|
|
63
|
+
formState: { errors, isDirty }
|
|
64
|
+
} = useForm<Form>({
|
|
65
|
+
resolver: zodResolver(schema),
|
|
66
|
+
defaultValues: { name: defaultName, slug: defaultSlug }
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const logoChanged = logo !== currentLogoKey
|
|
70
|
+
|
|
71
|
+
async function onSubmit(values: Form) {
|
|
72
|
+
setSubmitting(true)
|
|
73
|
+
|
|
74
|
+
const { error } = await organization.update({
|
|
75
|
+
organizationId: orgId,
|
|
76
|
+
data: {
|
|
77
|
+
name: values.name,
|
|
78
|
+
slug: values.slug,
|
|
79
|
+
logo: logo ?? undefined
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (error) {
|
|
84
|
+
toastErrorMessage(error.message ?? 'Failed to update organization')
|
|
85
|
+
setSubmitting(false)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (logoChanged && logo) {
|
|
90
|
+
await updateOrgLogo({
|
|
91
|
+
organizationId: orgId,
|
|
92
|
+
key: logo,
|
|
93
|
+
oldKey: currentLogoKey
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toastSuccessMessage('Organization updated')
|
|
98
|
+
setSubmitting(false)
|
|
99
|
+
router.refresh()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
|
104
|
+
<Field orientation="vertical">
|
|
105
|
+
<FieldContent>
|
|
106
|
+
<FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
|
|
107
|
+
Organization name
|
|
108
|
+
</FieldTitle>
|
|
109
|
+
<Input {...register('name')} aria-invalid={!!errors.name} />
|
|
110
|
+
<FieldError errors={[errors.name]} className="mt-1.5 text-[13px]" />
|
|
111
|
+
</FieldContent>
|
|
112
|
+
</Field>
|
|
113
|
+
|
|
114
|
+
<Field orientation="vertical">
|
|
115
|
+
<FieldContent>
|
|
116
|
+
<FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
|
|
117
|
+
Slug
|
|
118
|
+
</FieldTitle>
|
|
119
|
+
<Input {...register('slug')} aria-invalid={!!errors.slug} />
|
|
120
|
+
<FieldDescription className="mt-1.5 text-[13px]">
|
|
121
|
+
A unique identifier used in links.
|
|
122
|
+
</FieldDescription>
|
|
123
|
+
<FieldError errors={[errors.slug]} className="mt-1.5 text-[13px]" />
|
|
124
|
+
</FieldContent>
|
|
125
|
+
</Field>
|
|
126
|
+
|
|
127
|
+
<Field orientation="vertical">
|
|
128
|
+
<FieldContent>
|
|
129
|
+
<FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
|
|
130
|
+
Logo
|
|
131
|
+
</FieldTitle>
|
|
132
|
+
<div className="w-32">
|
|
133
|
+
<FileUpload
|
|
134
|
+
organizationId={orgId}
|
|
135
|
+
accept="image/*"
|
|
136
|
+
onClientUploadFinish={setLogo}
|
|
137
|
+
currentUrl={currentLogoUrl}
|
|
138
|
+
sizes="128px"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
</FieldContent>
|
|
142
|
+
</Field>
|
|
143
|
+
|
|
144
|
+
<div>
|
|
145
|
+
<Button
|
|
146
|
+
type="submit"
|
|
147
|
+
disabled={submitting || (!isDirty && !logoChanged)}
|
|
148
|
+
>
|
|
149
|
+
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
150
|
+
Save changes
|
|
151
|
+
</Button>
|
|
152
|
+
</div>
|
|
153
|
+
</form>
|
|
154
|
+
)
|
|
155
|
+
}
|