rallly/apps/web/src/components/participant-dropdown.tsx
2024-10-31 22:12:48 +00:00

294 lines
8 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@rallly/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@rallly/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIconLabel,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@rallly/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@rallly/ui/form";
import { Input } from "@rallly/ui/input";
import { PencilIcon, TagIcon, TrashIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import React from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { useMount } from "react-use";
import { z } from "zod";
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { useDeleteParticipantMutation } from "@/components/poll/mutations";
import { Trans } from "@/components/trans";
import { trpc } from "@/trpc/client";
import { useFormValidation } from "@/utils/form-validation";
import { usePostHog } from "@/utils/posthog";
export const ParticipantDropdown = ({
participant,
onEdit,
children,
disabled,
align,
}: {
disabled?: boolean;
participant: {
name: string;
userId?: string;
email?: string;
id: string;
};
align?: "start" | "end";
onEdit: () => void;
children: React.ReactNode;
}) => {
const [isChangeNameModalVisible, setIsChangeNameModalVisible] =
React.useState(false);
const [isDeleteParticipantModalVisible, setIsDeleteParticipantModalVisible] =
React.useState(false);
return (
<>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
disabled={disabled}
asChild={true}
data-testid="participant-menu"
>
{children}
</DropdownMenuTrigger>
<DropdownMenuContent align={align}>
<DropdownMenuLabel>
<div className="flex items-center gap-x-2">
<div>
<OptimizedAvatarImage name={participant.name} size="md" />
</div>
<div className="grid gap-0.5">
<div>{participant.name}</div>
{participant.email ? (
<div className="text-muted-foreground text-xs font-normal">
{participant.email}
</div>
) : null}
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onEdit}>
<DropdownMenuItemIconLabel icon={PencilIcon}>
<Trans i18nKey="editVotes" />
</DropdownMenuItemIconLabel>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsChangeNameModalVisible(true)}>
<DropdownMenuItemIconLabel icon={TagIcon}>
<Trans i18nKey="changeName" />
</DropdownMenuItemIconLabel>
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive"
onClick={() => setIsDeleteParticipantModalVisible(true)}
>
<TrashIcon className="size-4" />
<Trans i18nKey="delete" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ChangeNameModal
open={isChangeNameModalVisible}
onOpenChange={setIsChangeNameModalVisible}
oldName={participant.name}
participantId={participant.id}
/>
<DeleteParticipantModal
open={isDeleteParticipantModalVisible}
onOpenChange={setIsDeleteParticipantModalVisible}
participantId={participant.id}
participantName={participant.name}
/>
</>
);
};
const DeleteParticipantModal = ({
open,
onOpenChange,
participantId,
participantName,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
participantId: string;
participantName: string;
}) => {
const deleteParticipant = useDeleteParticipantMutation();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans
i18nKey="deleteParticipant"
values={{ name: participantName }}
/>
</DialogTitle>
<DialogDescription>
<Trans i18nKey="deleteParticipantDescription" />
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
onClick={() => {
onOpenChange(false);
}}
>
<Trans i18nKey="cancel" />
</Button>
<Button
loading={deleteParticipant.isLoading}
variant="destructive"
onClick={async () => {
deleteParticipant.mutate({
participantId,
});
onOpenChange(false);
}}
>
<Trans i18nKey="delete" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
type ChangeNameForm = {
name: string;
};
const changeNameSchema = z.object({
name: z.string().trim().min(1),
});
const ChangeNameModal = (props: {
oldName: string;
participantId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}) => {
const posthog = usePostHog();
const queryClient = trpc.useUtils();
const changeName = trpc.polls.participants.rename.useMutation({
onSuccess: (_, { participantId, newName }) => {
queryClient.polls.participants.invalidate();
posthog?.capture("changed name", {
participantId,
oldName: props.oldName,
newName,
});
},
});
const form = useForm<ChangeNameForm>({
defaultValues: {
name: props.oldName,
},
resolver: zodResolver(changeNameSchema),
});
const { control, reset, handleSubmit, setFocus, formState } = form;
useMount(() => {
setFocus("name", {
shouldSelect: true,
});
});
const handler = React.useCallback<SubmitHandler<ChangeNameForm>>(
async ({ name }) => {
if (formState.isDirty) {
// change name
await changeName.mutateAsync({
participantId: props.participantId,
newName: name,
});
}
props.onOpenChange(false);
},
[changeName, formState.isDirty, props],
);
const { requiredString } = useFormValidation();
const formName = `change-name-${props.participantId}`;
const { t } = useTranslation();
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("changeName")}</DialogTitle>
<DialogDescription>{t("changeNameDescription")}</DialogDescription>
</DialogHeader>
<Form {...form}>
<form id={formName} onSubmit={handleSubmit(handler)}>
<FormField
control={control}
name="name"
rules={{
validate: requiredString(t("name")),
}}
render={({ field }) => (
<FormItem>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input
className="w-full"
{...field}
disabled={formState.isSubmitting}
/>
</FormControl>
<FormDescription>{t("changeNameInfo")}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<Button
disabled={formState.isSubmitting}
onClick={() => {
reset();
props.onOpenChange(false);
}}
>
{t("cancel")}
</Button>
<Button
form={formName}
loading={formState.isSubmitting}
type="submit"
variant="primary"
>
{t("save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};