refactor: replace FormUpload with DialogUpload component for file uploads
This commit is contained in:
parent
07cced218e
commit
6f0a21dde6
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import type { TUploadSchema } from '~/layouts/admin/form-upload'
|
import type { TUploadSchema } from '~/layouts/admin/dialog-upload'
|
||||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||||
|
|
||||||
const uploadResponseSchema = z.object({
|
const uploadResponseSchema = z.object({
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
|
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
import { useAdminContext } from '~/contexts/admin'
|
import { DialogUpload } from './dialog-upload'
|
||||||
|
|
||||||
import { FormUpload } from './form-upload'
|
|
||||||
import { Navbar } from './navbar'
|
import { Navbar } from './navbar'
|
||||||
import { Sidebar } from './sidebar'
|
import { Sidebar } from './sidebar'
|
||||||
|
|
||||||
export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
||||||
const { children } = properties
|
const { children } = properties
|
||||||
const { isUploadOpen, setIsUploadOpen } = useAdminContext()
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@ -18,27 +14,7 @@ export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
|||||||
<div className="min-h-[calc(100dvh-80px)] flex-1 p-8">{children}</div>
|
<div className="min-h-[calc(100dvh-80px)] flex-1 p-8">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<DialogUpload />
|
||||||
open={!!isUploadOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setIsUploadOpen(undefined)
|
|
||||||
}}
|
|
||||||
className="relative z-50"
|
|
||||||
transition
|
|
||||||
>
|
|
||||||
<DialogBackdrop
|
|
||||||
className="fixed inset-0 bg-black/50 duration-300 ease-out data-[closed]:opacity-0"
|
|
||||||
transition
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
|
||||||
<DialogPanel
|
|
||||||
transition
|
|
||||||
className="max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
|
||||||
>
|
|
||||||
<FormUpload />
|
|
||||||
</DialogPanel>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
143
app/layouts/admin/dialog-upload.tsx
Normal file
143
app/layouts/admin/dialog-upload.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { Dialog, DialogBackdrop, DialogPanel, Input } from '@headlessui/react'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useEffect, useState, type ChangeEvent } from 'react'
|
||||||
|
import { useFetcher } from 'react-router'
|
||||||
|
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { Button } from '~/components/ui/button'
|
||||||
|
import { uploadCategorySchema, useAdminContext } from '~/contexts/admin'
|
||||||
|
|
||||||
|
export const uploadSchema = z.object({
|
||||||
|
file: z.instanceof(File),
|
||||||
|
category: uploadCategorySchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TUploadSchema = z.infer<typeof uploadSchema>
|
||||||
|
|
||||||
|
export const DialogUpload = () => {
|
||||||
|
const { isUploadOpen, setUploadedFile, setIsUploadOpen } = useAdminContext()
|
||||||
|
const fetcher = useFetcher()
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
const maxFileSize = 10 * 1024 // 10MB
|
||||||
|
|
||||||
|
const formMethods = useRemixForm<TUploadSchema>({
|
||||||
|
mode: 'onSubmit',
|
||||||
|
fetcher,
|
||||||
|
resolver: zodResolver(uploadSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleSubmit, register, setValue } = formMethods
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fetcher.data?.success) {
|
||||||
|
setError(fetcher.data?.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadedFile(fetcher.data.uploadData.data.file_url)
|
||||||
|
|
||||||
|
setError(undefined)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fetcher])
|
||||||
|
|
||||||
|
const handleChange = async function (event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (event.target.files && event.target.files[0]) {
|
||||||
|
const files: File[] = [...event.target.files]
|
||||||
|
|
||||||
|
onChange(files, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = async function (
|
||||||
|
files: File[],
|
||||||
|
event: ChangeEvent<HTMLInputElement>,
|
||||||
|
) {
|
||||||
|
const file = files[0]
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
setError('Please upload an image file.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxFileSize * 1024) {
|
||||||
|
setError(`File size is too big!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
handleFiles(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
img.src = URL.createObjectURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFiles = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = event.target.files
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0]
|
||||||
|
setValue('file', file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={!!isUploadOpen}
|
||||||
|
onClose={() => {
|
||||||
|
if (fetcher.state === 'idle') {
|
||||||
|
setIsUploadOpen(undefined)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="relative z-50"
|
||||||
|
transition
|
||||||
|
>
|
||||||
|
<DialogBackdrop
|
||||||
|
className="fixed inset-0 bg-black/50 duration-300 ease-out data-[closed]:opacity-0"
|
||||||
|
transition
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
||||||
|
<DialogPanel
|
||||||
|
transition
|
||||||
|
className="max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
||||||
|
>
|
||||||
|
<RemixFormProvider {...formMethods}>
|
||||||
|
<fetcher.Form
|
||||||
|
method="post"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="space-y-4"
|
||||||
|
action="/actions/admin/upload"
|
||||||
|
encType="multipart/form-data"
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div className="text-sm text-red-500 capitalize">{error}</div>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
id="input-file-upload"
|
||||||
|
accept="image/*"
|
||||||
|
className="h-[42px] w-full cursor-pointer rounded-md border border-[#DFDFDF] p-2"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
id="input-file-upload-type"
|
||||||
|
value={isUploadOpen}
|
||||||
|
{...register('category')}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={fetcher.state !== 'idle'}
|
||||||
|
isLoading={fetcher.state !== 'idle'}
|
||||||
|
type="submit"
|
||||||
|
className="w-full rounded-md py-2"
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
</fetcher.Form>
|
||||||
|
</RemixFormProvider>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { Input } from '@headlessui/react'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { useEffect, useState, type ChangeEvent } from 'react'
|
|
||||||
import { useFetcher } from 'react-router'
|
|
||||||
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import { Button } from '~/components/ui/button'
|
|
||||||
import { uploadCategorySchema, useAdminContext } from '~/contexts/admin'
|
|
||||||
|
|
||||||
export const uploadSchema = z.object({
|
|
||||||
file: z.instanceof(File),
|
|
||||||
category: uploadCategorySchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
export type TUploadSchema = z.infer<typeof uploadSchema>
|
|
||||||
|
|
||||||
export const FormUpload = () => {
|
|
||||||
const { isUploadOpen, setUploadedFile } = useAdminContext()
|
|
||||||
const fetcher = useFetcher()
|
|
||||||
const [error, setError] = useState<string>()
|
|
||||||
const maxFileSize = 10 * 1024 // 10MB
|
|
||||||
|
|
||||||
const formMethods = useRemixForm<TUploadSchema>({
|
|
||||||
mode: 'onSubmit',
|
|
||||||
fetcher,
|
|
||||||
resolver: zodResolver(uploadSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { handleSubmit, register, setValue } = formMethods
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!fetcher.data?.success) {
|
|
||||||
setError(fetcher.data?.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploadedFile(fetcher.data.uploadData.data.file_url)
|
|
||||||
|
|
||||||
setError(undefined)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [fetcher])
|
|
||||||
|
|
||||||
const handleChange = async function (event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
event.preventDefault()
|
|
||||||
if (event.target.files && event.target.files[0]) {
|
|
||||||
const files: File[] = [...event.target.files]
|
|
||||||
|
|
||||||
onChange(files, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChange = async function (
|
|
||||||
files: File[],
|
|
||||||
event: ChangeEvent<HTMLInputElement>,
|
|
||||||
) {
|
|
||||||
const file = files[0]
|
|
||||||
const img = new Image()
|
|
||||||
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
setError('Please upload an image file.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > maxFileSize * 1024) {
|
|
||||||
setError(`File size is too big!`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
img.addEventListener('load', () => {
|
|
||||||
handleFiles(event)
|
|
||||||
})
|
|
||||||
|
|
||||||
img.src = URL.createObjectURL(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFiles = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = event.target.files
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const file = files[0]
|
|
||||||
setValue('file', file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RemixFormProvider {...formMethods}>
|
|
||||||
<fetcher.Form
|
|
||||||
method="post"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className="space-y-4"
|
|
||||||
action="/actions/admin/upload"
|
|
||||||
encType="multipart/form-data"
|
|
||||||
>
|
|
||||||
{error && (
|
|
||||||
<div className="text-sm text-red-500 capitalize">{error}</div>
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
id="input-file-upload"
|
|
||||||
accept="image/*"
|
|
||||||
className="h-[42px] w-full cursor-pointer rounded-md border border-[#DFDFDF] p-2"
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="input-file-upload-type"
|
|
||||||
value={isUploadOpen}
|
|
||||||
{...register('category')}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={fetcher.state !== 'idle'}
|
|
||||||
isLoading={fetcher.state !== 'idle'}
|
|
||||||
type="submit"
|
|
||||||
className="w-full rounded-md py-2"
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</fetcher.Form>
|
|
||||||
</RemixFormProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ import { getValidatedFormData } from 'remix-hook-form'
|
|||||||
import { XiorError } from 'xior'
|
import { XiorError } from 'xior'
|
||||||
|
|
||||||
import { uploadFileRequest } from '~/apis/admin/upload-file'
|
import { uploadFileRequest } from '~/apis/admin/upload-file'
|
||||||
import { uploadSchema, type TUploadSchema } from '~/layouts/admin/form-upload'
|
import { uploadSchema, type TUploadSchema } from '~/layouts/admin/dialog-upload'
|
||||||
import { handleCookie } from '~/libs/cookies'
|
import { handleCookie } from '~/libs/cookies'
|
||||||
|
|
||||||
import type { Route } from './+types/actions.register'
|
import type { Route } from './+types/actions.register'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user