Редактор ачивок в админке + файловое хранилище ачивок (задел под титулы)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { api, unwrap } from "../api/client";
|
||||
import { ApiError, api, unwrap } from "../api/client";
|
||||
import { qk } from "../api/queryKeys";
|
||||
import type { AdminMe, MatchUpdate } from "../domain/types";
|
||||
|
||||
function readCsrfToken(): string | null {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]*)/);
|
||||
return m ? decodeURIComponent(m[1]) : null;
|
||||
}
|
||||
|
||||
export function useAdminMe() {
|
||||
return useQuery({
|
||||
queryKey: qk.adminMe,
|
||||
@@ -163,3 +168,84 @@ export function useAdminUpdateUser() {
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminUsers }),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Ачивки (редактор; выдача игрокам — на будущее) ──────────────────────────
|
||||
|
||||
interface AchievementInput {
|
||||
name?: string;
|
||||
description?: string;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export function useAdminAchievements() {
|
||||
return useQuery({
|
||||
queryKey: qk.adminAchievements,
|
||||
queryFn: async () => unwrap(await api.GET("/api/admin/achievements")),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateAchievement() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (body: AchievementInput & { name: string }) =>
|
||||
unwrap(await api.POST("/api/admin/achievements", { body })),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAchievement() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (args: { slug: string; body: AchievementInput }) =>
|
||||
unwrap(
|
||||
await api.PATCH("/api/admin/achievements/{slug}", {
|
||||
params: { path: { slug: args.slug } },
|
||||
body: args.body,
|
||||
}),
|
||||
),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteAchievement() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (slug: string) =>
|
||||
unwrap(
|
||||
await api.DELETE("/api/admin/achievements/{slug}", {
|
||||
params: { path: { slug } },
|
||||
}),
|
||||
),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
|
||||
});
|
||||
}
|
||||
|
||||
// Иконка ачивки — multipart отдельным fetch (как аватар), с CSRF из cookie.
|
||||
export function useUploadAchievementIcon() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ slug, file }: { slug: string; file: File }) => {
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name);
|
||||
const csrf = readCsrfToken();
|
||||
const base = import.meta.env.VITE_API_BASE_URL || "";
|
||||
const r = await fetch(`${base}/api/admin/achievements/${slug}/icon`, {
|
||||
method: "PUT",
|
||||
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.adminAchievements }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user