138 lines
4.0 KiB
TypeScript
138 lines
4.0 KiB
TypeScript
import React, { ChangeEvent, useCallback, useState } from 'react';
|
||
import Modal from '@/components/ui/Modal';
|
||
import Button from '@/components/ui/Button';
|
||
import api from '@/lib/api';
|
||
|
||
interface Props {
|
||
/** Управляет открытием/закрытием модального окна */
|
||
isOpen: boolean;
|
||
|
||
/** Колбэк, вызываемый, когда пользователь закрыл окно без сохранения */
|
||
onClose: () => void;
|
||
|
||
/** Колбэк, срабатывающий после успешной загрузки файла */
|
||
onSuccess: (uploaded: UploadedImage) => void;
|
||
}
|
||
|
||
/**
|
||
* Тип данных, которые реально уходят на бэкенд.
|
||
* Поля, которые не требуются при самой первой загрузке,
|
||
* объявлены с ?. Компилятор теперь не требует заполнять их
|
||
* каждый раз.
|
||
*/
|
||
export interface ImageBody {
|
||
file: File;
|
||
|
||
/** Если файл уже загружен, бэкенд может вернуть готовый URL */
|
||
url?: string;
|
||
|
||
title_ru?: string;
|
||
title_en?: string;
|
||
description_ru?: string;
|
||
description_en?: string;
|
||
}
|
||
|
||
/** Ответ от сервера после успешной загрузки */
|
||
export interface UploadedImage {
|
||
id: string;
|
||
url: string;
|
||
title_ru?: string;
|
||
title_en?: string;
|
||
description_ru?: string;
|
||
description_en?: string;
|
||
}
|
||
|
||
const ImageUploaderModal: React.FC<Props> = ({ isOpen, onClose, onSuccess }) => {
|
||
const [file, setFile] = useState<File | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const onFileChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||
setError(null);
|
||
const chosen = e.target.files?.[0];
|
||
if (chosen) {
|
||
setFile(chosen);
|
||
}
|
||
}, []);
|
||
|
||
const resetState = () => {
|
||
setFile(null);
|
||
setError(null);
|
||
setLoading(false);
|
||
};
|
||
|
||
const handleClose = () => {
|
||
if (!loading) {
|
||
resetState();
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
const upload = async () => {
|
||
if (!file) {
|
||
setError('Выберите изображение перед загрузкой.');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
|
||
try {
|
||
const body: ImageBody = { file }; // ничего лишнего не добавляем
|
||
|
||
// Формируем FormData, чтобы отправить файл «как есть»
|
||
const form = new FormData();
|
||
form.append('file', body.file);
|
||
|
||
// Если вам нужно сразу отправлять title/description,
|
||
// их можно добавить в form.append(...) при наличии
|
||
|
||
const { data } = await api.post<UploadedImage>('/images', form, {
|
||
headers: { 'Content-Type': 'multipart/form-data' }
|
||
});
|
||
|
||
onSuccess(data);
|
||
handleClose();
|
||
} catch (err) {
|
||
console.error(err);
|
||
setError('Не удалось загрузить файл. Попробуйте ещё раз.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal open={isOpen} onClose={handleClose} title="Загрузка изображения">
|
||
<div className="space-y-4">
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={onFileChange}
|
||
disabled={loading}
|
||
className="block w-full"
|
||
/>
|
||
|
||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||
|
||
<div className="flex justify-end gap-2">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={handleClose}
|
||
disabled={loading}
|
||
>
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
onClick={upload}
|
||
loading={loading}
|
||
disabled={!file || loading}
|
||
>
|
||
Сохранить
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default ImageUploaderModal; |