Allow making email required (#864)

This commit is contained in:
Luke Vella 2023-09-18 10:12:21 +01:00 committed by GitHub
parent b9d4b31f38
commit a9253bd972
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 516 additions and 495 deletions

View file

@ -1,21 +1,33 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { VoteType } from "@rallly/database";
import { Button } from "@rallly/ui/button";
import clsx from "clsx";
import { useTranslation } from "next-i18next";
import { useForm } from "react-hook-form";
import { useMount } from "react-use";
import z from "zod";
import { usePoll } from "@/contexts/poll";
import { useFormValidation } from "../utils/form-validation";
import { useModalContext } from "./modal/modal-provider";
import { useAddParticipantMutation } from "./poll/mutations";
import VoteIcon from "./poll/vote-icon";
import { usePoll } from "./poll-context";
import { TextInput } from "./text-input";
interface NewParticipantFormData {
name: string;
email?: string;
}
const requiredEmailSchema = z.object({
requireEmail: z.literal(true),
name: z.string().min(1),
email: z.string().email(),
});
const optionalEmailSchema = z.object({
requireEmail: z.literal(false),
name: z.string().min(1),
email: z.string().email().or(z.literal("")),
});
const schema = z.union([requiredEmailSchema, optionalEmailSchema]);
type NewParticipantFormData = z.infer<typeof schema>;
interface NewParticipantModalProps {
votes: { optionId: string; type: VoteType }[];
@ -70,116 +82,87 @@ const VoteSummary = ({
);
};
export const NewParticipantModal = (props: NewParticipantModalProps) => {
export const NewParticipantForm = (props: NewParticipantModalProps) => {
const { t } = useTranslation();
const poll = usePoll();
const isEmailRequired = poll.requireParticipantEmail;
const { register, formState, setFocus, handleSubmit } =
useForm<NewParticipantFormData>();
const { requiredString, validEmail } = useFormValidation();
const { poll } = usePoll();
useForm<NewParticipantFormData>({
resolver: zodResolver(schema),
defaultValues: {
requireEmail: isEmailRequired,
},
});
const addParticipant = useAddParticipantMutation();
useMount(() => {
setFocus("name");
});
return (
<div className="max-w-full p-4">
<div className="text-lg font-semibold text-gray-800">
{t("newParticipant")}
<form
onSubmit={handleSubmit(async (data) => {
const newParticipant = await addParticipant.mutateAsync({
name: data.name,
votes: props.votes,
email: data.email,
pollId: poll.id,
});
props.onSubmit?.(newParticipant);
})}
className="space-y-4"
>
<fieldset>
<label htmlFor="name" className="mb-1 text-gray-500">
{t("name")}
</label>
<TextInput
className="w-full"
data-1p-ignore="true"
error={!!formState.errors.name}
disabled={formState.isSubmitting}
placeholder={t("namePlaceholder")}
{...register("name")}
/>
{formState.errors.name?.message ? (
<div className="mt-2 text-sm text-rose-500">
{formState.errors.name.message}
</div>
) : null}
</fieldset>
<fieldset>
<label htmlFor="email" className="mb-1 text-gray-500">
{t("email")}
{!isEmailRequired ? ` (${t("optional")})` : null}
</label>
<TextInput
className="w-full"
error={!!formState.errors.email}
disabled={formState.isSubmitting}
placeholder={t("emailPlaceholder")}
{...register("email")}
/>
{formState.errors.email?.message ? (
<div className="mt-1 text-sm text-rose-500">
{formState.errors.email.message}
</div>
) : null}
</fieldset>
<fieldset>
<label className="mb-1 text-gray-500">{t("response")}</label>
<VoteSummary votes={props.votes} />
</fieldset>
<div className="flex gap-2">
<Button onClick={props.onCancel}>{t("cancel")}</Button>
<Button
type="submit"
variant="primary"
loading={formState.isSubmitting}
>
{t("submit")}
</Button>
</div>
<div className="mb-4">{t("newParticipantFormDescription")}</div>
<form
onSubmit={handleSubmit(async (data) => {
const newParticipant = await addParticipant.mutateAsync({
name: data.name,
votes: props.votes,
email: data.email,
pollId: poll.id,
});
props.onSubmit?.(newParticipant);
})}
className="space-y-4"
>
<fieldset>
<label htmlFor="name" className="mb-1 text-gray-500">
{t("name")}
</label>
<TextInput
className="w-full"
data-1p-ignore="true"
error={!!formState.errors.name}
disabled={formState.isSubmitting}
placeholder={t("namePlaceholder")}
{...register("name", { validate: requiredString(t("name")) })}
/>
{formState.errors.name?.message ? (
<div className="mt-2 text-sm text-rose-500">
{formState.errors.name.message}
</div>
) : null}
</fieldset>
<fieldset>
<label htmlFor="email" className="mb-1 text-gray-500">
{t("email")} ({t("optional")})
</label>
<TextInput
className="w-full"
error={!!formState.errors.email}
disabled={formState.isSubmitting}
placeholder={t("emailPlaceholder")}
{...register("email", {
validate: (value) => {
if (!value) return true;
return validEmail(value);
},
})}
/>
{formState.errors.email?.message ? (
<div className="mt-1 text-sm text-rose-500">
{formState.errors.email.message}
</div>
) : null}
</fieldset>
<fieldset>
<label className="mb-1 text-gray-500">{t("response")}</label>
<VoteSummary votes={props.votes} />
</fieldset>
<div className="flex gap-2">
<Button onClick={props.onCancel}>{t("cancel")}</Button>
<Button
type="submit"
variant="primary"
loading={formState.isSubmitting}
>
{t("submit")}
</Button>
</div>
</form>
</div>
</form>
);
};
export const useNewParticipantModal = () => {
const modalContext = useModalContext();
const showNewParticipantModal = (props: NewParticipantModalProps) => {
return modalContext.render({
showClose: true,
overlayClosable: true,
content: function Content({ close }) {
return (
<NewParticipantModal
{...props}
onSubmit={(data) => {
props.onSubmit?.(data);
close();
}}
onCancel={close}
/>
);
},
footer: null,
});
};
return showNewParticipantModal;
};