Add new features and enhancements to the WebUI and dashboard components

- Updated package.json to include new dependencies: `@radix-ui/react-dialog` and `cmdk`.
- Enhanced global styles in globals.css for improved visual depth in dark mode.
- Refactored dashboard page components to improve layout and accessibility, including new icons and responsive design adjustments.
- Implemented suspense handling in commands page for better loading states.
- Improved sidebar navigation with new icons and mobile responsiveness.
- Added FieldAnchor components across various forms for better accessibility and structure.
- Enhanced commands manager with search parameter handling for improved user experience.
- Updated multiple module forms to include FieldAnchor for consistent layout and accessibility.
This commit is contained in:
TheOnlyMace
2026-07-22 19:44:55 +02:00
parent f1d74f922d
commit 75ac91eecc
49 changed files with 2393 additions and 320 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import type { AutomodConfigDashboard } 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 { Input } from '@/components/ui/input';
@@ -44,19 +45,22 @@ export function AutomodForm({ guildId, initialValue }: AutomodFormProps) {
<CardDescription>{t('modulePages.automod.description')}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.automod.enabledLabel')}</Label>
<p className="text-sm text-muted-foreground">{t('modulePages.automod.enabledHint')}</p>
<FieldAnchor id="field-automod-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.automod.enabledLabel')}</Label>
<p className="text-sm text-muted-foreground">{t('modulePages.automod.enabledHint')}</p>
</div>
<Switch
checked={value.enabled}
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
<Switch
checked={value.enabled}
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
<FieldAnchor id="field-anti-raid">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.automod.antiRaidTitle')}</CardTitle>
@@ -97,7 +101,9 @@ export function AutomodForm({ guildId, initialValue }: AutomodFormProps) {
<p className="text-sm text-muted-foreground">{t('modulePages.automod.antiRaidActionHint')}</p>
</CardContent>
</Card>
</FieldAnchor>
<FieldAnchor id="field-anti-nuke">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.automod.antiNukeTitle')}</CardTitle>
@@ -146,18 +152,21 @@ export function AutomodForm({ guildId, initialValue }: AutomodFormProps) {
/>
</div>
</div>
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.automod.lockdownActive')}</Label>
<p className="text-sm text-muted-foreground">{t('modulePages.automod.lockdownActiveHint')}</p>
<FieldAnchor id="field-lockdown">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.automod.lockdownActive')}</Label>
<p className="text-sm text-muted-foreground">{t('modulePages.automod.lockdownActiveHint')}</p>
</div>
<Switch
checked={value.lockdownActive}
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, lockdownActive: checked }))}
/>
</div>
<Switch
checked={value.lockdownActive}
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, lockdownActive: checked }))}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
</FieldAnchor>
</div>
)}
</SettingsForm>

View File

@@ -2,6 +2,7 @@
import type { BirthdayConfigDashboard } 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';
@@ -48,28 +49,38 @@ export function BirthdaysForm({ guildId, initialValue }: BirthdaysFormProps) {
<CardDescription>{t('modulePages.birthdays.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-birthdays-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.birthdays.enabledLabel')}</Label>
<Switch checked={value.enabled} onCheckedChange={(enabled) => setValue((prev) => ({ ...prev, enabled }))} />
</div>
</FieldAnchor>
<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" />
</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" />
</div>
</FieldAnchor>
<FieldAnchor id="field-birthdays-timezone">
<div className="space-y-2">
<Label htmlFor={timezoneId}>{t('modulePages.birthdays.timezone')}</Label>
<Input id={timezoneId} value={value.timezone} onChange={(event) => setValue((prev) => ({ ...prev, timezone: event.target.value }))} placeholder="Europe/Berlin" />
</div>
</FieldAnchor>
</div>
<FieldAnchor id="field-birthdays-message">
<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')} />
</div>
</FieldAnchor>
</CardContent>
</Card>
)}

View File

@@ -3,6 +3,7 @@
import type { CaseDashboard } from '@nexumi/shared';
import { Search } from 'lucide-react';
import { useCallback, useState } 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';
@@ -48,6 +49,7 @@ export function CasesTable({ guildId, initialCases }: CasesTableProps) {
}, [action, guildId, search, t]);
return (
<FieldAnchor id="field-mod-cases">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.moderation.casesTitle')}</CardTitle>
@@ -126,5 +128,6 @@ export function CasesTable({ guildId, initialCases }: CasesTableProps) {
)}
</CardContent>
</Card>
</FieldAnchor>
);
}

View File

