fix: add missing logout button

This commit is contained in:
2026-03-21 22:42:22 +01:00
parent b2f73a0b95
commit 3cf56b335c
5 changed files with 118 additions and 84 deletions

View File

@@ -4,24 +4,26 @@ import type { UserListParams } from '@/lib/api/server/membership';
import { UserManagementTable } from '@/components/membership/UserManagementTable';
interface MembershipPageProps {
searchParams: {
searchParams: Promise<{
username?: string;
role_name?: string;
sort_by?: string;
sort_desc?: string;
page?: string;
};
}>;
}
export default async function MembershipPage({ searchParams }: MembershipPageProps) {
const session = await requireAuth();
const resolvedParams = await searchParams;
const params: UserListParams = {
username: searchParams.username,
role_name: searchParams.role_name,
sort_by: searchParams.sort_by || 'username',
sort_desc: searchParams.sort_desc === 'true',
page: parseInt(searchParams.page || '1'),
username: resolvedParams.username,
role_name: resolvedParams.role_name,
sort_by: resolvedParams.sort_by || 'username',
sort_desc: resolvedParams.sort_desc === 'true',
page: parseInt(resolvedParams.page || '1'),
limit: 20
};

View File

@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils';
import { ButtonHTMLAttributes } from 'react';
type ButtonVariant = 'primary' | 'ghost' | 'danger-outline' | 'danger-solid';
type ButtonSize = 'sm' | 'md';
type ButtonSize = 'sm' | 'md' | 'logout';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
@@ -12,13 +12,15 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
const variantClasses: Record<ButtonVariant, string> = {
primary: 'bg-btn-green border border-btn-green-hover text-white hover:bg-btn-green-hover',
ghost: 'bg-transparent border border-border text-secondary hover:bg-overlay hover:text-primary',
'danger-outline': 'bg-transparent border border-border text-red hover:border-red/40 hover:bg-red-bg',
'danger-solid': 'bg-overlay border border-red/20 text-red hover:bg-red-bg',
'danger-outline':
'bg-transparent border border-border text-red hover:border-red/40 hover:bg-red-bg',
'danger-solid': 'bg-overlay border border-red/20 text-red hover:bg-red-bg'
};
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
logout: 'px-2 py-2 cursor-pointer hover:bg-red'
};
export function Button({
@@ -34,7 +36,7 @@ export function Button({
{...props}
disabled={disabled}
className={cn(
'inline-flex items-center gap-1.5 justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue focus:ring-offset-1 focus:ring-offset-base',
'focus:ring-blue focus:ring-offset-base inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors focus:ring-2 focus:ring-offset-1 focus:outline-none',
'disabled:cursor-not-allowed disabled:opacity-40',
variantClasses[variant],
sizeClasses[size],

View File

@@ -8,10 +8,10 @@ export default function LogoutButton() {
const [, formAction] = useActionState(logoutAction, null);
return (
<form action={formAction}>
<Button type="submit" variant="ghost" size="sm" className="flex items-center gap-1">
<Button type="submit" title="Logout" variant="ghost" size="logout">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -23,7 +23,6 @@ export default function LogoutButton() {
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span className="hidden sm:inline">Logout</span>
</Button>
</form>
);

View File

@@ -3,87 +3,120 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import LogoutButton from './LogoutButton';
interface SidebarProps {
showUsers: boolean; // true if user has membership.view permission
showUsers: boolean; // true if user has membership.view permission
}
function ServerIcon({ active }: { active: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}>
<rect x="2" y="3" width="20" height="5" rx="1" />
<rect x="2" y="10" width="20" height="5" rx="1" />
<rect x="2" y="17" width="20" height="5" rx="1" />
<circle cx="6" cy="5.5" r="1" fill="currentColor" />
<circle cx="6" cy="12.5" r="1" fill="currentColor" />
<circle cx="6" cy="19.5" r="1" fill="currentColor" />
</svg>
);
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}
>
<rect x="2" y="3" width="20" height="5" rx="1" />
<rect x="2" y="10" width="20" height="5" rx="1" />
<rect x="2" y="17" width="20" height="5" rx="1" />
<circle cx="6" cy="5.5" r="1" fill="currentColor" />
<circle cx="6" cy="12.5" r="1" fill="currentColor" />
<circle cx="6" cy="19.5" r="1" fill="currentColor" />
</svg>
);
}
function UsersIcon({ active }: { active: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}>
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
</svg>
);
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}
>
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
</svg>
);
}
export function Sidebar({ showUsers }: SidebarProps) {
const pathname = usePathname();
const isServers = pathname === '/dashboard' || pathname.startsWith('/dashboard/server');
const isUsers = pathname.startsWith('/dashboard/membership');
const pathname = usePathname();
const isServers = pathname === '/dashboard' || pathname.startsWith('/dashboard/server');
const isUsers = pathname.startsWith('/dashboard/membership');
const navItems = [
{ href: '/dashboard', label: 'Servers', active: isServers, icon: <ServerIcon active={isServers} /> },
...(showUsers ? [{ href: '/dashboard/membership', label: 'Users', active: isUsers, icon: <UsersIcon active={isUsers} /> }] : []),
];
const navItems = [
{
href: '/dashboard',
label: 'Servers',
active: isServers,
icon: <ServerIcon active={isServers} />
},
...(showUsers
? [
{
href: '/dashboard/membership',
label: 'Users',
active: isUsers,
icon: <UsersIcon active={isUsers} />
}
]
: [])
];
return (
<>
<aside className="fixed inset-y-0 left-0 z-40 hidden w-14 flex-col items-center border-r border-border-muted bg-sidebar py-3 md:flex">
<div className="mb-4 flex h-8 w-8 items-center justify-center rounded-lg bg-blue">
<svg viewBox="0 0 24 24" fill="none" stroke="#0d1117" strokeWidth={2.5} className="h-4 w-4">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
return (
<>
<aside className="border-border-muted bg-sidebar fixed inset-y-0 left-0 z-40 hidden w-14 flex-col items-center border-r py-3 md:flex">
<div className="bg-blue mb-4 flex h-8 w-8 items-center justify-center rounded-lg">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#0d1117"
strokeWidth={2.5}
className="h-4 w-4"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
<nav className="flex flex-1 flex-col items-center gap-1">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
title={item.label}
className={cn(
'flex h-9 w-9 items-center justify-center rounded-lg transition-colors',
item.active ? 'bg-blue-bg' : 'hover:bg-overlay'
)}
>
{item.icon}
</Link>
))}
</nav>
<nav className="flex flex-1 flex-col items-center gap-1">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
title={item.label}
className={cn(
'flex h-9 w-9 items-center justify-center rounded-lg transition-colors',
item.active ? 'bg-blue-bg' : 'hover:bg-overlay'
)}
>
{item.icon}
</Link>
))}
</nav>
<div className="mt-auto h-7 w-7 rounded-full border-2 border-border bg-overlay" />
</aside>
<LogoutButton />
</aside>
<nav className="fixed inset-x-0 bottom-0 z-40 flex h-14 items-center justify-around border-t border-border-muted bg-sidebar md:hidden">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
'flex flex-col items-center justify-center gap-0.5 px-4 py-2 text-xs transition-colors',
item.active ? 'text-blue' : 'text-subtle'
)}
>
{item.icon}
<span>{item.label}</span>
</Link>
))}
</nav>
</>
);
<nav className="border-border-muted bg-sidebar fixed inset-x-0 bottom-0 z-40 flex h-14 items-center justify-around border-t md:hidden">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
'flex flex-col items-center justify-center gap-0.5 px-4 py-2 text-xs transition-colors',
item.active ? 'text-blue' : 'text-subtle'
)}
>
{item.icon}
<span>{item.label}</span>
</Link>
))}
</nav>
</>
);
}

View File

@@ -30,9 +30,7 @@ export async function fetchServerAPI<T>(
if (response.status == 401) {
redirect('/login?expired=true');
}
throw new Error(
`API Error: ${response.statusText} - ${method} - ${BASE_URL}${endpoint} - ${token}`
);
throw new Error(`API Error: ${response.statusText} - ${method} - ${BASE_URL}${endpoint}`);
}
if (response.headers.get('Content-Type')?.includes('application/json')) {