Кастомизация профиля (о себе, аватар) и просмотр профилей других игроков + общая статистика
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+100
-1
@@ -1,6 +1,6 @@
|
||||
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 { AuthConfig, Me } from "../domain/types";
|
||||
|
||||
@@ -104,6 +104,105 @@ export function useUpdateNickname() {
|
||||
});
|
||||
}
|
||||
|
||||
// «О себе».
|
||||
export function useUpdateBio() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (bio: string): Promise<Me> =>
|
||||
unwrap(await api.PATCH("/api/users/me/profile", { body: { bio } })),
|
||||
onSuccess: (me) => {
|
||||
qc.setQueryData(qk.me, me);
|
||||
qc.invalidateQueries({ queryKey: qk.home });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Аватар. Картинку уменьшаем на клиенте (≤512px) и грузим multipart'ом отдельным
|
||||
// fetch (openapi-fetch неудобен для файлов); CSRF-токен из cookie ставим вручную.
|
||||
function readCsrfToken(): string | null {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]*)/);
|
||||
return m ? decodeURIComponent(m[1]) : null;
|
||||
}
|
||||
|
||||
async function resizeImage(file: File, max = 512): Promise<Blob> {
|
||||
try {
|
||||
const dataUrl = await new Promise<string>((res, rej) => {
|
||||
const fr = new FileReader();
|
||||
fr.onload = () => res(fr.result as string);
|
||||
fr.onerror = () => rej(fr.error);
|
||||
fr.readAsDataURL(file);
|
||||
});
|
||||
const img = await new Promise<HTMLImageElement>((res, rej) => {
|
||||
const i = new Image();
|
||||
i.onload = () => res(i);
|
||||
i.onerror = () => rej(new Error("image load failed"));
|
||||
i.src = dataUrl;
|
||||
});
|
||||
let { width, height } = img;
|
||||
if (width > max || height > max) {
|
||||
const scale = Math.min(max / width, max / height);
|
||||
width = Math.round(width * scale);
|
||||
height = Math.round(height * scale);
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return file;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
const blob = await new Promise<Blob | null>((res) =>
|
||||
canvas.toBlob(res, "image/jpeg", 0.85),
|
||||
);
|
||||
return blob ?? file;
|
||||
} catch {
|
||||
return file; // не вышло уменьшить — отправим как есть (сервер проверит тип/размер)
|
||||
}
|
||||
}
|
||||
|
||||
export function useUploadAvatar() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (file: File): Promise<Me> => {
|
||||
const blob = await resizeImage(file);
|
||||
const form = new FormData();
|
||||
form.append("file", blob, "avatar.jpg");
|
||||
const csrf = readCsrfToken();
|
||||
const base = import.meta.env.VITE_API_BASE_URL || "";
|
||||
const r = await fetch(`${base}/api/users/me/avatar`, {
|
||||
method: "PUT",
|
||||
body: form,
|
||||
credentials: "include",
|
||||
headers: csrf ? { "X-CSRF-Token": csrf } : {},
|
||||
});
|
||||
if (!r.ok) {
|
||||
let env: { code?: string; message?: string; details?: unknown } | undefined;
|
||||
try {
|
||||
env = ((await r.json()) as { error?: typeof env }).error;
|
||||
} catch {
|
||||
/* тело без JSON */
|
||||
}
|
||||
throw new ApiError(env?.message || "Не удалось загрузить", env?.code || "ERROR", r.status, env?.details);
|
||||
}
|
||||
return (await r.json()) as Me;
|
||||
},
|
||||
onSuccess: (me) => {
|
||||
qc.setQueryData(qk.me, me);
|
||||
qc.invalidateQueries();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteAvatar() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (): Promise<Me> => unwrap(await api.DELETE("/api/users/me/avatar")),
|
||||
onSuccess: (me) => {
|
||||
qc.setQueryData(qk.me, me);
|
||||
qc.invalidateQueries();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSetActiveGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
|
||||
Reference in New Issue
Block a user