Партии: фото в общем комментарии (до 10), загрузка при завершении

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 21:34:39 +03:00
co-authored by Claude Opus 4.8
parent 020c694a44
commit 8ac1b5acef
14 changed files with 790 additions and 11 deletions
+29 -1
View File
@@ -2,11 +2,18 @@ import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { ApiError } from "../api/client";
import { MatchMedia } from "../components/MatchMedia";
import { Spinner } from "../components/Spinner";
import { formatDuration, formatTime } from "../domain/format";
import { WIN_REASONS, type WinReason, winReasonLabel } from "../domain/winReasons";
import { useToast } from "../context/ToastContext";
import { useDeleteMatch, useFinishMatch, useMatch } from "../hooks/matches";
import {
useDeleteMatch,
useDeleteMatchAttachment,
useFinishMatch,
useMatch,
useUploadMatchAttachment,
} from "../hooks/matches";
interface FinishRow {
user_id: number;
@@ -22,6 +29,8 @@ export function MatchDetailPage() {
const { data: match, isLoading } = useMatch(id);
const finish = useFinishMatch();
const del = useDeleteMatch();
const uploadAtt = useUploadMatchAttachment(id ?? 0);
const deleteAtt = useDeleteMatchAttachment(id ?? 0);
const toast = useToast();
const navigate = useNavigate();
@@ -126,6 +135,14 @@ export function MatchDetailPage() {
</div>
)}
{/* Медиа (read-only): для завершённых и для зрителей без прав на правку */}
{!(inProgress && canModify) && match.attachments.length > 0 && (
<div className="card">
<h3>Медиа</h3>
<MatchMedia attachments={match.attachments} editable={false} />
</div>
)}
{/* Незавершённая партия — состав + форма завершения */}
{inProgress && (
<>
@@ -187,6 +204,17 @@ export function MatchDetailPage() {
<textarea rows={2} value={overall} onChange={(e) => setOverall(e.target.value)} />
</div>
<div className="field">
<label className="label">Фото партии (до 10)</label>
<MatchMedia
attachments={match.attachments}
editable
onUpload={(f) => uploadAtt.mutateAsync(f)}
onDelete={(aid) => deleteAtt.mutateAsync(aid)}
pending={uploadAtt.isPending || deleteAtt.isPending}
/>
</div>
{error && <p className="error-text">{error}</p>}
<button