feat(web): профиль с аватаром, панель в углу, настройки сервера, скрытые ID
Правки пользователя: - в «Профиль → Основной» сверху квадратный аватар слева, справа от него настройки имени, «О себе», состояния и эмодзи во всю высоту; смена аватара — клик по нему, форматы (JPEG, PNG, WebP, GIF до 8 МБ) — в подсказке; - убраны дубли: часовой пояс только в «Язык и регион», статус — только в мини-профиле панели; - панель пользователя переехала в нижний левый угол под сайдбар, подсказка о выходе из сервера — на её прежнее место; панель ужата под 240 px; - шестерёнка сервера открывает новый пункт настроек «Сервер» (список серверов, общее, участники, роли, опасная зона), а «Профиль → Сервер» оставлен только для ника и «Применить ко всем серверам»; - окно настроек получило статический размер 70% рабочей области со скроллом; - ID пользователей скрыты везде, кроме раздела «Инстанс» у администратора; - список друзей свёрнут под кнопкой «Друзья (N)» и раскрывается снизу вверх; - 249 тестов Vitest (было 239), check/lint/test/build зелёные.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { fetchGuild, fetchMembers, guildMembersQueryKey, guildQueryKey } from '@/api/guilds';
|
||||
import { fetchMyGuilds, myGuildsQueryKey } from '@/api/users';
|
||||
import { ErrorNotice } from '@/components/ui/ErrorNotice';
|
||||
import { Card } from '@/components/ui/primitives';
|
||||
import { SkeletonLines } from '@/components/ui/Skeleton';
|
||||
import { canAdministerGuild } from '@/lib/identity';
|
||||
import { useCurrentUser } from '@/lib/hooks';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
import { GuildDangerSection } from '@/pages/settings/GuildDangerSection';
|
||||
import { GuildGeneralSection } from '@/pages/settings/GuildGeneralSection';
|
||||
import { GuildMembersSection } from '@/pages/settings/GuildMembersSection';
|
||||
import { GuildPicker } from '@/pages/settings/GuildPicker';
|
||||
import { GuildRolesSection } from '@/pages/settings/GuildRolesSection';
|
||||
|
||||
/**
|
||||
* Пункт «Сервер» окна настроек: администрирование одного сервера —
|
||||
* общее (имя и описание), участники, роли и «Опасная зона».
|
||||
*
|
||||
* В отличие от вкладки «Профиль → Сервер» (там только свой никнейм), сюда
|
||||
* попадают через шестерёнку в шапке сервера или через пункт меню настроек.
|
||||
* Сервер можно переключить тем же выпадающим списком.
|
||||
*/
|
||||
export function GuildSettingsPage() {
|
||||
const sessionGuilds = useSessionStore((state) => state.guilds);
|
||||
const upsertGuild = useSessionStore((state) => state.upsertGuild);
|
||||
const selectSettingsGuild = useSessionStore((state) => state.selectSettingsGuild);
|
||||
|
||||
// Подстраховка снапшота: без неё выбранный сервер не найти в сторе.
|
||||
const myGuilds = useQuery({
|
||||
queryKey: myGuildsQueryKey,
|
||||
queryFn: ({ signal }) => fetchMyGuilds(signal),
|
||||
retry: 0,
|
||||
enabled: sessionGuilds.length === 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionGuilds.length > 0 || myGuilds.data === undefined) {
|
||||
return;
|
||||
}
|
||||
for (const guild of myGuilds.data) {
|
||||
upsertGuild(guild);
|
||||
}
|
||||
}, [sessionGuilds.length, myGuilds.data, upsertGuild]);
|
||||
|
||||
const guilds = useMemo(
|
||||
() => (sessionGuilds.length > 0 ? sessionGuilds : (myGuilds.data ?? [])),
|
||||
[sessionGuilds, myGuilds.data],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<GuildPicker
|
||||
guilds={guilds}
|
||||
isPending={myGuilds.isPending && guilds.length === 0}
|
||||
onSelect={selectSettingsGuild}
|
||||
/>
|
||||
|
||||
<GuildSettingsBody guilds={guilds} />
|
||||
{myGuilds.isError ? (
|
||||
<Card>
|
||||
<ErrorNotice error={myGuilds.error} onRetry={() => void myGuilds.refetch()} />
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Секции выбранного сервера: данные тянутся по REST только для него. */
|
||||
function GuildSettingsBody({ guilds }: { guilds: { id: string }[] }) {
|
||||
const { t } = useTranslation();
|
||||
const currentUser = useCurrentUser();
|
||||
const selectedGuildId = useSessionStore((state) => state.settingsGuildId);
|
||||
const selectSettingsGuild = useSessionStore((state) => state.selectSettingsGuild);
|
||||
|
||||
const active = useMemo(
|
||||
() => guilds.find((guild) => guild.id === selectedGuildId) ?? null,
|
||||
[guilds, selectedGuildId],
|
||||
);
|
||||
|
||||
// Первый доступный сервер выбираем сами: список только что загрузился.
|
||||
useEffect(() => {
|
||||
if (active !== null || guilds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const first = guilds[0];
|
||||
if (first !== undefined) {
|
||||
selectSettingsGuild(first.id);
|
||||
}
|
||||
}, [active, guilds, selectSettingsGuild]);
|
||||
|
||||
const activeId = active?.id ?? null;
|
||||
|
||||
const detail = useQuery({
|
||||
queryKey: guildQueryKey(activeId ?? ''),
|
||||
queryFn: ({ signal }) => fetchGuild(activeId ?? '', signal),
|
||||
enabled: activeId !== null,
|
||||
retry: 0,
|
||||
});
|
||||
|
||||
const members = useQuery({
|
||||
queryKey: guildMembersQueryKey(activeId ?? ''),
|
||||
queryFn: ({ signal }) => fetchMembers(activeId ?? '', signal),
|
||||
enabled: activeId !== null,
|
||||
retry: 0,
|
||||
});
|
||||
|
||||
if (activeId === null) {
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-sm text-fg-muted">{t('settings.servers.notFound')}</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (detail.isPending) {
|
||||
return (
|
||||
<Card>
|
||||
<SkeletonLines rows={5} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const guild = detail.data;
|
||||
if (guild === undefined) {
|
||||
return (
|
||||
<Card>
|
||||
<ErrorNotice error={detail.error} onRetry={() => void detail.refetch()} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const canManage = canAdministerGuild(
|
||||
guild.my_permissions,
|
||||
guild.owner_id,
|
||||
currentUser.data?.id,
|
||||
currentUser.data?.is_instance_admin === true,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GuildGeneralSection key={guild.id} guild={guild} canEdit={canManage} />
|
||||
<GuildMembersSection
|
||||
members={members.data}
|
||||
isPending={members.isPending}
|
||||
error={members.error}
|
||||
onRetry={() => void members.refetch()}
|
||||
/>
|
||||
<GuildRolesSection key={`${guild.id}-roles`} guildId={guild.id} />
|
||||
<GuildDangerSection
|
||||
key={`${guild.id}-danger`}
|
||||
guildId={guild.id}
|
||||
guildName={guild.name}
|
||||
isOwner={currentUser.data?.id === guild.owner_id}
|
||||
isInstanceAdmin={currentUser.data?.is_instance_admin === true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GuildSettingsPage;
|
||||
Reference in New Issue
Block a user