130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
import { ApiError, api, unwrap } from "../api/client";
|
|
import { qk } from "../api/queryKeys";
|
|
import type { FactionRead, MatchCreate, MatchFinish, MatchRead } from "../domain/types";
|
|
import { resizeImage } from "../lib/image";
|
|
|
|
function readCsrfToken(): string | null {
|
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]*)/);
|
|
return m ? decodeURIComponent(m[1]) : null;
|
|
}
|
|
|
|
export function useMatch(matchId: number | null) {
|
|
return useQuery({
|
|
queryKey: matchId ? 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 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) });
|
|
qc.invalidateQueries({ queryKey: qk.groupStats(m.group_id) });
|
|
qc.invalidateQueries({ queryKey: qk.leaderboard });
|
|
qc.invalidateQueries({ queryKey: qk.home });
|
|
qc.invalidateQueries({ queryKey: qk.myStats });
|
|
},
|
|
});
|
|
}
|
|
|
|
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) => {
|
|
const blob = await resizeImage(file);
|
|
const form = new FormData();
|
|
form.append("file", blob, "photo.jpg");
|
|
const csrf = readCsrfToken();
|
|
const base = import.meta.env.VITE_API_BASE_URL || "";
|
|
const r = await fetch(`${base}/api/matches/${matchId}/attachments`, {
|
|
method: "POST",
|
|
body: form,
|
|
credentials: "include",
|
|
headers: csrf ? { "X-CSRF-Token": csrf } : {},
|
|
});
|
|
if (!r.ok) {
|
|
let env: { code?: string; message?: string } | undefined;
|
|
try {
|
|
env = ((await r.json()) as { error?: typeof env }).error;
|
|
} catch {
|
|
/* тело без JSON */
|
|
}
|
|
throw new ApiError(env?.message || "Не удалось загрузить", env?.code || "ERROR", r.status);
|
|
}
|
|
return await r.json();
|
|
},
|
|
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) }),
|
|
});
|
|
}
|