'use client'; import type { CommandGlobalRules, CommandOverrideDashboard, DiscordChannelOption, DiscordRoleOption } from '@nexumi/shared'; import { RotateCcw } from 'lucide-react'; import { useSearchParams } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { useTranslations } from '@/components/locale-provider'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { DiscordResourceMultiSelect } from '@/components/ui/discord-resource-multi-select'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; interface LocalOverride extends CommandOverrideDashboard { saving?: boolean; } interface CommandsManagerProps { guildId: string; initialCommands: CommandOverrideDashboard[]; initialGlobalRules: CommandGlobalRules; channels: DiscordChannelOption[]; roles: DiscordRoleOption[]; } export function CommandsManager({ guildId, initialCommands, initialGlobalRules, channels, roles }: CommandsManagerProps) { const t = useTranslations(); const searchParams = useSearchParams(); const [commands, setCommands] = useState(initialCommands); const [search, setSearch] = useState(''); const [globalRules, setGlobalRules] = useState(initialGlobalRules); const [globalSaving, setGlobalSaving] = useState(false); const [globalDirty, setGlobalDirty] = useState(false); const channelOptions = useMemo( () => channels.map((channel) => ({ id: channel.id, name: channel.name, prefix: '#' })), [channels] ); const roleOptions = useMemo( () => roles.map((role) => ({ id: role.id, name: role.name, prefix: '@' })), [roles] ); useEffect(() => { const q = searchParams.get('q'); if (q) { setSearch(q); } }, [searchParams]); const filtered = useMemo(() => { const query = search.trim().toLowerCase(); if (!query) { return commands; } return commands.filter((entry) => entry.commandName.toLowerCase().includes(query)); }, [commands, search]); function patchLocal(commandName: string, patch: Partial) { setCommands((prev) => prev.map((entry) => (entry.commandName === commandName ? { ...entry, ...patch } : entry)) ); } function patchGlobal(patch: Partial) { setGlobalRules((prev) => ({ ...prev, ...patch })); setGlobalDirty(true); } async function handleSaveGlobal() { setGlobalSaving(true); try { const response = await fetch(`/api/guilds/${guildId}/commands/globals`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(globalRules) }); const body = (await response.json().catch(() => null)) as (CommandGlobalRules & { error?: string }) | null; if (!response.ok || !body) { toast.error(body?.error ?? t('common.saveError')); return; } setGlobalRules({ allowedChannelIds: body.allowedChannelIds, deniedChannelIds: body.deniedChannelIds }); setGlobalDirty(false); toast.success(t('common.saveSuccess')); } catch { toast.error(t('common.saveError')); } finally { setGlobalSaving(false); } } async function handleSave(entry: LocalOverride) { patchLocal(entry.commandName, { saving: true }); try { const response = await fetch(`/api/guilds/${guildId}/commands/${entry.commandName}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: entry.enabled, allowedRoleIds: entry.allowedRoleIds, deniedRoleIds: entry.deniedRoleIds, allowedChannelIds: entry.allowedChannelIds, deniedChannelIds: entry.deniedChannelIds, cooldownSeconds: entry.cooldownSeconds }) }); const body = (await response.json().catch(() => null)) as | (CommandOverrideDashboard & { error?: string }) | null; if (!response.ok || !body) { toast.error(body?.error ?? t('common.saveError')); return; } patchLocal(entry.commandName, body); toast.success(t('common.saveSuccess')); } catch { toast.error(t('common.saveError')); } finally { patchLocal(entry.commandName, { saving: false }); } } async function handleReset(entry: LocalOverride) { patchLocal(entry.commandName, { saving: true }); try { const response = await fetch(`/api/guilds/${guildId}/commands/${entry.commandName}`, { method: 'DELETE' }); const body = (await response.json().catch(() => null)) as CommandOverrideDashboard | null; if (!response.ok || !body) { toast.error(t('common.saveError')); return; } patchLocal(entry.commandName, body); toast.success(t('common.saveSuccess')); } catch { toast.error(t('common.saveError')); } finally { patchLocal(entry.commandName, { saving: false }); } } return (
{t('modulePages.commands.globalTitle')} {t('modulePages.commands.globalDescription')}

{t('modulePages.commands.globalAllowedChannels')}

{t('modulePages.commands.globalAllowedHint')}

patchGlobal({ allowedChannelIds })} placeholder={t('modulePages.commands.channelSearchPlaceholder')} disabled={globalSaving} />

{t('modulePages.commands.globalDeniedChannels')}

{t('modulePages.commands.globalDeniedHint')}

patchGlobal({ deniedChannelIds })} placeholder={t('modulePages.commands.channelSearchPlaceholder')} disabled={globalSaving} />
{t('modulePages.commands.title')} {t('modulePages.commands.description')} setSearch(event.target.value)} placeholder={t('modulePages.commands.searchPlaceholder')} className="max-w-sm" />
{filtered.length === 0 && (

{t('modulePages.commands.empty')}

)} {filtered.map((entry) => (

/{entry.commandName}

patchLocal(entry.commandName, { enabled })} /> {entry.enabled ? t('common.enabled') : t('common.disabled')}

{t('modulePages.commands.cooldownSeconds')}

patchLocal(entry.commandName, { cooldownSeconds: event.target.value === '' ? null : Number(event.target.value) }) } placeholder={t('common.optional')} />

{t('modulePages.commands.allowedRoleIds')}

patchLocal(entry.commandName, { allowedRoleIds })} placeholder={t('modulePages.commands.roleSearchPlaceholder')} disabled={entry.saving} />

{t('modulePages.commands.deniedRoleIds')}

patchLocal(entry.commandName, { deniedRoleIds })} placeholder={t('modulePages.commands.roleSearchPlaceholder')} disabled={entry.saving} />

{t('modulePages.commands.allowedChannelIds')}

patchLocal(entry.commandName, { allowedChannelIds }) } placeholder={t('modulePages.commands.channelSearchPlaceholder')} disabled={entry.saving} />

{t('modulePages.commands.deniedChannelIds')}

patchLocal(entry.commandName, { deniedChannelIds }) } placeholder={t('modulePages.commands.channelSearchPlaceholder')} disabled={entry.saving} />
))}
); }