Партии: фото в общем комментарии (до 10), загрузка при завершении
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
import { ApiError } from "../api/client";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import type { AttachmentRead } from "../domain/types";
|
||||
|
||||
/**
|
||||
* Сетка фото партии. Переиспользуется на странице партии (игрок) и в админ-правке —
|
||||
* URL/хуки приходят снаружи (cookie игрока и админа доходят до своих путей).
|
||||
*/
|
||||
export function MatchMedia({
|
||||
attachments,
|
||||
editable = false,
|
||||
onUpload,
|
||||
onDelete,
|
||||
pending = false,
|
||||
max = 10,
|
||||
}: {
|
||||
attachments: AttachmentRead[];
|
||||
editable?: boolean;
|
||||
onUpload?: (file: File) => Promise<unknown>;
|
||||
onDelete?: (id: number) => Promise<unknown>;
|
||||
pending?: boolean;
|
||||
max?: number;
|
||||
}) {
|
||||
const toast = useToast();
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const full = attachments.length >= max;
|
||||
|
||||
const pick = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files ?? []);
|
||||
e.target.value = "";
|
||||
if (!onUpload) return;
|
||||
let slots = max - attachments.length;
|
||||
for (const f of files) {
|
||||
if (slots <= 0) {
|
||||
toast.show(`Максимум ${max} фото`);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await onUpload(f);
|
||||
slots--;
|
||||
} catch (err) {
|
||||
toast.show(err instanceof ApiError ? err.message : "Не удалось загрузить");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (id: number) => {
|
||||
if (!onDelete) return;
|
||||
try {
|
||||
await onDelete(id);
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||
}
|
||||
};
|
||||
|
||||
if (!editable && attachments.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{attachments.length > 0 && (
|
||||
<div className="media-grid">
|
||||
{attachments.map((a) => (
|
||||
<div key={a.id} className="media-item">
|
||||
<a href={a.url} target="_blank" rel="noreferrer">
|
||||
<img src={a.url} alt="фото партии" />
|
||||
</a>
|
||||
{editable && (
|
||||
<button
|
||||
className="media-del"
|
||||
onClick={() => remove(a.id)}
|
||||
disabled={pending}
|
||||
aria-label="Удалить фото"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{editable && (
|
||||
<>
|
||||
<button
|
||||
className="btn small"
|
||||
style={{ marginTop: 8 }}
|
||||
onClick={() => fileRef.current?.click()}
|
||||
disabled={pending || full}
|
||||
>
|
||||
{full ? `Максимум ${max} фото` : "Добавить фото"}
|
||||
</button>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
onChange={pick}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user