fix: add missing logout button
This commit is contained in:
@@ -4,24 +4,26 @@ import type { UserListParams } from '@/lib/api/server/membership';
|
|||||||
import { UserManagementTable } from '@/components/membership/UserManagementTable';
|
import { UserManagementTable } from '@/components/membership/UserManagementTable';
|
||||||
|
|
||||||
interface MembershipPageProps {
|
interface MembershipPageProps {
|
||||||
searchParams: {
|
searchParams: Promise<{
|
||||||
username?: string;
|
username?: string;
|
||||||
role_name?: string;
|
role_name?: string;
|
||||||
sort_by?: string;
|
sort_by?: string;
|
||||||
sort_desc?: string;
|
sort_desc?: string;
|
||||||
page?: string;
|
page?: string;
|
||||||
};
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function MembershipPage({ searchParams }: MembershipPageProps) {
|
export default async function MembershipPage({ searchParams }: MembershipPageProps) {
|
||||||
const session = await requireAuth();
|
const session = await requireAuth();
|
||||||
|
|
||||||
|
const resolvedParams = await searchParams;
|
||||||
|
|
||||||
const params: UserListParams = {
|
const params: UserListParams = {
|
||||||
username: searchParams.username,
|
username: resolvedParams.username,
|
||||||
role_name: searchParams.role_name,
|
role_name: resolvedParams.role_name,
|
||||||
sort_by: searchParams.sort_by || 'username',
|
sort_by: resolvedParams.sort_by || 'username',
|
||||||
sort_desc: searchParams.sort_desc === 'true',
|
sort_desc: resolvedParams.sort_desc === 'true',
|
||||||
page: parseInt(searchParams.page || '1'),
|
page: parseInt(resolvedParams.page || '1'),
|
||||||
limit: 20
|
limit: 20
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import { ButtonHTMLAttributes } from 'react';
|
import { ButtonHTMLAttributes } from 'react';
|
||||||
|
|
||||||
type ButtonVariant = 'primary' | 'ghost' | 'danger-outline' | 'danger-solid';
|
type ButtonVariant = 'primary' | 'ghost' | 'danger-outline' | 'danger-solid';
|
||||||
type ButtonSize = 'sm' | 'md';
|
type ButtonSize = 'sm' | 'md' | 'logout';
|
||||||
|
|
||||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
@@ -12,13 +12,15 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
const variantClasses: Record<ButtonVariant, string> = {
|
const variantClasses: Record<ButtonVariant, string> = {
|
||||||
primary: 'bg-btn-green border border-btn-green-hover text-white hover:bg-btn-green-hover',
|
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',
|
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-outline':
|
||||||
'danger-solid': 'bg-overlay border border-red/20 text-red hover:bg-red-bg',
|
'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> = {
|
const sizeClasses: Record<ButtonSize, string> = {
|
||||||
sm: 'px-3 py-1.5 text-xs',
|
sm: 'px-3 py-1.5 text-xs',
|
||||||
md: 'px-4 py-2 text-sm',
|
md: 'px-4 py-2 text-sm',
|
||||||
|
logout: 'px-2 py-2 cursor-pointer hover:bg-red'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
@@ -34,7 +36,7 @@ export function Button({
|
|||||||
{...props}
|
{...props}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
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',
|
'disabled:cursor-not-allowed disabled:opacity-40',
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ export default function LogoutButton() {
|
|||||||
const [, formAction] = useActionState(logoutAction, null);
|
const [, formAction] = useActionState(logoutAction, null);
|
||||||
return (
|
return (
|
||||||
<form action={formAction}>
|
<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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="h-4 w-4"
|
className="h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
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"
|
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>
|
</svg>
|
||||||
<span className="hidden sm:inline">Logout</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,87 +3,120 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import LogoutButton from './LogoutButton';
|
||||||
|
|
||||||
interface SidebarProps {
|
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 }) {
|
function ServerIcon({ active }: { active: boolean }) {
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}>
|
<svg
|
||||||
<rect x="2" y="3" width="20" height="5" rx="1" />
|
viewBox="0 0 24 24"
|
||||||
<rect x="2" y="10" width="20" height="5" rx="1" />
|
fill="none"
|
||||||
<rect x="2" y="17" width="20" height="5" rx="1" />
|
stroke="currentColor"
|
||||||
<circle cx="6" cy="5.5" r="1" fill="currentColor" />
|
strokeWidth={2}
|
||||||
<circle cx="6" cy="12.5" r="1" fill="currentColor" />
|
className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}
|
||||||
<circle cx="6" cy="19.5" r="1" fill="currentColor" />
|
>
|
||||||
</svg>
|
<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 }) {
|
function UsersIcon({ active }: { active: boolean }) {
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}>
|
<svg
|
||||||
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
|
viewBox="0 0 24 24"
|
||||||
<circle cx="9" cy="7" r="4" />
|
fill="none"
|
||||||
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
|
stroke="currentColor"
|
||||||
</svg>
|
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) {
|
export function Sidebar({ showUsers }: SidebarProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isServers = pathname === '/dashboard' || pathname.startsWith('/dashboard/server');
|
const isServers = pathname === '/dashboard' || pathname.startsWith('/dashboard/server');
|
||||||
const isUsers = pathname.startsWith('/dashboard/membership');
|
const isUsers = pathname.startsWith('/dashboard/membership');
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/dashboard', label: 'Servers', active: isServers, icon: <ServerIcon active={isServers} /> },
|
{
|
||||||
...(showUsers ? [{ href: '/dashboard/membership', label: 'Users', active: isUsers, icon: <UsersIcon active={isUsers} /> }] : []),
|
href: '/dashboard',
|
||||||
];
|
label: 'Servers',
|
||||||
|
active: isServers,
|
||||||
|
icon: <ServerIcon active={isServers} />
|
||||||
|
},
|
||||||
|
...(showUsers
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
href: '/dashboard/membership',
|
||||||
|
label: 'Users',
|
||||||
|
active: isUsers,
|
||||||
|
icon: <UsersIcon active={isUsers} />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
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">
|
<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="mb-4 flex h-8 w-8 items-center justify-center rounded-lg bg-blue">
|
<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">
|
<svg
|
||||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
fill="none"
|
||||||
</div>
|
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">
|
<nav className="flex flex-1 flex-col items-center gap-1">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
title={item.label}
|
title={item.label}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 w-9 items-center justify-center rounded-lg transition-colors',
|
'flex h-9 w-9 items-center justify-center rounded-lg transition-colors',
|
||||||
item.active ? 'bg-blue-bg' : 'hover:bg-overlay'
|
item.active ? 'bg-blue-bg' : 'hover:bg-overlay'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="mt-auto h-7 w-7 rounded-full border-2 border-border bg-overlay" />
|
<LogoutButton />
|
||||||
</aside>
|
</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">
|
<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) => (
|
{navItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col items-center justify-center gap-0.5 px-4 py-2 text-xs transition-colors',
|
'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.active ? 'text-blue' : 'text-subtle'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ export async function fetchServerAPI<T>(
|
|||||||
if (response.status == 401) {
|
if (response.status == 401) {
|
||||||
redirect('/login?expired=true');
|
redirect('/login?expired=true');
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(`API Error: ${response.statusText} - ${method} - ${BASE_URL}${endpoint}`);
|
||||||
`API Error: ${response.statusText} - ${method} - ${BASE_URL}${endpoint} - ${token}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.headers.get('Content-Type')?.includes('application/json')) {
|
if (response.headers.get('Content-Type')?.includes('application/json')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user