Implement PlaceholderHelp component across various forms for enhanced user guidance

- Integrated PlaceholderHelp component into multiple forms including WelcomeForm, TagsManager, and StatsForm to provide contextual hints for available placeholders.
- Updated DiscordEmbedBuilder to support placeholder presets, improving user experience when creating embeds.
- Enhanced localization files to include new placeholder descriptions, ensuring clarity in both English and German.
- Refactored related components to maintain consistency in placeholder handling across the application.
This commit is contained in:
TheOnlyMace
2026-07-22 22:07:08 +02:00
parent 55a4f7bf09
commit 8cfb149a18
21 changed files with 583 additions and 98 deletions

View File

@@ -9,6 +9,7 @@ import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
import { DiscordRoleSelect } 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 { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import type { SettingsSaveResult } from '@/components/settings/settings-form';
@@ -87,6 +88,7 @@ export function BirthdaysForm({ guildId, initialValue }: BirthdaysFormProps) {
<div className="space-y-2">
<Label>{t('modulePages.birthdays.message')}</Label>
<Textarea value={value.message ?? ''} onChange={(event) => setValue((prev) => ({ ...prev, message: event.target.value || null }))} placeholder={t('modulePages.birthdays.messagePlaceholder')} />
<PlaceholderHelp preset="birthdays" />
</div>
</FieldAnchor>
</CardContent>

View File

@@ -12,6 +12,7 @@ import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
import { DiscordRoleSelect } 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 { Switch } from '@/components/ui/switch';
@@ -206,6 +207,7 @@ export function FeedsManager({ guildId, initialFeeds }: FeedsManagerProps) {
onChange={(event) => setDraft((prev) => ({ ...prev, template: event.target.value }))}
placeholder={t('modulePages.feeds.templatePlaceholder')}
/>
<PlaceholderHelp preset="feeds" />
</div>
</div>
<Button type="button" variant="outline" onClick={handleCreate} disabled={creating}>

View File

@@ -14,6 +14,7 @@ import {
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 { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
@@ -250,6 +251,7 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, levelUpMessage: event.target.value || null }))}
placeholder="Congrats {user}, you reached level {level}!"
/>
<PlaceholderHelp preset="leveling" />
</div>
<FieldAnchor id="field-leveling-stack">
<div className="flex items-center justify-between rounded-md border border-border p-4">

View File

@@ -2,15 +2,16 @@
import type { LogChannelMapping, LogEventTypeDashboard, LoggingConfigDashboard } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
import { useMemo } from 'react';
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 { DiscordChannelMultiSelect, DiscordChannelSelect } from '@/components/ui/discord-channel-select';
import { DiscordResourceMultiSelect } from '@/components/ui/discord-resource-multi-select';
import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import type { SettingsSaveResult } from '@/components/settings/settings-form';
import { SettingsForm } from '@/components/settings/settings-form';
@@ -44,8 +45,10 @@ const LOG_EVENT_TYPES: LogEventTypeDashboard[] = [
'MOD_ACTION'
];
interface LocalLogChannel extends LogChannelMapping {
interface LocalLogChannelGroup {
clientKey: string;
channelId: string;
eventTypes: LogEventTypeDashboard[];
}
interface LocalLoggingValue {
@@ -54,37 +57,78 @@ interface LocalLoggingValue {
ignoredChannelIds: string[];
ignoredRoleIds: string[];
retentionDays: number;
logChannels: LocalLogChannel[];
logChannels: LocalLogChannelGroup[];
}
function toLocal(config: LoggingConfigDashboard): LocalLoggingValue {
const byChannel = new Map<string, LogEventTypeDashboard[]>();
for (const mapping of config.logChannels) {
const existing = byChannel.get(mapping.channelId) ?? [];
existing.push(mapping.eventType);
byChannel.set(mapping.channelId, existing);
}
const logChannels: LocalLogChannelGroup[] = [...byChannel.entries()].map(([channelId, eventTypes], index) => ({
clientKey: `${channelId}-${index}`,
channelId,
eventTypes: [...new Set(eventTypes)]
}));
return {
enabled: config.enabled,
ignoreBots: config.ignoreBots,
ignoredChannelIds: config.ignoredChannelIds,
ignoredRoleIds: config.ignoredRoleIds,
retentionDays: config.retentionDays,
logChannels: config.logChannels.map((mapping, index) => ({ ...mapping, clientKey: `${mapping.eventType}-${index}` }))
logChannels
};
}
function toRemote(value: LocalLoggingValue): LoggingConfigDashboard {
function toRemote(value: LocalLoggingValue): { ok: true; value: LoggingConfigDashboard } | { ok: false; error: string } {
const mappings: LogChannelMapping[] = [];
const seenEventTypes = new Set<LogEventTypeDashboard>();
for (const group of value.logChannels) {
if (!group.channelId.trim() || group.eventTypes.length === 0) {
continue;
}
for (const eventType of group.eventTypes) {
if (seenEventTypes.has(eventType)) {
return { ok: false, error: 'duplicateEventType' };
}
seenEventTypes.add(eventType);
mappings.push({ eventType, channelId: group.channelId });
}
}
return {
enabled: value.enabled,
ignoreBots: value.ignoreBots,
ignoredChannelIds: value.ignoredChannelIds,
ignoredRoleIds: value.ignoredRoleIds,
retentionDays: value.retentionDays,
logChannels: value.logChannels.map(({ clientKey: _clientKey, ...rest }) => rest)
ok: true,
value: {
enabled: value.enabled,
ignoreBots: value.ignoreBots,
ignoredChannelIds: value.ignoredChannelIds,
ignoredRoleIds: value.ignoredRoleIds,
retentionDays: value.retentionDays,
logChannels: mappings
}
};
}
async function saveLogging(guildId: string, value: LocalLoggingValue): Promise<SettingsSaveResult> {
async function saveLogging(
guildId: string,
value: LocalLoggingValue,
t: (key: string) => string
): Promise<SettingsSaveResult> {
const remote = toRemote(value);
if (!remote.ok) {
return { ok: false, error: t(`modulePages.logging.errors.${remote.error}`) };
}
try {
const response = await fetch(`/api/guilds/${guildId}/logging`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(toRemote(value))
body: JSON.stringify(remote.value)
});
if (!response.ok) {
const body = (await response.json().catch(() => null)) as { error?: string } | null;
@@ -104,10 +148,19 @@ interface LoggingFormProps {
export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
const t = useTranslations();
const eventOptions = useMemo(
() =>
LOG_EVENT_TYPES.map((eventType) => ({
id: eventType,
name: t(`modulePages.logging.eventTypes.${eventType}`)
})),
[t]
);
return (
<SettingsForm<LocalLoggingValue>
initialValue={toLocal(initialValue)}
onSave={(value) => saveLogging(guildId, value)}
onSave={(value) => saveLogging(guildId, value, t)}
>
{({ value, setValue }) => (
<div className="space-y-6">
@@ -186,63 +239,63 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
<p className="text-sm text-muted-foreground">{t('modulePages.logging.channelsEmpty')}</p>
)}
{value.logChannels.map((mapping) => (
<div key={mapping.clientKey} className="flex flex-wrap items-end gap-3 rounded-md border border-border p-3">
<div className="space-y-2">
<Label>{t('modulePages.logging.eventType')}</Label>
<Select
value={mapping.eventType}
onValueChange={(eventType) =>
<div key={mapping.clientKey} className="space-y-3 rounded-md border border-border p-3">
<div className="flex flex-wrap items-end gap-3">
<div className="min-w-[14rem] flex-1 space-y-2">
<Label>{t('modulePages.logging.channelId')}</Label>
<DiscordChannelSelect
guildId={guildId}
value={mapping.channelId}
onChange={(channelId) =>
setValue((prev) => ({
...prev,
logChannels: prev.logChannels.map((entry) =>
entry.clientKey === mapping.clientKey ? { ...entry, channelId } : entry
)
}))
}
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
aria-label={t('common.remove')}
onClick={() =>
setValue((prev) => ({
...prev,
logChannels: prev.logChannels.map((entry) =>
entry.clientKey === mapping.clientKey
? { ...entry, eventType: eventType as LogEventTypeDashboard }
: entry
)
logChannels: prev.logChannels.filter((entry) => entry.clientKey !== mapping.clientKey)
}))
}
>
<SelectTrigger className="w-56">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LOG_EVENT_TYPES.map((eventType) => (
<SelectItem key={eventType} value={eventType}>
{eventType}
</SelectItem>
))}
</SelectContent>
</Select>
<Trash2 className="size-4" />
</Button>
</div>
<div className="flex-1 space-y-2">
<Label>{t('modulePages.logging.channelId')}</Label>
<DiscordChannelSelect
guildId={guildId}
value={mapping.channelId}
onChange={(channelId) =>
<div className="space-y-2">
<Label>{t('modulePages.logging.eventTypesLabel')}</Label>
<DiscordResourceMultiSelect
options={eventOptions}
value={mapping.eventTypes}
showIdHint={false}
placeholder={t('modulePages.logging.eventTypesPlaceholder')}
onChange={(nextIds) => {
const eventTypes = nextIds as LogEventTypeDashboard[];
setValue((prev) => ({
...prev,
logChannels: prev.logChannels.map((entry) =>
entry.clientKey === mapping.clientKey ? { ...entry, channelId } : entry
)
}))
}
logChannels: prev.logChannels.map((entry) => {
if (entry.clientKey === mapping.clientKey) {
return { ...entry, eventTypes };
}
// Each event type can only map to one channel (DB unique).
return {
...entry,
eventTypes: entry.eventTypes.filter((eventType) => !eventTypes.includes(eventType))
};
})
}));
}}
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
aria-label={t('common.remove')}
onClick={() =>
setValue((prev) => ({
...prev,
logChannels: prev.logChannels.filter((entry) => entry.clientKey !== mapping.clientKey)
}))
}
>
<Trash2 className="size-4" />
</Button>
</div>
))}
<Button
@@ -253,7 +306,7 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
...prev,
logChannels: [
...prev.logChannels,
{ clientKey: `new-${Date.now()}`, eventType: 'MOD_ACTION', channelId: '' }
{ clientKey: `new-${Date.now()}`, channelId: '', eventTypes: [] }
]
}))
}
@@ -261,6 +314,7 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
<Plus className="size-4" />
{t('modulePages.logging.addMapping')}
</Button>
<p className="text-xs text-muted-foreground">{t('modulePages.logging.channelsHint')}</p>
</CardContent>
</Card>
</FieldAnchor>

View File

@@ -13,6 +13,7 @@ import { DiscordEmbedBuilder, normalizeEmbed } from '@/components/ui/discord-emb
import { DiscordRoleSelect } 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 { Textarea } from '@/components/ui/textarea';
const EMPTY_DRAFT = {
@@ -168,10 +169,15 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
onChange={(event) => setDraft((prev) => ({ ...prev, content: event.target.value }))}
rows={3}
/>
<PlaceholderHelp preset="scheduler" />
</div>
<div className="space-y-2">
<Label>{t('modulePages.scheduler.embed')}</Label>
<DiscordEmbedBuilder value={draft.embed} onChange={(embed) => setDraft((prev) => ({ ...prev, embed }))} />
<DiscordEmbedBuilder
value={draft.embed}
onChange={(embed) => setDraft((prev) => ({ ...prev, embed }))}
placeholderPreset="scheduler"
/>
</div>
<p className="text-xs text-muted-foreground">{t('modulePages.scheduler.cronHint')}</p>
<Button type="button" onClick={handleCreate} disabled={creating}>

View File

@@ -8,6 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { PlaceholderHelp } from '@/components/ui/placeholder-help';
import { Switch } from '@/components/ui/switch';
import type { SettingsSaveResult } from '@/components/settings/settings-form';
import { SettingsForm } from '@/components/settings/settings-form';
@@ -102,7 +103,7 @@ export function StatsForm({ guildId, initialValue }: StatsFormProps) {
<Input id={boostsTemplateId} value={value.boostsTemplate} onChange={(event) => setValue((prev) => ({ ...prev, boostsTemplate: event.target.value }))} />
</div>
</div>
<p className="text-xs text-muted-foreground">{t('modulePages.stats.templateHint')}</p>
<PlaceholderHelp preset="stats" />
</CardContent>
</Card>
)}

View File

@@ -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<LocalTag[]>(
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) {
<Input value={draft.triggerWord} onChange={(event) => setDraft((prev) => ({ ...prev, triggerWord: event.target.value }))} placeholder={t('common.optional')} />
</div>
</div>
<div className="space-y-2">
<Label>{t('modulePages.tags.content')}</Label>
<Textarea rows={3} value={draft.content} onChange={(event) => setDraft((prev) => ({ ...prev, content: event.target.value }))} />
</div>
{draft.responseType === 'TEXT' ? (
<div className="space-y-2">
<Label>{t('modulePages.tags.content')}</Label>
<Textarea rows={3} value={draft.content} onChange={(event) => setDraft((prev) => ({ ...prev, content: event.target.value }))} />
<PlaceholderHelp preset="tags" />
</div>
) : (
<div className="space-y-2">
<Label>{t('modulePages.tags.embed')}</Label>
<DiscordEmbedBuilder
value={draft.embed}
onChange={(embed) => setDraft((prev) => ({ ...prev, embed }))}
placeholderPreset="tags"
/>
</div>
)}
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t('modulePages.tags.allowedRoleIds')}</Label>
@@ -197,7 +232,6 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
</FieldAnchor>
<FieldAnchor id="field-tags-list">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.tags.listTitle')}</CardTitle>
@@ -232,10 +266,26 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
<Input value={tag.triggerWord ?? ''} onChange={(event) => setTags((prev) => prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, triggerWord: event.target.value || null } : entry)))} />
</div>
</div>
<div className="space-y-2">
<Label>{t('modulePages.tags.content')}</Label>
<Textarea rows={3} value={tag.content ?? ''} onChange={(event) => setTags((prev) => prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, content: event.target.value } : entry)))} />
</div>
{tag.responseType === 'TEXT' ? (
<div className="space-y-2">
<Label>{t('modulePages.tags.content')}</Label>
<Textarea rows={3} value={tag.content ?? ''} onChange={(event) => setTags((prev) => prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, content: event.target.value } : entry)))} />
<PlaceholderHelp preset="tags" />
</div>
) : (
<div className="space-y-2">
<Label>{t('modulePages.tags.embed')}</Label>
<DiscordEmbedBuilder
value={tag.embed ?? null}
onChange={(embed) =>
setTags((prev) =>
prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, embed } : entry))
)
}
placeholderPreset="tags"
/>
</div>
)}
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t('modulePages.tags.allowedRoleIds')}</Label>

