membership page

This commit is contained in:
Fran Jurmanović
2025-06-30 22:51:15 +02:00
parent 31bed1e6cb
commit 995b3e6a63
11 changed files with 832 additions and 57 deletions

View File

@@ -0,0 +1,185 @@
import { fetchAPIEvent } from '$api/apiService';
import type { User } from '$models/user';
import type { RequestEvent } from '@sveltejs/kit';
export interface MembershipFilter {
username?: string;
role_name?: string;
role_id?: string;
page?: number;
page_size?: number;
sort_by?: string;
sort_desc?: boolean;
}
export interface PaginatedUsers {
users: User[];
pagination: {
page: number;
page_size: number;
total: number;
total_pages: number;
};
}
export interface CreateUserRequest {
username: string;
password: string;
role: string;
}
export interface UpdateUserRequest {
username?: string;
password?: string;
roleId?: string;
}
export interface Role {
id: string;
name: string;
}
export const membershipService = {
async getUsers(event: RequestEvent, filter?: MembershipFilter): Promise<User[]> {
const queryParams = new URLSearchParams();
if (filter) {
if (filter.username) queryParams.append('username', filter.username);
if (filter.role_name) queryParams.append('role_name', filter.role_name);
if (filter.role_id) queryParams.append('role_id', filter.role_id);
if (filter.page) queryParams.append('page', filter.page.toString());
if (filter.page_size) queryParams.append('page_size', filter.page_size.toString());
if (filter.sort_by) queryParams.append('sort_by', filter.sort_by);
if (filter.sort_desc !== undefined)
queryParams.append('sort_desc', filter.sort_desc.toString());
}
const endpoint = `/membership${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
return await fetchAPIEvent(event, endpoint);
},
async getUsersPaginated(event: RequestEvent, filter?: MembershipFilter): Promise<PaginatedUsers> {
const users = await this.getUsers(event, filter);
const page = filter?.page || 1;
const pageSize = filter?.page_size || 10;
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedUsers = users.slice(startIndex, endIndex);
const totalPages = Math.ceil(users.length / pageSize);
return {
users: paginatedUsers,
pagination: {
page,
page_size: pageSize,
total: users.length,
total_pages: totalPages
}
};
},
async getUser(event: RequestEvent, userId: string): Promise<User> {
return await fetchAPIEvent(event, `/membership/${userId}`);
},
async createUser(event: RequestEvent, userData: CreateUserRequest): Promise<User> {
return await fetchAPIEvent(event, '/membership', 'POST', userData);
},
async updateUser(
event: RequestEvent,
userId: string,
userData: UpdateUserRequest
): Promise<User> {
return await fetchAPIEvent(event, `/membership/${userId}`, 'PUT', userData);
},
async deleteUser(event: RequestEvent, userId: string): Promise<void> {
await fetchAPIEvent(event, `/membership/${userId}`, 'DELETE');
},
async getRoles(event: RequestEvent): Promise<Role[]> {
return await fetchAPIEvent(event, '/membership/roles');
}
};
// Client-side service for browser usage (not currently used)
export const membershipClientService = {
async getUsers(filter?: MembershipFilter): Promise<User[]> {
const queryParams = new URLSearchParams();
if (filter) {
if (filter.username) queryParams.append('username', filter.username);
if (filter.role_name) queryParams.append('role_name', filter.role_name);
if (filter.role_id) queryParams.append('role_id', filter.role_id);
if (filter.page) queryParams.append('page', filter.page.toString());
if (filter.page_size) queryParams.append('page_size', filter.page_size.toString());
if (filter.sort_by) queryParams.append('sort_by', filter.sort_by);
if (filter.sort_desc !== undefined)
queryParams.append('sort_desc', filter.sort_desc.toString());
}
const endpoint = `/membership${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`Failed to fetch users: ${response.statusText}`);
}
return response.json();
},
async createUser(userData: CreateUserRequest): Promise<User> {
const response = await fetch('/membership', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`Failed to create user: ${response.statusText}`);
}
return response.json();
},
async updateUser(userId: string, userData: UpdateUserRequest): Promise<User> {
const response = await fetch(`/membership/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`Failed to update user: ${response.statusText}`);
}
return response.json();
},
async deleteUser(userId: string): Promise<void> {
const response = await fetch(`/membership/${userId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`Failed to delete user: ${response.statusText}`);
}
},
async getRoles(): Promise<Role[]> {
const response = await fetch('/membership/roles');
if (!response.ok) {
throw new Error(`Failed to fetch roles: ${response.statusText}`);
}
return response.json();
}
};

