Refactor channel and role selection in various forms to use Discord components
- Replaced standard input fields with DiscordChannelSelect and DiscordRoleSelect components in the BirthdaysForm, FeedsManager, GiveawaysManager, LevelingForm, LoggingForm, SchedulerManager, SelfRolesManager, StatsForm, SuggestionsConfigForm, TagsManager, TempVoiceForm, TicketConfigForm, and StarboardForm for improved user experience and consistency. - Updated state management to accommodate new component structures, enhancing data handling for channel and role selections across the application.
This commit is contained in:
@@ -5,6 +5,8 @@ import { useId } from 'react';
|
||||
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 { DiscordRoleSelect } from '@/components/ui/discord-role-select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -36,8 +38,6 @@ interface BirthdaysFormProps {
|
||||
|
||||
export function BirthdaysForm({ guildId, initialValue }: BirthdaysFormProps) {
|
||||
const t = useTranslations();
|
||||
const channelId = useId();
|
||||
const roleId = useId();
|
||||
const timezoneId = useId();
|
||||
|
||||
return (
|
||||
@@ -58,14 +58,22 @@ export function BirthdaysForm({ guildId, initialValue }: BirthdaysFormProps) {
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<FieldAnchor id="field-birthdays-channel">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={channelId}>{t('modulePages.birthdays.channelId')}</Label>
|
||||
<Input id={channelId} value={value.channelId} onChange={(event) => setValue((prev) => ({ ...prev, channelId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.birthdays.channelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.channelId}
|
||||
onChange={(channelId) => setValue((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-birthdays-role">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={roleId}>{t('modulePages.birthdays.roleId')}</Label>
|
||||
<Input id={roleId} value={value.roleId} onChange={(event) => setValue((prev) => ({ ...prev, roleId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.birthdays.roleId')}</Label>
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={value.roleId}
|
||||
onChange={(roleId) => setValue((prev) => ({ ...prev, roleId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-birthdays-timezone">
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import type {
|
||||
CommandGlobalRules,
|
||||
CommandOverrideDashboard,
|
||||
DiscordChannelOption,
|
||||
DiscordRoleOption
|
||||
DiscordChannelOption
|
||||
} from '@nexumi/shared';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
@@ -14,6 +13,7 @@ import { useTranslations } from '@/components/locale-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { DiscordResourceMultiSelect } from '@/components/ui/discord-resource-multi-select';
|
||||
import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
|
||||
@@ -26,15 +26,13 @@ interface CommandsManagerProps {
|
||||
initialCommands: CommandOverrideDashboard[];
|
||||
initialGlobalRules: CommandGlobalRules;
|
||||
channels: DiscordChannelOption[];
|
||||
roles: DiscordRoleOption[];
|
||||
}
|
||||
|
||||
export function CommandsManager({
|
||||
guildId,
|
||||
initialCommands,
|
||||
initialGlobalRules,
|
||||
channels,
|
||||
roles
|
||||
channels
|
||||
}: CommandsManagerProps) {
|
||||
const t = useTranslations();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -48,10 +46,6 @@ export function CommandsManager({
|
||||
() => channels.map((channel) => ({ id: channel.id, name: channel.name, prefix: '#' })),
|
||||
[channels]
|
||||
);
|
||||
const roleOptions = useMemo(
|
||||
() => roles.map((role) => ({ id: role.id, name: role.name, prefix: '@' })),
|
||||
[roles]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const q = searchParams.get('q');
|
||||
@@ -250,8 +244,8 @@ export function CommandsManager({
|
||||
</div>
|
||||
<div className="space-y-1 sm:col-span-2">
|
||||
<p className="text-xs text-muted-foreground">{t('modulePages.commands.allowedRoleIds')}</p>
|
||||
<DiscordResourceMultiSelect
|
||||
options={roleOptions}
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={entry.allowedRoleIds}
|
||||
onChange={(allowedRoleIds) => patchLocal(entry.commandName, { allowedRoleIds })}
|
||||
placeholder={t('modulePages.commands.roleSearchPlaceholder')}
|
||||
@@ -260,8 +254,8 @@ export function CommandsManager({
|
||||
</div>
|
||||
<div className="space-y-1 sm:col-span-2">
|
||||
<p className="text-xs text-muted-foreground">{t('modulePages.commands.deniedRoleIds')}</p>
|
||||
<DiscordResourceMultiSelect
|
||||
options={roleOptions}
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={entry.deniedRoleIds}
|
||||
onChange={(deniedRoleIds) => patchLocal(entry.commandName, { deniedRoleIds })}
|
||||
placeholder={t('modulePages.commands.roleSearchPlaceholder')}
|
||||
|
||||
@@ -8,6 +8,8 @@ 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 { DiscordRoleSelect } 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';
|
||||
@@ -182,17 +184,18 @@ export function FeedsManager({ guildId, initialFeeds }: FeedsManagerProps) {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.feeds.channelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={draft.channelId}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, channelId: event.target.value }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(channelId) => setDraft((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.feeds.rolePingId')}</Label>
|
||||
<Input
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={draft.rolePingId}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, rolePingId: event.target.value }))}
|
||||
onChange={(rolePingId) => setDraft((prev) => ({ ...prev, rolePingId }))}
|
||||
placeholder={t('common.optional')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useTranslations } from '@/components/locale-provider';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
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 { DiscordRoleSelect } from '@/components/ui/discord-role-select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
@@ -127,10 +129,10 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.giveaways.channelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={draft.channelId}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, channelId: event.target.value }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(channelId) => setDraft((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -156,9 +158,10 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.giveaways.requiredRoleId')}</Label>
|
||||
<Input
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={draft.requiredRoleId}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, requiredRoleId: event.target.value }))}
|
||||
onChange={(requiredRoleId) => setDraft((prev) => ({ ...prev, requiredRoleId }))}
|
||||
placeholder={t('common.optional')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { LevelingConfigDashboard } from '@nexumi/shared';
|
||||
import { useId } from 'react';
|
||||
import { FieldAnchor } from '@/components/layout/field-anchor';
|
||||
import { useTranslations } from '@/components/locale-provider';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { DiscordChannelMultiSelect, DiscordChannelSelect } from '@/components/ui/discord-channel-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';
|
||||
@@ -14,24 +15,11 @@ import type { SettingsSaveResult } from '@/components/settings/settings-form';
|
||||
import { SettingsForm } from '@/components/settings/settings-form';
|
||||
|
||||
interface LocalLevelingValue
|
||||
extends Omit<LevelingConfigDashboard, 'noXpChannelIds' | 'noXpRoleIds' | 'roleMultipliers' | 'channelMultipliers'> {
|
||||
noXpChannelIdsText: string;
|
||||
noXpRoleIdsText: string;
|
||||
extends Omit<LevelingConfigDashboard, 'roleMultipliers' | 'channelMultipliers'> {
|
||||
roleMultipliersText: string;
|
||||
channelMultipliersText: string;
|
||||
}
|
||||
|
||||
function toCsv(ids: string[]): string {
|
||||
return ids.join(', ');
|
||||
}
|
||||
|
||||
function fromCsv(text: string): string[] {
|
||||
return text
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
}
|
||||
|
||||
function toMultiplierText(map: Record<string, number>): string {
|
||||
return Object.entries(map)
|
||||
.map(([id, multiplier]) => `${id}:${multiplier}`)
|
||||
@@ -58,11 +46,9 @@ function fromMultiplierText(text: string): { value: Record<string, number>; erro
|
||||
}
|
||||
|
||||
function toLocal(config: LevelingConfigDashboard): LocalLevelingValue {
|
||||
const { noXpChannelIds, noXpRoleIds, roleMultipliers, channelMultipliers, ...rest } = config;
|
||||
const { roleMultipliers, channelMultipliers, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
noXpChannelIdsText: toCsv(noXpChannelIds),
|
||||
noXpRoleIdsText: toCsv(noXpRoleIds),
|
||||
roleMultipliersText: toMultiplierText(roleMultipliers),
|
||||
channelMultipliersText: toMultiplierText(channelMultipliers)
|
||||
};
|
||||
@@ -78,12 +64,10 @@ async function saveLeveling(guildId: string, value: LocalLevelingValue): Promise
|
||||
return { ok: false, error: channelMultipliers.error };
|
||||
}
|
||||
|
||||
const { noXpChannelIdsText, noXpRoleIdsText, roleMultipliersText: _roleMultipliersText, channelMultipliersText: _channelMultipliersText, ...rest } = value;
|
||||
const { roleMultipliersText: _roleMultipliersText, channelMultipliersText: _channelMultipliersText, ...rest } = value;
|
||||
|
||||
const payload: LevelingConfigDashboard = {
|
||||
...rest,
|
||||
noXpChannelIds: fromCsv(noXpChannelIdsText),
|
||||
noXpRoleIds: fromCsv(noXpRoleIdsText),
|
||||
roleMultipliers: roleMultipliers.value,
|
||||
channelMultipliers: channelMultipliers.value
|
||||
};
|
||||
@@ -111,8 +95,6 @@ interface LevelingFormProps {
|
||||
|
||||
export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
|
||||
const t = useTranslations();
|
||||
const noXpChannelsId = useId();
|
||||
const noXpRolesId = useId();
|
||||
|
||||
return (
|
||||
<SettingsForm<LocalLevelingValue>
|
||||
@@ -192,23 +174,21 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
|
||||
|
||||
<FieldAnchor id="field-leveling-no-xp-channels">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={noXpChannelsId}>{t('modulePages.leveling.noXpChannels')}</Label>
|
||||
<Textarea
|
||||
id={noXpChannelsId}
|
||||
value={value.noXpChannelIdsText}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, noXpChannelIdsText: event.target.value }))}
|
||||
placeholder={t('modulePages.logging.idsPlaceholder')}
|
||||
<Label>{t('modulePages.leveling.noXpChannels')}</Label>
|
||||
<DiscordChannelMultiSelect
|
||||
guildId={guildId}
|
||||
value={value.noXpChannelIds}
|
||||
onChange={(noXpChannelIds) => setValue((prev) => ({ ...prev, noXpChannelIds }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-leveling-no-xp-roles">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={noXpRolesId}>{t('modulePages.leveling.noXpRoles')}</Label>
|
||||
<Textarea
|
||||
id={noXpRolesId}
|
||||
value={value.noXpRoleIdsText}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, noXpRoleIdsText: event.target.value }))}
|
||||
placeholder={t('modulePages.logging.idsPlaceholder')}
|
||||
<Label>{t('modulePages.leveling.noXpRoles')}</Label>
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={value.noXpRoleIds}
|
||||
onChange={(noXpRoleIds) => setValue((prev) => ({ ...prev, noXpRoleIds }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
@@ -271,10 +251,10 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.leveling.levelUpChannelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.levelUpChannelId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, levelUpChannelId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(levelUpChannelId) => setValue((prev) => ({ ...prev, levelUpChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
import type { LogChannelMapping, LogEventTypeDashboard, LoggingConfigDashboard } from '@nexumi/shared';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { useId } 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 { 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 { Textarea } from '@/components/ui/textarea';
|
||||
import type { SettingsSaveResult } from '@/components/settings/settings-form';
|
||||
import { SettingsForm } from '@/components/settings/settings-form';
|
||||
|
||||
@@ -51,29 +51,18 @@ interface LocalLogChannel extends LogChannelMapping {
|
||||
interface LocalLoggingValue {
|
||||
enabled: boolean;
|
||||
ignoreBots: boolean;
|
||||
ignoredChannelIdsText: string;
|
||||
ignoredRoleIdsText: string;
|
||||
ignoredChannelIds: string[];
|
||||
ignoredRoleIds: string[];
|
||||
retentionDays: number;
|
||||
logChannels: LocalLogChannel[];
|
||||
}
|
||||
|
||||
function toCsv(ids: string[]): string {
|
||||
return ids.join(', ');
|
||||
}
|
||||
|
||||
function fromCsv(text: string): string[] {
|
||||
return text
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
}
|
||||
|
||||
function toLocal(config: LoggingConfigDashboard): LocalLoggingValue {
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
ignoreBots: config.ignoreBots,
|
||||
ignoredChannelIdsText: toCsv(config.ignoredChannelIds),
|
||||
ignoredRoleIdsText: toCsv(config.ignoredRoleIds),
|
||||
ignoredChannelIds: config.ignoredChannelIds,
|
||||
ignoredRoleIds: config.ignoredRoleIds,
|
||||
retentionDays: config.retentionDays,
|
||||
logChannels: config.logChannels.map((mapping, index) => ({ ...mapping, clientKey: `${mapping.eventType}-${index}` }))
|
||||
};
|
||||
@@ -83,8 +72,8 @@ function toRemote(value: LocalLoggingValue): LoggingConfigDashboard {
|
||||
return {
|
||||
enabled: value.enabled,
|
||||
ignoreBots: value.ignoreBots,
|
||||
ignoredChannelIds: fromCsv(value.ignoredChannelIdsText),
|
||||
ignoredRoleIds: fromCsv(value.ignoredRoleIdsText),
|
||||
ignoredChannelIds: value.ignoredChannelIds,
|
||||
ignoredRoleIds: value.ignoredRoleIds,
|
||||
retentionDays: value.retentionDays,
|
||||
logChannels: value.logChannels.map(({ clientKey: _clientKey, ...rest }) => rest)
|
||||
};
|
||||
@@ -114,8 +103,6 @@ interface LoggingFormProps {
|
||||
|
||||
export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
|
||||
const t = useTranslations();
|
||||
const ignoredChannelsId = useId();
|
||||
const ignoredRolesId = useId();
|
||||
|
||||
return (
|
||||
<SettingsForm<LocalLoggingValue>
|
||||
@@ -150,26 +137,22 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-logging-ignored-channels">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={ignoredChannelsId}>{t('modulePages.logging.ignoredChannelsLabel')}</Label>
|
||||
<Textarea
|
||||
id={ignoredChannelsId}
|
||||
value={value.ignoredChannelIdsText}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, ignoredChannelIdsText: event.target.value }))}
|
||||
placeholder={t('modulePages.logging.idsPlaceholder')}
|
||||
<Label>{t('modulePages.logging.ignoredChannelsLabel')}</Label>
|
||||
<DiscordChannelMultiSelect
|
||||
guildId={guildId}
|
||||
value={value.ignoredChannelIds}
|
||||
onChange={(ignoredChannelIds) => setValue((prev) => ({ ...prev, ignoredChannelIds }))}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('modulePages.logging.idsHint')}</p>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-logging-ignored-roles">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={ignoredRolesId}>{t('modulePages.logging.ignoredRolesLabel')}</Label>
|
||||
<Textarea
|
||||
id={ignoredRolesId}
|
||||
value={value.ignoredRoleIdsText}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, ignoredRoleIdsText: event.target.value }))}
|
||||
placeholder={t('modulePages.logging.idsPlaceholder')}
|
||||
<Label>{t('modulePages.logging.ignoredRolesLabel')}</Label>
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={value.ignoredRoleIds}
|
||||
onChange={(ignoredRoleIds) => setValue((prev) => ({ ...prev, ignoredRoleIds }))}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('modulePages.logging.idsHint')}</p>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-logging-retention">
|
||||
@@ -233,17 +216,17 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<Label>{t('modulePages.logging.channelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={mapping.channelId}
|
||||
onChange={(event) =>
|
||||
onChange={(channelId) =>
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
logChannels: prev.logChannels.map((entry) =>
|
||||
entry.clientKey === mapping.clientKey ? { ...entry, channelId: event.target.value.trim() } : entry
|
||||
entry.clientKey === mapping.clientKey ? { ...entry, channelId } : entry
|
||||
)
|
||||
}))
|
||||
}
|
||||
placeholder="123456789012345678"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -8,6 +8,8 @@ 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 { DiscordRoleSelect } from '@/components/ui/discord-role-select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -108,17 +110,18 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.scheduler.channelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={draft.channelId}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, channelId: event.target.value }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(channelId) => setDraft((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.scheduler.rolePingId')}</Label>
|
||||
<Input
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={draft.rolePingId}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, rolePingId: event.target.value }))}
|
||||
onChange={(rolePingId) => setDraft((prev) => ({ ...prev, rolePingId }))}
|
||||
placeholder={t('common.optional')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
@@ -164,7 +165,11 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.selfroles.channelId')}</Label>
|
||||
<Input value={draft.channelId} onChange={(event) => setDraft((prev) => ({ ...prev, channelId: event.target.value }))} placeholder="123456789012345678" />
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={draft.channelId}
|
||||
onChange={(channelId) => setDraft((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.selfroles.panelTitle')}</Label>
|
||||
@@ -237,10 +242,13 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.selfroles.channelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={panel.channelId}
|
||||
onChange={(event) =>
|
||||
setPanels((prev) => prev.map((entry) => (entry.clientKey === panel.clientKey ? { ...entry, channelId: event.target.value } : entry)))
|
||||
onChange={(channelId) =>
|
||||
setPanels((prev) =>
|
||||
prev.map((entry) => (entry.clientKey === panel.clientKey ? { ...entry, channelId } : entry))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,53 +5,19 @@ import { useId } from 'react';
|
||||
import { FieldAnchor } from '@/components/layout/field-anchor';
|
||||
import { useTranslations } from '@/components/locale-provider';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { DiscordChannelMultiSelect, DiscordChannelSelect } from '@/components/ui/discord-channel-select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { SettingsSaveResult } from '@/components/settings/settings-form';
|
||||
import { SettingsForm } from '@/components/settings/settings-form';
|
||||
|
||||
interface LocalValue {
|
||||
enabled: boolean;
|
||||
channelId: string;
|
||||
emoji: string;
|
||||
threshold: number;
|
||||
allowSelfStar: boolean;
|
||||
ignoredChannelIdsText: string;
|
||||
}
|
||||
|
||||
function toLocal(config: StarboardConfigDashboard): LocalValue {
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
channelId: config.channelId,
|
||||
emoji: config.emoji,
|
||||
threshold: config.threshold,
|
||||
allowSelfStar: config.allowSelfStar,
|
||||
ignoredChannelIdsText: config.ignoredChannelIds.join(', ')
|
||||
};
|
||||
}
|
||||
|
||||
function toRemote(value: LocalValue): StarboardConfigDashboard {
|
||||
return {
|
||||
enabled: value.enabled,
|
||||
channelId: value.channelId,
|
||||
emoji: value.emoji,
|
||||
threshold: value.threshold,
|
||||
allowSelfStar: value.allowSelfStar,
|
||||
ignoredChannelIds: value.ignoredChannelIdsText
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
async function saveStarboard(guildId: string, value: LocalValue): Promise<SettingsSaveResult> {
|
||||
async function saveStarboard(guildId: string, value: StarboardConfigDashboard): Promise<SettingsSaveResult> {
|
||||
try {
|
||||
const response = await fetch(`/api/guilds/${guildId}/starboard`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(toRemote(value))
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as { error?: string } | null;
|
||||
@@ -70,13 +36,14 @@ interface StarboardFormProps {
|
||||
|
||||
export function StarboardForm({ guildId, initialValue }: StarboardFormProps) {
|
||||
const t = useTranslations();
|
||||
const channelId = useId();
|
||||
const emojiId = useId();
|
||||
const thresholdId = useId();
|
||||
const ignoredId = useId();
|
||||
|
||||
return (
|
||||
<SettingsForm<LocalValue> initialValue={toLocal(initialValue)} onSave={(value) => saveStarboard(guildId, value)}>
|
||||
<SettingsForm<StarboardConfigDashboard>
|
||||
initialValue={initialValue}
|
||||
onSave={(value) => saveStarboard(guildId, value)}
|
||||
>
|
||||
{({ value, setValue }) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -93,8 +60,12 @@ export function StarboardForm({ guildId, initialValue }: StarboardFormProps) {
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<FieldAnchor id="field-starboard-channel">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={channelId}>{t('modulePages.starboard.channelId')}</Label>
|
||||
<Input id={channelId} value={value.channelId} onChange={(event) => setValue((prev) => ({ ...prev, channelId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.starboard.channelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.channelId}
|
||||
onChange={(channelId) => setValue((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-starboard-emoji">
|
||||
@@ -118,9 +89,12 @@ export function StarboardForm({ guildId, initialValue }: StarboardFormProps) {
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-starboard-ignored">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={ignoredId}>{t('modulePages.starboard.ignoredChannelIds')}</Label>
|
||||
<Textarea id={ignoredId} value={value.ignoredChannelIdsText} onChange={(event) => setValue((prev) => ({ ...prev, ignoredChannelIdsText: event.target.value }))} />
|
||||
<p className="text-xs text-muted-foreground">{t('modulePages.starboard.idsHint')}</p>
|
||||
<Label>{t('modulePages.starboard.ignoredChannelIds')}</Label>
|
||||
<DiscordChannelMultiSelect
|
||||
guildId={guildId}
|
||||
value={value.ignoredChannelIds}
|
||||
onChange={(ignoredChannelIds) => setValue((prev) => ({ ...prev, ignoredChannelIds }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</CardContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useId } from 'react';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -35,9 +36,6 @@ interface StatsFormProps {
|
||||
|
||||
export function StatsForm({ guildId, initialValue }: StatsFormProps) {
|
||||
const t = useTranslations();
|
||||
const membersId = useId();
|
||||
const onlineId = useId();
|
||||
const boostsId = useId();
|
||||
const membersTemplateId = useId();
|
||||
const onlineTemplateId = useId();
|
||||
const boostsTemplateId = useId();
|
||||
@@ -60,8 +58,13 @@ export function StatsForm({ guildId, initialValue }: StatsFormProps) {
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<FieldAnchor id="field-stats-members">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={membersId}>{t('modulePages.stats.membersChannelId')}</Label>
|
||||
<Input id={membersId} value={value.membersChannelId} onChange={(event) => setValue((prev) => ({ ...prev, membersChannelId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.stats.membersChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
kinds={['voice']}
|
||||
value={value.membersChannelId}
|
||||
onChange={(membersChannelId) => setValue((prev) => ({ ...prev, membersChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<div className="space-y-2">
|
||||
@@ -70,8 +73,13 @@ export function StatsForm({ guildId, initialValue }: StatsFormProps) {
|
||||
</div>
|
||||
<FieldAnchor id="field-stats-online">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={onlineId}>{t('modulePages.stats.onlineChannelId')}</Label>
|
||||
<Input id={onlineId} value={value.onlineChannelId} onChange={(event) => setValue((prev) => ({ ...prev, onlineChannelId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.stats.onlineChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
kinds={['voice']}
|
||||
value={value.onlineChannelId}
|
||||
onChange={(onlineChannelId) => setValue((prev) => ({ ...prev, onlineChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<div className="space-y-2">
|
||||
@@ -80,8 +88,13 @@ export function StatsForm({ guildId, initialValue }: StatsFormProps) {
|
||||
</div>
|
||||
<FieldAnchor id="field-stats-boosts">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={boostsId}>{t('modulePages.stats.boostsChannelId')}</Label>
|
||||
<Input id={boostsId} value={value.boostsChannelId} onChange={(event) => setValue((prev) => ({ ...prev, boostsChannelId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.stats.boostsChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
kinds={['voice']}
|
||||
value={value.boostsChannelId}
|
||||
onChange={(boostsChannelId) => setValue((prev) => ({ ...prev, boostsChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type { SuggestionConfigDashboard } from '@nexumi/shared';
|
||||
import { useId } from 'react';
|
||||
import { FieldAnchor } from '@/components/layout/field-anchor';
|
||||
import { useTranslations } from '@/components/locale-provider';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import type { SettingsSaveResult } from '@/components/settings/settings-form';
|
||||
@@ -38,9 +37,6 @@ interface SuggestionsConfigFormProps {
|
||||
|
||||
export function SuggestionsConfigForm({ guildId, initialValue }: SuggestionsConfigFormProps) {
|
||||
const t = useTranslations();
|
||||
const openId = useId();
|
||||
const approvedId = useId();
|
||||
const deniedId = useId();
|
||||
|
||||
return (
|
||||
<SettingsForm<SuggestionConfigDashboard>
|
||||
@@ -63,20 +59,34 @@ export function SuggestionsConfigForm({ guildId, initialValue }: SuggestionsConf
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<FieldAnchor id="field-suggestions-open">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={openId}>{t('modulePages.suggestions.openChannelId')}</Label>
|
||||
<Input id={openId} value={value.openChannelId} onChange={(event) => setValue((prev) => ({ ...prev, openChannelId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.suggestions.openChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.openChannelId}
|
||||
onChange={(openChannelId) => setValue((prev) => ({ ...prev, openChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-suggestions-approved">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={approvedId}>{t('modulePages.suggestions.approvedChannelId')}</Label>
|
||||
<Input id={approvedId} value={value.approvedChannelId} onChange={(event) => setValue((prev) => ({ ...prev, approvedChannelId: event.target.value.trim() }))} placeholder={t('common.optional')} />
|
||||
<Label>{t('modulePages.suggestions.approvedChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.approvedChannelId}
|
||||
onChange={(approvedChannelId) => setValue((prev) => ({ ...prev, approvedChannelId }))}
|
||||
placeholder={t('common.optional')}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-suggestions-denied">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={deniedId}>{t('modulePages.suggestions.deniedChannelId')}</Label>
|
||||
<Input id={deniedId} value={value.deniedChannelId} onChange={(event) => setValue((prev) => ({ ...prev, deniedChannelId: event.target.value.trim() }))} placeholder={t('common.optional')} />
|
||||
<Label>{t('modulePages.suggestions.deniedChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.deniedChannelId}
|
||||
onChange={(deniedChannelId) => setValue((prev) => ({ ...prev, deniedChannelId }))}
|
||||
placeholder={t('common.optional')}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ 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 } from '@/components/ui/discord-channel-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';
|
||||
@@ -15,17 +17,8 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
const RESPONSE_TYPES: TagResponseType[] = ['TEXT', 'EMBED'];
|
||||
|
||||
function toCsv(ids: string[]): string {
|
||||
return ids.join(', ');
|
||||
}
|
||||
function fromCsv(text: string): string[] {
|
||||
return text.split(',').map((entry) => entry.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
interface LocalTag extends TagDashboard {
|
||||
clientKey: string;
|
||||
allowedRoleIdsText: string;
|
||||
allowedChannelIdsText: string;
|
||||
saving?: boolean;
|
||||
}
|
||||
|
||||
@@ -34,8 +27,8 @@ const EMPTY_DRAFT = {
|
||||
content: '',
|
||||
responseType: 'TEXT' as TagResponseType,
|
||||
triggerWord: '',
|
||||
allowedRoleIdsText: '',
|
||||
allowedChannelIdsText: ''
|
||||
allowedRoleIds: [] as string[],
|
||||
allowedChannelIds: [] as string[]
|
||||
};
|
||||
|
||||
interface TagsManagerProps {
|
||||
@@ -48,9 +41,7 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
|
||||
const [tags, setTags] = useState<LocalTag[]>(
|
||||
initialTags.map((tag, index) => ({
|
||||
...tag,
|
||||
clientKey: tag.id ?? `existing-${index}`,
|
||||
allowedRoleIdsText: toCsv(tag.allowedRoleIds),
|
||||
allowedChannelIdsText: toCsv(tag.allowedChannelIds)
|
||||
clientKey: tag.id ?? `existing-${index}`
|
||||
}))
|
||||
);
|
||||
const [draft, setDraft] = useState(EMPTY_DRAFT);
|
||||
@@ -71,8 +62,8 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
|
||||
content: draft.content.trim() || null,
|
||||
responseType: draft.responseType,
|
||||
triggerWord: draft.triggerWord.trim() || null,
|
||||
allowedRoleIds: fromCsv(draft.allowedRoleIdsText),
|
||||
allowedChannelIds: fromCsv(draft.allowedChannelIdsText)
|
||||
allowedRoleIds: draft.allowedRoleIds,
|
||||
allowedChannelIds: draft.allowedChannelIds
|
||||
})
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as (TagDashboard & { error?: string }) | null;
|
||||
@@ -83,9 +74,7 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
|
||||
setTags((prev) => [
|
||||
{
|
||||
...body,
|
||||
clientKey: body.id ?? `new-${Date.now()}`,
|
||||
allowedRoleIdsText: toCsv(body.allowedRoleIds),
|
||||
allowedChannelIdsText: toCsv(body.allowedChannelIds)
|
||||
clientKey: body.id ?? `new-${Date.now()}`
|
||||
},
|
||||
...prev
|
||||
]);
|
||||
@@ -109,8 +98,8 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
|
||||
content: tag.content,
|
||||
responseType: tag.responseType,
|
||||
triggerWord: tag.triggerWord,
|
||||
allowedRoleIds: fromCsv(tag.allowedRoleIdsText),
|
||||
allowedChannelIds: fromCsv(tag.allowedChannelIdsText)
|
||||
allowedRoleIds: tag.allowedRoleIds,
|
||||
allowedChannelIds: tag.allowedChannelIds
|
||||
})
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as (TagDashboard & { error?: string }) | null;
|
||||
@@ -184,11 +173,19 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tags.allowedRoleIds')}</Label>
|
||||
<Input value={draft.allowedRoleIdsText} onChange={(event) => setDraft((prev) => ({ ...prev, allowedRoleIdsText: event.target.value }))} placeholder={t('modulePages.tags.idsPlaceholder')} />
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={draft.allowedRoleIds}
|
||||
onChange={(allowedRoleIds) => setDraft((prev) => ({ ...prev, allowedRoleIds }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tags.allowedChannelIds')}</Label>
|
||||
<Input value={draft.allowedChannelIdsText} onChange={(event) => setDraft((prev) => ({ ...prev, allowedChannelIdsText: event.target.value }))} placeholder={t('modulePages.tags.idsPlaceholder')} />
|
||||
<DiscordChannelMultiSelect
|
||||
guildId={guildId}
|
||||
value={draft.allowedChannelIds}
|
||||
onChange={(allowedChannelIds) => setDraft((prev) => ({ ...prev, allowedChannelIds }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="button" onClick={handleCreate} disabled={creating}>
|
||||
@@ -242,11 +239,27 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tags.allowedRoleIds')}</Label>
|
||||
<Input value={tag.allowedRoleIdsText} onChange={(event) => setTags((prev) => prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, allowedRoleIdsText: event.target.value } : entry)))} />
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={tag.allowedRoleIds}
|
||||
onChange={(allowedRoleIds) =>
|
||||
setTags((prev) =>
|
||||
prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, allowedRoleIds } : entry))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tags.allowedChannelIds')}</Label>
|
||||
<Input value={tag.allowedChannelIdsText} onChange={(event) => setTags((prev) => prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, allowedChannelIdsText: event.target.value } : entry)))} />
|
||||
<DiscordChannelMultiSelect
|
||||
guildId={guildId}
|
||||
value={tag.allowedChannelIds}
|
||||
onChange={(allowedChannelIds) =>
|
||||
setTags((prev) =>
|
||||
prev.map((entry) => (entry.clientKey === tag.clientKey ? { ...entry, allowedChannelIds } : entry))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useId } from 'react';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -35,8 +36,6 @@ interface TempVoiceFormProps {
|
||||
|
||||
export function TempVoiceForm({ guildId, initialValue }: TempVoiceFormProps) {
|
||||
const t = useTranslations();
|
||||
const hubId = useId();
|
||||
const categoryId = useId();
|
||||
const nameId = useId();
|
||||
const limitId = useId();
|
||||
|
||||
@@ -58,14 +57,24 @@ export function TempVoiceForm({ guildId, initialValue }: TempVoiceFormProps) {
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<FieldAnchor id="field-tempvoice-hub">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={hubId}>{t('modulePages.tempvoice.hubChannelId')}</Label>
|
||||
<Input id={hubId} value={value.hubChannelId} onChange={(event) => setValue((prev) => ({ ...prev, hubChannelId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.tempvoice.hubChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
kinds={['voice']}
|
||||
value={value.hubChannelId}
|
||||
onChange={(hubChannelId) => setValue((prev) => ({ ...prev, hubChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-tempvoice-category">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={categoryId}>{t('modulePages.tempvoice.categoryId')}</Label>
|
||||
<Input id={categoryId} value={value.categoryId} onChange={(event) => setValue((prev) => ({ ...prev, categoryId: event.target.value.trim() }))} placeholder="123456789012345678" />
|
||||
<Label>{t('modulePages.tempvoice.categoryId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
kinds={['category']}
|
||||
value={value.categoryId}
|
||||
onChange={(categoryId) => setValue((prev) => ({ ...prev, categoryId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-tempvoice-name">
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
@@ -16,18 +17,7 @@ interface LocalCategory extends TicketCategoryDashboard {
|
||||
saving?: boolean;
|
||||
}
|
||||
|
||||
function toCsv(ids: string[]): string {
|
||||
return ids.join(', ');
|
||||
}
|
||||
|
||||
function fromCsv(text: string): string[] {
|
||||
return text
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
}
|
||||
|
||||
const EMPTY_DRAFT = { name: '', description: '', emoji: '', supportRoleIdsText: '' };
|
||||
const EMPTY_DRAFT = { name: '', description: '', emoji: '', supportRoleIds: [] as string[] };
|
||||
|
||||
interface TicketCategoriesManagerProps {
|
||||
guildId: string;
|
||||
@@ -56,7 +46,7 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
name: draft.name.trim(),
|
||||
description: draft.description.trim() || null,
|
||||
emoji: draft.emoji.trim() || null,
|
||||
supportRoleIds: fromCsv(draft.supportRoleIdsText)
|
||||
supportRoleIds: draft.supportRoleIds
|
||||
})
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as (TicketCategoryDashboard & { error?: string }) | null;
|
||||
@@ -178,18 +168,16 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tickets.supportRoleIds')}</Label>
|
||||
<Input
|
||||
value={toCsv(category.supportRoleIds)}
|
||||
onChange={(event) =>
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={category.supportRoleIds}
|
||||
onChange={(supportRoleIds) =>
|
||||
setCategories((prev) =>
|
||||
prev.map((entry) =>
|
||||
entry.clientKey === category.clientKey
|
||||
? { ...entry, supportRoleIds: fromCsv(event.target.value) }
|
||||
: entry
|
||||
entry.clientKey === category.clientKey ? { ...entry, supportRoleIds } : entry
|
||||
)
|
||||
)
|
||||
}
|
||||
placeholder={t('modulePages.tickets.idsPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
@@ -224,10 +212,10 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tickets.supportRoleIds')}</Label>
|
||||
<Input
|
||||
value={draft.supportRoleIdsText}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, supportRoleIdsText: event.target.value }))}
|
||||
placeholder={t('modulePages.tickets.idsPlaceholder')}
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={draft.supportRoleIds}
|
||||
onChange={(supportRoleIds) => setDraft((prev) => ({ ...prev, supportRoleIds }))}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={handleCreate} disabled={creating}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useId } from 'react';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
@@ -36,8 +37,6 @@ interface TicketConfigFormProps {
|
||||
|
||||
export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProps) {
|
||||
const t = useTranslations();
|
||||
const categoryId = useId();
|
||||
const logChannelId = useId();
|
||||
const inactivityId = useId();
|
||||
|
||||
return (
|
||||
@@ -80,24 +79,23 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
|
||||
|
||||
<FieldAnchor id="field-tickets-category">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={categoryId}>{t('modulePages.tickets.categoryId')}</Label>
|
||||
<Input
|
||||
id={categoryId}
|
||||
<Label>{t('modulePages.tickets.categoryId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
kinds={['category']}
|
||||
value={value.categoryId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, categoryId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(categoryId) => setValue((prev) => ({ ...prev, categoryId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
<FieldAnchor id="field-tickets-log">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={logChannelId}>{t('modulePages.tickets.logChannelId')}</Label>
|
||||
<Input
|
||||
id={logChannelId}
|
||||
<Label>{t('modulePages.tickets.logChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.logChannelId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, logChannelId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(logChannelId) => setValue((prev) => ({ ...prev, logChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { VerificationConfigDashboard } 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 { DiscordRoleSelect } 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';
|
||||
@@ -104,30 +106,30 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
|
||||
<FieldAnchor id="field-verify-channel">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.channelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.channelId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, channelId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(channelId) => setValue((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-role">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.verifiedRoleId')}</Label>
|
||||
<Input
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={value.verifiedRoleId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, verifiedRoleId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(verifiedRoleId) => setValue((prev) => ({ ...prev, verifiedRoleId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-unverified">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.unverifiedRoleId')}</Label>
|
||||
<Input
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={value.unverifiedRoleId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, unverifiedRoleId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(unverifiedRoleId) => setValue((prev) => ({ ...prev, unverifiedRoleId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { WelcomeConfigDashboard } from '@nexumi/shared';
|
||||
import { useId } from 'react';
|
||||
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 { 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';
|
||||
@@ -13,34 +14,19 @@ 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, 'userAutoroleIds' | 'botAutoroleIds' | 'welcomeEmbed' | 'leaveEmbed'> {
|
||||
userAutoroleIdsText: string;
|
||||
botAutoroleIdsText: string;
|
||||
interface LocalWelcomeValue extends Omit<WelcomeConfigDashboard, 'welcomeEmbed' | 'leaveEmbed'> {
|
||||
welcomeEmbedText: string;
|
||||
leaveEmbedText: string;
|
||||
}
|
||||
|
||||
function toCsv(ids: string[]): string {
|
||||
return ids.join(', ');
|
||||
}
|
||||
|
||||
function fromCsv(text: string): string[] {
|
||||
return text
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
}
|
||||
|
||||
function toJsonText(value: Record<string, unknown> | null | undefined): string {
|
||||
return value ? JSON.stringify(value, null, 2) : '';
|
||||
}
|
||||
|
||||
function toLocal(config: WelcomeConfigDashboard): LocalWelcomeValue {
|
||||
const { userAutoroleIds, botAutoroleIds, welcomeEmbed, leaveEmbed, ...rest } = config;
|
||||
const { welcomeEmbed, leaveEmbed, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
userAutoroleIdsText: toCsv(userAutoroleIds),
|
||||
botAutoroleIdsText: toCsv(botAutoroleIds),
|
||||
welcomeEmbedText: toJsonText(welcomeEmbed),
|
||||
leaveEmbedText: toJsonText(leaveEmbed)
|
||||
};
|
||||
@@ -71,12 +57,10 @@ async function saveWelcome(guildId: string, value: LocalWelcomeValue): Promise<S
|
||||
return { ok: false, error: leaveEmbed.error };
|
||||
}
|
||||
|
||||
const { userAutoroleIdsText, botAutoroleIdsText, welcomeEmbedText: _welcomeEmbedText, leaveEmbedText: _leaveEmbedText, ...rest } = value;
|
||||
const { welcomeEmbedText: _welcomeEmbedText, leaveEmbedText: _leaveEmbedText, ...rest } = value;
|
||||
|
||||
const payload: WelcomeConfigDashboard = {
|
||||
...rest,
|
||||
userAutoroleIds: fromCsv(userAutoroleIdsText),
|
||||
botAutoroleIds: fromCsv(botAutoroleIdsText),
|
||||
welcomeEmbed: welcomeEmbed.value,
|
||||
leaveEmbed: leaveEmbed.value
|
||||
};
|
||||
@@ -104,8 +88,6 @@ interface WelcomeFormProps {
|
||||
|
||||
export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
|
||||
const t = useTranslations();
|
||||
const userAutorolesId = useId();
|
||||
const botAutorolesId = useId();
|
||||
|
||||
return (
|
||||
<SettingsForm<LocalWelcomeValue> initialValue={toLocal(initialValue)} onSave={(value) => saveWelcome(guildId, value)}>
|
||||
@@ -130,10 +112,10 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
|
||||
<FieldAnchor id="field-welcome-channel">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.welcome.welcomeChannelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.welcomeChannelId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, welcomeChannelId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(welcomeChannelId) => setValue((prev) => ({ ...prev, welcomeChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
@@ -199,23 +181,21 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-welcome-user-autoroles">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={userAutorolesId}>{t('modulePages.welcome.userAutoroles')}</Label>
|
||||
<Textarea
|
||||
id={userAutorolesId}
|
||||
value={value.userAutoroleIdsText}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, userAutoroleIdsText: event.target.value }))}
|
||||
placeholder={t('modulePages.logging.idsPlaceholder')}
|
||||
<Label>{t('modulePages.welcome.userAutoroles')}</Label>
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={value.userAutoroleIds}
|
||||
onChange={(userAutoroleIds) => setValue((prev) => ({ ...prev, userAutoroleIds }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-welcome-bot-autoroles">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={botAutorolesId}>{t('modulePages.welcome.botAutoroles')}</Label>
|
||||
<Textarea
|
||||
id={botAutorolesId}
|
||||
value={value.botAutoroleIdsText}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, botAutoroleIdsText: event.target.value }))}
|
||||
placeholder={t('modulePages.logging.idsPlaceholder')}
|
||||
<Label>{t('modulePages.welcome.botAutoroles')}</Label>
|
||||
<DiscordRoleMultiSelect
|
||||
guildId={guildId}
|
||||
value={value.botAutoroleIds}
|
||||
onChange={(botAutoroleIds) => setValue((prev) => ({ ...prev, botAutoroleIds }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
@@ -260,10 +240,10 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
|
||||
<FieldAnchor id="field-leave-channel">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.welcome.leaveChannelId')}</Label>
|
||||
<Input
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.leaveChannelId}
|
||||
onChange={(event) => setValue((prev) => ({ ...prev, leaveChannelId: event.target.value.trim() }))}
|
||||
placeholder="123456789012345678"
|
||||
onChange={(leaveChannelId) => setValue((prev) => ({ ...prev, leaveChannelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
Reference in New Issue
Block a user