@@ -186,63 +239,63 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
{t('modulePages.logging.channelsEmpty')}
)}
{value.logChannels.map((mapping) => (
-
-
-
-
-
{t('modulePages.stats.templateHint')}
+
)}
diff --git a/apps/webui/src/components/modules/tags-manager.tsx b/apps/webui/src/components/modules/tags-manager.tsx
index b051443..3ac9077 100644
--- a/apps/webui/src/components/modules/tags-manager.tsx
+++ b/apps/webui/src/components/modules/tags-manager.tsx
@@ -1,6 +1,6 @@
'use client';
-import type { TagDashboard, TagResponseType } from '@nexumi/shared';
+import type { TagDashboard, TagResponseType, WelcomeEmbed } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
@@ -9,9 +9,11 @@ import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { DiscordChannelMultiSelect } from '@/components/ui/discord-channel-select';
+import { DiscordEmbedBuilder, normalizeEmbed } from '@/components/ui/discord-embed-builder';
import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
+import { PlaceholderHelp } from '@/components/ui/placeholder-help';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
@@ -25,6 +27,7 @@ interface LocalTag extends TagDashboard {
const EMPTY_DRAFT = {
name: '',
content: '',
+ embed: null as WelcomeEmbed | null,
responseType: 'TEXT' as TagResponseType,
triggerWord: '',
allowedRoleIds: [] as string[],
@@ -41,6 +44,7 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
const [tags, setTags] = useState
(
initialTags.map((tag, index) => ({
...tag,
+ embed: tag.embed ?? null,
clientKey: tag.id ?? `existing-${index}`
}))
);
@@ -52,6 +56,11 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
toast.error(t('modulePages.tags.nameRequired'));
return;
}
+ const embed = draft.responseType === 'EMBED' ? normalizeEmbed(draft.embed) : null;
+ if (draft.responseType === 'EMBED' && !embed?.title && !embed?.description) {
+ toast.error(t('modulePages.tags.embedRequired'));
+ return;
+ }
setCreating(true);
try {
const response = await fetch(`/api/guilds/${guildId}/tags`, {
@@ -60,6 +69,7 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
body: JSON.stringify({
name: draft.name.trim(),
content: draft.content.trim() || null,
+ embed,
responseType: draft.responseType,
triggerWord: draft.triggerWord.trim() || null,
allowedRoleIds: draft.allowedRoleIds,
@@ -88,6 +98,11 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
}
async function handleSave(tag: LocalTag) {
+ const embed = tag.responseType === 'EMBED' ? normalizeEmbed(tag.embed ?? null) : null;
+ if (tag.responseType === 'EMBED' && !embed?.title && !embed?.description) {
+ toast.error(t('modulePages.tags.embedRequired'));
+ return;
+ }
setTags((prev) => prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, saving: true } : entry)));
try {
const response = await fetch(`/api/guilds/${guildId}/tags/${tag.id}`, {
@@ -96,6 +111,7 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
body: JSON.stringify({
name: tag.name,
content: tag.content,
+ embed,
responseType: tag.responseType,
triggerWord: tag.triggerWord,
allowedRoleIds: tag.allowedRoleIds,
@@ -105,12 +121,19 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
const body = (await response.json().catch(() => null)) as (TagDashboard & { error?: string }) | null;
if (!response.ok || !body) {
toast.error(body?.error ?? t('common.saveError'));
+ setTags((prev) =>
+ prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, saving: false } : entry))
+ );
return;
}
+ setTags((prev) =>
+ prev.map((entry) =>
+ entry.clientKey === tag.clientKey ? { ...entry, ...body, clientKey: entry.clientKey, saving: false } : entry
+ )
+ );
toast.success(t('common.saveSuccess'));
} catch {
toast.error(t('common.saveError'));
- } finally {
setTags((prev) => prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, saving: false } : entry)));
}
}
@@ -166,10 +189,22 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
setDraft((prev) => ({ ...prev, triggerWord: event.target.value }))} placeholder={t('common.optional')} />