diff --git a/apps/web/src/components/new-participant-modal.tsx b/apps/web/src/components/new-participant-modal.tsx index 67869701c..003c3d852 100644 --- a/apps/web/src/components/new-participant-modal.tsx +++ b/apps/web/src/components/new-participant-modal.tsx @@ -13,6 +13,7 @@ import z from "zod"; import { usePoll } from "@/contexts/poll"; import { useTranslation } from "@/i18n/client"; +import { useTimezone } from "@/features/timezone"; import { useAddParticipantMutation } from "./poll/mutations"; import VoteIcon from "./poll/vote-icon"; import { useUser } from "./user-provider"; @@ -89,7 +90,7 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => { const poll = usePoll(); const isEmailRequired = poll.requireParticipantEmail; - + const { timezone } = useTimezone(); const { user, createGuestIfNeeded } = useUser(); const isLoggedIn = !user.isGuest; const { register, setError, formState, handleSubmit } = @@ -117,6 +118,7 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => { votes: props.votes, email: data.email, pollId: poll.id, + timeZone: timezone, }); props.onSubmit?.(newParticipant); } catch (error) { diff --git a/apps/web/src/trpc/routers/polls.ts b/apps/web/src/trpc/routers/polls.ts index b290945a5..7eb51a595 100644 --- a/apps/web/src/trpc/routers/polls.ts +++ b/apps/web/src/trpc/routers/polls.ts @@ -574,6 +574,7 @@ export const polls = router({ name: true, email: true, locale: true, + timeZone: true, user: { select: { email: true, @@ -661,7 +662,8 @@ export const polls = router({ inviteeName: p.name, inviteeEmail: p.user?.email ?? p.email ?? `${p.id}@rallly.co`, - inviteeTimeZone: p.user?.timeZone ?? poll.timeZone, // We should track participant's timezone + inviteeTimeZone: + p.user?.timeZone ?? p.timeZone ?? poll.timeZone, status: ( { yes: "accepted", @@ -758,6 +760,7 @@ export const polls = router({ name: string; email: string; locale: string | undefined; + timeZone: string | null; }> = []; if (input.notify === "all") { @@ -768,6 +771,7 @@ export const polls = router({ name: p.name, email: p.email, locale: p.locale ?? undefined, + timeZone: p.timeZone, }); } }); @@ -781,6 +785,7 @@ export const polls = router({ name: p.name, email: p.email, locale: p.locale ?? undefined, + timeZone: p.timeZone, }); } }); @@ -821,7 +826,7 @@ export const polls = router({ end: scheduledEvent.end, allDay: scheduledEvent.allDay, timeZone: scheduledEvent.timeZone, - // inviteeTimeZone: p.timeZone, // TODO: implement this + inviteeTimeZone: p.timeZone, }); getEmailClient(p.locale ?? undefined).queueTemplate( "FinalizeParticipantEmail", diff --git a/apps/web/src/trpc/routers/polls/participants.ts b/apps/web/src/trpc/routers/polls/participants.ts index aec79bf08..6dc639613 100644 --- a/apps/web/src/trpc/routers/polls/participants.ts +++ b/apps/web/src/trpc/routers/polls/participants.ts @@ -129,6 +129,7 @@ export const participants = router({ pollId: z.string(), name: z.string().min(1, "Participant name is required").max(100), email: z.string().optional(), + timeZone: z.string().optional(), votes: z .object({ optionId: z.string(), @@ -137,100 +138,103 @@ export const participants = router({ .array(), }), ) - .mutation(async ({ ctx, input: { pollId, votes, name, email } }) => { - const { user } = ctx; + .mutation( + async ({ ctx, input: { pollId, votes, name, email, timeZone } }) => { + const { user } = ctx; - const participant = await prisma.$transaction(async (prisma) => { - const participantCount = await prisma.participant.count({ - where: { - pollId, - deleted: false, - }, - }); - - if (participantCount >= MAX_PARTICIPANTS) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `This poll has reached its maximum limit of ${MAX_PARTICIPANTS} participants`, + const participant = await prisma.$transaction(async (prisma) => { + const participantCount = await prisma.participant.count({ + where: { + pollId, + deleted: false, + }, }); - } - const participant = await prisma.participant.create({ - data: { - pollId: pollId, - name: name, - email, - ...(user.isGuest ? { guestId: user.id } : { userId: user.id }), - locale: user.locale ?? undefined, - }, - include: { - poll: { - select: { - id: true, - title: true, + if (participantCount >= MAX_PARTICIPANTS) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `This poll has reached its maximum limit of ${MAX_PARTICIPANTS} participants`, + }); + } + + const participant = await prisma.participant.create({ + data: { + pollId: pollId, + name: name, + email, + timeZone, + ...(user.isGuest ? { guestId: user.id } : { userId: user.id }), + locale: user.locale ?? undefined, + }, + include: { + poll: { + select: { + id: true, + title: true, + }, }, }, - }, - }); + }); - const options = await prisma.option.findMany({ - where: { - pollId, - }, - select: { - id: true, - }, - }); - - const existingOptionIds = new Set(options.map((option) => option.id)); - - const validVotes = votes.filter(({ optionId }) => - existingOptionIds.has(optionId), - ); - - await prisma.vote.createMany({ - data: validVotes.map(({ optionId, type }) => ({ - optionId, - type, - pollId, - participantId: participant.id, - })), - }); - - return participant; - }); - - if (email) { - const token = await createToken( - { userId: user.id }, - { - ttl: 0, // basically forever - }, - ); - - ctx.user - .getEmailClient() - .queueTemplate("NewParticipantConfirmationEmail", { - to: email, - props: { - title: participant.poll.title, - editSubmissionUrl: absoluteUrl( - `/invite/${participant.poll.id}?token=${token}`, - ), + const options = await prisma.option.findMany({ + where: { + pollId, + }, + select: { + id: true, }, }); - } - waitUntil( - sendNewParticipantNotifcationEmail({ - pollId, - pollTitle: participant.poll.title, - participantName: participant.name, - }), - ); + const existingOptionIds = new Set(options.map((option) => option.id)); - return participant; - }), + const validVotes = votes.filter(({ optionId }) => + existingOptionIds.has(optionId), + ); + + await prisma.vote.createMany({ + data: validVotes.map(({ optionId, type }) => ({ + optionId, + type, + pollId, + participantId: participant.id, + })), + }); + + return participant; + }); + + if (email) { + const token = await createToken( + { userId: user.id }, + { + ttl: 0, // basically forever + }, + ); + + ctx.user + .getEmailClient() + .queueTemplate("NewParticipantConfirmationEmail", { + to: email, + props: { + title: participant.poll.title, + editSubmissionUrl: absoluteUrl( + `/invite/${participant.poll.id}?token=${token}`, + ), + }, + }); + } + + waitUntil( + sendNewParticipantNotifcationEmail({ + pollId, + pollTitle: participant.poll.title, + participantName: participant.name, + }), + ); + + return participant; + }, + ), rename: publicProcedure .input(z.object({ participantId: z.string(), newName: z.string() })) .mutation(async ({ input: { participantId, newName } }) => { diff --git a/packages/database/prisma/migrations/20250711140850_add_time_zone_column/migration.sql b/packages/database/prisma/migrations/20250711140850_add_time_zone_column/migration.sql new file mode 100644 index 000000000..a27664123 --- /dev/null +++ b/packages/database/prisma/migrations/20250711140850_add_time_zone_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "participants" ADD COLUMN "time_zone" TEXT; diff --git a/packages/database/prisma/models/poll.prisma b/packages/database/prisma/models/poll.prisma index d63662b5a..f619a9e18 100644 --- a/packages/database/prisma/models/poll.prisma +++ b/packages/database/prisma/models/poll.prisma @@ -76,6 +76,7 @@ model Participant { guestId String? @map("guest_id") pollId String @map("poll_id") locale String? + timeZone String? @map("time_zone") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime? @updatedAt @map("updated_at") deleted Boolean @default(false)