diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts
index ff8fb94..c5830ac 100644
--- a/frontend/src/api/schema.d.ts
+++ b/frontend/src/api/schema.d.ts
@@ -1357,6 +1357,8 @@ export interface components {
code: string;
/** Name Ru */
name_ru: string;
+ /** Name Ru Prepositional */
+ name_ru_prepositional: string;
/** Expansion Code */
expansion_code: string;
/** Games */
@@ -1761,6 +1763,8 @@ export interface components {
bio?: string | null;
/** Avatar Url */
avatar_url?: string | null;
+ /** Favorite Faction Id */
+ favorite_faction_id?: number | null;
/**
* Groups
* @default []
@@ -1881,7 +1885,7 @@ export interface components {
factions: components["schemas"]["FactionStat"][];
best_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;
/**
* Recent Form
@@ -1898,6 +1902,8 @@ export interface components {
ProfileUpdate: {
/** Bio */
bio?: string | null;
+ /** Favorite Faction Id */
+ favorite_faction_id?: number | null;
};
/** PublicProfile */
PublicProfile: {
@@ -2004,6 +2010,8 @@ export interface components {
bio?: string | null;
/** Avatar Url */
avatar_url?: string | null;
+ /** Favorite Faction Id */
+ favorite_faction_id?: number | null;
};
/**
* UserSuggestion
diff --git a/frontend/src/components/ProfileStatsCard.tsx b/frontend/src/components/ProfileStatsCard.tsx
index adc1de7..496d9e2 100644
--- a/frontend/src/components/ProfileStatsCard.tsx
+++ b/frontend/src/components/ProfileStatsCard.tsx
@@ -38,16 +38,17 @@ export function ProfileStatsCard({
Очки (рейтинг)
{o.score ?? "—"}
- {stats.most_played_faction && (
+ {/* Любимая — личный выбор игрока в профиле; ниже — статистика по партиям. */}
+ {stats.favorite_faction && (
Любимая фракция
-
{stats.most_played_faction.name_ru}
+
{stats.favorite_faction.name_ru}
)}
{stats.main_faction && (
-
Основная фракция
-
{stats.main_faction.name_ru}
+
Чаще всего играет на
+
{stats.main_faction.name_ru_prepositional}
)}
diff --git a/frontend/src/hooks/auth.ts b/frontend/src/hooks/auth.ts
index aef11de..4c36ee8 100644
--- a/frontend/src/hooks/auth.ts
+++ b/frontend/src/hooks/auth.ts
@@ -117,6 +117,26 @@ export function useUpdateBio() {
});
}
+// Любимая фракция — личный выбор игрока (в статистике не вычисляется). null снимает выбор.
+// Шлём только это поле: роутер обновляет лишь пришедшие, «о себе» не затирается.
+export function useUpdateFavoriteFaction() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async (factionId: number | null): Promise =>
+ 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'ом отдельным
// fetch (openapi-fetch неудобен для файлов); CSRF-токен из cookie ставим вручную.
function readCsrfToken(): string | null {
diff --git a/frontend/src/hooks/reference.ts b/frontend/src/hooks/reference.ts
index 7f5133d..0b8abca 100644
--- a/frontend/src/hooks/reference.ts
+++ b/frontend/src/hooks/reference.ts
@@ -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) {
return useQuery({
queryKey: groupId ? qk.groupFactions(groupId) : ["group", "none", "factions"],
diff --git a/frontend/src/pages/AccountPage.tsx b/frontend/src/pages/AccountPage.tsx
index 14aac89..0914290 100644
--- a/frontend/src/pages/AccountPage.tsx
+++ b/frontend/src/pages/AccountPage.tsx
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import { ApiError } from "../api/client";
import { Avatar } from "../components/Avatar";
+import { PickerSelect } from "../components/PickerSelect";
import { ProfileStatsCard } from "../components/ProfileStatsCard";
import { Spinner } from "../components/Spinner";
import { useToast } from "../context/ToastContext";
@@ -11,9 +12,11 @@ import {
useLogout,
useMe,
useUpdateBio,
+ useUpdateFavoriteFaction,
useUpdateNickname,
useUploadAvatar,
} from "../hooks/auth";
+import { useExpansions, useFactions } from "../hooks/reference";
import { useMyStats } from "../hooks/stats";
export function AccountPage() {
@@ -21,6 +24,9 @@ export function AccountPage() {
const { data: stats } = useMyStats();
const updateNick = useUpdateNickname();
const updateBio = useUpdateBio();
+ const updateFavorite = useUpdateFavoriteFaction();
+ const { data: factions } = useFactions();
+ const { data: expansions } = useExpansions();
const uploadAvatar = useUploadAvatar();
const deleteAvatar = useDeleteAvatar();
const logout = useLogout();
@@ -35,6 +41,20 @@ export function AccountPage() {
// 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 nick = nickname.trim();
if (!nick) return;
@@ -140,6 +160,36 @@ export function AccountPage() {
+
+
+
+
(
+ <>
+ {f.name_ru}
+
+ {expansionName(f.expansion_id)}
+
+ >
+ )}
+ renderValue={(f) => {f.name_ru}}
+ onPick={(f) => saveFavorite(f.id)}
+ />
+ {favorite && (
+
+ )}
+
+
+