@@ -2,7 +2,8 @@
import type { CommandOverrideDashboard } from '@nexumi/shared';
import { RotateCcw } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner';
import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
@@ -32,9 +33,17 @@ interface CommandsManagerProps {
export function CommandsManager({ guildId, initialCommands }: CommandsManagerProps) {
const t = useTranslations();
const searchParams = useSearchParams();
const [commands, setCommands] = useState<LocalOverride[]>(initialCommands);
const [search, setSearch] = useState('');
useEffect(() => {
const q = searchParams.get('q');
if (q) {
setSearch(q);
}
}, [searchParams]);
const filtered = useMemo(() => {
const query = search.trim().toLowerCase();
if (!query) {
@@ -64,7 +73,9 @@ export function CommandsManager({ guildId, initialCommands }: CommandsManagerPro
cooldownSeconds: entry.cooldownSeconds
})
});
const body = (await response.json().catch(() => null)) as (CommandOverrideDashboard & { error?: string }) | null;
const body = (await response.json().catch(() => null)) as
| (CommandOverrideDashboard & { error?: string })
| null;
if (!response.ok || !body) {
toast.error(body?.error ?? t('common.saveError'));
return;
@@ -81,7 +92,9 @@ export function CommandsManager({ guildId, initialCommands }: CommandsManagerPro
async function handleReset(entry: LocalOverride) {
patchLocal(entry.commandName, { saving: true });
try {
const response = await fetch(`/api/guilds/${guildId}/commands/${entry.commandName}`, { method: 'DELETE' });
const response = await fetch(`/api/guilds/${guildId}/commands/${entry.commandName}`, {
method: 'DELETE'
});
const body = (await response.json().catch(() => null)) as CommandOverrideDashboard | null;
if (!response.ok || !body) {
toast.error(t('common.saveError'));
@@ -147,7 +160,9 @@ export function CommandsManager({ guildId, initialCommands }: CommandsManagerPro
<p className="text-xs text-muted-foreground">{t('modulePages.commands.allowedRoleIds')}</p>
<Input
value={toCsv(entry.allowedRoleIds)}
onChange={(event) => patchLocal(entry.commandName, { allowedRoleIds: fromCsv(event.target.value) })}
onChange={(event) =>
patchLocal(entry.commandName, { allowedRoleIds: fromCsv(event.target.value) })
}
placeholder={t('modulePages.commands.idsPlaceholder')}
/>
</div>
@@ -155,7 +170,9 @@ export function CommandsManager({ guildId, initialCommands }: CommandsManagerPro
<p className="text-xs text-muted-foreground">{t('modulePages.commands.deniedRoleIds')}</p>
<Input
value={toCsv(entry.deniedRoleIds)}
onChange={(event) => patchLocal(entry.commandName, { deniedRoleIds: fromCsv(event.target.value) })}
onChange={(event) =>
patchLocal(entry.commandName, { deniedRoleIds: fromCsv(event.target.value) })
}
placeholder={t('modulePages.commands.idsPlaceholder')}
/>
</div>
@@ -164,7 +181,9 @@ export function CommandsManager({ guildId, initialCommands }: CommandsManagerPro
<Input
value={toCsv(entry.allowedChannelIds)}
onChange={(event) =>
patchLocal(entry.commandName, { allowedChannelIds: fromCsv(event.target.value) })
patchLocal(entry.commandName, {
allowedChannelIds: fromCsv(event.target.value)
})
}
placeholder={t('modulePages.commands.idsPlaceholder')}
/>
@@ -174,18 +193,32 @@ export function CommandsManager({ guildId, initialCommands }: CommandsManagerPro
<Input
value={toCsv(entry.deniedChannelIds)}
onChange={(event) =>
patchLocal(entry.commandName, { deniedChannelIds: fromCsv(event.target.value) })
patchLocal(entry.commandName, {
deniedChannelIds: fromCsv(event.target.value)
})
}
placeholder={t('modulePages.commands.idsPlaceholder')}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm" disabled={entry.saving} onClick={() => handleReset(entry)}>
<Button
type="button"
variant="ghost"
size="sm"
disabled={entry.saving}
onClick={() => handleReset(entry)}
>
<RotateCcw className="size-4" />
{t('modulePages.commands.reset')}
</Button>
<Button type="button" variant="outline" size="sm" disabled={entry.saving} onClick={() => handleSave(entry)}>
<Button
type="button"
variant="outline"
size="sm"
disabled={entry.saving}
onClick={() => handleSave(entry)}
>
{entry.saving ? t('common.saving') : t('common.save')}
</Button>
</div>

View File

@@ -1,6 +1,7 @@
'use client';
import type { EconomyConfigDashboard } 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 { Input } from '@/components/ui/input';
@@ -44,6 +45,7 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
<CardDescription>{t('modulePages.economy.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-economy-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.economy.enabledLabel')}</Label>
<Switch
@@ -51,7 +53,9 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
</FieldAnchor>
<div className="grid gap-4 sm:grid-cols-2">
<FieldAnchor id="field-economy-currency-name">
<div className="space-y-2">
<Label>{t('modulePages.economy.currencyName')}</Label>
<Input
@@ -59,6 +63,8 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, currencyName: event.target.value }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-economy-currency-symbol">
<div className="space-y-2">
<Label>{t('modulePages.economy.currencySymbol')}</Label>
<Input
@@ -66,6 +72,7 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, currencySymbol: event.target.value }))}
/>
</div>
</FieldAnchor>
</div>
</CardContent>
</Card>
@@ -75,6 +82,7 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
<CardTitle>{t('modulePages.economy.amountsTitle')}</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<FieldAnchor id="field-economy-daily">
<div className="space-y-2">
<Label>{t('modulePages.economy.dailyAmount')}</Label>
<Input
@@ -84,6 +92,8 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, dailyAmount: Number(event.target.value) || 0 }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-economy-weekly">
<div className="space-y-2">
<Label>{t('modulePages.economy.weeklyAmount')}</Label>
<Input
@@ -93,6 +103,8 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, weeklyAmount: Number(event.target.value) || 0 }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-economy-work-min">
<div className="space-y-2">
<Label>{t('modulePages.economy.workMin')}</Label>
<Input
@@ -102,6 +114,8 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, workMin: Number(event.target.value) || 0 }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-economy-work-max">
<div className="space-y-2">
<Label>{t('modulePages.economy.workMax')}</Label>
<Input
@@ -111,6 +125,8 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, workMax: Number(event.target.value) || 0 }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-economy-work-cooldown">
<div className="space-y-2">
<Label>{t('modulePages.economy.workCooldownSeconds')}</Label>
<Input
@@ -122,6 +138,8 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-economy-starting">
<div className="space-y-2">
<Label>{t('modulePages.economy.startingBalance')}</Label>
<Input
@@ -133,6 +151,7 @@ export function EconomyForm({ guildId, initialValue }: EconomyFormProps) {
}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
</div>

View File

@@ -4,6 +4,7 @@ import type { SocialFeedDashboard } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -152,6 +153,7 @@ export function FeedsManager({ guildId, initialFeeds }: FeedsManagerProps) {
</div>
))}
<FieldAnchor id="field-feeds-add">
<div className="space-y-3 rounded-md border border-dashed border-border p-4">
<p className="text-sm font-medium">{t('modulePages.feeds.addFeed')}</p>
<div className="grid gap-3 sm:grid-cols-2">
@@ -208,6 +210,7 @@ export function FeedsManager({ guildId, initialFeeds }: FeedsManagerProps) {
{t('modulePages.feeds.addFeed')}
</Button>
</div>
</FieldAnchor>
</CardContent>
</Card>
);

