Ревью кода (#82) #90
@@ -2,6 +2,7 @@ import { Scissors } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { Avatar } from "./Avatar";
|
||||
import { MAX_COUNT, parseCount } from "../domain/matchCounts";
|
||||
|
||||
export interface PlacePlayer {
|
||||
user_id: number;
|
||||
@@ -241,19 +242,11 @@ export function PlaceEditor({
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
max={99}
|
||||
max={MAX_COUNT}
|
||||
placeholder="—"
|
||||
disabled={locked}
|
||||
value={locked ? 0 : (counts?.[userId]?.[field] ?? "")}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.trim();
|
||||
const num = Number(raw);
|
||||
onCount?.(
|
||||
userId,
|
||||
field,
|
||||
raw === "" || !Number.isFinite(num) ? null : Math.min(99, Math.max(0, Math.trunc(num))),
|
||||
);
|
||||
}}
|
||||
onChange={(e) => onCount?.(userId, field, parseCount(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Верхняя граница целей и миров — отсечка мусора, та же, что в схеме API (Count). */
|
||||
export const MAX_COUNT = 99;
|
||||
|
||||
/** Поле «цели»/«миры» формы: пустое — не указано (null), иначе целое 0..MAX_COUNT. */
|
||||
export function parseCount(raw: string): number | null {
|
||||
const text = raw.trim();
|
||||
const num = Number(text);
|
||||
if (text === "" || !Number.isFinite(num)) return null;
|
||||
return Math.min(MAX_COUNT, Math.max(0, Math.trunc(num)));
|
||||
}
|
||||
@@ -52,6 +52,18 @@ const countDict = (counts: Counts, field: CountField): Record<string, number> =>
|
||||
|
||||
const survivorsIn = (blocks: number[][]) => blocks.reduce((sum, ids) => sum + ids.length, 0);
|
||||
|
||||
// Места по раскладке: блоки сверху вниз (competition ranking — ничья съедает следующие
|
||||
// места), затем выбывшие с общим последним местом.
|
||||
const placeRows = (blocks: number[][], eliminated: number[]) => {
|
||||
let place = 1;
|
||||
const survivors = blocks.flatMap((ids) => {
|
||||
const rows = ids.map((uid) => ({ uid, place, eliminated: false }));
|
||||
place += ids.length;
|
||||
return rows;
|
||||
});
|
||||
return [...survivors, ...eliminated.map((uid) => ({ uid, place, eliminated: true }))];
|
||||
};
|
||||
|
||||
export function MatchDetailPage() {
|
||||
const { matchId } = useParams();
|
||||
// Number("abc") — NaN, а не null: без проверки запрос уходил бы на /api/matches/NaN.
|
||||
@@ -193,21 +205,14 @@ export function MatchDetailPage() {
|
||||
|
||||
const outcomeSeats = (): OutcomeSeat[] => {
|
||||
const byId = new Map(match.participants.map((p) => [p.user_id, p]));
|
||||
const seat = (uid: number, place: number, eliminated: boolean): OutcomeSeat => ({
|
||||
return placeRows(finishBlocks, elim).map(({ uid, place, eliminated }) => ({
|
||||
userId: uid,
|
||||
nickname: byId.get(uid)?.nickname ?? "",
|
||||
place,
|
||||
eliminated,
|
||||
objectives: finishCounts[uid]?.objectives ?? null,
|
||||
worlds: eliminated ? 0 : (finishCounts[uid]?.worlds ?? null),
|
||||
});
|
||||
let place = 1;
|
||||
const rows = finishBlocks.flatMap((ids) => {
|
||||
const out = ids.map((uid) => seat(uid, place, false));
|
||||
place += ids.length;
|
||||
return out;
|
||||
});
|
||||
return [...rows, ...elim.map((uid) => seat(uid, place, true))];
|
||||
}));
|
||||
};
|
||||
|
||||
const warnings = () =>
|
||||
@@ -218,35 +223,16 @@ export function MatchDetailPage() {
|
||||
maxRounds: match.max_rounds,
|
||||
});
|
||||
|
||||
// Строки результатов для API: места по блокам (competition ranking), затем выбывшие.
|
||||
const resultRows = () => {
|
||||
const commentOf = (uid: number) => (finishComments[uid] ?? "").trim() || null;
|
||||
const countsFor = (uid: number, eliminated: boolean) => ({
|
||||
// Строки результатов для API. Место выбывшего и его миры (0) проставит сервер.
|
||||
const resultRows = () =>
|
||||
placeRows(finishBlocks, elim).map(({ uid, place, eliminated }) => ({
|
||||
user_id: uid,
|
||||
place: eliminated ? null : place,
|
||||
eliminated,
|
||||
comment: (finishComments[uid] ?? "").trim() || null,
|
||||
objectives: finishCounts[uid]?.objectives ?? null,
|
||||
// У выбывшего миров нет — сервер сам запишет 0.
|
||||
worlds: eliminated ? null : (finishCounts[uid]?.worlds ?? null),
|
||||
});
|
||||
let place = 1;
|
||||
const survivors = finishBlocks.flatMap((ids) => {
|
||||
const rows = ids.map((uid) => ({
|
||||
user_id: uid,
|
||||
place,
|
||||
eliminated: false,
|
||||
comment: commentOf(uid),
|
||||
...countsFor(uid, false),
|
||||
}));
|
||||
place += ids.length; // competition ranking: ничья съедает следующие места
|
||||
return rows;
|
||||
});
|
||||
const eliminated = elim.map((uid) => ({
|
||||
user_id: uid,
|
||||
place: null,
|
||||
eliminated: true,
|
||||
comment: commentOf(uid),
|
||||
...countsFor(uid, true),
|
||||
}));
|
||||
return [...survivors, ...eliminated];
|
||||
};
|
||||
|
||||
const submitFinish = async () => {
|
||||
if (!id || !match) return;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MatchMedia } from "../../components/MatchMedia";
|
||||
import { Spinner } from "../../components/Spinner";
|
||||
import { Switch } from "../../components/Switch";
|
||||
import { finishWarnings } from "../../domain/finishWarnings";
|
||||
import { MAX_COUNT, parseCount } from "../../domain/matchCounts";
|
||||
import {
|
||||
LAST_STANDING,
|
||||
reasonForSurvivors,
|
||||
@@ -34,12 +35,6 @@ interface Row {
|
||||
worlds: number | null;
|
||||
}
|
||||
|
||||
// Пустое поле — «не указано» (null); иначе целое 0..99.
|
||||
const parseCount = (raw: string): number | null => {
|
||||
const num = Number(raw.trim());
|
||||
return raw.trim() === "" || !Number.isFinite(num) ? null : Math.min(99, Math.max(0, Math.trunc(num)));
|
||||
};
|
||||
|
||||
export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose: () => void }) {
|
||||
const { data: match, isLoading } = useAdminMatch(matchId);
|
||||
const { data: factions } = useAdminFactions();
|
||||
@@ -218,7 +213,7 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
|
||||
style={{ flex: "0 0 70px" }}
|
||||
type="number"
|
||||
min={0}
|
||||
max={99}
|
||||
max={MAX_COUNT}
|
||||
placeholder="цели"
|
||||
title="Цели на конец партии"
|
||||
value={r.objectives ?? ""}
|
||||
@@ -228,7 +223,7 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
|
||||
style={{ flex: "0 0 70px" }}
|
||||
type="number"
|
||||
min={0}
|
||||
max={99}
|
||||
max={MAX_COUNT}
|
||||
placeholder="миры"
|
||||
title="Миры на конец партии"
|
||||
disabled={r.eliminated}
|
||||
|
||||
Reference in New Issue
Block a user