diff --git a/app/(website)/settings/components/UploadImageForm.tsx b/app/(website)/settings/components/UploadImageForm.tsx index 7c7ed53..298e21a 100644 --- a/app/(website)/settings/components/UploadImageForm.tsx +++ b/app/(website)/settings/components/UploadImageForm.tsx @@ -18,11 +18,18 @@ type UploadImageFormProps = { export default function UploadImageForm({ type }: UploadImageFormProps) { const t = useT("pages.settings.components.uploadImage"); const [file, setFile] = useState(null); + const [hasChanged, setHasChanged] = useState(false); const [isFileUploading, setIsFileUploading] = useState(false); const { self } = useSelf(); - const { trigger: triggerUserUpload } = useUserUpload(); + const { toast } = useToast(); + + const handleFileChange = (f: File | null) => { + setFile(f); + if (f) + setHasChanged(true); + }; const localizedType = t(`types.${type}`); @@ -33,13 +40,20 @@ export default function UploadImageForm({ type }: UploadImageFormProps) { const urlToFetch = type === "avatar" ? self.avatar_url : self.banner_url; fetch(urlToFetch).then(async (res) => { - const file = await res.blob(); - setFile(new File([file], "file.png")); + const blob = await res.blob(); + const ext + = blob.type === "image/gif" + ? "gif" + : blob.type === "image/png" + ? "png" + : blob.type === "image/webp" + ? "webp" + : "jpg"; + + setFile(new File([blob], `file.${ext}`, { type: blob.type })); }); }, [file, self, type]); - const { toast } = useToast(); - const uploadFile = async () => { if (file === null) return; @@ -52,15 +66,16 @@ export default function UploadImageForm({ type }: UploadImageFormProps) { type, }, { - onSuccess(_data, _key, _config) { + onSuccess() { toast({ title: t("toast.success", { type: localizedType }), variant: "success", className: "capitalize", }); setIsFileUploading(false); + setHasChanged(false); }, - onError(err, _key, _config) { + onError(err) { toast({ title: err?.message ?? t("toast.error"), variant: "destructive", @@ -74,16 +89,18 @@ export default function UploadImageForm({ type }: UploadImageFormProps) { return ( <> + + + + + + + + + + + + ); +} diff --git a/components/General/ImageSelect.tsx b/components/General/ImageSelect.tsx index 2080f7b..4926d09 100644 --- a/components/General/ImageSelect.tsx +++ b/components/General/ImageSelect.tsx @@ -1,15 +1,18 @@ -import Image from "next/image"; +import { useEffect, useId, useState } from "react"; +import ImageCropDialog from "@/components/General/ImageCropDialog"; import Spinner from "@/components/Spinner"; import { AspectRatio } from "@/components/ui/aspect-ratio"; import { useToast } from "@/hooks/use-toast"; import { useT } from "@/lib/i18n/utils"; type Props = { - setFile: React.Dispatch>; + setFile: (file: File | null) => void; file: File | null; isWide?: boolean; maxFileSizeBytes?: number; + enableCrop?: boolean; + type?: "avatar" | "banner"; }; export default function ImageSelect({ @@ -17,66 +20,121 @@ export default function ImageSelect({ file, isWide, maxFileSizeBytes, + enableCrop, + type, }: Props) { const t = useT("components.imageSelect"); - const uniqueId = Math.random().toString(36).slice(7); + const inputId = useId(); const { toast } = useToast(); + const [rawFileForCrop, setRawFileForCrop] = useState(null); + const [isCropOpen, setIsCropOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); + + useEffect(() => { + if (!file) { + setPreviewUrl(null); + return; + } + + const url = URL.createObjectURL(file); + setPreviewUrl(url); + + return () => URL.revokeObjectURL(url); + }, [file]); + + const handleFileSelected = (nextFile: File) => { + const isGif = nextFile.type === "image/gif"; + const shouldCrop = Boolean(enableCrop && type && !isGif); + + if (shouldCrop) { + setRawFileForCrop(nextFile); + setIsCropOpen(true); + return; + } + + setFile(nextFile); + }; + + const handleCropped = (croppedFile: File) => { + setFile(croppedFile); + setRawFileForCrop(null); + }; + return ( -
-