View File

@@ -1,6 +1,7 @@
'use client';
import type { FunConfigDashboard } 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 { Label } from '@/components/ui/label';
@@ -42,6 +43,7 @@ export function FunForm({ guildId, initialValue }: FunFormProps) {
<CardDescription>{t('modulePages.fun.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-fun-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.fun.enabledLabel')}</Label>
@@ -52,6 +54,8 @@ export function FunForm({ guildId, initialValue }: FunFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-fun-media">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.fun.mediaEnabledLabel')}</Label>
@@ -62,6 +66,7 @@ export function FunForm({ guildId, initialValue }: FunFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, mediaEnabled: checked }))}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
)}

View File

@@ -4,6 +4,7 @@ import type { GiveawayDashboard } from '@nexumi/shared';
import { Pause, Play, Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -116,6 +117,7 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
return (
<div className="space-y-6">
<FieldAnchor id="field-giveaways-create">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.giveaways.createTitle')}</CardTitle>
@@ -187,6 +189,9 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
</Button>
</CardContent>
</Card>
</FieldAnchor>
<FieldAnchor id="field-giveaways-list">
<Card>
<CardHeader>
@@ -281,6 +286,7 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
})}
</CardContent>
</Card>
</FieldAnchor>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import type { GuildBackupDashboard } from '@nexumi/shared';
import { Download, Info, Plus, RotateCcw, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -130,6 +131,7 @@ export function GuildBackupManager({ guildId, initialBackups, isOwner }: GuildBa
}
return (
<FieldAnchor id="field-backup">
<div className="space-y-6">
<Card>
<CardHeader>
@@ -211,5 +213,6 @@ export function GuildBackupManager({ guildId, initialBackups, isOwner }: GuildBa
</CardContent>
</Card>
</div>
</FieldAnchor>
);
}

View File

@@ -2,6 +2,7 @@
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 { Input } from '@/components/ui/input';
@@ -126,6 +127,7 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
<CardDescription>{t('modulePages.leveling.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-leveling-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.leveling.enabledLabel')}</Label>
<Switch
@@ -133,8 +135,10 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
</FieldAnchor>
<div className="grid gap-4 sm:grid-cols-3">
<FieldAnchor id="field-leveling-text-min">
<div className="space-y-2">
<Label>{t('modulePages.leveling.textXpMin')}</Label>
<Input
@@ -144,6 +148,8 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, textXpMin: Number(event.target.value) || 1 }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-leveling-text-max">
<div className="space-y-2">
<Label>{t('modulePages.leveling.textXpMax')}</Label>
<Input
@@ -153,6 +159,8 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, textXpMax: Number(event.target.value) || 1 }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-leveling-cooldown">
<div className="space-y-2">
<Label>{t('modulePages.leveling.textCooldownSeconds')}</Label>
<Input
@@ -164,8 +172,10 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
}
/>
</div>
</FieldAnchor>
</div>
<FieldAnchor id="field-leveling-voice">
<div className="space-y-2">
<Label>{t('modulePages.leveling.voiceXpPerMinute')}</Label>
<Input
@@ -178,7 +188,9 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-leveling-no-xp-channels">
<div className="space-y-2">
<Label htmlFor={noXpChannelsId}>{t('modulePages.leveling.noXpChannels')}</Label>
<Textarea
@@ -188,6 +200,8 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
placeholder={t('modulePages.logging.idsPlaceholder')}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-leveling-no-xp-roles">
<div className="space-y-2">
<Label htmlFor={noXpRolesId}>{t('modulePages.leveling.noXpRoles')}</Label>
<Textarea
@@ -197,8 +211,10 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
placeholder={t('modulePages.logging.idsPlaceholder')}
/>
</div>
</FieldAnchor>
<div className="grid gap-4 sm:grid-cols-2">
<FieldAnchor id="field-leveling-role-multipliers">
<div className="space-y-2">
<Label>{t('modulePages.leveling.roleMultipliers')}</Label>
<Textarea
@@ -210,6 +226,8 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
/>
<p className="text-xs text-muted-foreground">{t('modulePages.leveling.multiplierHint')}</p>
</div>
</FieldAnchor>
<FieldAnchor id="field-leveling-channel-multipliers">
<div className="space-y-2">
<Label>{t('modulePages.leveling.channelMultipliers')}</Label>
<Textarea
@@ -221,10 +239,12 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
/>
<p className="text-xs text-muted-foreground">{t('modulePages.leveling.multiplierHint')}</p>
</div>
</FieldAnchor>
</div>
</CardContent>
</Card>
<FieldAnchor id="field-leveling-levelup">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.leveling.levelUpTitle')}</CardTitle>
@@ -266,6 +286,7 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
placeholder="Congrats {user}, you reached level {level}!"
/>
</div>
<FieldAnchor id="field-leveling-stack">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.leveling.stackRewards')}</Label>
<Switch
@@ -273,9 +294,12 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, stackRewards: checked }))}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
</FieldAnchor>
<FieldAnchor id="field-leveling-rank-card">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.leveling.rankCardTitle')}</CardTitle>
@@ -313,6 +337,7 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) {
</div>
</CardContent>
</Card>
</FieldAnchor>
</div>
)}
</SettingsForm>

