Files
Nexumi/apps/webui/src/components/modules/messages-manager.tsx
TheOnlyMace 4e135bcf43 Enhance component message handling and introduce new features
- Added `ComponentMessageBinding` model to manage message components in the database.
- Updated `WelcomeConfig`, `Tag`, and `ScheduledMessage` models to support new component fields.
- Integrated component handling in various modules, including Scheduler and Tags, to improve message customization.
- Enhanced user experience by implementing new commands for creating and managing component messages in the utility module.
- Updated localization files to reflect new component-related features and improve user guidance.
2026-07-22 22:55:37 +02:00

148 lines
4.9 KiB
TypeScript

'use client';
import {
componentsV2HasContent,
embedHasContent,
type DashboardMessageMode,
type MessageComponentsV2,
type WelcomeEmbed
} from '@nexumi/shared';
import { Send } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
import {
DiscordComponentsV2Builder,
emptyComponentsV2,
normalizeComponentsV2
} from '@/components/ui/discord-components-v2-builder';
import { DiscordEmbedBuilder, emptyEmbed, normalizeEmbed } from '@/components/ui/discord-embed-builder';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
interface MessagesManagerProps {
guildId: string;
}
export function MessagesManager({ guildId }: MessagesManagerProps) {
const t = useTranslations();
const [channelId, setChannelId] = useState('');
const [mode, setMode] = useState<DashboardMessageMode>('EMBED');
const [embed, setEmbed] = useState<WelcomeEmbed | null>(emptyEmbed());
const [components, setComponents] = useState<MessageComponentsV2 | null>(emptyComponentsV2());
const [sending, setSending] = useState(false);
async function handleSend() {
if (!channelId.trim()) {
toast.error(t('modulePages.messages.channelRequired'));
return;
}
const normalizedEmbed = mode === 'EMBED' ? normalizeEmbed(embed) : null;
const normalizedComponents = mode === 'COMPONENTS_V2' ? normalizeComponentsV2(components) : null;
if (
mode === 'EMBED' &&
!embedHasContent(normalizedEmbed) &&
normalizedEmbed?.color === undefined
) {
toast.error(t('modulePages.messages.embedRequired'));
return;
}
if (mode === 'COMPONENTS_V2' && !componentsV2HasContent(normalizedComponents)) {
toast.error(t('modulePages.messages.componentsRequired'));
return;
}
setSending(true);
try {
const response = await fetch(`/api/guilds/${guildId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channelId: channelId.trim(),
mode,
embed: normalizedEmbed,
components: normalizedComponents
})
});
const body = (await response.json().catch(() => null)) as { error?: string; messageId?: string | null } | null;
if (!response.ok || !body) {
toast.error(body?.error ?? t('common.saveError'));
return;
}
toast.success(
body.messageId
? t('modulePages.messages.sentWithId', { id: body.messageId })
: t('modulePages.messages.sent')
);
} catch {
toast.error(t('common.saveError'));
} finally {
setSending(false);
}
}
return (
<FieldAnchor id="field-messages-send">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.messages.sendTitle')}</CardTitle>
<CardDescription>{t('modulePages.messages.sendDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>{t('modulePages.messages.channelId')}</Label>
<DiscordChannelSelect guildId={guildId} value={channelId} onChange={setChannelId} />
</div>
<div className="space-y-2">
<Label>{t('modulePages.messages.mode')}</Label>
<div className="flex flex-wrap gap-2">
{(['EMBED', 'COMPONENTS_V2'] as DashboardMessageMode[]).map((entry) => (
<Button
key={entry}
type="button"
variant={mode === entry ? 'default' : 'outline'}
size="sm"
className={cn(mode !== entry && 'text-muted-foreground')}
onClick={() => setMode(entry)}
>
{t(`modulePages.messages.modes.${entry}`)}
</Button>
))}
</div>
</div>
{mode === 'EMBED' ? (
<DiscordEmbedBuilder
value={embed}
onChange={setEmbed}
placeholderPreset="scheduler"
showClearHint={false}
/>
) : (
<DiscordComponentsV2Builder
guildId={guildId}
value={components}
onChange={setComponents}
placeholderPreset="scheduler"
showClearHint={false}
/>
)}
<Button type="button" onClick={handleSend} disabled={sending}>
<Send className="size-4" />
{sending ? t('modulePages.messages.sending') : t('modulePages.messages.send')}
</Button>
</CardContent>
</Card>
</FieldAnchor>
);
}