From 63872b71e1476b36dac6bfa32a1f205e83380975 Mon Sep 17 00:00:00 2001 From: Justin Chow Date: Tue, 5 Nov 2024 15:13:05 +0700 Subject: [PATCH 1/3] feat: change endpoint --- src/app/(auth)/signin/hooks/useLoginMutation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(auth)/signin/hooks/useLoginMutation.ts b/src/app/(auth)/signin/hooks/useLoginMutation.ts index 03a4c28..976b22d 100644 --- a/src/app/(auth)/signin/hooks/useLoginMutation.ts +++ b/src/app/(auth)/signin/hooks/useLoginMutation.ts @@ -26,7 +26,7 @@ export default function useLoginMutation() { const token = res.data.token; setToken(token); - const user = await api.get>("/users/me"); + const user = await api.get>("/users/me/"); if (user) login({ ...user.data.data, token: token }); return res; From 99d70f998e29438de31d5c3da7e2e41a0ebad579 Mon Sep 17 00:00:00 2001 From: Justin Chow Date: Tue, 5 Nov 2024 21:15:05 +0700 Subject: [PATCH 2/3] feat(design-system): add upload file --- package.json | 2 + pnpm-lock.yaml | 64 ++++++ src/app/sandbox/form/page.tsx | 74 ++++++- src/components/form/UploadImage.tsx | 209 ++++++++++++++++++ src/components/image/ImagePreview.tsx | 89 ++++++++ src/components/image/ImagePreviewCard.tsx | 76 +++++++ .../image/ImagePreviewWithFetch.tsx | 105 +++++++++ src/types/form/dropzone.ts | 3 + 8 files changed, 618 insertions(+), 4 deletions(-) create mode 100644 src/components/form/UploadImage.tsx create mode 100644 src/components/image/ImagePreview.tsx create mode 100644 src/components/image/ImagePreviewCard.tsx create mode 100644 src/components/image/ImagePreviewWithFetch.tsx create mode 100644 src/types/form/dropzone.ts diff --git a/package.json b/package.json index c88fef9..150432c 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,14 @@ "next-sitemap": "^4.2.3", "react": "^18", "react-dom": "^18", + "react-dropzone": "^14.3.5", "react-hook-form": "^7.51.3", "react-hot-toast": "^2.4.1", "react-icons": "^5.1.0", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", "universal-cookie": "^7.1.0", + "yet-another-react-lightbox": "^3.21.6", "zustand": "^5.0.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a4ebe5..4590e06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) + react-dropzone: + specifier: ^14.3.5 + version: 14.3.5(react@18.2.0) react-hook-form: specifier: ^7.51.3 version: 7.51.3(react@18.2.0) @@ -80,6 +83,9 @@ importers: universal-cookie: specifier: ^7.1.0 version: 7.1.4 + yet-another-react-lightbox: + specifier: ^3.21.6 + version: 3.21.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) zustand: specifier: ^5.0.0 version: 5.0.0(@types/react@18.2.78)(immer@10.1.1)(react@18.2.0) @@ -1711,6 +1717,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + attr-accept@2.2.4: + resolution: {integrity: sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w==} + engines: {node: '>=4'} + auto-zustand-selectors-hook@2.0.5: resolution: {integrity: sha512-42vgvFylb4TvMT7SSyXG1YCkA4fmgS7mInXCDBk/EEZfsTyFOVypKpwnse7SBtz77ZqWU5V9nEYmFRtDLnZW9Q==} engines: {node: '>=18.0.0'} @@ -2070,6 +2080,10 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + file-selector@2.1.0: + resolution: {integrity: sha512-ZuXAqGePcSPz4JuerOY06Dzzq0hrmQ6VGoXVzGyFI1npeOfBgqGIKKpznfYWRkSLJlXutkqVC5WvGZtkFVhu9Q==} + engines: {node: '>= 12'} + fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -2855,6 +2869,9 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2874,6 +2891,12 @@ packages: peerDependencies: react: ^18.2.0 + react-dropzone@14.3.5: + resolution: {integrity: sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-hook-form@7.51.3: resolution: {integrity: sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==} engines: {node: '>=12.22.0'} @@ -2892,6 +2915,9 @@ packages: peerDependencies: react: '*' + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-remove-scroll-bar@2.3.6: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -3200,6 +3226,9 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} engines: {node: '>=10'} @@ -3326,6 +3355,13 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yet-another-react-lightbox@3.21.6: + resolution: {integrity: sha512-uKcRmmezsj1Fbj38B6hFOGwbAu94fPr8d5H6I0+1FmcToX56freEGXXXtdA1oRo6036ug+UgrKZzzvsw/MIM/w==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5785,6 +5821,8 @@ snapshots: asynckit@0.4.0: {} + attr-accept@2.2.4: {} + auto-zustand-selectors-hook@2.0.5(zustand@5.0.0(@types/react@18.2.78)(immer@10.1.1)(react@18.2.0)): dependencies: zustand: 5.0.0(@types/react@18.2.78)(immer@10.1.1)(react@18.2.0) @@ -6172,6 +6210,10 @@ snapshots: dependencies: reusify: 1.0.4 + file-selector@2.1.0: + dependencies: + tslib: 2.8.1 + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 @@ -6853,6 +6895,12 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.2.0 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -6867,6 +6915,13 @@ snapshots: react: 18.2.0 scheduler: 0.23.0 + react-dropzone@14.3.5(react@18.2.0): + dependencies: + attr-accept: 2.2.4 + file-selector: 2.1.0 + prop-types: 15.8.1 + react: 18.2.0 + react-hook-form@7.51.3(react@18.2.0): dependencies: react: 18.2.0 @@ -6883,6 +6938,8 @@ snapshots: dependencies: react: 18.2.0 + react-is@16.13.1: {} + react-remove-scroll-bar@2.3.6(@types/react@18.2.78)(react@18.2.0): dependencies: react: 18.2.0 @@ -7202,6 +7259,8 @@ snapshots: tslib@2.6.2: {} + tslib@2.8.1: {} + type-fest@0.18.1: {} type-fest@0.6.0: {} @@ -7310,6 +7369,11 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yet-another-react-lightbox@3.21.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + yocto-queue@0.1.0: {} zustand@5.0.0(@types/react@18.2.78)(immer@10.1.1)(react@18.2.0): diff --git a/src/app/sandbox/form/page.tsx b/src/app/sandbox/form/page.tsx index cd179dc..8aa3624 100644 --- a/src/app/sandbox/form/page.tsx +++ b/src/app/sandbox/form/page.tsx @@ -1,24 +1,90 @@ "use client"; -import { FormProvider, useForm } from "react-hook-form"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import Input from "@/components/form/Input"; +import UploadImage from "@/components/form/UploadImage"; +import Button from "@/components/buttons/Button"; +import axios, { AxiosError, AxiosResponse } from "axios"; +import { ApiError, ApiResponse } from "@/types/api"; +import { useMutation } from "@tanstack/react-query"; +import api from "@/lib/api"; +import toast from "react-hot-toast"; +import useAuthStore from "@/app/stores/useAuthStore"; export default function FormSandbox() { const methods = useForm(); + const { handleSubmit } = methods; + + const { user } = useAuthStore(); + + const { mutate: handleImageUpload, isPending } = useMutation< + // biome-ignore lint/suspicious/noExplicitAny: + any, + ApiError, + FormData + >({ + mutationFn: async (data) => { + await axios; + api + .post("/users/file", data, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((res) => { + // const data = res.data.data; + toast.success("File uploaded!"); + return res; + }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + // biome-ignore lint/suspicious/noExplicitAny: + const onSubmit: SubmitHandler = (data) => { + // Prepare the FormData object + const formData = new FormData(); + + // Append the image file from react-hook-form + if (data.image && data.image[0]) { + formData.append("file", data.image[0]); // data.image[0] is the uploaded image file + } + + // Trigger the mutation to upload the image + handleImageUpload(formData); + // console.log(data); + }; + return (
-
- + + {/* + /> */} +
+ +
+
diff --git a/src/components/form/UploadImage.tsx b/src/components/form/UploadImage.tsx new file mode 100644 index 0000000..3af8aa2 --- /dev/null +++ b/src/components/form/UploadImage.tsx @@ -0,0 +1,209 @@ +import * as React from "react"; +import { Accept, FileRejection, useDropzone } from "react-dropzone"; +import { + Controller, + get, + RegisterOptions, + useFormContext, +} from "react-hook-form"; +import { HiOutlineArrowUpCircle } from "react-icons/hi2"; + +import ErrorMessage from "@/components/form/ErrorMessage"; +import HelperText from "@/components/form/HelperText"; +import ImagePreviewCard from "@/components/image/ImagePreviewCard"; +import Typography from "@/components/Typography"; +import clsxm from "@/lib/clsxm"; +import { FileWithPreview } from "@/types/form/dropzone"; + +export type DropzoneInputProps = { + id: string; + label?: string; + helperText?: string; + hideError?: boolean; + validation?: RegisterOptions; + accept?: Accept; + maxFiles?: number; + disabled?: boolean; + maxSize?: number; + className?: string; +}; + +export default function UploadImage({ + id, + label, + helperText, + hideError = false, + validation, + accept = { + "image/*": [".jpg", ".jpeg", ".png"], + "application/pdf": [".pdf"], + }, + maxFiles = 1, + maxSize = 1000000, + className, + disabled = false, +}: DropzoneInputProps) { + const { + control, + setValue, + setError, + clearErrors, + formState: { errors }, + } = useFormContext(); + + const error = get(errors, id); + + const dropzoneRef = React.useRef(null); + React.useEffect(() => { + if (error) { + dropzoneRef.current?.focus(); + } + }, [error]); + + const [files, setFiles] = React.useState([]); + + const onDrop = React.useCallback( + (acceptedFiles: T[], rejectedFiles: FileRejection[]) => { + if (rejectedFiles.length > 0) { + setError(id, { + type: "manual", + message: + rejectedFiles[0].errors[0].code === "file-too-large" + ? `File cannot exceed ${maxSize / 1000000} MB` + : "Unsupported file type", + }); + return; + } + + // Generate previews and update the form value + const acceptedFilesPreview = acceptedFiles.map((file: T) => + Object.assign(file, { preview: URL.createObjectURL(file) }), + ); + + setFiles(acceptedFilesPreview.slice(0, maxFiles)); + setValue(id, acceptedFilesPreview.slice(0, maxFiles)); + clearErrors(id); + }, + [clearErrors, maxFiles, maxSize, setError, setValue, id], + ); + + const onDelete = (index: number) => { + const updatedFiles = files.filter((_, i) => i !== index); + setFiles(updatedFiles); + setValue(id, updatedFiles); + }; + + React.useEffect(() => { + // Clean up object URLs + return () => { + files.forEach((file) => URL.revokeObjectURL(file.preview)); + }; + }, [files]); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept, + maxFiles, + maxSize, + }); + + return ( +
+ {label && ( + + )} + + {files.length < maxFiles && ( + ( +
+ { + onChange(e.target.files); // Register files with react-hook-form + }, + })} + /> +
+
+ + + + + Drag and drop file + +
+ + Or + +
+ + Upload from Computer + +
+ + Files allowed: {accept["image/*"].join(", ")} up to{" "} + {maxSize / 1000000} MB + +
+
+ )} + /> + )} + + {files.length > 0 && + files.map((file, index) => ( + onDelete(index)} + isLoading={false} + /> + ))} + + {!hideError && error && {error.message}} + {!error && helperText && {helperText}} +
+ ); +} diff --git a/src/components/image/ImagePreview.tsx b/src/components/image/ImagePreview.tsx new file mode 100644 index 0000000..1ad9bb6 --- /dev/null +++ b/src/components/image/ImagePreview.tsx @@ -0,0 +1,89 @@ +import "yet-another-react-lightbox/styles.css"; +import "yet-another-react-lightbox/plugins/captions.css"; + +import Image from "next/legacy/image"; +import * as React from "react"; +import { HiOutlineExternalLink } from "react-icons/hi"; +import Lightbox from "yet-another-react-lightbox"; +import Captions from "yet-another-react-lightbox/plugins/captions"; +import Download from "yet-another-react-lightbox/plugins/download"; +import Zoom from "yet-another-react-lightbox/plugins/zoom"; + +import IconLink from "@/components/links/IconLink"; + +type CardPreview = { + label?: string; + width?: number; + height?: number; + imgSrc?: string; + imgClassName?: string; + alt: string; +} & React.ComponentPropsWithoutRef<"div">; + +const ImagePreview = ({ + imgSrc, + label, + alt, + width = 300, + height = 160, + className, + imgClassName, + ...props +}: CardPreview) => { + const [isOpen, setIsOpen] = React.useState(false); + const [isFile, setIsFile] = React.useState(false); + return ( + <> +
+ {imgSrc && ( +
+ {isFile ? ( + + ) : ( + {alt} setIsOpen(true)} + onError={() => { + setIsFile(true); + }} + /> + )} +
+ )} + {isOpen && ( + setIsOpen(false)} + slides={[ + { + src: imgSrc as string, + alt: alt, + title: `${label}`, + description: "", + }, + ]} + plugins={[Captions, Zoom, Download]} + animation={{ zoom: 500 }} + captions={{ + descriptionTextAlign: "start", + }} + /> + )} +
+ + ); +}; + +export default ImagePreview; diff --git a/src/components/image/ImagePreviewCard.tsx b/src/components/image/ImagePreviewCard.tsx new file mode 100644 index 0000000..70aaca5 --- /dev/null +++ b/src/components/image/ImagePreviewCard.tsx @@ -0,0 +1,76 @@ +import { GiCancel } from "react-icons/gi"; + +import ImagePreview from "@/components/image/ImagePreview"; +import ImagePreviewWithFetch from "@/components/image/ImagePreviewWithFetch"; +import Typography from "@/components/Typography"; +import clsxm from "@/lib/clsxm"; + +type ImagePreviewCardProps = { + imgPath: string; + label?: string; + caption?: string; + withFetch?: boolean; + onDelete?: () => void; + isLoading: boolean; + onDeleteLoading?: boolean; +} & React.ComponentPropsWithoutRef<"div">; + +export default function ImagePreviewCard({ + imgPath, + label = "", + isLoading, + withFetch = false, + onDelete, + onDeleteLoading, +}: ImagePreviewCardProps) { + return ( +
+ {withFetch ? ( + + ) : ( + + )} +
+ + {label} + +
+
+
+
+ {onDelete && ( + + )} +
+ ); +} diff --git a/src/components/image/ImagePreviewWithFetch.tsx b/src/components/image/ImagePreviewWithFetch.tsx new file mode 100644 index 0000000..1a5a957 --- /dev/null +++ b/src/components/image/ImagePreviewWithFetch.tsx @@ -0,0 +1,105 @@ +import "yet-another-react-lightbox/styles.css"; +import "yet-another-react-lightbox/plugins/captions.css"; + +import Image from "next/legacy/image"; +import * as React from "react"; +import Lightbox from "yet-another-react-lightbox"; +import Captions from "yet-another-react-lightbox/plugins/captions"; +import Download from "yet-another-react-lightbox/plugins/download"; +import Zoom from "yet-another-react-lightbox/plugins/zoom"; + +import api from "@/lib/api"; + +type CardPreviewWithFetchProps = { + imgPath: string; + label?: string; + width?: number; + height?: number; + imgClassName?: string; + alt: string; +} & React.ComponentPropsWithoutRef<"div">; + +const CardPreviewWithFetch = ({ + imgPath, + label, + alt, + width = 300, + height = 160, + className, + imgClassName, + ...props +}: CardPreviewWithFetchProps) => { + const [imgSrc, setImgSrc] = React.useState(); + const [isOpen, setIsOpen] = React.useState(false); + + const getImageURL = React.useCallback(async ({ url }: { url: string }) => { + api + .get(url, { + responseType: "arraybuffer", + }) + .then((res) => { + const base64string = Buffer.from( + new Uint8Array(res.data).reduce(function (data, byte) { + return data + String.fromCharCode(byte); + }, ""), + "binary", + ).toString("base64"); + + const contentType = res.headers["content-type"]; + return { + data: `data:${contentType};base64,${base64string}`, + }; + }) + .then((res) => { + setImgSrc(res.data); + }); + }, []); + + React.useEffect(() => { + if (imgPath) { + getImageURL({ url: `${imgPath}` }); + } + }, [getImageURL, imgPath]); + + return ( + <> +
+ {imgSrc && ( +
+ {alt} setIsOpen(true)} + /> +
+ )} + {isOpen && ( + setIsOpen(false)} + slides={[ + { + src: imgSrc as string, + alt: alt, + title: `${label}`, + description: "", + }, + ]} + plugins={[Captions, Zoom, Download]} + animation={{ zoom: 500 }} + captions={{ + descriptionTextAlign: "start", + }} + /> + )} +
+ + ); +}; + +export default CardPreviewWithFetch; diff --git a/src/types/form/dropzone.ts b/src/types/form/dropzone.ts new file mode 100644 index 0000000..050af23 --- /dev/null +++ b/src/types/form/dropzone.ts @@ -0,0 +1,3 @@ +import { FileWithPath } from "react-dropzone"; + +export type FileWithPreview = FileWithPath & { preview: string }; From 86795aaaa6c32684eb072f1f3e628e2833afb2fc Mon Sep 17 00:00:00 2001 From: Justin Chow Date: Wed, 6 Nov 2024 20:51:52 +0700 Subject: [PATCH 3/3] feat: fix upload image --- .../(auth)/signin/hooks/useLoginMutation.ts | 2 +- src/app/(auth)/signup/page.tsx | 44 +++++++++++++++---- src/app/sandbox/form/page.tsx | 31 +++++-------- src/components/form/HelperText.tsx | 6 +-- .../form/{UploadImage.tsx => UploadFile.tsx} | 26 ++++++----- 5 files changed, 64 insertions(+), 45 deletions(-) rename src/components/form/{UploadImage.tsx => UploadFile.tsx} (90%) diff --git a/src/app/(auth)/signin/hooks/useLoginMutation.ts b/src/app/(auth)/signin/hooks/useLoginMutation.ts index 976b22d..03a4c28 100644 --- a/src/app/(auth)/signin/hooks/useLoginMutation.ts +++ b/src/app/(auth)/signin/hooks/useLoginMutation.ts @@ -26,7 +26,7 @@ export default function useLoginMutation() { const token = res.data.token; setToken(token); - const user = await api.get>("/users/me/"); + const user = await api.get>("/users/me"); if (user) login({ ...user.data.data, token: token }); return res; diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 3d1f02b..7d308c4 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -2,6 +2,7 @@ import Button from "@/components/buttons/Button"; import Input from "@/components/form/Input"; +import UploadFile from "@/components/form/UploadFile"; import NextImage from "@/components/NextImage"; import Typography from "@/components/Typography"; import api from "@/lib/api"; @@ -18,7 +19,7 @@ type SignUpRequest = { username: string; email: string; password: string; - ttd: string; + ttd: File | null; }; export default function SignUp() { @@ -35,7 +36,30 @@ export default function SignUp() { SignUpRequest >({ mutationFn: async (data: SignUpRequest) => { - return await api.post("/users/signup", data); + // return await api.post("/users/signup", data); + // return await api.post("/users/signup", data, { + // headers: { + // "Content-Type": "multipart/form-data", + // }, + // }); + // }, + const formData = new FormData(); + + // Append form fields to the FormData + formData.append("name", data.name); + formData.append("username", data.username); + formData.append("email", data.email); + formData.append("password", data.password); + + if (data.ttd) { + formData.append("ttd", data.ttd); + } + + return await api.post("/users/signup", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); }, onSuccess: (_, variables) => { const { username } = variables; @@ -116,13 +140,15 @@ export default function SignUp() { }, }} /> - +
+ +