View File

@@ -3,6 +3,7 @@
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';
@@ -129,6 +130,7 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
<CardDescription>{t('modulePages.logging.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-logging-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.logging.enabledLabel')}</Label>
<Switch
@@ -136,6 +138,8 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-logging-ignore-bots">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.logging.ignoreBotsLabel')}</Label>
<Switch
@@ -143,6 +147,8 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, ignoreBots: checked }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-logging-ignored-channels">
<div className="space-y-2">
<Label htmlFor={ignoredChannelsId}>{t('modulePages.logging.ignoredChannelsLabel')}</Label>
<Textarea
@@ -153,6 +159,8 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
/>
<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
@@ -163,6 +171,8 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
/>
<p className="text-xs text-muted-foreground">{t('modulePages.logging.idsHint')}</p>
</div>
</FieldAnchor>
<FieldAnchor id="field-logging-retention">
<div className="space-y-2">
<Label htmlFor="logging-retention">{t('modulePages.logging.retentionLabel')}</Label>
<Input
@@ -178,9 +188,11 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
/>
<p className="text-xs text-muted-foreground">{t('modulePages.logging.retentionHint')}</p>
</div>
</FieldAnchor>
</CardContent>
</Card>
<FieldAnchor id="field-logging-channels">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.logging.channelsTitle')}</CardTitle>
@@ -268,6 +280,7 @@ export function LoggingForm({ guildId, initialValue }: LoggingFormProps) {
</Button>
</CardContent>
</Card>
</FieldAnchor>
</div>
)}
</SettingsForm>

View File

