diff --git a/apps/webui/src/app/api/guilds/[guildId]/emojis/route.ts b/apps/webui/src/app/api/guilds/[guildId]/emojis/route.ts new file mode 100644 index 0000000..5da4ce3 --- /dev/null +++ b/apps/webui/src/app/api/guilds/[guildId]/emojis/route.ts @@ -0,0 +1,18 @@ +import { type NextRequest, NextResponse } from 'next/server'; +import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth'; +import { listGuildEmojisForDashboard } from '@/lib/discord-guild-resources'; + +interface RouteParams { + params: Promise<{ guildId: string }>; +} + +export async function GET(_request: NextRequest, { params }: RouteParams) { + const { guildId } = await params; + try { + await requireGuildAccess(guildId); + const emojis = await listGuildEmojisForDashboard(guildId); + return NextResponse.json(emojis); + } catch (error) { + return toApiErrorResponse(error); + } +} diff --git a/apps/webui/src/components/modules/selfroles-manager.tsx b/apps/webui/src/components/modules/selfroles-manager.tsx index a01da92..f50402e 100644 --- a/apps/webui/src/components/modules/selfroles-manager.tsx +++ b/apps/webui/src/components/modules/selfroles-manager.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { SelfRoleBehavior, SelfRoleEntry, SelfRoleMode, SelfRolePanelDashboard } from '@nexumi/shared'; +import type { SelfRoleBehavior, SelfRoleMode, SelfRolePanelDashboard } from '@nexumi/shared'; import { Plus, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; @@ -12,35 +12,20 @@ import { DiscordChannelSelect } from '@/components/ui/discord-channel-select'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Textarea } from '@/components/ui/textarea'; +import { + builderRowsToRoles, + emptyBuilderRow, + rolesToBuilderRows, + type SelfRoleBuilderRow, + SelfRoleEntriesBuilder +} from '@/components/ui/self-role-entries-builder'; const MODES: SelfRoleMode[] = ['BUTTONS', 'DROPDOWN', 'REACTIONS']; const BEHAVIORS: SelfRoleBehavior[] = ['NORMAL', 'UNIQUE', 'VERIFY', 'TEMPORARY']; -function rolesToText(roles: SelfRoleEntry[]): string { - return roles.map((role) => [role.roleId, role.label, role.emoji ?? '', role.description ?? ''].join(' | ')).join('\n'); -} - -function textToRoles(text: string): SelfRoleEntry[] { - return text - .split('\n') - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .map((line) => { - const [roleId, label, emoji, description] = line.split('|').map((part) => part.trim()); - return { - roleId: roleId ?? '', - label: label || roleId || '', - emoji: emoji || undefined, - description: description || undefined - }; - }) - .filter((role) => role.roleId.length > 0); -} - -interface LocalPanel extends SelfRolePanelDashboard { +interface LocalPanel extends Omit { clientKey: string; - rolesText: string; + roles: SelfRoleBuilderRow[]; saving?: boolean; } @@ -50,7 +35,7 @@ const EMPTY_DRAFT = { description: '', mode: 'BUTTONS' as SelfRoleMode, behavior: 'NORMAL' as SelfRoleBehavior, - rolesText: '' + roles: [emptyBuilderRow()] as SelfRoleBuilderRow[] }; interface SelfRolesManagerProps { @@ -58,24 +43,43 @@ interface SelfRolesManagerProps { initialPanels: SelfRolePanelDashboard[]; } +function toastBuilderError( + t: ReturnType, + error: 'incomplete' | 'emojiRequired' | 'tooMany' +) { + if (error === 'emojiRequired') { + toast.error(t('modulePages.selfroles.emojiRequired')); + return; + } + if (error === 'tooMany') { + toast.error(t('modulePages.selfroles.tooManyRoles')); + return; + } + toast.error(t('modulePages.selfroles.formIncomplete')); +} + export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerProps) { const t = useTranslations(); const [panels, setPanels] = useState( initialPanels.map((panel, index) => ({ ...panel, clientKey: panel.id ?? `existing-${index}`, - rolesText: rolesToText(panel.roles) + roles: rolesToBuilderRows(panel.roles) })) ); const [draft, setDraft] = useState(EMPTY_DRAFT); const [creating, setCreating] = useState(false); async function handleCreate() { - const roles = textToRoles(draft.rolesText); - if (!draft.channelId.trim() || !draft.title.trim() || roles.length === 0) { + if (!draft.channelId.trim() || !draft.title.trim()) { toast.error(t('modulePages.selfroles.formIncomplete')); return; } + const parsed = builderRowsToRoles(draft.roles, draft.mode === 'REACTIONS'); + if (parsed.error || !parsed.value.length) { + toastBuilderError(t, parsed.error ?? 'incomplete'); + return; + } setCreating(true); try { const response = await fetch(`/api/guilds/${guildId}/selfroles`, { @@ -87,7 +91,7 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro description: draft.description.trim() || null, mode: draft.mode, behavior: draft.behavior, - roles + roles: parsed.value }) }); const body = (await response.json().catch(() => null)) as (SelfRolePanelDashboard & { error?: string }) | null; @@ -96,10 +100,14 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro return; } setPanels((prev) => [ - { ...body, clientKey: body.id ?? `new-${Date.now()}`, rolesText: rolesToText(body.roles) }, + { + ...body, + clientKey: body.id ?? `new-${Date.now()}`, + roles: rolesToBuilderRows(body.roles) + }, ...prev ]); - setDraft(EMPTY_DRAFT); + setDraft({ ...EMPTY_DRAFT, roles: [emptyBuilderRow()] }); toast.success(t('common.saveSuccess')); } catch { toast.error(t('common.saveError')); @@ -109,6 +117,14 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro } async function handleSave(panel: LocalPanel) { + if (!panel.id) { + return; + } + const parsed = builderRowsToRoles(panel.roles, panel.mode === 'REACTIONS'); + if (parsed.error || !parsed.value.length) { + toastBuilderError(t, parsed.error ?? 'incomplete'); + return; + } setPanels((prev) => prev.map((entry) => (entry.clientKey === panel.clientKey ? { ...entry, saving: true } : entry))); try { const response = await fetch(`/api/guilds/${guildId}/selfroles/${panel.id}`, { @@ -120,7 +136,7 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro description: panel.description, mode: panel.mode, behavior: panel.behavior, - roles: textToRoles(panel.rolesText) + roles: parsed.value }) }); const body = (await response.json().catch(() => null)) as (SelfRolePanelDashboard & { error?: string }) | null; @@ -128,11 +144,25 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro toast.error(body?.error ?? t('common.saveError')); return; } + setPanels((prev) => + prev.map((entry) => + entry.clientKey === panel.clientKey + ? { + ...entry, + ...body, + roles: rolesToBuilderRows(body.roles), + saving: false + } + : entry + ) + ); toast.success(t('common.saveSuccess')); } catch { toast.error(t('common.saveError')); } finally { - setPanels((prev) => prev.map((entry) => (entry.clientKey === panel.clientKey ? { ...entry, saving: false } : entry))); + setPanels((prev) => + prev.map((entry) => (entry.clientKey === panel.clientKey ? { ...entry, saving: false } : entry)) + ); } } @@ -156,176 +186,220 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro return (
- - - {t('modulePages.selfroles.createTitle')} - {t('modulePages.selfroles.createDescription')} - - -
+ + + {t('modulePages.selfroles.createTitle')} + {t('modulePages.selfroles.createDescription')} + + +
+
+ + setDraft((prev) => ({ ...prev, channelId }))} + /> +
+
+ + setDraft((prev) => ({ ...prev, title: event.target.value }))} + /> +
+
+ + +
+
+ + +
+
- - setDraft((prev) => ({ ...prev, channelId }))} + + setDraft((prev) => ({ ...prev, description: event.target.value }))} />
- - setDraft((prev) => ({ ...prev, title: event.target.value }))} /> + + setDraft((prev) => ({ ...prev, roles }))} + />
-
- - -
-
- - -
-
-
- - setDraft((prev) => ({ ...prev, description: event.target.value }))} /> -
-
- -