View File

@@ -8,6 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { PlaceholderHelp } from '@/components/ui/placeholder-help';
import { Switch } from '@/components/ui/switch';
import type { SettingsSaveResult } from '@/components/settings/settings-form';
import { SettingsForm } from '@/components/settings/settings-form';
@@ -81,6 +82,7 @@ export function TempVoiceForm({ guildId, initialValue }: TempVoiceFormProps) {
<div className="space-y-2">
<Label htmlFor={nameId}>{t('modulePages.tempvoice.defaultName')}</Label>
<Input id={nameId} value={value.defaultName} onChange={(event) => setValue((prev) => ({ ...prev, defaultName: event.target.value }))} placeholder="{user}'s Channel" />
<PlaceholderHelp preset="tempvoice" />
</div>
</FieldAnchor>
<FieldAnchor id="field-tempvoice-limit">

View File

@@ -13,6 +13,7 @@ import {
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 { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
@@ -131,7 +132,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, welcomeContent: event.target.value || null }))}
placeholder={t('modulePages.welcome.contentPlaceholder')}
/>
<p className="text-xs text-muted-foreground">{t('modulePages.welcome.placeholdersHint')}</p>
<PlaceholderHelp preset="welcome" />
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-embed">
@@ -141,6 +142,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
value={value.welcomeEmbed}
onChange={(welcomeEmbed) => setValue((prev) => ({ ...prev, welcomeEmbed }))}
previewVars={WELCOME_PREVIEW_VARS}
placeholderPreset="welcome"
/>
</div>
</FieldAnchor>
@@ -158,6 +160,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, welcomeDmContent: event.target.value || null }))}
placeholder={t('modulePages.welcome.dmContentPlaceholder')}
/>
<PlaceholderHelp preset="welcomeDm" />
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-user-autoroles">
@@ -200,6 +203,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
</div>
</FieldAnchor>
</div>
<PlaceholderHelp preset="welcome" />
</CardContent>
</Card>
@@ -236,6 +240,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, leaveContent: event.target.value || null }))}
placeholder={t('modulePages.welcome.contentPlaceholder')}
/>
<PlaceholderHelp preset="welcome" />
</div>
</FieldAnchor>
<FieldAnchor id="field-leave-embed">
@@ -245,6 +250,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
value={value.leaveEmbed}
onChange={(leaveEmbed) => setValue((prev) => ({ ...prev, leaveEmbed }))}
previewVars={WELCOME_PREVIEW_VARS}
placeholderPreset="welcome"
/>
</div>
</FieldAnchor>