View File

@@ -110,15 +110,15 @@ export const updateConfig = async (
); );
}; };
export const restartService = async (event: RequestEvent, serverId: number) => { export const restartService = async (event: RequestEvent, serverId: string) => {
return fetchAPIEvent(event, '/api/restart', 'POST', { serverId }); return fetchAPIEvent(event, '/api/restart', 'POST', { serverId });
}; };
export const startService = async (event: RequestEvent, serverId: number) => { export const startService = async (event: RequestEvent, serverId: string) => {
return fetchAPIEvent(event, '/api/start', 'POST', { serverId }); return fetchAPIEvent(event, '/api/start', 'POST', { serverId });
}; };
export const stopService = async (event: RequestEvent, serverId: number) => { export const stopService = async (event: RequestEvent, serverId: string) => {
return fetchAPIEvent(event, '/api/stop', 'POST', { serverId }); return fetchAPIEvent(event, '/api/stop', 'POST', { serverId });
}; };

View File

@@ -0,0 +1,95 @@
<script lang="ts">
export let currentPage: number = 1;
export let totalPages: number = 1;
export let onPageChange: (page: number) => void = () => {};
$: pages = generatePageNumbers(currentPage, totalPages);
function generatePageNumbers(current: number, total: number): (number | string)[] {
if (total <= 7) {
return Array.from({ length: total }, (_, i) => i + 1);
}
const pages: (number | string)[] = [];
if (current <= 4) {
// Show first 5 pages, then ellipsis, then last page
pages.push(...[1, 2, 3, 4, 5]);
if (total > 6) pages.push('...');
pages.push(total);
} else if (current >= total - 3) {
// Show first page, ellipsis, then last 5 pages
pages.push(1);
if (total > 6) pages.push('...');
pages.push(...[total - 4, total - 3, total - 2, total - 1, total]);
} else {
// Show first page, ellipsis, current and neighbors, ellipsis, last page
pages.push(1, '...', current - 1, current, current + 1, '...', total);
}
return pages;
}
function handlePageClick(page: number | string) {
if (typeof page === 'number' && page !== currentPage) {
onPageChange(page);
}
}
function handlePrevious() {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
}
function handleNext() {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
}
</script>
<div class="flex items-center justify-center space-x-1">
<!-- Previous button -->
<button
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {currentPage === 1
? 'cursor-not-allowed bg-gray-700 text-gray-500'
: 'border border-gray-600 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white'}"
disabled={currentPage === 1}
onclick={handlePrevious}
>
Previous
</button>
<!-- Page numbers -->
{#each pages as page}
{#if page === '...'}
<span class="px-3 py-2 text-sm text-gray-500">...</span>
{:else}
<button
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {page === currentPage
? 'bg-blue-600 text-white'
: 'border border-gray-600 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white'}"
onclick={() => handlePageClick(page)}
>
{page}
</button>
{/if}
{/each}
<!-- Next button -->
<button
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {currentPage === totalPages
? 'cursor-not-allowed bg-gray-700 text-gray-500'
: 'border border-gray-600 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white'}"
disabled={currentPage === totalPages}
onclick={handleNext}
>
Next
</button>
</div>
<!-- Page info -->
<div class="mt-2 text-center text-sm text-gray-400">
Page {currentPage} of {totalPages}
</div>

View File

@@ -34,11 +34,11 @@
<div class="mt-4 grid grid-cols-2 gap-2 text-sm text-gray-300"> <div class="mt-4 grid grid-cols-2 gap-2 text-sm text-gray-300">
<div> <div>
<span class="text-gray-500">Track:</span> <span class="text-gray-500">Track:</span>
{server.state.track} {server.state?.track}
</div> </div>
<div> <div>
<span class="text-gray-500">Players:</span> <span class="text-gray-500">Players:</span>
{server.state.playerCount} {server.state?.playerCount}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@ interface State {
} }
export interface Server { export interface Server {
id: number; id: string;
name: string; name: string;
status: ServiceStatus; status: ServiceStatus;
state: State; state: State;

View File

@@ -0,0 +1,27 @@
import { fetchAPIEvent } from '$api/apiService';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
try {
const { url } = event;
const queryParams = url.searchParams.toString();
const endpoint = `/membership${queryParams ? `?${queryParams}` : ''}`;
const users = await fetchAPIEvent(event, endpoint);
return json(users);
} catch (error) {
console.error('Failed to fetch users:', error);
return json({ error: 'Failed to fetch users' }, { status: 500 });
}
}
export async function POST(event: RequestEvent) {
try {
const userData = await event.request.json();
const user = await fetchAPIEvent(event, '/membership', 'POST', userData);
return json(user);
} catch (error) {
console.error('Failed to create user:', error);
return json({ error: 'Failed to create user' }, { status: 500 });
}
}

View File

@@ -0,0 +1,42 @@
import { fetchAPIEvent } from '$api/apiService';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
try {
const { params } = event;
const userId = params.id;
const user = await fetchAPIEvent(event, `/membership/${userId}`);
return json(user);
} catch (error) {
console.error('Failed to fetch user:', error);
return json({ error: 'Failed to fetch user' }, { status: 500 });
}
}
export async function PUT(event: RequestEvent) {
try {
const { params } = event;
const userId = params.id;
const userData = await event.request.json();
const user = await fetchAPIEvent(event, `/membership/${userId}`, 'PUT', userData);
return json(user);
} catch (error) {
console.error('Failed to update user:', error);
return json({ error: 'Failed to update user' }, { status: 500 });
}
}
export async function DELETE(event: RequestEvent) {
try {
const { params } = event;
const userId = params.id;
await fetchAPIEvent(event, `/membership/${userId}`, 'DELETE');
return json({ success: true });
} catch (error) {
console.error('Failed to delete user:', error);
return json({ error: 'Failed to delete user' }, { status: 500 });
}
}

View File

@@ -11,7 +11,7 @@ export const load = async (event: RequestEvent) => {
// Helper function to create a server action with validation and error handling // Helper function to create a server action with validation and error handling
const createServerAction = ( const createServerAction = (
action: (event: RequestEvent, id: number) => Promise<void>, action: (event: RequestEvent, id: string) => Promise<void>,
{ success, failure }: { success: string; failure: string } { success, failure }: { success: string; failure: string }
) => { ) => {
return async (event: RequestEvent) => { return async (event: RequestEvent) => {
@@ -22,13 +22,8 @@ const createServerAction = (
return fail(400, { message: 'Invalid server ID provided.' }); return fail(400, { message: 'Invalid server ID provided.' });
} }
const serverId = Number(id);
if (isNaN(serverId)) {
return fail(400, { message: 'Server ID must be a number.' });
}
try { try {
await action(event, serverId); await action(event, id);
return { success: true, message: success }; return { success: true, message: success };
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'; const message = err instanceof Error ? err.message : 'Unknown error';

View File

@@ -17,7 +17,7 @@
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8"> <div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<h1 class="text-2xl font-bold">ACC Server Manager</h1> <h1 class="text-2xl font-bold">ACC Server Manager</h1>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
{#if false && hasPermission($user, 'membership.view')} {#if hasPermission($user, 'membership.view')}
<a href="/dashboard/membership" class="flex items-center text-gray-300 hover:text-white"> <a href="/dashboard/membership" class="flex items-center text-gray-300 hover:text-white">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,18 +1,116 @@
import { redirect } from '@sveltejs/kit'; import { fail, redirect, type Actions } from '@sveltejs/kit';
import { membershipService } from '$api/membershipService';
export const load = async ({ locals, fetch }) => { export const load = async (event) => {
if (!locals.user) { if (!event.locals.user) {
throw redirect(303, '/login'); throw redirect(303, '/login');
} }
const response = await fetch('/api/users'); // Get filter parameters from URL
if (!response.ok) { const page = parseInt(event.url.searchParams.get('page') || '1');
return { users: [] }; const pageSize = parseInt(event.url.searchParams.get('page_size') || '10');
} const username = event.url.searchParams.get('username') || '';
const roleName = event.url.searchParams.get('role_name') || '';
const sortBy = event.url.searchParams.get('sort_by') || 'username';
const sortDesc = event.url.searchParams.get('sort_desc') === 'true';
const users = await response.json(); const filter = {
page,
page_size: pageSize,
username: username || undefined,
role_name: roleName || undefined,
sort_by: sortBy,
sort_desc: sortDesc
};
try {
const [users, roles] = await Promise.all([
membershipService.getUsers(event, filter),
membershipService.getRoles(event)
]);
console.log(users);
// Simple client-side pagination
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedUsers = users.slice(startIndex, endIndex);
const totalPages = Math.ceil(users.length / pageSize);
return { return {
users users: paginatedUsers,
roles,
pagination: {
page,
page_size: pageSize,
total: users.length,
total_pages: totalPages
},
filter: {
username,
role_name: roleName,
sort_by: sortBy,
sort_desc: sortDesc
}
}; };
} catch (error) {
console.error('Failed to load users or roles:', error);
return {
users: [],
roles: [],
pagination: {
page: 1,
page_size: 10,
total: 0,
total_pages: 0
},
filter: {
username: '',
role_name: '',
sort_by: 'username',
sort_desc: false
}
}; };
}
};
export const actions = {
create: async (event) => {
const formData = await event.request.formData();
const username = formData.get('username');
const password = formData.get('password');
const role = formData.get('role');
if (!username || !password || !role) {
return fail(400, { message: 'All fields are required' });
}
try {
await membershipService.createUser(event, {
username: username.toString(),
password: password.toString(),
role: role.toString()
});
return { success: true, message: 'User created successfully' };
} catch (error) {
console.error('Failed to create user:', error);
return fail(500, { message: 'Failed to create user' });
}
},
delete: async (event) => {
const formData = await event.request.formData();
const id = formData.get('id');
if (!id) {
return fail(400, { message: 'User ID is required' });
}
try {
await membershipService.deleteUser(event, id.toString());
return { success: true, message: 'User deleted successfully' };
} catch (error) {
console.error('Failed to delete user:', error);
return fail(500, { message: 'Failed to delete user' });
}
}
} satisfies Actions;

View File

@@ -1,46 +1,379 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import Pagination from '$components/Pagination.svelte';
import Toast from '$components/Toast.svelte';
import { user, hasPermission } from '$stores/user'; import { user, hasPermission } from '$stores/user';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; const { data, form }: { data: PageData; form?: any } = $props();
$: users = data.users || []; let users = $derived(data.users || []);
let roles = $derived(data.roles || []);
let pagination = $derived(data.pagination);
let filter = $derived(data.filter);
let usernameFilter = $state('');
let roleNameFilter = $state('');
let sortBy = $state('username');
let sortDesc = $state(false);
let showCreateModal = $state(false);
let showDeleteModal = $state(false);
let selectedUser: any = $state(null);
let createForm = $state({
username: '',
password: '',
role: ''
});
// Initialize filters from data
$effect(() => {
usernameFilter = filter.username || '';
roleNameFilter = filter.role_name || '';
sortBy = filter.sort_by || 'username';
sortDesc = filter.sort_desc || false;
});
function applyFilters() {
const params = new URLSearchParams();
// Set filter parameters
if (usernameFilter) params.set('username', usernameFilter);
if (roleNameFilter) params.set('role_name', roleNameFilter);
params.set('sort_by', sortBy);
params.set('sort_desc', sortDesc.toString());
params.set('page', '1'); // Reset to first page
goto(`?${params.toString()}`, { invalidateAll: true });
}
function resetFilters() {
usernameFilter = '';
roleNameFilter = '';
sortBy = 'username';
sortDesc = false;
goto('/dashboard/membership', { invalidateAll: true });
}
function handlePageChange(newPage: number) {
const params = new URLSearchParams($page.url.searchParams);
params.set('page', newPage.toString());
goto(`?${params.toString()}`, { invalidateAll: true });
}
function handleSort(field: string) {
if (sortBy === field) {
sortDesc = !sortDesc;
} else {
sortBy = field;
sortDesc = false;
}
applyFilters();
}
function getSortIcon(field: string) {
if (sortBy !== field) return '↕️';
return sortDesc ? '↓' : '↑';
}
function openCreateModal() {
showCreateModal = true;
createForm = { username: '', password: '', role: '' };
}
function closeCreateModal() {
showCreateModal = false;
}
function openDeleteModal(userItem: any) {
selectedUser = userItem;
showDeleteModal = true;
}
function closeDeleteModal() {
selectedUser = null;
showDeleteModal = false;
}
// Close modals after successful form submission
$effect(() => {
if (form?.success) {
showCreateModal = false;
showDeleteModal = false;
}
});
</script> </script>
<div class="container mx-auto p-4"> {#if form?.message}
<h1 class="mb-4 text-2xl font-bold">User Management</h1> <Toast message={form.message} type={form.success ? 'success' : 'error'} />
{#if hasPermission($user, 'membership.create')}
<div class="mb-4 flex justify-end">
<button class="btn btn-primary">Create User</button>
</div>
{/if} {/if}
<div class="overflow-x-auto"> <div class="min-h-screen bg-gray-900 text-white">
<table class="table w-full"> <header class="bg-gray-800 shadow-md">
<thead> <div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<div class="flex items-center space-x-4">
<a href="/dashboard" class="text-gray-300 hover:text-white" aria-label="Back to dashboard">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
</a>
<h1 class="text-2xl font-bold">User Management</h1>
</div>
{#if hasPermission($user, 'membership.create')}
<button
onclick={openCreateModal}
class="rounded-md bg-green-600 px-4 py-2 text-sm font-medium hover:bg-green-700"
>
Create User
</button>
{/if}
</div>
</header>
<main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<!-- Filters -->
<div class="mb-6 rounded-lg border border-gray-700 bg-gray-800 p-4">
<h2 class="mb-3 text-lg font-semibold">Filters</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div>
<label for="username" class="block text-sm font-medium text-gray-300">Username</label>
<input
id="username"
type="text"
bind:value={usernameFilter}
placeholder="Search by username..."
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div>
<label for="role" class="block text-sm font-medium text-gray-300">Role</label>
<input
id="role"
type="text"
bind:value={roleNameFilter}
placeholder="Filter by role..."
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
</div>
<div class="mt-4 flex space-x-2">
<button
onclick={applyFilters}
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium hover:bg-blue-700"
>
Apply Filters
</button>
<button
onclick={resetFilters}
class="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium hover:bg-gray-700"
>
Reset
</button>
</div>
</div>
<!-- Results Summary -->
<div class="mb-4 text-sm text-gray-400">
Showing {users.length} of {pagination.total} users
{#if pagination.total_pages > 1}
(Page {pagination.page} of {pagination.total_pages})
{/if}
</div>
<!-- Users Table -->
<div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-900">
<tr> <tr>
<th>Username</th> <th class="px-6 py-3 text-left">
<th>Role</th> <button
class="flex items-center space-x-1 text-xs font-medium tracking-wider text-gray-400 uppercase hover:text-white"
onclick={() => handleSort('username')}
>
<span>Username</span>
<span>{getSortIcon('username')}</span>
</button>
</th>
<th class="px-6 py-3 text-left">
<button
class="flex items-center space-x-1 text-xs font-medium tracking-wider text-gray-400 uppercase hover:text-white"
onclick={() => handleSort('role')}
>
<span>Role</span>
<span>{getSortIcon('role')}</span>
</button>
</th>
{#if hasPermission($user, 'membership.edit')} {#if hasPermission($user, 'membership.edit')}
<th>Actions</th> <th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase"
>
Actions
</th>
{/if} {/if}
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-gray-700 bg-gray-800">
{#each users as user (user.id)} {#each users as userItem (userItem.id)}
<tr> <tr class="hover:bg-gray-700">
<td>{user.username}</td> <td class="px-6 py-4 text-sm font-medium whitespace-nowrap text-white">
<td>{user.role.name}</td> {userItem.username}
{#if hasPermission(user, 'membership.edit')} </td>
<td> <td class="px-6 py-4 text-sm whitespace-nowrap">
<button class="btn btn-sm">Edit</button> <span
<button class="btn btn-sm btn-error">Delete</button> class="inline-flex rounded-full bg-blue-900 px-2 py-1 text-xs font-semibold text-blue-300"
>
{userItem.role.name}
</span>
</td>
{#if hasPermission($user, 'membership.edit')}
<td class="px-6 py-4 text-sm font-medium whitespace-nowrap">
<div class="flex space-x-2">
<button class="text-blue-400 hover:text-blue-300"> Edit </button>
{#if hasPermission($user, 'membership.delete')}
<button
onclick={() => openDeleteModal(userItem)}
class="text-red-400 hover:text-red-300"
>
Delete
</button>
{/if}
</div>
</td> </td>
{/if} {/if}
</tr> </tr>
{:else}
<tr>
<td colspan="3" class="px-6 py-4 text-center text-sm text-gray-400">
No users found
</td>
</tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Pagination -->
{#if pagination.total_pages > 1}
<div class="mt-6 flex justify-center">
<Pagination
currentPage={pagination.page}
totalPages={pagination.total_pages}
onPageChange={handlePageChange}
/>
</div> </div>
{/if}
</main>
</div>
<!-- Create User Modal -->
{#if showCreateModal}
<div class="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
<div class="w-full max-w-md rounded-lg bg-gray-800 p-6">
<h3 class="mb-4 text-lg font-semibold text-white">Create New User</h3>
<form method="POST" action="?/create">
<div class="mb-4">
<label for="create-username" class="block text-sm font-medium text-gray-300"
>Username</label
>
<input
id="create-username"
name="username"
type="text"
bind:value={createForm.username}
required
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div class="mb-4">
<label for="create-password" class="block text-sm font-medium text-gray-300"
>Password</label
>
<input
id="create-password"
name="password"
type="password"
bind:value={createForm.password}
required
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div class="mb-6">
<label for="create-role" class="block text-sm font-medium text-gray-300">Role</label>
<select
id="create-role"
name="role"
bind:value={createForm.role}
required
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
>
<option value="">Select a role...</option>
{#each roles as role}
<option value={role.name}>{role.name}</option>
{/each}
</select>
</div>
<div class="flex justify-end space-x-2">
<button
type="button"
onclick={closeCreateModal}
class="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium hover:bg-gray-700"
>
Cancel
</button>
<button
type="submit"
class="rounded-md bg-green-600 px-4 py-2 text-sm font-medium hover:bg-green-700"
>
Create User
</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Delete Confirmation Modal -->
{#if showDeleteModal && selectedUser}
<div class="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
<div class="w-full max-w-md rounded-lg bg-gray-800 p-6">
<h3 class="mb-4 text-lg font-semibold text-white">Delete User</h3>
<p class="mb-6 text-gray-300">
Are you sure you want to delete the user "{selectedUser.username}"? This action cannot be
undone.
</p>
<form method="POST" action="?/delete">
<input type="hidden" name="id" value={selectedUser.id} />
<div class="flex justify-end space-x-2">
<button
type="button"
onclick={closeDeleteModal}
class="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium hover:bg-gray-700"
>
Cancel
</button>
<button
type="submit"
class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium hover:bg-red-700"
>
Delete User
</button>
</div>
</form>
</div>
</div>
{/if}
<svelte:head>
<title>User Management - ACC Server Manager</title>
</svelte:head>