Изменение логики любимых и основных фракций (#17) #20
Vendored
+9
-1
@@ -1357,6 +1357,8 @@ export interface components {
|
|||||||
code: string;
|
code: string;
|
||||||
/** Name Ru */
|
/** Name Ru */
|
||||||
name_ru: string;
|
name_ru: string;
|
||||||
|
/** Name Ru Prepositional */
|
||||||
|
name_ru_prepositional: string;
|
||||||
/** Expansion Code */
|
/** Expansion Code */
|
||||||
expansion_code: string;
|
expansion_code: string;
|
||||||
/** Games */
|
/** Games */
|
||||||
@@ -1761,6 +1763,8 @@ export interface components {
|
|||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
/** Avatar Url */
|
/** Avatar Url */
|
||||||
avatar_url?: string | null;
|
avatar_url?: string | null;
|
||||||
|
/** Favorite Faction Id */
|
||||||
|
favorite_faction_id?: number | null;
|
||||||
/**
|
/**
|
||||||
* Groups
|
* Groups
|
||||||
* @default []
|
* @default []
|
||||||
@@ -1881,7 +1885,7 @@ export interface components {
|
|||||||
factions: components["schemas"]["FactionStat"][];
|
factions: components["schemas"]["FactionStat"][];
|
||||||
best_faction?: components["schemas"]["FactionStat"] | null;
|
best_faction?: components["schemas"]["FactionStat"] | null;
|
||||||
worst_faction?: components["schemas"]["FactionStat"] | null;
|
worst_faction?: components["schemas"]["FactionStat"] | null;
|
||||||
most_played_faction?: components["schemas"]["FactionStat"] | null;
|
favorite_faction?: components["schemas"]["FactionRead"] | null;
|
||||||
main_faction?: components["schemas"]["FactionStat"] | null;
|
main_faction?: components["schemas"]["FactionStat"] | null;
|
||||||
/**
|
/**
|
||||||
* Recent Form
|
* Recent Form
|
||||||
@@ -1898,6 +1902,8 @@ export interface components {
|
|||||||
ProfileUpdate: {
|
ProfileUpdate: {
|
||||||
/** Bio */
|
/** Bio */
|
||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
|
/** Favorite Faction Id */
|
||||||
|
favorite_faction_id?: number | null;
|
||||||
};
|
};
|
||||||
/** PublicProfile */
|
/** PublicProfile */
|
||||||
PublicProfile: {
|
PublicProfile: {
|
||||||
@@ -2004,6 +2010,8 @@ export interface components {
|
|||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
/** Avatar Url */
|
/** Avatar Url */
|
||||||
avatar_url?: string | null;
|
avatar_url?: string | null;
|
||||||
|
/** Favorite Faction Id */
|
||||||
|
favorite_faction_id?: number | null;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* UserSuggestion
|
* UserSuggestion
|
||||||
|
|||||||
@@ -38,16 +38,17 @@ export function ProfileStatsCard({
|
|||||||
<div>Очки (рейтинг)</div>
|
<div>Очки (рейтинг)</div>
|
||||||
<b className={"lb-score" + (provisional ? " provisional" : "")}>{o.score ?? "—"}</b>
|
<b className={"lb-score" + (provisional ? " provisional" : "")}>{o.score ?? "—"}</b>
|
||||||
</div>
|
</div>
|
||||||
{stats.most_played_faction && (
|
{/* Любимая — личный выбор игрока в профиле; ниже — статистика по партиям. */}
|
||||||
|
{stats.favorite_faction && (
|
||||||
<div className="row-between">
|
<div className="row-between">
|
||||||
<div>Любимая фракция</div>
|
<div>Любимая фракция</div>
|
||||||
<b>{stats.most_played_faction.name_ru}</b>
|
<b>{stats.favorite_faction.name_ru}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{stats.main_faction && (
|
{stats.main_faction && (
|
||||||
<div className="row-between">
|
<div className="row-between">
|
||||||
<div>Основная фракция</div>
|
<div>Чаще всего играет на</div>
|
||||||
<b>{stats.main_faction.name_ru}</b>
|
<b>{stats.main_faction.name_ru_prepositional}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -117,6 +117,26 @@ export function useUpdateBio() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Любимая фракция — личный выбор игрока (в статистике не вычисляется). null снимает выбор.
|
||||||
|
// Шлём только это поле: роутер обновляет лишь пришедшие, «о себе» не затирается.
|
||||||
|
export function useUpdateFavoriteFaction() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (factionId: number | null): Promise<Me> =>
|
||||||
|
unwrap(
|
||||||
|
await api.PATCH("/api/users/me/profile", {
|
||||||
|
body: { favorite_faction_id: factionId },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
onSuccess: (me) => {
|
||||||
|
qc.setQueryData(qk.me, me);
|
||||||
|
// Карточка статистики показывает любимую фракцию — её тоже перезапрашиваем.
|
||||||
|
qc.invalidateQueries({ queryKey: qk.myStats });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.home });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Аватар. Картинку уменьшаем на клиенте (≤512px) и грузим multipart'ом отдельным
|
// Аватар. Картинку уменьшаем на клиенте (≤512px) и грузим multipart'ом отдельным
|
||||||
// fetch (openapi-fetch неудобен для файлов); CSRF-токен из cookie ставим вручную.
|
// fetch (openapi-fetch неудобен для файлов); CSRF-токен из cookie ставим вручную.
|
||||||
function readCsrfToken(): string | null {
|
function readCsrfToken(): string | null {
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ export function useExpansions() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Весь справочник фракций (для выбора любимой в профиле — он не зависит от группы).
|
||||||
|
export function useFactions() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.factions,
|
||||||
|
queryFn: async () => unwrap(await api.GET("/api/factions")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useGroupFactions(groupId: number | null) {
|
export function useGroupFactions(groupId: number | null) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: groupId ? qk.groupFactions(groupId) : ["group", "none", "factions"],
|
queryKey: groupId ? qk.groupFactions(groupId) : ["group", "none", "factions"],
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { ApiError } from "../api/client";
|
import { ApiError } from "../api/client";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
|
import { PickerSelect } from "../components/PickerSelect";
|
||||||
import { ProfileStatsCard } from "../components/ProfileStatsCard";
|
import { ProfileStatsCard } from "../components/ProfileStatsCard";
|
||||||
import { Spinner } from "../components/Spinner";
|
import { Spinner } from "../components/Spinner";
|
||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../context/ToastContext";
|
||||||
@@ -11,9 +12,11 @@ import {
|
|||||||
useLogout,
|
useLogout,
|
||||||
useMe,
|
useMe,
|
||||||
useUpdateBio,
|
useUpdateBio,
|
||||||
|
useUpdateFavoriteFaction,
|
||||||
useUpdateNickname,
|
useUpdateNickname,
|
||||||
useUploadAvatar,
|
useUploadAvatar,
|
||||||
} from "../hooks/auth";
|
} from "../hooks/auth";
|
||||||
|
import { useExpansions, useFactions } from "../hooks/reference";
|
||||||
import { useMyStats } from "../hooks/stats";
|
import { useMyStats } from "../hooks/stats";
|
||||||
|
|
||||||
export function AccountPage() {
|
export function AccountPage() {
|
||||||
@@ -21,6 +24,9 @@ export function AccountPage() {
|
|||||||
const { data: stats } = useMyStats();
|
const { data: stats } = useMyStats();
|
||||||
const updateNick = useUpdateNickname();
|
const updateNick = useUpdateNickname();
|
||||||
const updateBio = useUpdateBio();
|
const updateBio = useUpdateBio();
|
||||||
|
const updateFavorite = useUpdateFavoriteFaction();
|
||||||
|
const { data: factions } = useFactions();
|
||||||
|
const { data: expansions } = useExpansions();
|
||||||
const uploadAvatar = useUploadAvatar();
|
const uploadAvatar = useUploadAvatar();
|
||||||
const deleteAvatar = useDeleteAvatar();
|
const deleteAvatar = useDeleteAvatar();
|
||||||
const logout = useLogout();
|
const logout = useLogout();
|
||||||
@@ -35,6 +41,20 @@ export function AccountPage() {
|
|||||||
// bio редактируется локально; до первого ввода берём значение из профиля.
|
// bio редактируется локально; до первого ввода берём значение из профиля.
|
||||||
const bioValue = bio ?? me.bio ?? "";
|
const bioValue = bio ?? me.bio ?? "";
|
||||||
|
|
||||||
|
// Любимая фракция выбирается из ПОЛНОГО справочника: это свойство игрока, а не
|
||||||
|
// группы, и не должно зависеть от подключённых у неё дополнений.
|
||||||
|
const favorite = factions?.find((f) => f.id === me.favorite_faction_id) ?? null;
|
||||||
|
const expansionName = (id: number) => expansions?.find((e) => e.id === id)?.name_ru ?? "";
|
||||||
|
|
||||||
|
const saveFavorite = async (factionId: number | null) => {
|
||||||
|
try {
|
||||||
|
await updateFavorite.mutateAsync(factionId);
|
||||||
|
toast.show(factionId == null ? "Любимая фракция убрана" : "Любимая фракция сохранена");
|
||||||
|
} catch (e) {
|
||||||
|
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const saveNick = async () => {
|
const saveNick = async () => {
|
||||||
const nick = nickname.trim();
|
const nick = nickname.trim();
|
||||||
if (!nick) return;
|
if (!nick) return;
|
||||||
@@ -140,6 +160,36 @@ export function AccountPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="field" style={{ marginTop: 10 }}>
|
||||||
|
<label className="label">Любимая фракция</label>
|
||||||
|
<div className="row">
|
||||||
|
<PickerSelect
|
||||||
|
selected={favorite}
|
||||||
|
options={factions ?? []}
|
||||||
|
placeholder="Не выбрана"
|
||||||
|
renderOption={(f) => (
|
||||||
|
<>
|
||||||
|
<span className="nick">{f.name_ru}</span>
|
||||||
|
<span className="muted small" style={{ marginLeft: "auto" }}>
|
||||||
|
{expansionName(f.expansion_id)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
renderValue={(f) => <span className="nick">{f.name_ru}</span>}
|
||||||
|
onPick={(f) => saveFavorite(f.id)}
|
||||||
|
/>
|
||||||
|
{favorite && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost small"
|
||||||
|
onClick={() => saveFavorite(null)}
|
||||||
|
disabled={updateFavorite.isPending}
|
||||||
|
>
|
||||||
|
Убрать
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="field" style={{ marginTop: 10 }}>
|
<div className="field" style={{ marginTop: 10 }}>
|
||||||
<label className="label">Сменить никнейм</label>
|
<label className="label">Сменить никнейм</label>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
|||||||
Reference in New Issue
Block a user