@@ -3,6 +3,7 @@
import type { EscalationAction, EscalationRuleDashboard, ModerationDashboard } 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';
@@ -160,71 +161,75 @@ export function ModerationForm({ guildId, initialValue }: ModerationFormProps) {
<CardDescription>{t('modulePages.moderation.description')}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.moderation.enabledLabel')}</Label>
<p className="text-sm text-muted-foreground">{t('modulePages.moderation.enabledHint')}</p>
<FieldAnchor id="field-mod-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.moderation.enabledLabel')}</Label>
<p className="text-sm text-muted-foreground">{t('modulePages.moderation.enabledHint')}</p>
</div>
<Switch
checked={value.moderationEnabled}
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, moderationEnabled: checked }))}
/>
</div>
<Switch
checked={value.moderationEnabled}
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, moderationEnabled: checked }))}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('modulePages.moderation.escalationsTitle')}</CardTitle>
<CardDescription>{t('modulePages.moderation.escalationsDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{value.escalations.length === 0 && (
<p className="text-sm text-muted-foreground">{t('modulePages.moderation.noEscalations')}</p>
)}
{value.escalations.map((rule) => (
<EscalationRow
key={rule.clientKey}
rule={rule}
onChange={(updated) =>
<FieldAnchor id="field-mod-escalations">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.moderation.escalationsTitle')}</CardTitle>
<CardDescription>{t('modulePages.moderation.escalationsDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{value.escalations.length === 0 && (
<p className="text-sm text-muted-foreground">{t('modulePages.moderation.noEscalations')}</p>
)}
{value.escalations.map((rule) => (
<EscalationRow
key={rule.clientKey}
rule={rule}
onChange={(updated) =>
setValue((prev) => ({
...prev,
escalations: prev.escalations.map((entry) => (entry.clientKey === rule.clientKey ? updated : entry))
}))
}
onRemove={() =>
setValue((prev) => ({
...prev,
escalations: prev.escalations.filter((entry) => entry.clientKey !== rule.clientKey)
}))
}
/>
))}
<Button
type="button"
variant="outline"
onClick={() =>
setValue((prev) => ({
...prev,
escalations: prev.escalations.map((entry) => (entry.clientKey === rule.clientKey ? updated : entry))
escalations: [
...prev.escalations,
{
clientKey: `new-${Date.now()}-${prev.escalations.length}`,
warnCount: prev.escalations.length + 1,
action: 'TIMEOUT',
durationMs: 3_600_000,
reason: null,
enabled: true
}
]
}))
}
onRemove={() =>
setValue((prev) => ({
...prev,
escalations: prev.escalations.filter((entry) => entry.clientKey !== rule.clientKey)
}))
}
/>
))}
<Button
type="button"
variant="outline"
onClick={() =>
setValue((prev) => ({
...prev,
escalations: [
...prev.escalations,
{
clientKey: `new-${Date.now()}-${prev.escalations.length}`,
warnCount: prev.escalations.length + 1,
action: 'TIMEOUT',
durationMs: 3_600_000,
reason: null,
enabled: true
}
]
}))
}
>
<Plus className="size-4" />
{t('modulePages.moderation.addEscalation')}
</Button>
</CardContent>
</Card>
>
<Plus className="size-4" />
{t('modulePages.moderation.addEscalation')}
</Button>
</CardContent>
</Card>
</FieldAnchor>
</div>
)}
</SettingsForm>

View File

@@ -4,6 +4,7 @@ import type { ScheduledMessageDashboard } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -97,6 +98,7 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
return (
<div className="space-y-6">
<FieldAnchor id="field-scheduler-create">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.scheduler.createTitle')}</CardTitle>
@@ -153,6 +155,7 @@ export function SchedulerManager({ guildId, initialSchedules }: SchedulerManager
</Button>
</CardContent>
</Card>
</FieldAnchor>
<Card>
<CardHeader>

View File

@@ -4,6 +4,7 @@ import type { SelfRoleBehavior, SelfRoleEntry, SelfRoleMode, SelfRolePanelDashbo
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -153,6 +154,7 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro
return (
<div className="space-y-6">
<FieldAnchor id="field-selfroles-create">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.selfroles.createTitle')}</CardTitle>
@@ -219,6 +221,9 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro
</Button>
</CardContent>
</Card>
</FieldAnchor>
<FieldAnchor id="field-selfroles-list">
<Card>
<CardHeader>
@@ -313,6 +318,7 @@ export function SelfRolesManager({ guildId, initialPanels }: SelfRolesManagerPro
))}
</CardContent>
</Card>
</FieldAnchor>
</div>
);
}

View File

