/
VVRONGG
/
backside
Обзор
Документация
Войти
/
VVRONGG
/
backside
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
master
admin/src/components/ImageManager.tsx
123 строки
4 KB
VVrongg
add kubec deploy
24 май 2026, 20:16
24 май 2026, 20:16
db9ffee
Код
Авторство
О чём код?
import { useRef, useState } from 'react'; import { api, errMsg, imageSrc, type ImageRef } from '../api'; interface Props { objectiveId: number; images: ImageRef[]; onChange: (images: ImageRef[]) => void; } /** Read a File as raw base64 (without the `data:...;base64,` prefix). */ function fileToBase64(file: File): Promise<string> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve((reader.result as string).split(',', 2)[1]); reader.onerror = () => reject(reader.error); reader.readAsDataURL(file); }); } export default function ImageManager({ objectiveId, images, onChange }: Props) { const [busy, setBusy] = useState(false); const [error, setError] = useState<string | null>(null); const fileInput = useRef<HTMLInputElement>(null); const run = async (fn: () => Promise<void>) => { setBusy(true); setError(null); try { await fn(); } catch (e) { setError(errMsg(e)); } finally { setBusy(false); } }; const onAdd = (files: FileList | null) => { if (!files || files.length === 0) return; run(async () => { const encoded = await Promise.all(Array.from(files).map(fileToBase64)); onChange(await api.addImages(objectiveId, encoded)); if (fileInput.current) fileInput.current.value = ''; }); }; const onDelete = (imageId: number) => run(async () => { await api.deleteImage(objectiveId, imageId); // Delete doesn't renumber the survivors, and they're already in order. onChange(images.filter((img) => img.id !== imageId)); }); const move = (index: number, delta: number) => { const target = index + delta; if (target < 0 || target >= images.length) return; const order = images.map((img) => img.id); [order[index], order[target]] = [order[target], order[index]]; run(async () => { onChange(await api.reorderImages(objectiveId, order)); }); }; return ( <div className="images"> <div className="images-head"> <h3>Изображения</h3> <label className="btn btn-secondary"> Добавить изображения <input ref={fileInput} type="file" accept="image/*" multiple hidden disabled={busy} onChange={(e) => onAdd(e.target.files)} /> </label> </div> {error && <p className="error">{error}</p>} {images.length === 0 ? ( <p className="muted">Пока нет изображений. Первое изображение используется как превью.</p> ) : ( <ul className="thumb-grid"> {images.map((img, i) => ( <li key={img.id} className="thumb"> <img src={imageSrc(img)} alt="" loading="lazy" /> <div className="thumb-bar"> <button className="btn btn-tiny" disabled={busy || i === 0} title="Сдвинуть раньше" onClick={() => move(i, -1)} > ← </button> {i === 0 && <span className="badge">превью</span>} <button className="btn btn-tiny" disabled={busy || i === images.length - 1} title="Сдвинуть позже" onClick={() => move(i, 1)} > → </button> <button className="btn btn-tiny btn-danger" disabled={busy} title="Удалить" onClick={() => onDelete(img.id)} > ✕ </button> </div> </li> ))} </ul> )} </div> ); }