| 1 | // @ts-nocheck |
| 2 | "use client"; |
| 3 | |
| 4 | import { nanoid } from "nanoid"; |
| 5 | import { useCallback, useEffect, useState } from "react"; |
| 6 | |
| 7 | import { useUploadThing } from "@/hooks/globals/useUploadthing"; |
| 8 | |
| 9 | export type PreviewImage = { id: string; file: File }; |
| 10 | export type Attachment = { id: string; url: string; type: string }; |
| 11 | export type IsImageUploading = { id: string; isLoading: boolean }; |
| 12 | export type ImagePreview = { id: string; url: string }; |
| 13 | |
| 14 | const MAX_IMAGES = 3; |
| 15 | |
| 16 | export function useImageUpload() { |
| 17 | const [images, setImages] = useState<PreviewImage[]>([]); |
| 18 | const [attachments, setAttachments] = useState<Attachment[]>([]); |
| 19 | const [isImageUploading, setIsImageUploading] = useState<IsImageUploading[]>( |
| 20 | [], |
| 21 | ); |
| 22 | const [previewImages, setPreviewImages] = useState<ImagePreview[]>([]); |
| 23 | const { startUpload } = useUploadThing("imageUploader"); |
| 24 | |
| 25 | // Generate preview URLs when images change |
| 26 | useEffect(() => { |
| 27 | let active = true; |
| 28 | const loadImages = async () => { |
| 29 | const urls = await Promise.all( |
| 30 | images.map( |
| 31 | (image) => |
| 32 | new Promise<ImagePreview>((resolve, reject) => { |
| 33 | const reader = new FileReader(); |
| 34 | reader.onloadend = () => |
| 35 | resolve({ id: image.id, url: reader.result as string }); |
| 36 | reader.onerror = reject; |
| 37 | reader.readAsDataURL(image.file); |
| 38 | }), |
| 39 | ), |
| 40 | ); |
| 41 | if (active) { |
| 42 | setPreviewImages(urls); |
| 43 | } |
| 44 | }; |
| 45 | void loadImages(); |
| 46 | return () => { |
| 47 | active = false; |
| 48 | }; |
| 49 | }, [images]); |
| 50 | |
| 51 | const handleFiles = useCallback( |
| 52 | async (files: File[]) => { |
| 53 | const acceptedFiles = files.filter((f) => f.type.startsWith("image/")); |
| 54 | if (acceptedFiles.length === 0) return; |
| 55 | |
| 56 | // Limit to MAX_IMAGES |
| 57 | const remaining = MAX_IMAGES - images.length; |
| 58 | if (remaining <= 0) return; |
| 59 | const filesToAdd = acceptedFiles.slice(0, remaining); |
| 60 | |
| 61 | const id = nanoid(); |
| 62 | const fileType = filesToAdd[0]?.type || "image/unknown"; |
| 63 | |
| 64 | setImages((prev) => [ |
| 65 | ...prev, |
| 66 | ...filesToAdd.map((file) => ({ id, file })), |
| 67 | ]); |
| 68 | setIsImageUploading((prev) => [...prev, { id, isLoading: true }]); |
| 69 | |
| 70 | try { |
| 71 | const response = await startUpload(filesToAdd); |
| 72 | const newAttachments = |
| 73 | response?.map((image) => ({ |
| 74 | id, |
| 75 | url: image.ufsUrl, |
| 76 | type: fileType, |
| 77 | })) ?? []; |
| 78 | |
| 79 | setIsImageUploading((prev) => |
| 80 | prev.map((item) => |
| 81 | item.id === id ? { ...item, isLoading: false } : item, |
| 82 | ), |
| 83 | ); |
| 84 | setAttachments((prev) => [...prev, ...newAttachments]); |
| 85 | } catch (error) { |
| 86 | console.error("Upload failed", error); |
| 87 | setIsImageUploading((prev) => prev.filter((item) => item.id !== id)); |
| 88 | setImages((prev) => prev.filter((img) => img.id !== id)); |
| 89 | } |
| 90 | }, |
| 91 | [images.length, startUpload], |
| 92 | ); |
| 93 | |
| 94 | const handleFileChange = useCallback( |
| 95 | async (event: React.ChangeEvent<HTMLInputElement>) => { |
| 96 | const files = event.target.files; |
| 97 | if (!files || files.length === 0) return; |
| 98 | await handleFiles(Array.from(files)); |
| 99 | event.target.value = ""; |
| 100 | }, |
| 101 | [handleFiles], |
| 102 | ); |
| 103 | |
| 104 | const handlePaste = useCallback( |
| 105 | async (event: React.ClipboardEvent) => { |
| 106 | const items = event.clipboardData?.items; |
| 107 | if (!items) return; |
| 108 | |
| 109 | const files: File[] = []; |
| 110 | for (const item of items) { |
| 111 | if (item.kind === "file" && item.type.startsWith("image/")) { |
| 112 | const file = item.getAsFile(); |
| 113 | if (file) files.push(file); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | if (files.length > 0) { |
| 118 | event.preventDefault(); |
| 119 | await handleFiles(files); |
| 120 | } |
| 121 | }, |
| 122 | [handleFiles], |
| 123 | ); |
| 124 | |
| 125 | const removeImage = useCallback((id: string) => { |
| 126 | setImages((prev) => prev.filter((img) => img.id !== id)); |
| 127 | setAttachments((prev) => prev.filter((att) => att.id !== id)); |
| 128 | setIsImageUploading((prev) => prev.filter((item) => item.id !== id)); |
| 129 | }, []); |
| 130 | |
| 131 | const clearImages = useCallback(() => { |
| 132 | setImages([]); |
| 133 | setAttachments([]); |
| 134 | setIsImageUploading([]); |
| 135 | setPreviewImages([]); |
| 136 | }, []); |
| 137 | |
| 138 | return { |
| 139 | images, |
| 140 | attachments, |
| 141 | isImageUploading, |
| 142 | previewImages, |
| 143 | handleFileChange, |
| 144 | handlePaste, |
| 145 | removeImage, |
| 146 | clearImages, |
| 147 | MAX_IMAGES, |
| 148 | }; |
| 149 | } |
| 150 |