import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import { createGuild, fetchGuildTemplates, fetchMyGuildTemplates, guildTemplatesQueryKey, myGuildTemplatesQueryKey, type GuildTemplate, } from '@/api/guilds'; import { instanceQueryKey } from '@/api/instance'; import { myGuildsQueryKey } from '@/api/users'; import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { Field } from '@/components/ui/Field'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/primitives'; import { useInstance } from '@/lib/hooks'; import { useSessionStore } from '@/stores/session'; import { useUiStore } from '@/stores/ui'; /** Значение радио личного шаблона: так его понимает `POST /guilds` (7.3). */ function personalTemplateValue(templateId: string): string { return `personal:${templateId}`; } interface TemplateOptionProps { value: string; testId: string; template: GuildTemplate; selected: boolean; onSelect: (value: string) => void; } /** Карточка шаблона в списке выбора: имя, счётчики и описание. */ function TemplateOption({ value, testId, template, selected, onSelect }: TemplateOptionProps) { const { t } = useTranslation(); return ( ); } /** * Единственное модальное окно создания сервера (`POST /guilds {name}`). * Открывается из рейки и с пустого экрана «У вас пока нет серверов», * поэтому смонтировано в `AppLayout`, а состояние — в `ui`-сторе. */ export function CreateGuildModal() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const instance = useInstance(); const open = useUiStore((state) => state.guildCreateOpen); const close = useUiStore((state) => state.closeGuildCreate); const guilds = useSessionStore((state) => state.guilds); const upsertGuild = useSessionStore((state) => state.upsertGuild); const [name, setName] = useState(''); const [templateId, setTemplateId] = useState('empty'); // Встроенные шаблоны: список небольшой и статический, кэшируется на сессию. const templates = useQuery({ queryKey: guildTemplatesQueryKey, queryFn: ({ signal }) => fetchGuildTemplates(signal), enabled: open, staleTime: 300_000, retry: 0, }); // Свои шаблоны (Фаза 7): сервер отдаёт только шаблоны текущего пользователя. const myTemplates = useQuery({ queryKey: myGuildTemplatesQueryKey, queryFn: ({ signal }) => fetchMyGuildTemplates(signal), enabled: open, staleTime: 60_000, retry: 0, }); const create = useMutation({ mutationFn: (guildName: string) => createGuild(guildName, templateId), onSuccess: (guild) => { close(); setName(''); upsertGuild(guild); void queryClient.invalidateQueries({ queryKey: myGuildsQueryKey }); void queryClient.invalidateQueries({ queryKey: instanceQueryKey }); void navigate(`/app/${guild.id}`); }, }); const limitReached = instance.data !== undefined && guilds.length >= instance.data.max_guilds_per_user; const mine = myTemplates.data ?? []; return ( } >
{ event.preventDefault(); if (name.trim() !== '') { create.mutate(name.trim()); } }} > setName(event.target.value)} maxLength={64} hint={t('guilds.create.hint')} /> {/* Шаблон задаёт структуру комнат и ролей (AGENT.md 7.3). */}
{t('guilds.create.template')} {(templates.data ?? []).map((template) => ( ))}

{t('guilds.create.myTemplates')}

{mine.length === 0 ? (

{t('guilds.create.myTemplatesEmpty')}

) : ( mine.map((template) => ( )) )} {templates.isError ? ( void templates.refetch()} /> ) : null}
{instance.data === undefined ? null : (

{t('guilds.create.limit', { count: instance.data.max_guilds_per_user })}

)} {limitReached && !create.isError ? (

{t('guilds.create.limitReached')}

) : null} {create.isError ? : null}
); }