From effe5854839bdce2ef710313fe1a779976963fa6 Mon Sep 17 00:00:00 2001 From: ap-justin <89639563+ap-justin@users.noreply.github.com> Date: Wed, 4 Sep 2024 18:28:14 +0800 Subject: [PATCH] Add incrementers in DAF form (#3285) * convert daf noncontext * add error message * reuse incrementer in daf form * add spacing --- src/components/Signup/ConfirmForm.tsx | 6 +- .../donation/Steps/DonateMethods/Daf/Form.tsx | 68 +++++++------------ .../Steps/DonateMethods/Daf/useRhf.ts | 56 +++++++++++++++ .../Steps/DonateMethods/Stripe/Form.tsx | 2 +- .../Steps/DonateMethods/Stripe/types.ts | 2 - .../Steps/DonateMethods/Stripe/useRhf.ts | 3 +- .../Stripe => common}/Incrementers.tsx | 7 +- src/components/form/Form.tsx | 39 ++++++----- src/components/form/index.ts | 2 +- src/pages/Admin/Charity/Media/VideoEditor.tsx | 4 +- .../ProgramEditor/Milestone/Milestone.tsx | 4 +- .../ProgramEditor/ProgramInfo/ProgramInfo.tsx | 6 +- src/pages/Admin/Charity/Settings/Form.tsx | 7 +- src/pages/Widget/Configurer/Configurer.tsx | 7 +- 14 files changed, 126 insertions(+), 87 deletions(-) create mode 100644 src/components/donation/Steps/DonateMethods/Daf/useRhf.ts rename src/components/donation/Steps/{DonateMethods/Stripe => common}/Incrementers.tsx (91%) diff --git a/src/components/Signup/ConfirmForm.tsx b/src/components/Signup/ConfirmForm.tsx index 9b22ed9dfe..367f670648 100644 --- a/src/components/Signup/ConfirmForm.tsx +++ b/src/components/Signup/ConfirmForm.tsx @@ -1,6 +1,6 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { AuthError, confirmSignUp, resendSignUpCode } from "aws-amplify/auth"; -import { Field, Form } from "components/form"; +import { Field, RhfForm } from "components/form"; import { useErrorContext } from "contexts/ErrorContext"; import { useState } from "react"; import { type UseFormReturn, useForm } from "react-hook-form"; @@ -27,7 +27,7 @@ export default function ConfirmForm(props: Props) { type FV = typeof methods extends UseFormReturn ? U : never; return ( -
Resend code -
+ ); } diff --git a/src/components/donation/Steps/DonateMethods/Daf/Form.tsx b/src/components/donation/Steps/DonateMethods/Daf/Form.tsx index 09dfc22910..808c84d2d4 100644 --- a/src/components/donation/Steps/DonateMethods/Daf/Form.tsx +++ b/src/components/donation/Steps/DonateMethods/Daf/Form.tsx @@ -1,63 +1,32 @@ -import { yupResolver } from "@hookform/resolvers/yup"; import CurrencySelector from "components/CurrencySelector"; -import { Field, Form as FormContainer } from "components/form"; -import { useController, useForm } from "react-hook-form"; -import { schema, stringNumber } from "schemas/shape"; -import type { DetailedCurrency } from "types/components"; +import { NativeField as Field, Form as FormContainer } from "components/form"; import { useDonationState } from "../../Context"; import ContinueBtn from "../../common/ContinueBtn"; +import Incrementers from "../../common/Incrementers"; import { ProgramSelector } from "../../common/ProgramSelector"; -import { DEFAULT_PROGRAM } from "../../common/constants"; +import { usdOption } from "../../common/constants"; import { nextFormState } from "../helpers"; -import type { FormValues as FV, Props } from "./types"; - -/** - * Only USD donations are permissible for DAF donations. - * The minimum amount differs depending on which provider is selected. - */ -const USD_CURRENCY: DetailedCurrency = { code: "usd", rate: 1, min: 1 }; +import type { Props } from "./types"; +import { useRhf } from "./useRhf"; export default function Form(props: Props) { const { setState } = useDonationState(); - - const initial: FV = { - amount: "", - currency: USD_CURRENCY, - program: DEFAULT_PROGRAM, - }; - - const methods = useForm({ - defaultValues: props.details || initial, - resolver: yupResolver( - schema({ - amount: stringNumber( - (s) => s.required("Please enter an amount"), - (n) => n.positive("Amount must be greater than 0") - ), - }) - ), - }); - const { handleSubmit, control } = methods; - - const { field: program } = useController({ - control: control, - name: "program", - }); + const rhf = useRhf(props); return ( + disabled={rhf.isSubmitting} + onSubmit={rhf.handleSubmit((fv) => setState((prev) => nextFormState(prev, { ...fv, method: "daf" })) )} className="grid gap-4" > {}} - value={USD_CURRENCY} + value={usdOption} classes={{ label: "font-semibold", combobox: "field-container-donate", @@ -65,20 +34,29 @@ export default function Form(props: Props) { }} required /> - - name="amount" + + + {(props.init.recipient.progDonationsAllowed ?? true) && ( )} diff --git a/src/components/donation/Steps/DonateMethods/Daf/useRhf.ts b/src/components/donation/Steps/DonateMethods/Daf/useRhf.ts new file mode 100644 index 0000000000..8e6fcbba6a --- /dev/null +++ b/src/components/donation/Steps/DonateMethods/Daf/useRhf.ts @@ -0,0 +1,56 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import { useController, useForm } from "react-hook-form"; +import { schema, stringNumber } from "schemas/shape"; +import type { OnIncrement } from "../../common/Incrementers"; +import { DEFAULT_PROGRAM, usdOption } from "../../common/constants"; +import type { FormValues as FV, Props } from "./types"; + +export function useRhf(props: Props) { + const initial: FV = { + amount: "", + currency: usdOption, + program: DEFAULT_PROGRAM, + }; + + const { + register, + control, + handleSubmit, + getValues, + trigger, + setValue, + formState: { isSubmitting, errors }, + } = useForm({ + defaultValues: props.details || initial, + resolver: yupResolver( + schema({ + amount: stringNumber( + (s) => s.required("Please enter an amount"), + (n) => n.positive("Amount must be greater than 0") + ), + }) + ), + }); + + const { field: program } = useController({ + control: control, + name: "program", + }); + + const onIncrement: OnIncrement = (inc) => { + const amntNum = Number(getValues("amount")); + if (Number.isNaN(amntNum)) return trigger("amount", { shouldFocus: true }); + setValue("amount", `${inc + amntNum}`); + }; + + return { + handleSubmit, + isSubmitting, + register, + errors, + //controllers + program, + //utils + onIncrement, + }; +} diff --git a/src/components/donation/Steps/DonateMethods/Stripe/Form.tsx b/src/components/donation/Steps/DonateMethods/Stripe/Form.tsx index a9eca85a71..cc55b8c9f1 100644 --- a/src/components/donation/Steps/DonateMethods/Stripe/Form.tsx +++ b/src/components/donation/Steps/DonateMethods/Stripe/Form.tsx @@ -8,11 +8,11 @@ import { userIsSignedIn } from "types/auth"; import type { Currency } from "types/components"; import { useDonationState } from "../../Context"; import ContinueBtn from "../../common/ContinueBtn"; +import Incrementers from "../../common/Incrementers"; import { ProgramSelector } from "../../common/ProgramSelector"; import { USD_CODE } from "../../common/constants"; import { nextFormState } from "../helpers"; import Frequency from "./Frequency"; -import Incrementers from "./Incrementers"; import type { FormProps, Props } from "./types"; import { useRhf } from "./useRhf"; diff --git a/src/components/donation/Steps/DonateMethods/Stripe/types.ts b/src/components/donation/Steps/DonateMethods/Stripe/types.ts index 8d7fbaf234..8a3388a39d 100644 --- a/src/components/donation/Steps/DonateMethods/Stripe/types.ts +++ b/src/components/donation/Steps/DonateMethods/Stripe/types.ts @@ -8,5 +8,3 @@ export type FormProps = Props & { currencies: DetailedCurrency[]; defaultCurr?: DetailedCurrency; }; - -export type OnIncrement = (increment: number) => void; diff --git a/src/components/donation/Steps/DonateMethods/Stripe/useRhf.ts b/src/components/donation/Steps/DonateMethods/Stripe/useRhf.ts index 36cf24f00b..48bc74c2e4 100644 --- a/src/components/donation/Steps/DonateMethods/Stripe/useRhf.ts +++ b/src/components/donation/Steps/DonateMethods/Stripe/useRhf.ts @@ -3,8 +3,9 @@ import { useController, useForm } from "react-hook-form"; import { schema, stringNumber } from "schemas/shape"; import { requiredString } from "schemas/string"; import type { Currency } from "types/components"; +import type { OnIncrement } from "../../common/Incrementers"; import { DEFAULT_PROGRAM, usdOption } from "../../common/constants"; -import type { FormValues as FV, FormProps, OnIncrement } from "./types"; +import type { FormValues as FV, FormProps } from "./types"; export function useRhf(props: Omit) { const initial: FV = { diff --git a/src/components/donation/Steps/DonateMethods/Stripe/Incrementers.tsx b/src/components/donation/Steps/common/Incrementers.tsx similarity index 91% rename from src/components/donation/Steps/DonateMethods/Stripe/Incrementers.tsx rename to src/components/donation/Steps/common/Incrementers.tsx index 142c3357bb..ba495b8f2c 100644 --- a/src/components/donation/Steps/DonateMethods/Stripe/Incrementers.tsx +++ b/src/components/donation/Steps/common/Incrementers.tsx @@ -1,20 +1,23 @@ import { DONATION_INCREMENTS } from "constants/common"; import { humanize, roundDownToNum } from "helpers"; -import type { OnIncrement } from "./types"; + +export type OnIncrement = (increment: number) => void; interface Props { rate: number; code: string; onIncrement: OnIncrement; increments?: number[]; + classes?: string; } export default function Incrementers({ increments = DONATION_INCREMENTS, + classes = "", ...props }: Props) { return ( -
+
{increments .toSorted((a, b) => a - b) .map((inc) => ( diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx index cffbe43a89..b08c0a4db0 100644 --- a/src/components/form/Form.tsx +++ b/src/components/form/Form.tsx @@ -1,27 +1,32 @@ import { type FormHTMLAttributes, forwardRef } from "react"; import { FormProvider, type UseFormReturn } from "react-hook-form"; -type Props = FormHTMLAttributes & { - methods?: UseFormReturn; +type El = HTMLFormElement; + +interface IForm extends FormHTMLAttributes { disabled?: boolean; -}; -export default forwardRef(function Form( - { methods, disabled, children, ...props }, - ref -) { - const form = ( -
- {disabled ? ( +} + +export const Form = forwardRef( + ({ disabled, children, ...props }, ref) => { + return ( +
{children}
- ) : ( - children - )} - - ); + + ); + } +); - if (!methods) return form; +interface Props extends IForm { + methods: UseFormReturn; +} - return {form}; +export default forwardRef(({ methods, ...props }, ref) => { + return ( + +
+ + ); }); diff --git a/src/components/form/index.ts b/src/components/form/index.ts index 11771c417b..07b4f51d6a 100644 --- a/src/components/form/index.ts +++ b/src/components/form/index.ts @@ -4,6 +4,6 @@ export * from "./Input"; export * from "./PasswordInput"; export { CheckField, NativeCheckField } from "./CheckField"; export { Radio } from "./Radio"; -export { default as Form } from "./Form"; +export { default as RhfForm, Form } from "./Form"; export { dateToFormFormat } from "./helpers"; export { default as DateInput } from "./DateInput"; diff --git a/src/pages/Admin/Charity/Media/VideoEditor.tsx b/src/pages/Admin/Charity/Media/VideoEditor.tsx index ca9c37ce1b..c49cd4eaaa 100644 --- a/src/pages/Admin/Charity/Media/VideoEditor.tsx +++ b/src/pages/Admin/Charity/Media/VideoEditor.tsx @@ -2,7 +2,7 @@ import { yupResolver } from "@hookform/resolvers/yup"; import Icon from "components/Icon"; import Modal from "components/Modal"; import Prompt from "components/Prompt"; -import { Field, Form } from "components/form"; +import { Field, RhfForm } from "components/form"; import { useErrorContext } from "contexts/ErrorContext"; import { useModalContext } from "contexts/ModalContext"; import { type UseFormReturn, useForm } from "react-hook-form"; @@ -63,7 +63,7 @@ export default function VideoEditor(props: Props) { }); } })} - as={Form} + as={RhfForm} methods={methods} className="fixed-center z-10 grid text-navy-d4 dark:text-white bg-white dark:bg-blue-d4 sm:w-full w-[90vw] sm:max-w-lg rounded overflow-hidden" > diff --git a/src/pages/Admin/Charity/ProgramEditor/Milestone/Milestone.tsx b/src/pages/Admin/Charity/ProgramEditor/Milestone/Milestone.tsx index 322a25e7b5..506a099de4 100644 --- a/src/pages/Admin/Charity/ProgramEditor/Milestone/Milestone.tsx +++ b/src/pages/Admin/Charity/ProgramEditor/Milestone/Milestone.tsx @@ -7,7 +7,7 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { DrawerIcon } from "components/Icon"; import ImgEditor from "components/ImgEditor/ImgEditor"; import { RichTextEditor } from "components/RichText"; -import { Field, Form, Label, dateToFormFormat } from "components/form"; +import { Field, Label, RhfForm, dateToFormFormat } from "components/form"; import { useForm } from "react-hook-form"; import type { Milestone as TMilestone } from "types/aws"; import { MAX_CHARS, MAX_SIZE_IN_BYTES, VALID_MIME_TYPES } from "../common"; @@ -53,7 +53,7 @@ export default function Milestone(props: Props) {
`${ open ? "border-t border-gray-l4" : "" diff --git a/src/pages/Admin/Charity/ProgramEditor/ProgramInfo/ProgramInfo.tsx b/src/pages/Admin/Charity/ProgramEditor/ProgramInfo/ProgramInfo.tsx index 1d58d20d26..dc7af8db11 100644 --- a/src/pages/Admin/Charity/ProgramEditor/ProgramInfo/ProgramInfo.tsx +++ b/src/pages/Admin/Charity/ProgramEditor/ProgramInfo/ProgramInfo.tsx @@ -2,7 +2,7 @@ import { yupResolver } from "@hookform/resolvers/yup"; import Group from "components/Group"; import ImgEditor from "components/ImgEditor"; import { RichTextEditor } from "components/RichText"; -import { Field, Form, Label } from "components/form"; +import { Field, Label, RhfForm } from "components/form"; import { useForm } from "react-hook-form"; import type { Program } from "types/aws"; import { MAX_CHARS, MAX_SIZE_IN_BYTES, VALID_MIME_TYPES } from "../common"; @@ -33,7 +33,7 @@ export default function ProgramInfo(props: Program) { return ( - Save changes - + ); } diff --git a/src/pages/Admin/Charity/Settings/Form.tsx b/src/pages/Admin/Charity/Settings/Form.tsx index d21bd05a60..242dc04aa8 100644 --- a/src/pages/Admin/Charity/Settings/Form.tsx +++ b/src/pages/Admin/Charity/Settings/Form.tsx @@ -2,7 +2,7 @@ import { ErrorMessage } from "@hookform/error-message"; import { yupResolver } from "@hookform/resolvers/yup"; import { DonateMethods, fill } from "components/DonateMethods"; import { LockedSplitSlider } from "components/donation"; -import { CheckField, Field, Form as _Form } from "components/form"; +import { CheckField, Field, RhfForm } from "components/form"; import { useController, useForm } from "react-hook-form"; import { schema, stringNumber } from "schemas/shape"; import type { Endowment, EndowmentSettingsAttributes } from "types/aws"; @@ -52,7 +52,6 @@ export default function Form(props: Props) { const { reset, handleSubmit, - resetField, formState: { isSubmitting, isDirty, errors }, control, } = methods; @@ -68,7 +67,7 @@ export default function Form(props: Props) { }); return ( - <_Form + { @@ -198,6 +197,6 @@ export default function Form(props: Props) { Submit changes
- + ); } diff --git a/src/pages/Widget/Configurer/Configurer.tsx b/src/pages/Widget/Configurer/Configurer.tsx index fe86def6eb..0217e92746 100644 --- a/src/pages/Widget/Configurer/Configurer.tsx +++ b/src/pages/Widget/Configurer/Configurer.tsx @@ -2,7 +2,7 @@ import { ErrorMessage } from "@hookform/error-message"; import { yupResolver } from "@hookform/resolvers/yup"; import { DonateMethods } from "components/DonateMethods"; import { LockedSplitSlider, ProgramSelector } from "components/donation"; -import { CheckField, Field, Form } from "components/form"; +import { CheckField, Field, RhfForm } from "components/form"; import type { Dispatch, SetStateAction } from "react"; import { type SubmitHandler, useController, useForm } from "react-hook-form"; import type { WidgetConfig } from "types/widget"; @@ -33,7 +33,6 @@ export default function Configurer({ const { handleSubmit, reset: hookFormReset, - resetField, formState: { isDirty, errors, isSubmitting }, setValue, watch, @@ -63,7 +62,7 @@ export default function Configurer({ }; return ( -
-
+ ); }