xLog/app/components/dashboard/AvatarForm.tsx

135 lines
3.5 KiB
TypeScript

import { Dialog } from "@headlessui/react"
import { useEffect, useRef, useState } from "react"
import ReactAvatarEditor from "react-avatar-editor"
import { getUserContentsUrl } from "~/lib/user-contents"
import { Avatar } from "~/components/ui/Avatar"
import { Button } from "~/components/ui/Button"
import { useFetcher } from "@remix-run/react"
import toast from "react-hot-toast"
const AvatarEditorModal: React.FC<{
isOpen: boolean
image?: File | null
setIsOpen: (open: boolean) => void
site?: string
}> = ({ isOpen, setIsOpen, image, site }) => {
const editorRef = useRef<ReactAvatarEditor | null>(null)
const fetcher = useFetcher()
const cropAndSave = async () => {
if (!editorRef.current) return
const canvas = editorRef.current.getImage()
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) return reject(new Error("Canvas is empty"))
resolve(blob)
})
})
const form = new FormData()
if (site) {
form.append("site", site)
}
form.append("file", blob)
fetcher.submit(form, {
action: "/api/save-avatar",
method: "post",
encType: "multipart/form-data",
})
}
useEffect(() => {
if (fetcher.type === "done") {
setIsOpen(false)
toast.success("Updated!")
}
}, [fetcher.type, setIsOpen])
return (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="fixed z-10 inset-0 overflow-y-auto"
>
<div className="flex items-center justify-center min-h-screen">
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
<div className="relative bg-white rounded-lg max-w-md w-full mx-auto">
<Dialog.Title className="px-5 h-12 flex items-center border-b">
Adjust the picture
</Dialog.Title>
<div className="py-5">
{image && (
<ReactAvatarEditor
ref={editorRef}
className="mx-auto rounded"
image={image}
borderRadius={9999}
/>
)}
</div>
<div className="h-16 border-t flex items-center px-5">
<Button
isBlock
onClick={cropAndSave}
isLoading={fetcher.state === "submitting"}
>
Crop and Save
</Button>
</div>
</div>
</div>
</Dialog>
)
}
export const AvatarForm: React.FC<{
filename: string | undefined
name: string
site?: string
}> = ({ filename, name, site }) => {
const [isOpen, setIsOpen] = useState(false)
const inputEl = useRef<HTMLInputElement>(null)
const [image, setImage] = useState<File | null>(null)
const onClick = () => {
inputEl.current?.click()
}
const handleChange = (e: any) => {
const files = e.target.files as File[]
if (files.length > 0) {
setImage(files[0])
setIsOpen(true)
}
}
return (
<>
<AvatarEditorModal
isOpen={isOpen}
setIsOpen={setIsOpen}
image={image}
site={site}
/>
<input
aria-hidden
className="hidden"
type="file"
ref={inputEl}
onChange={handleChange}
accept="image/*"
/>
<Avatar
images={[getUserContentsUrl(filename)]}
size={140}
name={name}
tabIndex={-1}
className="cursor-default focus:ring-2 ring-offset-1 ring-zinc-200"
onClick={onClick}
/>
</>
)
}