Enhance guild management features in the owner panel
- Updated the guilds API to support searching by both guild name and ID, improving user experience. - Increased the number of guilds retrieved from the database from 100 to 200 for better visibility. - Implemented invite creation functionality, allowing owners to generate invites directly from the guild management interface. - Enhanced the UI to display guild names, icons, and member counts, providing a more informative overview. - Updated localization files to reflect new search capabilities and invite-related messages in both English and German.
This commit is contained in:
@@ -3,9 +3,10 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useMemo, useState, useTransition } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import type { BotPresenceConfig, OwnerRole } from '@nexumi/shared';
|
||||
import type { BotPresenceConfig, OwnerGuildListItem, OwnerRole } from '@nexumi/shared';
|
||||
import { useTranslations } from '@/components/locale-provider';
|
||||
import { SettingsForm } from '@/components/settings/settings-form';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -195,16 +196,23 @@ export function UsersManager({
|
||||
export function GuildsManager({
|
||||
initialGuilds
|
||||
}: {
|
||||
initialGuilds: Array<{ id: string; locale: string; createdAt: string; blacklisted: boolean }>;
|
||||
initialGuilds: OwnerGuildListItem[];
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
const [query, setQuery] = useState('');
|
||||
const [pending, startTransition] = useTransition();
|
||||
const filtered = useMemo(
|
||||
() => initialGuilds.filter((guild) => guild.id.includes(query.trim())),
|
||||
[initialGuilds, query]
|
||||
);
|
||||
const filtered = useMemo(() => {
|
||||
const needle = query.trim().toLowerCase();
|
||||
if (!needle) {
|
||||
return initialGuilds;
|
||||
}
|
||||
return initialGuilds.filter(
|
||||
(guild) =>
|
||||
guild.id.includes(needle) ||
|
||||
(guild.name?.toLowerCase().includes(needle) ?? false)
|
||||
);
|
||||
}, [initialGuilds, query]);
|
||||
|
||||
async function postAction(action: string, guildId: string, reason?: string) {
|
||||
const response = await fetch('/api/owner/guilds', {
|
||||
@@ -215,6 +223,7 @@ export function GuildsManager({
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response));
|
||||
}
|
||||
return (await response.json()) as { ok?: boolean; url?: string };
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -225,58 +234,103 @@ export function GuildsManager({
|
||||
{filtered.length === 0 ? (
|
||||
<p className="p-4 text-sm text-muted-foreground">{t('owner.common.empty')}</p>
|
||||
) : (
|
||||
filtered.map((guild) => (
|
||||
<div key={guild.id} className="flex flex-col gap-3 px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-medium">{guild.id}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{guild.locale} · {new Date(guild.createdAt).toLocaleDateString()}
|
||||
{guild.blacklisted ? ` · ${t('owner.guilds.blacklisted')}` : ''}
|
||||
</p>
|
||||
filtered.map((guild) => {
|
||||
const displayName = guild.name ?? t('owner.guilds.unknownName');
|
||||
const initials = displayName.slice(0, 2).toUpperCase();
|
||||
return (
|
||||
<div
|
||||
key={guild.id}
|
||||
className="flex flex-col gap-3 px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Avatar className="h-10 w-10 shrink-0">
|
||||
{guild.iconUrl ? <AvatarImage src={guild.iconUrl} alt="" /> : null}
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{displayName}</p>
|
||||
<p className="truncate text-muted-foreground">
|
||||
<span className="font-mono text-xs">{guild.id}</span>
|
||||
{' · '}
|
||||
{guild.memberCount != null
|
||||
? t('owner.guilds.members', { count: String(guild.memberCount) })
|
||||
: t('owner.guilds.membersUnknown')}
|
||||
{' · '}
|
||||
{guild.locale} · {new Date(guild.createdAt).toLocaleDateString()}
|
||||
{guild.blacklisted ? ` · ${t('owner.guilds.blacklisted')}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await postAction('createInvite', guild.id);
|
||||
if (result.url) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(result.url);
|
||||
toast.success(t('owner.guilds.inviteCopied', { url: result.url }));
|
||||
} catch {
|
||||
toast.success(t('owner.guilds.inviteCreated', { url: result.url }));
|
||||
}
|
||||
} else {
|
||||
toast.success(t('common.saveSuccess'));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('common.saveError'));
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('owner.guilds.createInvite')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await postAction(guild.blacklisted ? 'unblacklist' : 'blacklist', guild.id, 'Owner panel');
|
||||
toast.success(t('common.saveSuccess'));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('common.saveError'));
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{guild.blacklisted ? t('owner.guilds.unblacklist') : t('owner.guilds.blacklist')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
if (!window.confirm(t('owner.guilds.leaveConfirm'))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await postAction('leave', guild.id);
|
||||
toast.success(t('common.saveSuccess'));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('common.saveError'));
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('owner.guilds.leave')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await postAction(guild.blacklisted ? 'unblacklist' : 'blacklist', guild.id, 'Owner panel');
|
||||
toast.success(t('common.saveSuccess'));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('common.saveError'));
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{guild.blacklisted ? t('owner.guilds.unblacklist') : t('owner.guilds.blacklist')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
if (!window.confirm(t('owner.guilds.leaveConfirm'))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await postAction('leave', guild.id);
|
||||
toast.success(t('common.saveSuccess'));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('common.saveError'));
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('owner.guilds.leave')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user