added many images in post
This commit is contained in:
@@ -1,87 +1,106 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { compressImage } from "../utils/compressImage";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
const MAX_SIZE_MB = 5;
|
||||
export const ACCEPT = "image/jpeg,image/png,image/webp";
|
||||
|
||||
/**
|
||||
* Управляет выбором, сжатием и удалением фотографии в форме объявления.
|
||||
*
|
||||
* @param {string|null} existingUrl — URL существующей картинки с сервера
|
||||
* (передаётся при редактировании)
|
||||
* @returns {{
|
||||
* imageFile: File|null,
|
||||
* displayImage: string|null,
|
||||
* imgError: string,
|
||||
* compressing: boolean,
|
||||
* fileInputRef: React.RefObject,
|
||||
* handleFileChange: function,
|
||||
* removeImage: function,
|
||||
* }}
|
||||
*/
|
||||
export function useImageUpload(existingUrl = null) {
|
||||
// Сжатый файл, готовый к отправке в FormData
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
// ObjectURL для предпросмотра нового файла
|
||||
const [preview, setPreview] = useState(null);
|
||||
// true — пользователь явно удалил фото, existingUrl больше не показываем
|
||||
const [removed, setRemoved] = useState(false);
|
||||
const [imgError, setImgError] = useState("");
|
||||
// true пока идёт сжатие — нужен чтобы заблокировать сабмит
|
||||
let idCounter = 0;
|
||||
const nextId = () => `img-${++idCounter}-${Date.now()}`;
|
||||
|
||||
export function useImageUpload(existingUrls = []) {
|
||||
const [images, setImages] = useState([]);
|
||||
const [imgError, setImgError] = useState("");
|
||||
const [compressing, setCompressing] = useState(false);
|
||||
// ref на <input type="file"> — нужен для сброса значения после removeImage
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// Что показывать в блоке превью:
|
||||
// новый preview имеет приоритет, иначе existingUrl (если не удалён)
|
||||
const displayImage = preview || (!removed ? existingUrl : null);
|
||||
const existingUrlList = Array.isArray(existingUrls) ? existingUrls : [];
|
||||
|
||||
/** Проверяет файл, сжимает его и создаёт превью. */
|
||||
const handleFileChange = async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
useEffect(() => {
|
||||
if (existingUrlList.length > 0) {
|
||||
setImages(existingUrlList.map((url, i) => ({
|
||||
id: `existing-${i}`,
|
||||
file: null,
|
||||
preview: `${API_URL}${url}`,
|
||||
isExisting: true,
|
||||
serverUrl: url,
|
||||
})));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleFileChange = useCallback(async (e) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (!files.length) return;
|
||||
setImgError("");
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
setImgError("Только изображения (JPEG, PNG, WebP)");
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_SIZE_MB * 1024 * 1024) {
|
||||
setImgError(`Файл слишком большой — максимум ${MAX_SIZE_MB} МБ`);
|
||||
return;
|
||||
for (const file of files) {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
setImgError("Только изображения (JPEG, PNG, WebP)");
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_SIZE_MB * 1024 * 1024) {
|
||||
setImgError(`Файл слишком большой — максимум ${MAX_SIZE_MB} МБ`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCompressing(true);
|
||||
try {
|
||||
const compressed = await compressImage(file);
|
||||
setImageFile(compressed);
|
||||
setPreview(URL.createObjectURL(compressed));
|
||||
setRemoved(false);
|
||||
const compressedFiles = await Promise.all(files.map(f => compressImage(f)));
|
||||
const newImages = compressedFiles.map(file => ({
|
||||
id: nextId(),
|
||||
file,
|
||||
preview: URL.createObjectURL(file),
|
||||
isExisting: false,
|
||||
serverUrl: null,
|
||||
}));
|
||||
setImages(prev => [...prev, ...newImages]);
|
||||
} catch {
|
||||
setImgError("Не удалось обработать изображение");
|
||||
} finally {
|
||||
setCompressing(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** Удаляет фото, освобождает ObjectURL и сбрасывает input. */
|
||||
const removeImage = () => {
|
||||
setImageFile(null);
|
||||
if (preview) URL.revokeObjectURL(preview); // предотвращаем утечку памяти
|
||||
setPreview(null);
|
||||
setRemoved(true);
|
||||
setImgError("");
|
||||
// Сброс нужен, иначе повторный выбор того же файла не вызовет onChange
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
};
|
||||
const removeImage = useCallback((id) => {
|
||||
setImages(prev => {
|
||||
const img = prev.find(i => i.id === id);
|
||||
if (img && !img.isExisting && img.preview) {
|
||||
URL.revokeObjectURL(img.preview);
|
||||
}
|
||||
return prev.filter(i => i.id !== id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getFilesForUpload = useCallback(() => {
|
||||
return images.filter(i => !i.isExisting).map(i => i.file);
|
||||
}, [images]);
|
||||
|
||||
const getExistingUrlsToKeep = useCallback(() => {
|
||||
return images.filter(i => i.isExisting).map(i => i.serverUrl);
|
||||
}, [images]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
images.forEach(img => {
|
||||
if (!img.isExisting && img.preview) {
|
||||
URL.revokeObjectURL(img.preview);
|
||||
}
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return {
|
||||
imageFile,
|
||||
displayImage,
|
||||
images,
|
||||
imgError,
|
||||
compressing,
|
||||
fileInputRef,
|
||||
handleFileChange,
|
||||
removeImage,
|
||||
getFilesForUpload,
|
||||
getExistingUrlsToKeep,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user