Files
ForbiddenStarsApp/frontend/src/hooks/matches.ts
T
NotBigGhostandClaude Opus 5 5035ee41e6 События: общий рейтинг обновляется у всех, а не только в группе партии
Рейтинг с #80 один на всё приложение: завершённая или удалённая партия двигает
топ, историю и профили всех, кто играл после неё, и страницы других групп.
Событие match по-прежнему идёт группе партии, остальным активным игрокам —
ratings без подробностей о партии. Фронт по обоим сбрасывает все рейтинговые
витрины (invalidateRatingViews); participant_ids больше не нужен. #88

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-18 23:29:37 +03:00

151 lines
5.4 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api, unwrap } from "../api/client";
import { invalidateRatingViews, qk } from "../api/queryKeys";
import type {
FactionRead,
MatchCreate,
MatchFinish,
MatchFinishDraftData,
MatchRead,
MatchUpdate,
} from "../domain/types";
import { uploadFile } from "../lib/upload";
export function useMatch(matchId: number | null) {
return useQuery({
// Ключ и enabled должны сходиться: иначе запрос без валидного id кэшировал бы
// свою ошибку под общим ключом-заглушкой.
queryKey: matchId != null ? qk.match(matchId) : ["match", "none"],
enabled: matchId != null,
queryFn: async () =>
unwrap(
await api.GET("/api/matches/{match_id}", {
params: { path: { match_id: matchId as number } },
}),
),
});
}
/**
* Общий черновик формы завершения: то, что видят все, кто заполняет партию.
* Ответ — свежая партия целиком, поэтому кладём его прямо в кэш, без инвалидации:
* перезапрос на каждое движение тайла был бы расточительным.
*/
export function useSaveFinishDraft(matchId: number | null) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (data: MatchFinishDraftData): Promise<MatchRead> =>
unwrap(
await api.PUT("/api/matches/{match_id}/finish-draft", {
params: { path: { match_id: matchId as number } },
body: data,
}),
),
onSuccess: (m) => qc.setQueryData(qk.match(m.id), m),
});
}
export function useRandomizeFaction() {
return useMutation({
mutationFn: async (args: { group_id: number; exclude_faction_ids: number[] }): Promise<FactionRead> => {
const res = unwrap(await api.POST("/api/matches/randomize-faction", { body: args }));
return res.faction;
},
});
}
/** Этап 1: старт партии (игроки + фракции, без мест). */
export function useStartMatch() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: MatchCreate): Promise<MatchRead> =>
unwrap(await api.POST("/api/matches", { body })),
onSuccess: (m) => {
qc.invalidateQueries({ queryKey: qk.groupMatches(m.group_id) });
qc.invalidateQueries({ queryKey: qk.home });
},
});
}
/** Этап 2: завершение партии (места, причина победы). */
export function useFinishMatch() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (args: { matchId: number; body: MatchFinish }): Promise<MatchRead> =>
unwrap(
await api.POST("/api/matches/{match_id}/finish", {
params: { path: { match_id: args.matchId } },
body: args.body,
}),
),
onSuccess: (m) => {
qc.invalidateQueries({ queryKey: qk.match(m.id) });
qc.invalidateQueries({ queryKey: qk.groupMatches(m.group_id) });
invalidateRatingViews(qc);
},
});
}
/** Правка завершённой партии участником группы (у админки свой эндпойнт). */
export function useUpdateMatch() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (args: { matchId: number; body: MatchUpdate }): Promise<MatchRead> =>
unwrap(
await api.PATCH("/api/matches/{match_id}", {
params: { path: { match_id: args.matchId } },
body: args.body,
}),
),
onSuccess: (m) => {
qc.setQueryData(qk.match(m.id), m);
qc.invalidateQueries({ queryKey: qk.groupMatches(m.group_id) });
// Места изменились — значит изменились рейтинги, топ, истории и профили.
invalidateRatingViews(qc);
},
});
}
export function useDeleteMatch() {
const qc = useQueryClient();
return useMutation({
// expectedVersion — оптимистичная блокировка: отмена устаревшей версии вернёт 409 STALE_WRITE.
mutationFn: async (args: { matchId: number; expectedVersion?: string }) =>
unwrap(
await api.DELETE("/api/matches/{match_id}", {
params: {
path: { match_id: args.matchId },
query: args.expectedVersion ? { expected_version: args.expectedVersion } : {},
},
}),
),
onSuccess: () => qc.invalidateQueries(),
});
}
// Медиа партии: фото грузим multipart'ом отдельным fetch (с ресайзом и CSRF из cookie).
export function useUploadMatchAttachment(matchId: number) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (file: File) =>
uploadFile(`/api/matches/${matchId}/attachments`, file),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.match(matchId) }),
});
}
export function useDeleteMatchAttachment(matchId: number) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (attachmentId: number) =>
unwrap(
await api.DELETE("/api/matches/{match_id}/attachments/{attachment_id}", {
params: { path: { match_id: matchId, attachment_id: attachmentId } },
}),
),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.match(matchId) }),
});
}