@@ -2,6 +2,7 @@
import type { StarboardConfigDashboard } 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';
@@ -83,33 +84,45 @@ export function StarboardForm({ guildId, initialValue }: StarboardFormProps) {
<CardDescription>{t('modulePages.starboard.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-starboard-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.starboard.enabledLabel')}</Label>
<Switch checked={value.enabled} onCheckedChange={(enabled) => setValue((prev) => ({ ...prev, enabled }))} />
</div>
</FieldAnchor>
<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" />
</div>
</FieldAnchor>
<FieldAnchor id="field-starboard-emoji">
<div className="space-y-2">
<Label htmlFor={emojiId}>{t('modulePages.starboard.emoji')}</Label>
<Input id={emojiId} value={value.emoji} onChange={(event) => setValue((prev) => ({ ...prev, emoji: event.target.value }))} />
</div>
</FieldAnchor>
<FieldAnchor id="field-starboard-threshold">
<div className="space-y-2">
<Label htmlFor={thresholdId}>{t('modulePages.starboard.threshold')}</Label>
<Input id={thresholdId} type="number" min={1} value={value.threshold} onChange={(event) => setValue((prev) => ({ ...prev, threshold: Number(event.target.value) || 1 }))} />
</div>
</FieldAnchor>
</div>
<FieldAnchor id="field-starboard-self">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.starboard.allowSelfStar')}</Label>
<Switch checked={value.allowSelfStar} onCheckedChange={(allowSelfStar) => setValue((prev) => ({ ...prev, allowSelfStar }))} />
</div>
</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>
</div>
</FieldAnchor>
</CardContent>
</Card>
)}

View File

@@ -2,6 +2,7 @@
import type { StatsConfigDashboard } 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';
@@ -50,31 +51,39 @@ export function StatsForm({ guildId, initialValue }: StatsFormProps) {
<CardDescription>{t('modulePages.stats.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-stats-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.stats.enabledLabel')}</Label>
<Switch checked={value.enabled} onCheckedChange={(enabled) => setValue((prev) => ({ ...prev, enabled }))} />
</div>
</FieldAnchor>
<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" />
</div>
</FieldAnchor>
<div className="space-y-2">
<Label htmlFor={membersTemplateId}>{t('modulePages.stats.membersTemplate')}</Label>
<Input id={membersTemplateId} value={value.membersTemplate} onChange={(event) => setValue((prev) => ({ ...prev, membersTemplate: event.target.value }))} />
</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" />
</div>
</FieldAnchor>
<div className="space-y-2">
<Label htmlFor={onlineTemplateId}>{t('modulePages.stats.onlineTemplate')}</Label>
<Input id={onlineTemplateId} value={value.onlineTemplate} onChange={(event) => setValue((prev) => ({ ...prev, onlineTemplate: event.target.value }))} />
</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" />
</div>
</FieldAnchor>
<div className="space-y-2">
<Label htmlFor={boostsTemplateId}>{t('modulePages.stats.boostsTemplate')}</Label>
<Input id={boostsTemplateId} value={value.boostsTemplate} onChange={(event) => setValue((prev) => ({ ...prev, boostsTemplate: event.target.value }))} />

View File

@@ -2,6 +2,7 @@
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';
@@ -53,24 +54,33 @@ export function SuggestionsConfigForm({ guildId, initialValue }: SuggestionsConf
<CardDescription>{t('modulePages.suggestions.configDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-suggestions-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.suggestions.enabledLabel')}</Label>
<Switch checked={value.enabled} onCheckedChange={(enabled) => setValue((prev) => ({ ...prev, enabled }))} />
</div>
</FieldAnchor>
<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" />
</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')} />
</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')} />
</div>
</FieldAnchor>
</div>
<FieldAnchor id="field-suggestions-thread">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.suggestions.createThread')}</Label>
@@ -78,6 +88,7 @@ export function SuggestionsConfigForm({ guildId, initialValue }: SuggestionsConf
</div>
<Switch checked={value.createThread} onCheckedChange={(createThread) => setValue((prev) => ({ ...prev, createThread }))} />
</div>
</FieldAnchor>
</CardContent>
</Card>
)}

View File

@@ -3,6 +3,7 @@
import type { SuggestionAction, SuggestionDashboard } from '@nexumi/shared';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Badge, type BadgeProps } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -82,6 +83,7 @@ export function SuggestionsListManager({ guildId, initialSuggestions }: Suggesti
}
return (
<FieldAnchor id="field-suggestions-list">
<Card>
<CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
@@ -174,5 +176,6 @@ export function SuggestionsListManager({ guildId, initialSuggestions }: Suggesti
))}
</CardContent>
</Card>
</FieldAnchor>
);
}

View File

@@ -4,6 +4,7 @@ import type { TagDashboard, TagResponseType } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -144,6 +145,7 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
return (
<div className="space-y-6">
<FieldAnchor id="field-tags-create">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.tags.createTitle')}</CardTitle>
@@ -195,6 +197,9 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
</Button>
</CardContent>
</Card>
</FieldAnchor>
<FieldAnchor id="field-tags-list">
<Card>
<CardHeader>
@@ -256,6 +261,7 @@ export function TagsManager({ guildId, initialTags }: TagsManagerProps) {
))}
</CardContent>
</Card>
</FieldAnchor>
</div>
);
}

View File

