added many images in post

This commit is contained in:
SlimusMinus
2026-07-01 00:23:27 +03:00
parent e9183fd165
commit 139621be0a
5 changed files with 256 additions and 112 deletions

View File

@@ -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,
};
}