import { useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { fileContentUrl } from '@/api/files'; import { COSMETICS_MAX_PER_GUILD, COSMETIC_MAX_BYTES, COSMETIC_NAME_MAX, deleteGuildCosmetic, fetchGuildCosmetics, guildCosmeticsQueryKey, renameGuildCosmetic, uploadGuildCosmetic, type CosmeticKind, type GuildCosmetic, } from '@/api/cosmetics'; import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { Field } from '@/components/ui/Field'; import { Button } from '@/components/ui/primitives'; import { SkeletonLines } from '@/components/ui/Skeleton'; import { useUnsavedChanges } from '@/lib/unsavedChanges'; import { SettingsSection } from '@/pages/settings/SettingsSection'; interface GuildCosmeticsSectionProps { guildId: string; canManage: boolean; } /** * «Оформление» сервера (AGENT.md 7.4): галерея рамок и иконок. Файлы хранятся * обычными загрузками, поэтому анимированные GIF/WebP сохраняют анимацию; * элементы выдаются ролям в разделе «Роли». */ export function GuildCosmeticsSection({ guildId, canManage }: GuildCosmeticsSectionProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const frameInput = useRef(null); const badgeInput = useRef(null); const [renaming, setRenaming] = useState(null); const [name, setName] = useState(''); const [error, setError] = useState(null); const cosmetics = useQuery({ queryKey: guildCosmeticsQueryKey(guildId), queryFn: ({ signal }) => fetchGuildCosmetics(guildId, signal), enabled: canManage, retry: 0, }); const invalidate = (): void => { void queryClient.invalidateQueries({ queryKey: guildCosmeticsQueryKey(guildId) }); }; const upload = useMutation({ mutationFn: (input: { kind: CosmeticKind; name: string; file: File }) => uploadGuildCosmetic(guildId, input), onSuccess: () => { setError(null); invalidate(); }, onError: (cause: unknown) => setError(cause), }); const rename = useMutation({ mutationFn: (input: { id: string; name: string }) => renameGuildCosmetic(guildId, input.id, input.name), onSuccess: () => { setRenaming(null); invalidate(); }, onError: (cause: unknown) => setError(cause), }); const remove = useMutation({ mutationFn: (id: string) => deleteGuildCosmetic(guildId, id), onSuccess: invalidate, onError: (cause: unknown) => setError(cause), }); // Незаконченное переименование: черновик живёт в поле, пока форма открыта. useUnsavedChanges({ dirty: renaming !== null && name.trim() !== '' && name.trim() !== renaming.name, save: async () => { if (renaming === null || name.trim() === '') { return; } await rename.mutateAsync({ id: renaming.id, name: name.trim() }); }, }); if (!canManage) { return null; } /** Проверяем файл на клиенте: сервер всё равно проверит, но так быстрее. */ const pick = (kind: CosmeticKind, file: File | undefined): void => { if (file === undefined) { return; } if (!file.type.startsWith('image/')) { setError(new Error(t('settings.servers.cosmetics.notImage'))); return; } if (file.size > COSMETIC_MAX_BYTES) { setError(new Error(t('settings.servers.cosmetics.tooLarge'))); return; } upload.mutate({ kind, name: file.name.replace(/\.[^.]+$/u, ''), file }); }; const list = cosmetics.data ?? []; const frames = list.filter((item) => item.kind === 'frame'); const badges = list.filter((item) => item.kind === 'badge'); return (
{cosmetics.isPending ? : null} {cosmetics.isError ? ( void cosmetics.refetch()} /> ) : null} {error === null ? null : }

{t('settings.servers.cosmetics.limit', { used: list.length, total: COSMETICS_MAX_PER_GUILD, })}

{[ { kind: 'frame' as const, items: frames, input: frameInput }, { kind: 'badge' as const, items: badges, input: badgeInput }, ].map((group) => (
{t(`settings.servers.cosmetics.kind.${group.kind}`)} { pick(group.kind, event.target.files?.[0]); event.target.value = ''; }} />
{group.items.length === 0 ? (

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

) : (
    {group.items.map((item) => (
  • {item.name}
  • ))}
)}
))} {renaming === null ? null : (
{ event.preventDefault(); if (name.trim() !== '') { rename.mutate({ id: renaming.id, name: name.trim() }); } }} > setName(event.target.value)} /> )}
); }