mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 09:59:00 +02:00
✨ Allow making email required (#864)
This commit is contained in:
parent
b9d4b31f38
commit
a9253bd972
16 changed files with 516 additions and 495 deletions
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue