import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { fetchRoles, guildRolesQueryKey, updateRole } from '@/api/guilds'; import { NICK_EFFECTS, fetchGuildCosmetics, guildCosmeticsQueryKey, type GuildCosmetic, type NickEffect, } from '@/api/cosmetics'; import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { SkeletonLines } from '@/components/ui/Skeleton'; import { NICK_COLOR_PALETTE } from '@/components/profile/cosmeticsStyle'; import { roleColorHex } from '@/lib/format'; import { SettingsSection } from '@/pages/settings/SettingsSection'; import type { Role } from '@/api/types'; interface GuildRolesSectionProps { guildId: string; /** Право MANAGE_ROLES: без него оформление роли показывается только для чтения. */ canManage?: boolean; } /** * «Роли»: список ролей и — при праве MANAGE_ROLES — оформление роли * (AGENT.md 7.4): рамка, иконка, цвет и эффект ника. Рамки и иконки берутся из * галереи сервера, поэтому она читается тем же разделом. */ export function GuildRolesSection({ guildId, canManage = false }: GuildRolesSectionProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const roles = useQuery({ queryKey: guildRolesQueryKey(guildId), queryFn: ({ signal }) => fetchRoles(guildId, signal), retry: 0, }); const cosmetics = useQuery({ queryKey: guildCosmeticsQueryKey(guildId), queryFn: ({ signal }) => fetchGuildCosmetics(guildId, signal), retry: 0, }); const patch = useMutation({ mutationFn: (input: { roleId: string; body: RoleDecorationInput }) => updateRole(guildId, input.roleId, input.body), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: guildRolesQueryKey(guildId) }); // Оформление участников в ленте пересчитывается из списка участников. void queryClient.invalidateQueries({ queryKey: ['guilds', guildId, 'members'] }); }, }); const sorted = (roles.data ?? []).slice().sort((left, right) => right.position - left.position); const frames = (cosmetics.data ?? []).filter((item) => item.kind === 'frame'); const badges = (cosmetics.data ?? []).filter((item) => item.kind === 'badge'); return ( {roles.isPending ? ( ) : roles.isError ? ( void roles.refetch()} /> ) : sorted.length === 0 ? (

{t('settings.servers.roles.empty')}

) : (
    {sorted.map((role) => { const color = roleColorHex(role.color); return (
  • {canManage ? ( patch.mutate({ roleId: role.id, body })} /> ) : null}
  • ); })}
)} {patch.isError ? : null} {patch.isSuccess ? (

{t('settings.servers.roles.decorationSaved')}

) : null}
); } /** Тело запроса правки роли: только оформление. */ export interface RoleDecorationInput { cosmetic_frame_id?: string; cosmetic_badge_id?: string; nick_color?: number; nick_effect?: NickEffect; } interface RoleDecorationProps { role: Role; frames: GuildCosmetic[]; badges: GuildCosmetic[]; disabled: boolean; onChange: (body: RoleDecorationInput) => void; } /** Оформление роли: рамка, иконка, цвет и эффект ника. */ function RoleDecoration({ role, frames, badges, disabled, onChange }: RoleDecorationProps) { const { t } = useTranslation(); return (
({ id: item.id, label: item.name }))} emptyLabel={t('settings.profile.style.none')} onChange={(value) => onChange({ cosmetic_frame_id: value })} /> ({ id: item.id, label: item.name }))} emptyLabel={t('settings.profile.style.none')} onChange={(value) => onChange({ cosmetic_badge_id: value })} /> ({ id: effect, label: t(`settings.profile.style.effects.${effect}`), }))} onChange={(value) => onChange({ nick_effect: value as NickEffect })} />
); } interface DecorationSelectProps { label: string; testId: string; value: string; disabled: boolean; options: { id: string; label: string }[]; emptyLabel?: string; onChange: (value: string) => void; } function DecorationSelect({ label, testId, value, disabled, options, emptyLabel, onChange, }: DecorationSelectProps) { return ( ); }