@@ -2,6 +2,7 @@
import type { TempVoiceConfigDashboard } 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';
@@ -48,27 +49,37 @@ export function TempVoiceForm({ guildId, initialValue }: TempVoiceFormProps) {
<CardDescription>{t('modulePages.tempvoice.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-tempvoice-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.tempvoice.enabledLabel')}</Label>
<Switch checked={value.enabled} onCheckedChange={(enabled) => setValue((prev) => ({ ...prev, enabled }))} />
</div>
</FieldAnchor>
<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" />
</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" />
</div>
</FieldAnchor>
<FieldAnchor id="field-tempvoice-name">
<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" />
</div>
</FieldAnchor>
<FieldAnchor id="field-tempvoice-limit">
<div className="space-y-2">
<Label htmlFor={limitId}>{t('modulePages.tempvoice.defaultLimit')}</Label>
<Input id={limitId} type="number" min={0} max={99} value={value.defaultLimit} onChange={(event) => setValue((prev) => ({ ...prev, defaultLimit: Number(event.target.value) || 0 }))} />
</div>
</FieldAnchor>
</div>
<p className="text-xs text-muted-foreground">{t('modulePages.tempvoice.defaultLimitHint')}</p>
</CardContent>

View File

@@ -4,6 +4,7 @@ import type { TicketCategoryDashboard } from '@nexumi/shared';
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FieldAnchor } from '@/components/layout/field-anchor';
import { useTranslations } from '@/components/locale-provider';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -120,6 +121,7 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
}
return (
<FieldAnchor id="field-tickets-categories">
<Card>
<CardHeader>
<CardTitle>{t('modulePages.tickets.categoriesTitle')}</CardTitle>
@@ -235,5 +237,6 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
</div>
</CardContent>
</Card>
</FieldAnchor>
);
}

View File

@@ -2,6 +2,7 @@
import type { TicketConfigDashboard } 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';
@@ -51,6 +52,7 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
<CardDescription>{t('modulePages.tickets.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-tickets-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.tickets.enabledLabel')}</Label>
<Switch
@@ -58,8 +60,10 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
onCheckedChange={(enabled) => setValue((prev) => ({ ...prev, enabled }))}
/>
</div>
</FieldAnchor>
<div className="grid gap-4 sm:grid-cols-2">
<FieldAnchor id="field-tickets-mode">
<div className="space-y-2">
<Label>{t('modulePages.tickets.mode')}</Label>
<Select value={value.mode} onValueChange={(mode) => setValue((prev) => ({ ...prev, mode: mode as TicketConfigDashboard['mode'] }))}>
@@ -72,7 +76,9 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
</SelectContent>
</Select>
</div>
</FieldAnchor>
<FieldAnchor id="field-tickets-category">
<div className="space-y-2">
<Label htmlFor={categoryId}>{t('modulePages.tickets.categoryId')}</Label>
<Input
@@ -82,7 +88,9 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
placeholder="123456789012345678"
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-tickets-log">
<div className="space-y-2">
<Label htmlFor={logChannelId}>{t('modulePages.tickets.logChannelId')}</Label>
<Input
@@ -92,7 +100,9 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
placeholder="123456789012345678"
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-tickets-inactivity">
<div className="space-y-2">
<Label htmlFor={inactivityId}>{t('modulePages.tickets.inactivityHours')}</Label>
<Input
@@ -105,8 +115,10 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
}
/>
</div>
</FieldAnchor>
</div>
<FieldAnchor id="field-tickets-transcript">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<div className="space-y-0.5">
<Label>{t('modulePages.tickets.transcriptDm')}</Label>
@@ -117,7 +129,9 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
onCheckedChange={(transcriptDm) => setValue((prev) => ({ ...prev, transcriptDm }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-tickets-rating">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.tickets.ratingEnabled')}</Label>
<Switch
@@ -125,6 +139,7 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
onCheckedChange={(ratingEnabled) => setValue((prev) => ({ ...prev, ratingEnabled }))}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
)}

View File

@@ -1,6 +1,7 @@
'use client';
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 { Input } from '@/components/ui/input';
@@ -47,6 +48,7 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
<CardDescription>{t('modulePages.verification.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-verify-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.verification.enabledLabel')}</Label>
<Switch
@@ -54,8 +56,10 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
</FieldAnchor>
<div className="grid gap-4 sm:grid-cols-2">
<FieldAnchor id="field-verify-mode">
<div className="space-y-2">
<Label>{t('modulePages.verification.mode')}</Label>
<Select
@@ -73,6 +77,8 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
</SelectContent>
</Select>
</div>
</FieldAnchor>
<FieldAnchor id="field-verify-fail">
<div className="space-y-2">
<Label>{t('modulePages.verification.failAction')}</Label>
<Select
@@ -91,9 +97,11 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
</SelectContent>
</Select>
</div>
</FieldAnchor>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<FieldAnchor id="field-verify-channel">
<div className="space-y-2">
<Label>{t('modulePages.verification.channelId')}</Label>
<Input
@@ -102,6 +110,8 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
placeholder="123456789012345678"
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-verify-role">
<div className="space-y-2">
<Label>{t('modulePages.verification.verifiedRoleId')}</Label>
<Input
@@ -110,6 +120,8 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
placeholder="123456789012345678"
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-verify-unverified">
<div className="space-y-2">
<Label>{t('modulePages.verification.unverifiedRoleId')}</Label>
<Input
@@ -118,8 +130,10 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
placeholder="123456789012345678"
/>
</div>
</FieldAnchor>
</div>
<FieldAnchor id="field-verify-age">
<div className="space-y-2">
<Label>{t('modulePages.verification.minAccountAgeDays')}</Label>
<Input
@@ -132,6 +146,7 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
}
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
)}

