Enhance embed functionality across scheduler and welcome components

- Updated SchedulerManager and WelcomeForm to integrate DiscordEmbedBuilder for managing embeds, replacing JSON text areas with a more user-friendly interface.
- Modified data handling in the scheduler and welcome configurations to support embed structures, ensuring proper parsing and validation.
- Updated localization files to reflect changes in embed handling, improving user guidance and clarity in both English and German.
- Enhanced validation schemas to require embed titles or descriptions where necessary, ensuring robust data integrity.
This commit is contained in:
TheOnlyMace
2026-07-22 21:49:38 +02:00
parent 6a223892dd
commit 55a4f7bf09
10 changed files with 392 additions and 82 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import type { ScheduledMessageDashboard } from '@nexumi/shared';
import type { ScheduledMessageDashboard, WelcomeEmbed } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
@@ -9,6 +9,7 @@ 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 { DiscordEmbedBuilder, normalizeEmbed } from '@/components/ui/discord-embed-builder';
import { DiscordRoleSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -17,6 +18,7 @@ import { Textarea } from '@/components/ui/textarea';
const EMPTY_DRAFT = {
channelId: '',
content: '',
embed: null as WelcomeEmbed | null,
rolePingId: '',
cron: '',
runAt: ''
@@ -35,6 +37,19 @@ function formatTiming(schedule: ScheduledMessageDashboard, t: (key: string) => s
return '—';
}
function schedulePreview(schedule: ScheduledMessageDashboard, t: (key: string) => string): string {
if (schedule.content?.trim()) {
return schedule.content;
}
if (schedule.embed?.title?.trim()) {
return schedule.embed.title;
}
if (schedule.embed?.description?.trim()) {
return schedule.embed.description;
}
return t('modulePages.scheduler.noContent');
}
interface SchedulerManagerProps {
guildId: string;
initialSchedules: ScheduledMessageDashboard[];
@@ -47,7 +62,9 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
const [creating, setCreating] = useState(false);
async function handleCreate() {
if (!draft.channelId.trim() || !draft.content.trim() || (!draft.cron.trim() && !draft.runAt)) {
const embed = normalizeEmbed(draft.embed);
const hasContent = Boolean(draft.content.trim()) || Boolean(embed?.title) || Boolean(embed?.description);
if (!draft.channelId.trim() || !hasContent || (!draft.cron.trim() && !draft.runAt)) {
toast.error(t('modulePages.scheduler.formIncomplete'));
return;
}
@@ -58,7 +75,8 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channelId: draft.channelId.trim(),
content: draft.content.trim(),
content: draft.content.trim() || null,
embed,
rolePingId: draft.rolePingId.trim() || undefined,
cron: draft.cron.trim() || null,
runAt: draft.cron.trim() ? null : draft.runAt ? new Date(draft.runAt).toISOString() : null
@@ -151,6 +169,10 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
rows={3}
/>
</div>
<div className="space-y-2">
<Label>{t('modulePages.scheduler.embed')}</Label>
<DiscordEmbedBuilder value={draft.embed} onChange={(embed) => setDraft((prev) => ({ ...prev, embed }))} />
</div>
<p className="text-xs text-muted-foreground">{t('modulePages.scheduler.cronHint')}</p>
<Button type="button" onClick={handleCreate} disabled={creating}>
<Plus className="size-4" />
@@ -172,7 +194,7 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
{schedules.map((schedule) => (
<div key={schedule.id} className="flex flex-wrap items-start justify-between gap-3 rounded-md border border-border p-4">
<div>
<p className="font-medium">{schedule.content ?? t('modulePages.scheduler.noContent')}</p>
<p className="font-medium">{schedulePreview(schedule, t)}</p>
<p className="text-xs text-muted-foreground">
#{schedule.channelId} · {formatTiming(schedule, t)}
{schedule.rolePingId ? ` · @${schedule.rolePingId}` : ''}

View File

@@ -1,10 +1,15 @@
'use client';
import type { WelcomeConfigDashboard } from '@nexumi/shared';
import type { WelcomeConfigDashboard, WelcomeEmbed } from '@nexumi/shared';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
import {
DiscordEmbedBuilder,
embedFromUnknown,
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';
@@ -14,55 +19,33 @@ import { Textarea } from '@/components/ui/textarea';
import type { SettingsSaveResult } from '@/components/settings/settings-form';
import { SettingsForm } from '@/components/settings/settings-form';
interface LocalWelcomeValue extends Omit<WelcomeConfigDashboard, 'welcomeEmbed' | 'leaveEmbed'> {
welcomeEmbedText: string;
leaveEmbedText: string;
}
const WELCOME_PREVIEW_VARS = {
user: '@Alex',
'user.name': 'Alex',
'user.tag': 'Alex',
'user.id': '123456789012345678',
server: 'Nexumi',
memberCount: '128'
};
function toJsonText(value: Record<string, unknown> | null | undefined): string {
return value ? JSON.stringify(value, null, 2) : '';
interface LocalWelcomeValue extends Omit<WelcomeConfigDashboard, 'welcomeEmbed' | 'leaveEmbed'> {
welcomeEmbed: WelcomeEmbed | null;
leaveEmbed: WelcomeEmbed | null;
}
function toLocal(config: WelcomeConfigDashboard): LocalWelcomeValue {
const { welcomeEmbed, leaveEmbed, ...rest } = config;
return {
...rest,
welcomeEmbedText: toJsonText(welcomeEmbed),
leaveEmbedText: toJsonText(leaveEmbed)
...config,
welcomeEmbed: embedFromUnknown(config.welcomeEmbed),
leaveEmbed: embedFromUnknown(config.leaveEmbed)
};
}
function parseEmbedJson(text: string): { value: Record<string, unknown> | null; error?: string } {
if (!text.trim()) {
return { value: null };
}
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return { value: parsed as Record<string, unknown> };
}
return { value: null, error: 'Invalid embed JSON (must be an object)' };
} catch {
return { value: null, error: 'Invalid embed JSON' };
}
}
async function saveWelcome(guildId: string, value: LocalWelcomeValue): Promise<SettingsSaveResult> {
const welcomeEmbed = parseEmbedJson(value.welcomeEmbedText);
if (welcomeEmbed.error) {
return { ok: false, error: welcomeEmbed.error };
}
const leaveEmbed = parseEmbedJson(value.leaveEmbedText);
if (leaveEmbed.error) {
return { ok: false, error: leaveEmbed.error };
}
const { welcomeEmbedText: _welcomeEmbedText, leaveEmbedText: _leaveEmbedText, ...rest } = value;
const payload: WelcomeConfigDashboard = {
...rest,
welcomeEmbed: welcomeEmbed.value,
leaveEmbed: leaveEmbed.value
...value,
welcomeEmbed: normalizeEmbed(value.welcomeEmbed),
leaveEmbed: normalizeEmbed(value.leaveEmbed)
};
try {
@@ -154,12 +137,10 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
<FieldAnchor id="field-welcome-embed">
<div className="space-y-2">
<Label>{t('modulePages.welcome.welcomeEmbed')}</Label>
<Textarea
className="font-mono"
rows={6}
value={value.welcomeEmbedText}
onChange={(event) => setValue((prev) => ({ ...prev, welcomeEmbedText: event.target.value }))}
placeholder='{ "title": "Welcome!", "color": 6591981 }'
<DiscordEmbedBuilder
value={value.welcomeEmbed}
onChange={(welcomeEmbed) => setValue((prev) => ({ ...prev, welcomeEmbed }))}
previewVars={WELCOME_PREVIEW_VARS}
/>
</div>
</FieldAnchor>
@@ -260,12 +241,10 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
<FieldAnchor id="field-leave-embed">
<div className="space-y-2">
<Label>{t('modulePages.welcome.leaveEmbed')}</Label>
<Textarea
className="font-mono"
rows={6}
value={value.leaveEmbedText}
onChange={(event) => setValue((prev) => ({ ...prev, leaveEmbedText: event.target.value }))}
placeholder='{ "title": "Goodbye", "color": 15158332 }'
<DiscordEmbedBuilder
value={value.leaveEmbed}
onChange={(leaveEmbed) => setValue((prev) => ({ ...prev, leaveEmbed }))}
previewVars={WELCOME_PREVIEW_VARS}
/>
</div>
</FieldAnchor>