mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 18:26:34 +02:00
294 lines
8 KiB
TypeScript
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>
|
|
);
|
|
};
|