diff --git a/src/pages/Funds/CreateFund/CreateFund.tsx b/src/pages/Funds/CreateFund/CreateFund.tsx index 81ebc0cd90..315bd2b1f7 100644 --- a/src/pages/Funds/CreateFund/CreateFund.tsx +++ b/src/pages/Funds/CreateFund/CreateFund.tsx @@ -1,4 +1,4 @@ -import { yupResolver } from "@hookform/resolvers/yup"; +import { valibotResolver } from "@hookform/resolvers/valibot"; import { ControlledImgEditor as ImgEditor } from "components/ImgEditor"; import Prompt from "components/Prompt"; import { @@ -22,8 +22,7 @@ import { useCreateFundMutation } from "services/aws/funds"; import type { Fund } from "types/aws"; import { GoalSelector, MAX_SIZE_IN_BYTES, VALID_MIME_TYPES } from "../common"; import { EndowmentSelector } from "./EndowmentSelector"; -import { schema } from "./schema"; -import type { FormValues as FV } from "./types"; +import { type FV, schema } from "./schema"; export default withAuth(function CreateFund() { const { @@ -36,7 +35,7 @@ export default withAuth(function CreateFund() { formState: { errors, isSubmitting }, watch, } = useForm({ - resolver: yupResolver(schema), + resolver: valibotResolver(schema), defaultValues: { name: "", description: "", @@ -49,7 +48,9 @@ export default withAuth(function CreateFund() { from: "fund", allowBgTip: true, }, - targetType: "smart", + target: { + type: "smart", + }, }, }); const { field: banner } = useController({ control, name: "banner" }); @@ -60,7 +61,7 @@ export default withAuth(function CreateFund() { }); const { field: targetType } = useController({ control, - name: "targetType", + name: "target.type", }); const customAllowBgTipRef = useRef(true); @@ -99,11 +100,11 @@ export default withAuth(function CreateFund() { allowBgTip: fv.settings.allowBgTip, }, target: - fv.targetType === "none" + fv.target.type === "none" ? `${0}` - : fv.targetType === "smart" + : fv.target.type === "smart" ? "smart" - : `${+fv.fixedTarget}`, + : `${+fv.target.value}`, //fixedTarget is required when targetType is fixed }; if (fv.expiration) fund.expiration = fv.expiration; @@ -145,6 +146,7 @@ export default withAuth(function CreateFund() { className="grid border border-gray-l4 rounded-lg p-6 my-4 w-full max-w-4xl" >

Fund information

+ {targetType.value === "fixed" && ( )} @@ -266,7 +268,7 @@ export default withAuth(function CreateFund() { dropzone: "aspect-[1/1] w-60", }} maxSize={MAX_SIZE_IN_BYTES} - error={errors.banner?.file?.message} + error={errors.logo?.file?.message} /> void; +type OnChange = (opts: EndowOption[]) => void; interface Props { - values: EndowmentOption[]; + values: EndowOption[]; onChange: OnChange; classes?: string; disabled?: boolean; @@ -87,8 +85,8 @@ export const EndowmentSelector = forwardRef((props, ref) => { ); }); -interface ISelectedOption extends EndowmentOption { - onDeselect: (thisOpt: EndowmentOption) => void; +interface ISelectedOption extends EndowOption { + onDeselect: (thisOpt: EndowOption) => void; } function SelectedOption({ onDeselect, ...props }: ISelectedOption) { diff --git a/src/pages/Funds/CreateFund/EndowmentSelector/Options.tsx b/src/pages/Funds/CreateFund/EndowmentSelector/Options.tsx index 85a0df5b3a..9c89c12012 100644 --- a/src/pages/Funds/CreateFund/EndowmentSelector/Options.tsx +++ b/src/pages/Funds/CreateFund/EndowmentSelector/Options.tsx @@ -3,7 +3,7 @@ import Image from "components/Image"; import { ErrorStatus, Info, LoadingStatus } from "components/Status"; import useDebouncer from "hooks/useDebouncer"; import { useEndowmentCardsQuery } from "services/aws/aws"; -import type { FundMember } from "../types"; +import type { EndowOption } from "../schema"; interface Props { searchText: string; @@ -52,7 +52,7 @@ export function Options({ classes = "", searchText }: Props) { logo: o.card_img, name: o.name, id: o.id, - } satisfies FundMember + } satisfies EndowOption } className="flex gap-x-2 p-2 data-[selected]:text-blue-d1 data-[selected]:pointer-events-none hover:bg-blue-l4 select-none" > diff --git a/src/pages/Funds/CreateFund/schema.ts b/src/pages/Funds/CreateFund/schema.ts index a030734cc7..4729c1ce5f 100644 --- a/src/pages/Funds/CreateFund/schema.ts +++ b/src/pages/Funds/CreateFund/schema.ts @@ -1,41 +1,57 @@ -import type { ImgLink } from "components/ImgEditor"; -import { genFileSchema } from "schemas/file"; -import { schema as schemaFn, stringNumber } from "schemas/shape"; -import { requiredString } from "schemas/string"; -import { array, string } from "yup"; -import { MAX_SIZE_IN_BYTES, VALID_MIME_TYPES } from "../common"; -import type { FormValues as FV } from "./types"; +import * as v from "valibot"; +import { MAX_SIZE_IN_BYTES, VALID_MIME_TYPES, target } from "../common"; -const fileObj = schemaFn({ - file: genFileSchema(MAX_SIZE_IN_BYTES, VALID_MIME_TYPES).required("required"), +const str = v.pipe(v.string(), v.trim()); + +const fileObject = v.object({ + name: str, + publicUrl: v.pipe(str, v.url()), +}); + +export const imgLink = v.object({ + file: v.optional( + v.pipe( + v.file(), + v.mimeType(VALID_MIME_TYPES, "invalid type"), + v.maxSize(MAX_SIZE_IN_BYTES, "exceeds size limit") + ) + ), + preview: v.pipe(str, v.url()), + ...fileObject.entries, }); -const targetTypeKey: keyof FV = "targetType"; +export const endowOption = v.object({ + id: v.number(), + name: str, + logo: v.optional(v.pipe(str, v.url())), +}); -export const schema = schemaFn({ - name: requiredString, - description: requiredString, - banner: fileObj, - logo: fileObj, - members: array().min(1, "must contain at least one endowment"), - expiration: string() - .transform((v) => { - if (!v) return ""; - return new Date(v).toISOString(); - }) - .datetime("invalid date") - .test( - "", - "must be in the future", - (v) => !v || v >= new Date().toISOString() - ), +export const settings = v.object({ + from: str, + allowBgTip: v.boolean(), +}); - fixedTarget: stringNumber( - (s) => - s.when(targetTypeKey, (values, schema) => { - const [type] = values as [FV["targetType"]]; - return type === "fixed" ? schema.required("required") : schema; - }), - (n) => n.positive("must be greater than 0") +export const schema = v.object({ + name: v.pipe(str, v.nonEmpty("required")), + description: v.pipe(str, v.nonEmpty("required")), + banner: imgLink, + logo: imgLink, + members: v.pipe( + v.array(endowOption), + v.minLength(1, "must contain at least one endowment") ), + featured: v.boolean(), + settings, + expiration: v.pipe( + str, + v.nonEmpty("required"), + v.transform((v) => new Date(v).toISOString()), + v.isoTimestamp("invalid date"), + v.minValue(new Date().toISOString(), "must be in the future") + ), + target, }); + +export interface FundMember extends v.InferOutput {} +export interface EndowOption extends FundMember {} +export type FV = v.InferOutput; diff --git a/src/pages/Funds/CreateFund/types.ts b/src/pages/Funds/CreateFund/types.ts deleted file mode 100644 index 3a4fed5872..0000000000 --- a/src/pages/Funds/CreateFund/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ImgLink } from "components/ImgEditor"; -import type { TargetType } from "../common"; - -export interface FundMember { - id: number; - name: string; - logo?: string; -} - -export interface Settings { - /** endowname or fund */ - from: string; - allowBgTip: boolean; -} - -export interface FormValues { - name: string; - description: string; - logo: ImgLink; - banner: ImgLink; - expiration: string; - featured: boolean; - members: FundMember[]; - settings: Settings; - targetType: TargetType; - fixedTarget: string; -} diff --git a/src/pages/Funds/CreateFund/useEndow.ts b/src/pages/Funds/CreateFund/useEndow.ts index 33eeee6d7a..c16510499b 100644 --- a/src/pages/Funds/CreateFund/useEndow.ts +++ b/src/pages/Funds/CreateFund/useEndow.ts @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { useLazyProfileQuery } from "services/aws/aws"; import type { Endowment } from "types/aws"; -import type { FundMember } from "./types"; +import type { FundMember } from "./schema"; export type Endow = Pick; diff --git a/src/pages/Funds/common/GoalSelector.tsx b/src/pages/Funds/common/GoalSelector.tsx index 91c560fabf..3165de6b3f 100644 --- a/src/pages/Funds/common/GoalSelector.tsx +++ b/src/pages/Funds/common/GoalSelector.tsx @@ -1,6 +1,5 @@ import { Field, Label, Radio, RadioGroup } from "@headlessui/react"; - -export type TargetType = "fixed" | "none" | "smart"; +import type { TargetType } from "./types"; const options: { [T in TargetType]: string } = { smart: "Use smart milestones", diff --git a/src/pages/Funds/common/index.ts b/src/pages/Funds/common/index.ts index fcf509e10b..91ff67f7a7 100644 --- a/src/pages/Funds/common/index.ts +++ b/src/pages/Funds/common/index.ts @@ -1,4 +1,5 @@ import type { ImageMIMEType } from "types/lists"; +export { target, type TargetType } from "./types"; export * from "./GoalSelector"; diff --git a/src/pages/Funds/common/types.ts b/src/pages/Funds/common/types.ts new file mode 100644 index 0000000000..d7fedb6aa6 --- /dev/null +++ b/src/pages/Funds/common/types.ts @@ -0,0 +1,20 @@ +import * as v from "valibot"; +export const target = v.variant("type", [ + v.object({ + type: v.literal("fixed"), + value: v.pipe( + v.string("required"), + v.nonEmpty("required"), + v.transform((x) => +x), + v.number("invalid number"), + v.minValue(0, "must be greater than 0"), + /** so that the inferred type is string */ + v.transform((x) => x.toString()) + ), + }), + v.object({ type: v.literal("smart"), value: v.optional(v.string()) }), + v.object({ type: v.literal("none"), value: v.optional(v.string()) }), +]); + +export type Target = v.InferOutput; +export type TargetType = Target["type"]; diff --git a/src/pages/Registration/Steps/ContactDetails/Form/index.tsx b/src/pages/Registration/Steps/ContactDetails/Form/index.tsx index 08588b9a00..deb9809056 100644 --- a/src/pages/Registration/Steps/ContactDetails/Form/index.tsx +++ b/src/pages/Registration/Steps/ContactDetails/Form/index.tsx @@ -60,8 +60,6 @@ export default function Form({ classes = "" }: { classes?: string }) { } }; - console.log({ errors }); - return (