返回 presentation-ai
useImageUpload.ts
根目录 / src / hooks / presentation / useImageUpload.ts
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
150 lines TYPESCRIPT