View File

@@ -2,6 +2,7 @@
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 { Input } from '@/components/ui/input';
@@ -116,6 +117,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
<CardDescription>{t('modulePages.welcome.welcomeDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-welcome-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.welcome.welcomeEnabledLabel')}</Label>
<Switch
@@ -123,7 +125,9 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, welcomeEnabled: checked }))}
/>
</div>
</FieldAnchor>
<div className="grid gap-4 sm:grid-cols-2">
<FieldAnchor id="field-welcome-channel">
<div className="space-y-2">
<Label>{t('modulePages.welcome.welcomeChannelId')}</Label>
<Input
@@ -132,6 +136,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder="123456789012345678"
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-type">
<div className="space-y-2">
<Label>{t('modulePages.welcome.welcomeType')}</Label>
<Select
@@ -150,7 +156,9 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
</SelectContent>
</Select>
</div>
</FieldAnchor>
</div>
<FieldAnchor id="field-welcome-content">
<div className="space-y-2">
<Label>{t('modulePages.welcome.welcomeContent')}</Label>
<Textarea
@@ -160,6 +168,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
/>
<p className="text-xs text-muted-foreground">{t('modulePages.welcome.placeholdersHint')}</p>
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-embed">
<div className="space-y-2">
<Label>{t('modulePages.welcome.welcomeEmbed')}</Label>
<Textarea
@@ -170,6 +180,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder='{ "title": "Welcome!", "color": 6591981 }'
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-dm">
<div className="space-y-2">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.welcome.welcomeDmEnabled')}</Label>
@@ -184,6 +196,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder={t('modulePages.welcome.dmContentPlaceholder')}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-user-autoroles">
<div className="space-y-2">
<Label htmlFor={userAutorolesId}>{t('modulePages.welcome.userAutoroles')}</Label>
<Textarea
@@ -193,6 +207,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder={t('modulePages.logging.idsPlaceholder')}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-bot-autoroles">
<div className="space-y-2">
<Label htmlFor={botAutorolesId}>{t('modulePages.welcome.botAutoroles')}</Label>
<Textarea
@@ -202,7 +218,9 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder={t('modulePages.logging.idsPlaceholder')}
/>
</div>
</FieldAnchor>
<div className="grid gap-4 sm:grid-cols-2">
<FieldAnchor id="field-welcome-image-title">
<div className="space-y-2">
<Label>{t('modulePages.welcome.imageTitle')}</Label>
<Input
@@ -210,6 +228,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, imageTitle: event.target.value || null }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-welcome-image-subtitle">
<div className="space-y-2">
<Label>{t('modulePages.welcome.imageSubtitle')}</Label>
<Input
@@ -217,6 +237,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
onChange={(event) => setValue((prev) => ({ ...prev, imageSubtitle: event.target.value || null }))}
/>
</div>
</FieldAnchor>
</div>
</CardContent>
</Card>
@@ -227,6 +248,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
<CardDescription>{t('modulePages.welcome.leaveDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FieldAnchor id="field-leave-enabled">
<div className="flex items-center justify-between rounded-md border border-border p-4">
<Label>{t('modulePages.welcome.leaveEnabledLabel')}</Label>
<Switch
@@ -234,6 +256,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, leaveEnabled: checked }))}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-leave-channel">
<div className="space-y-2">
<Label>{t('modulePages.welcome.leaveChannelId')}</Label>
<Input
@@ -242,6 +266,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder="123456789012345678"
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-leave-content">
<div className="space-y-2">
<Label>{t('modulePages.welcome.leaveContent')}</Label>
<Textarea
@@ -250,6 +276,8 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder={t('modulePages.welcome.contentPlaceholder')}
/>
</div>
</FieldAnchor>
<FieldAnchor id="field-leave-embed">
<div className="space-y-2">
<Label>{t('modulePages.welcome.leaveEmbed')}</Label>
<Textarea
@@ -260,6 +288,7 @@ export function WelcomeForm({ guildId, initialValue }: WelcomeFormProps) {
placeholder='{ "title": "Goodbye", "color": 15158332 }'
/>
</div>
</FieldAnchor>
</CardContent>
</Card>
</div>