From dc5a041ad4e9580873eeaabed00f144aca62a5f9 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 26 Jun 2025 00:01:32 +0200 Subject: [PATCH 1/3] feat: change team email components + action --- .../dashboard/team/email-change-dialog.tsx | 135 ++++++++++++++++++ src/features/dashboard/team/info-card.tsx | 88 +++++++----- src/server/team/team-actions.ts | 33 ++++- src/server/team/types.ts | 7 +- 4 files changed, 227 insertions(+), 36 deletions(-) create mode 100644 src/features/dashboard/team/email-change-dialog.tsx diff --git a/src/features/dashboard/team/email-change-dialog.tsx b/src/features/dashboard/team/email-change-dialog.tsx new file mode 100644 index 000000000..64f486a98 --- /dev/null +++ b/src/features/dashboard/team/email-change-dialog.tsx @@ -0,0 +1,135 @@ +'use client' + +import * as React from 'react' +import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks' +import { zodResolver } from '@hookform/resolvers/zod' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/ui/primitives/dialog' +import { Button } from '@/ui/primitives/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/ui/primitives/form' +import { Input } from '@/ui/primitives/input' +import { updateTeamEmailAction } from '@/server/team/team-actions' +import { + toast, + defaultSuccessToast, + defaultErrorToast, +} from '@/lib/hooks/use-toast' +import { UpdateTeamEmailSchema } from '@/server/team/types' + +interface EmailChangeDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + teamId: string + currentEmail: string +} + +export function EmailChangeDialog({ + open, + onOpenChange, + teamId, + currentEmail, +}: EmailChangeDialogProps) { + const { + form, + resetFormAndAction, + handleSubmitWithAction, + action: { isExecuting }, + } = useHookFormAction( + updateTeamEmailAction, + zodResolver(UpdateTeamEmailSchema), + { + formProps: { + defaultValues: { + teamId, + email: currentEmail, + }, + }, + actionProps: { + onSuccess: () => { + toast(defaultSuccessToast('Team email was updated.')) + handleDialogChange(false) + }, + onError: () => { + toast(defaultErrorToast('Failed to update team email.')) + }, + }, + } + ) + + const handleDialogChange = (value: boolean) => { + onOpenChange(value) + + if (value) return + + resetFormAndAction() + } + + return ( + + + + Change Team Email + + Update the email address associated with this team. + + + +
+ +
+ ( + + Team Email + + + + + + )} + /> +
+ + + + + +
+ +
+
+ ) +} diff --git a/src/features/dashboard/team/info-card.tsx b/src/features/dashboard/team/info-card.tsx index c6068f14a..86e062a5d 100644 --- a/src/features/dashboard/team/info-card.tsx +++ b/src/features/dashboard/team/info-card.tsx @@ -1,6 +1,5 @@ 'use client' -import { Input } from '@/ui/primitives/input' import { Skeleton } from '@/ui/primitives/skeleton' import { useSelectedTeam } from '@/lib/hooks/use-teams' import { @@ -12,6 +11,10 @@ import { } from '@/ui/primitives/card' import { Badge } from '@/ui/primitives/badge' import CopyButton from '@/ui/copy-button' +import { useState } from 'react' +import { EmailChangeDialog } from './email-change-dialog' +import { Button } from '@/ui/primitives/button' +import { Pencil } from 'lucide-react' interface InfoCardProps { className?: string @@ -19,41 +22,58 @@ interface InfoCardProps { export function InfoCard({ className }: InfoCardProps) { const team = useSelectedTeam() + const [isEmailChangeDialogOpen, setIsEmailChangeDialogOpen] = useState(false) return ( - - - Information - - Additional information about this team. - - - - {team ? ( -
-
- E-Mail - {team.email} - + <> + + + Information + + Additional information about this team. + + + + {team ? ( +
+
+ E-Mail + {team.email} + + +
+
+ Team ID + {team.id} + +
-
- Team ID - {team.id} - -
-
- ) : ( - - )} - - + ) : ( + + )} + + + + ) } diff --git a/src/server/team/team-actions.ts b/src/server/team/team-actions.ts index bc260e751..806f45851 100644 --- a/src/server/team/team-actions.ts +++ b/src/server/team/team-actions.ts @@ -15,7 +15,11 @@ import { logWarning } from '@/lib/clients/logger' import { returnValidationErrors } from 'next-safe-action' import { getTeam } from './get-team' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { CreateTeamSchema, UpdateTeamNameSchema } from '@/server/team/types' +import { + CreateTeamSchema, + UpdateTeamNameSchema, + UpdateTeamEmailSchema, +} from '@/server/team/types' import { CreateTeamsResponse } from '@/types/billing' export const updateTeamNameAction = authActionClient @@ -311,3 +315,30 @@ export const uploadTeamProfilePictureAction = authActionClient return data }) + +export const updateTeamEmailAction = authActionClient + .schema(UpdateTeamEmailSchema) + .metadata({ actionName: 'updateTeamEmail' }) + .action(async ({ parsedInput, ctx }) => { + const { teamId, email } = parsedInput + const { user } = ctx + + const isAuthorized = await checkUserTeamAuthorization(user.id, teamId) + + if (!isAuthorized) { + return returnServerError('User is not authorized to update this team') + } + + const { data, error } = await supabaseAdmin + .from('teams') + .update({ email }) + .eq('id', teamId) + + if (error) { + throw error + } + + revalidatePath('/dashboard', 'layout') + + return data + }) diff --git a/src/server/team/types.ts b/src/server/team/types.ts index f332cdfa3..a49a24f63 100644 --- a/src/server/team/types.ts +++ b/src/server/team/types.ts @@ -38,4 +38,9 @@ const CreateTeamSchema = z.object({ name: TeamNameSchema, }) -export { UpdateTeamNameSchema, CreateTeamSchema } +const UpdateTeamEmailSchema = z.object({ + teamId: z.string().uuid(), + email: z.string().email({ message: 'Invalid email address' }), +}) + +export { UpdateTeamNameSchema, CreateTeamSchema, UpdateTeamEmailSchema } From 8cdff97c311cefe369bdf0ca9b5f4f9b79d3a0b8 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 26 Jun 2025 00:20:58 +0200 Subject: [PATCH 2/3] chore: rename dialog --- .../{email-change-dialog.tsx => change-email-dialog.tsx} | 8 +++++--- src/features/dashboard/team/info-card.tsx | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) rename src/features/dashboard/team/{email-change-dialog.tsx => change-email-dialog.tsx} (96%) diff --git a/src/features/dashboard/team/email-change-dialog.tsx b/src/features/dashboard/team/change-email-dialog.tsx similarity index 96% rename from src/features/dashboard/team/email-change-dialog.tsx rename to src/features/dashboard/team/change-email-dialog.tsx index 64f486a98..0ea5a82db 100644 --- a/src/features/dashboard/team/email-change-dialog.tsx +++ b/src/features/dashboard/team/change-email-dialog.tsx @@ -29,19 +29,21 @@ import { } from '@/lib/hooks/use-toast' import { UpdateTeamEmailSchema } from '@/server/team/types' -interface EmailChangeDialogProps { +interface ChangeEmailDialogProps { open: boolean onOpenChange: (open: boolean) => void teamId: string currentEmail: string } -export function EmailChangeDialog({ +export function ChangeEmailDialog({ open, onOpenChange, teamId, currentEmail, -}: EmailChangeDialogProps) { +}: ChangeEmailDialogProps) { + 'use no memo' + const { form, resetFormAndAction, diff --git a/src/features/dashboard/team/info-card.tsx b/src/features/dashboard/team/info-card.tsx index 86e062a5d..d1db2e40c 100644 --- a/src/features/dashboard/team/info-card.tsx +++ b/src/features/dashboard/team/info-card.tsx @@ -12,7 +12,7 @@ import { import { Badge } from '@/ui/primitives/badge' import CopyButton from '@/ui/copy-button' import { useState } from 'react' -import { EmailChangeDialog } from './email-change-dialog' +import { ChangeEmailDialog } from './change-email-dialog' import { Button } from '@/ui/primitives/button' import { Pencil } from 'lucide-react' @@ -68,7 +68,7 @@ export function InfoCard({ className }: InfoCardProps) { )} - Date: Thu, 26 Jun 2025 00:24:31 +0200 Subject: [PATCH 3/3] chore: dialog prose --- src/features/dashboard/team/change-email-dialog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/team/change-email-dialog.tsx b/src/features/dashboard/team/change-email-dialog.tsx index 0ea5a82db..e537f118e 100644 --- a/src/features/dashboard/team/change-email-dialog.tsx +++ b/src/features/dashboard/team/change-email-dialog.tsx @@ -83,9 +83,9 @@ export function ChangeEmailDialog({ - Change Team Email + Change Team E-Mail - Update the email address associated with this team. + Update the e-mail address associated with this team. @@ -97,7 +97,7 @@ export function ChangeEmailDialog({ name="email" render={({ field }) => ( - Team Email + Team E-Mail - Update Email + Update E-Mail