Ревью кода (#82) #90

Merged
NotBigGhost merged 4 commits from issue-82-code-review into dev 2026-09-18 23:42:36 +03:00
4 changed files with 38 additions and 54 deletions
Showing only changes of commit 6c35cb04d4 - Show all commits
+3 -10
View File
@@ -2,6 +2,7 @@ import { Scissors } from "lucide-react";
import { useRef } from "react"; import { useRef } from "react";
import { Avatar } from "./Avatar"; import { Avatar } from "./Avatar";
import { MAX_COUNT, parseCount } from "../domain/matchCounts";
export interface PlacePlayer { export interface PlacePlayer {
user_id: number; user_id: number;
@@ -241,19 +242,11 @@ export function PlaceEditor({
type="number" type="number"
inputMode="numeric" inputMode="numeric"
min={0} min={0}
max={99} max={MAX_COUNT}
placeholder="—" placeholder="—"
disabled={locked} disabled={locked}
value={locked ? 0 : (counts?.[userId]?.[field] ?? "")} value={locked ? 0 : (counts?.[userId]?.[field] ?? "")}
onChange={(e) => { onChange={(e) => onCount?.(userId, field, parseCount(e.target.value))}
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))),
);
}}
/> />
</label> </label>
); );
+10
View File
@@ -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)));
}
+22 -36
View File
@@ -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); 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() { export function MatchDetailPage() {
const { matchId } = useParams(); const { matchId } = useParams();
// Number("abc") — NaN, а не null: без проверки запрос уходил бы на /api/matches/NaN. // Number("abc") — NaN, а не null: без проверки запрос уходил бы на /api/matches/NaN.
@@ -193,21 +205,14 @@ export function MatchDetailPage() {
const outcomeSeats = (): OutcomeSeat[] => { const outcomeSeats = (): OutcomeSeat[] => {
const byId = new Map(match.participants.map((p) => [p.user_id, p])); 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, userId: uid,
nickname: byId.get(uid)?.nickname ?? "", nickname: byId.get(uid)?.nickname ?? "",
place, place,
eliminated, eliminated,
objectives: finishCounts[uid]?.objectives ?? null, objectives: finishCounts[uid]?.objectives ?? null,
worlds: eliminated ? 0 : (finishCounts[uid]?.worlds ?? 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 = () => const warnings = () =>
@@ -218,35 +223,16 @@ export function MatchDetailPage() {
maxRounds: match.max_rounds, maxRounds: match.max_rounds,
}); });
// Строки результатов для API: места по блокам (competition ranking), затем выбывшие. // Строки результатов для API. Место выбывшего и его миры (0) проставит сервер.
const resultRows = () => { const resultRows = () =>
const commentOf = (uid: number) => (finishComments[uid] ?? "").trim() || null; placeRows(finishBlocks, elim).map(({ uid, place, eliminated }) => ({
const countsFor = (uid: number, eliminated: boolean) => ({
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, user_id: uid,
place: null, place: eliminated ? null : place,
eliminated: true, eliminated,
comment: commentOf(uid), comment: (finishComments[uid] ?? "").trim() || null,
...countsFor(uid, true), objectives: finishCounts[uid]?.objectives ?? null,
worlds: eliminated ? null : (finishCounts[uid]?.worlds ?? null),
})); }));
return [...survivors, ...eliminated];
};
const submitFinish = async () => { const submitFinish = async () => {
if (!id || !match) return; if (!id || !match) return;
+3 -8
View File
@@ -6,6 +6,7 @@ import { MatchMedia } from "../../components/MatchMedia";
import { Spinner } from "../../components/Spinner"; import { Spinner } from "../../components/Spinner";
import { Switch } from "../../components/Switch"; import { Switch } from "../../components/Switch";
import { finishWarnings } from "../../domain/finishWarnings"; import { finishWarnings } from "../../domain/finishWarnings";
import { MAX_COUNT, parseCount } from "../../domain/matchCounts";
import { import {
LAST_STANDING, LAST_STANDING,
reasonForSurvivors, reasonForSurvivors,
@@ -34,12 +35,6 @@ interface Row {
worlds: number | null; 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 }) { export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose: () => void }) {
const { data: match, isLoading } = useAdminMatch(matchId); const { data: match, isLoading } = useAdminMatch(matchId);
const { data: factions } = useAdminFactions(); const { data: factions } = useAdminFactions();
@@ -218,7 +213,7 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
style={{ flex: "0 0 70px" }} style={{ flex: "0 0 70px" }}
type="number" type="number"
min={0} min={0}
max={99} max={MAX_COUNT}
placeholder="цели" placeholder="цели"
title="Цели на конец партии" title="Цели на конец партии"
value={r.objectives ?? ""} value={r.objectives ?? ""}
@@ -228,7 +223,7 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
style={{ flex: "0 0 70px" }} style={{ flex: "0 0 70px" }}
type="number" type="number"
min={0} min={0}
max={99} max={MAX_COUNT}
placeholder="миры" placeholder="миры"
title="Миры на конец партии" title="Миры на конец партии"
disabled={r.eliminated} disabled={r.eliminated}