fix(frontend): enforce lint locally and in CI, fix all pre-existing violations
Some checks failed
CI / Unit & Component Tests (push) Successful in 1m59s
CI / E2E Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled

## Pre-commit hook
- Add .husky/pre-commit at repo root: runs `cd frontend && npm run lint`
- Update prepare script in package.json to auto-configure git hooks path
  on npm install (git -C .. config core.hooksPath .husky)
- Add lint step to CI unit-tests job so it catches issues before tests run
- Add generated dirs to .prettierignore (paraglide_bak*, test-results, .auth)
- Add src/lib/paraglide_bak* to .gitignore so ESLint can ignore them

## ESLint fixes (all pre-existing)
- Disable svelte/no-navigation-without-resolve: false positive in SvelteKit
  (rule targets Svelte 5 standalone routing, not SvelteKit <a href>)
- Fix svelte/require-each-key: add (item.id)/(item) keys to all {#each} blocks
  across 10 files — improves Svelte reconciliation performance
- Fix svelte/prefer-writable-derived in PersonTypeahead: $state+$effect → $derived
- Fix svelte/prefer-svelte-reactivity: URLSearchParams → SvelteURLSearchParams,
  Map → SvelteMap (enables Svelte reactive tracking)
- Fix @typescript-eslint/no-unused-vars: remove dead imports/variables

## Prettier
- Run npm run format to bring all source files in line with .prettierrc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-20 15:55:42 +01:00
parent 28dea45cc3
commit db2fc33e99
53 changed files with 2522 additions and 2061 deletions

View File

@@ -5,125 +5,127 @@ import { getErrorMessage } from '$lib/errors';
type ApiResult = { response: Response; error?: unknown };
function toActionResult(result: ApiResult) {
if (!result.response.ok) {
const code = (result.error as { code?: string } | undefined)?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
if (!result.response.ok) {
const code = (result.error as { code?: string } | undefined)?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
}
export async function load({ fetch, locals }) {
const user = locals.user;
const hasAdmin = user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'));
if (!hasAdmin) throw error(403, getErrorMessage('FORBIDDEN'));
const user = locals.user;
const hasAdmin = user?.groups?.some((g: { permissions: string[] }) =>
g.permissions.includes('ADMIN')
);
if (!hasAdmin) throw error(403, getErrorMessage('FORBIDDEN'));
const api = createApiClient(fetch);
const api = createApiClient(fetch);
const [usersResult, groupsResult, tagsResult] = await Promise.all([
api.GET('/api/users'),
api.GET('/api/groups'),
api.GET('/api/tags')
]);
const [usersResult, groupsResult, tagsResult] = await Promise.all([
api.GET('/api/users'),
api.GET('/api/groups'),
api.GET('/api/tags')
]);
return {
users: usersResult.data ?? [],
groups: groupsResult.data ?? [],
tags: tagsResult.data ?? []
};
return {
users: usersResult.data ?? [],
groups: groupsResult.data ?? [],
tags: tagsResult.data ?? []
};
}
export const actions = {
createUser: async ({ request, fetch }) => {
const data = await request.formData();
const api = createApiClient(fetch);
createUser: async ({ request, fetch }) => {
const data = await request.formData();
const api = createApiClient(fetch);
const result = await api.POST('/api/users', {
body: {
username: data.get('username') as string,
initialPassword: data.get('password') as string,
groupIds: data.getAll('groupIds') as string[]
}
});
const result = await api.POST('/api/users', {
body: {
username: data.get('username') as string,
initialPassword: data.get('password') as string,
groupIds: data.getAll('groupIds') as string[]
}
});
return toActionResult(result);
},
return toActionResult(result);
},
deleteUser: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
deleteUser: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
const result = await api.DELETE('/api/users/{id}', {
params: { path: { id } }
});
const result = await api.DELETE('/api/users/{id}', {
params: { path: { id } }
});
return toActionResult(result);
},
return toActionResult(result);
},
updateTag: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
updateTag: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
const result = await api.PUT('/api/tags/{id}', {
params: { path: { id } },
body: { name: data.get('name') as string }
});
const result = await api.PUT('/api/tags/{id}', {
params: { path: { id } },
body: { name: data.get('name') as string }
});
return toActionResult(result);
},
return toActionResult(result);
},
deleteTag: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
deleteTag: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
const result = await api.DELETE('/api/tags/{id}', {
params: { path: { id } }
});
const result = await api.DELETE('/api/tags/{id}', {
params: { path: { id } }
});
return toActionResult(result);
},
return toActionResult(result);
},
createGroup: async ({ request, fetch }) => {
const data = await request.formData();
const api = createApiClient(fetch);
createGroup: async ({ request, fetch }) => {
const data = await request.formData();
const api = createApiClient(fetch);
const result = await api.POST('/api/groups', {
body: {
name: data.get('name') as string,
permissions: data.getAll('permissions') as string[]
}
});
const result = await api.POST('/api/groups', {
body: {
name: data.get('name') as string,
permissions: data.getAll('permissions') as string[]
}
});
return toActionResult(result);
},
return toActionResult(result);
},
updateGroup: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
updateGroup: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
const result = await api.PATCH('/api/groups/{id}', {
params: { path: { id } },
body: {
name: data.get('name') as string,
permissions: data.getAll('permissions') as string[]
}
});
const result = await api.PATCH('/api/groups/{id}', {
params: { path: { id } },
body: {
name: data.get('name') as string,
permissions: data.getAll('permissions') as string[]
}
});
return toActionResult(result);
},
return toActionResult(result);
},
deleteGroup: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
deleteGroup: async ({ request, fetch }) => {
const data = await request.formData();
const id = data.get('id') as string;
const api = createApiClient(fetch);
const result = await api.DELETE('/api/groups/{id}', {
params: { path: { id } }
});
const result = await api.DELETE('/api/groups/{id}', {
params: { path: { id } }
});
return toActionResult(result);
}
return toActionResult(result);
}
};

View File

@@ -1,67 +1,67 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { slide } from 'svelte/transition';
import { m } from '$lib/paraglide/messages.js';
import { enhance } from '$app/forms';
import { slide } from 'svelte/transition';
import { m } from '$lib/paraglide/messages.js';
let { data, form } = $props();
let { data, form } = $props();
let activeTab = $state('users');
let editingTagId: string | null = $state(null);
let editingTagName = $state('');
let editingUserId: string | null = $state(null);
let editingGroupId: string | null = $state(null);
let activeTab = $state('users');
let editingTagId: string | null = $state(null);
let editingTagName = $state('');
let editingUserId: string | null = $state(null);
let editingGroupId: string | null = $state(null);
const availablePermissions = ['WRITE_ALL', 'ADMIN', 'ADMIN_USER', 'ADMIN_TAG', 'ADMIN_PERMISSION'];
const availablePermissions = ['WRITE_ALL', 'ADMIN', 'ADMIN_USER', 'ADMIN_TAG', 'ADMIN_PERMISSION'];
function startEditTag(tag: { id: string; name: string }) {
editingTagId = tag.id;
editingTagName = tag.name;
}
function startEditTag(tag: { id: string; name: string }) {
editingTagId = tag.id;
editingTagName = tag.name;
}
function cancelEditTag() {
editingTagId = null;
editingTagName = '';
}
function cancelEditTag() {
editingTagId = null;
editingTagName = '';
}
function startEditUser(id: string) {
editingUserId = id;
}
function startEditUser(id: string) {
editingUserId = id;
}
function cancelEditUser() {
editingUserId = null;
}
function cancelEditUser() {
editingUserId = null;
}
function startEditGroup(id: string) {
editingGroupId = id;
}
function startEditGroup(id: string) {
editingGroupId = id;
}
function cancelEditGroup() {
editingGroupId = null;
}
function cancelEditGroup() {
editingGroupId = null;
}
</script>
<div class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8 font-sans">
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-serif text-brand-navy">{m.admin_heading()}</h1>
<div class="mx-auto max-w-7xl py-8 font-sans sm:px-6 lg:px-8">
<div class="mb-8 flex items-center justify-between">
<h1 class="font-serif text-3xl text-brand-navy">{m.admin_heading()}</h1>
<!-- Tabs -->
<div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200">
<div class="flex rounded-lg border border-gray-200 bg-white p-1 shadow-sm">
<button
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
class="rounded-md px-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
'users'
? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}"
onclick={() => (activeTab = 'users')}>{m.admin_tab_users()}</button
>
<button
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
class="rounded-md px-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
'groups'
? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}"
onclick={() => (activeTab = 'groups')}>{m.admin_tab_groups()}</button
>
<button
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
class="rounded-md px-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
'tags'
? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}"
@@ -71,40 +71,40 @@
</div>
{#if form?.message}
<div class="bg-brand-mint/20 text-brand-navy p-4 rounded mb-6 border border-brand-mint/50">
<div class="mb-6 rounded border border-brand-mint/50 bg-brand-mint/20 p-4 text-brand-navy">
{form.message}
</div>
{/if}
{#if activeTab === 'users'}
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
<div class="overflow-hidden rounded-lg border border-brand-sand bg-white shadow-sm" in:slide>
<div class="flex items-center justify-between border-b border-gray-100 p-6">
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_users()}</h2>
</div>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-gray-500 uppercase"
>{m.admin_col_login()}</th
>
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-gray-500 uppercase"
>{m.admin_col_groups()}</th
>
{#if editingUserId}
<th
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-right text-xs font-bold tracking-wider text-gray-500 uppercase"
>{m.admin_col_password()}</th
>
{/if}
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each data.users as user}
<tbody class="divide-y divide-gray-200 bg-white">
{#each data.users as user (user.id)}
<tr class="group/row hover:bg-gray-50">
{#if editingUserId === user.id}
<!-- === EDIT MODE === -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
{user.username}
<input
type="hidden"
@@ -119,9 +119,9 @@
name="groupIds"
multiple
form="edit-form-{user.id}"
class="block w-full rounded border-brand-mint text-xs p-1 min-h-[80px]"
class="block min-h-[80px] w-full rounded border-brand-mint p-1 text-xs"
>
{#each data.groups as group}
{#each data.groups as group (group.id)}
<option
value={group.id}
selected={user.groups.some((g: { id: string }) => g.id === group.id)}
@@ -130,10 +130,10 @@
</option>
{/each}
</select>
<p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint()}</p>
<p class="mt-1 text-[10px] text-gray-400">{m.admin_multiselect_hint()}</p>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right align-top">
<td class="px-6 py-4 text-right align-top whitespace-nowrap">
<form
id="edit-form-{user.id}"
method="POST"
@@ -149,20 +149,20 @@
type="password"
name="password"
placeholder={m.admin_password_placeholder()}
class="w-32 py-1 px-2 text-xs border border-brand-mint rounded"
class="w-32 rounded border border-brand-mint px-2 py-1 text-xs"
/>
<div class="flex gap-2 mt-1">
<div class="mt-1 flex gap-2">
<button
type="submit"
class="bg-green-600 text-white px-2 py-1 rounded text-xs font-bold uppercase hover:bg-green-700"
class="rounded bg-green-600 px-2 py-1 text-xs font-bold text-white uppercase hover:bg-green-700"
>
{m.btn_save()}
</button>
<button
type="button"
onclick={cancelEditUser}
class="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-bold uppercase hover:bg-gray-300"
class="rounded bg-gray-200 px-2 py-1 text-xs font-bold text-gray-600 uppercase hover:bg-gray-300"
>
{m.btn_cancel()}
</button>
@@ -171,15 +171,15 @@
</td>
{:else}
<!-- === VIEW MODE === -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
{user.username}
</td>
<td class="px-6 py-4 text-sm text-gray-500">
<div class="flex flex-wrap gap-1">
{#if user.groups && user.groups.length > 0}
{#each user.groups as group}
{#each user.groups as group (group.id)}
<span
class="px-2 py-0.5 text-[10px] font-bold uppercase rounded-full bg-blue-50 text-blue-700 border border-blue-100"
class="rounded-full border border-blue-100 bg-blue-50 px-2 py-0.5 text-[10px] font-bold text-blue-700 uppercase"
>
{group.name}
</span>
@@ -189,11 +189,11 @@
{/if}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<td class="px-6 py-4 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-4">
<button
onclick={() => startEditUser(user.id)}
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
class="text-sm font-bold tracking-wide text-brand-mint uppercase hover:text-brand-navy"
>
{m.btn_edit()}
</button>
@@ -213,10 +213,10 @@
>
<input type="hidden" name="id" value={user.id} />
<button
class="text-gray-300 hover:text-red-600 transition-colors p-1"
class="p-1 text-gray-300 transition-colors hover:text-red-600"
title={m.admin_btn_delete_user_title()}
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@@ -235,66 +235,66 @@
</table>
<!-- Create User Form -->
<div class="p-6 bg-gray-50 border-t border-gray-200">
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
<div class="border-t border-gray-200 bg-gray-50 p-6">
<h3 class="mb-4 text-xs font-bold tracking-wide text-gray-500 uppercase">
{m.admin_section_new_user()}
</h3>
<form
method="POST"
action="?/createUser"
use:enhance
class="grid grid-cols-1 md:grid-cols-6 gap-4 items-start"
class="grid grid-cols-1 items-start gap-4 md:grid-cols-6"
>
<input
type="text"
name="username"
placeholder="Login"
required
class="rounded border-gray-300 text-sm w-full"
class="w-full rounded border-gray-300 text-sm"
/>
<input
type="password"
name="password"
placeholder={m.admin_col_password()}
required
class="rounded border-gray-300 text-sm w-full"
class="w-full rounded border-gray-300 text-sm"
/>
<div class="md:col-span-3">
<select
name="groupIds"
multiple
class="rounded border-gray-300 text-sm w-full h-[42px] py-1"
class="h-[42px] w-full rounded border-gray-300 py-1 text-sm"
required
title={m.admin_multiselect_hint_multi()}
>
{#each data.groups as group}
{#each data.groups as group (group.id)}
<option value={group.id}>{group.name}</option>
{/each}
</select>
<p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint_full()}</p>
<p class="mt-1 text-[10px] text-gray-400">{m.admin_multiselect_hint_full()}</p>
</div>
<button
type="submit"
class="bg-brand-navy text-white h-[42px] rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full"
class="h-[42px] w-full rounded bg-brand-navy text-sm font-bold text-white uppercase hover:bg-brand-mint hover:text-brand-navy"
>{m.btn_create()}</button
>
</form>
</div>
</div>
{:else if activeTab === 'tags'}
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
<div class="p-6 border-b border-gray-100 bg-yellow-50/50">
<div class="overflow-hidden rounded-lg border border-brand-sand bg-white shadow-sm" in:slide>
<div class="border-b border-gray-100 bg-yellow-50/50 p-6">
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_tags()}</h2>
<p class="text-xs text-yellow-800 mt-1">
<p class="mt-1 text-xs text-yellow-800">
{m.admin_tags_warning()}
</p>
</div>
<ul class="divide-y divide-gray-100 max-h-[600px] overflow-y-auto">
{#each data.tags as tag}
<li class="px-6 py-3 flex items-center justify-between hover:bg-gray-50 group">
<ul class="max-h-[600px] divide-y divide-gray-100 overflow-y-auto">
{#each data.tags as tag (tag.id)}
<li class="group flex items-center justify-between px-6 py-3 hover:bg-gray-50">
{#if editingTagId === tag.id}
<form
method="POST"
@@ -304,17 +304,17 @@
await update();
cancelEditTag();
}}
class="flex-1 flex gap-2 items-center"
class="flex flex-1 items-center gap-2"
>
<input type="hidden" name="id" value={tag.id} />
<input
type="text"
name="name"
bind:value={editingTagName}
class="flex-1 border-brand-mint ring-1 ring-brand-mint rounded px-2 py-1 text-sm"
class="flex-1 rounded border-brand-mint px-2 py-1 text-sm ring-1 ring-brand-mint"
/>
<button aria-label={m.btn_save()} class="text-green-600 hover:text-green-800"
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
@@ -328,7 +328,7 @@
onclick={cancelEditTag}
aria-label={m.btn_cancel()}
class="text-gray-400 hover:text-gray-600"
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
@@ -339,18 +339,18 @@
>
</form>
{:else}
<span class="text-sm font-medium text-brand-navy bg-brand-sand/30 px-2 py-1 rounded">
<span class="rounded bg-brand-sand/30 px-2 py-1 text-sm font-medium text-brand-navy">
{tag.name}
</span>
<div
class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"
class="flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100"
>
<button
onclick={() => startEditTag(tag)}
aria-label={m.admin_btn_edit_tag_label()}
class="p-1 text-gray-400 hover:text-brand-navy"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
@@ -375,8 +375,11 @@
class="inline"
>
<input type="hidden" name="id" value={tag.id} />
<button aria-label={m.admin_btn_delete_tag_label()} class="p-1 text-gray-400 hover:text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<button
aria-label={m.admin_btn_delete_tag_label()}
class="p-1 text-gray-400 hover:text-red-600"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
@@ -393,28 +396,28 @@
</ul>
</div>
{:else if activeTab === 'groups'}
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
<div class="overflow-hidden rounded-lg border border-brand-sand bg-white shadow-sm" in:slide>
<div class="flex items-center justify-between border-b border-gray-100 p-6">
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_groups()}</h2>
</div>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-gray-500 uppercase"
>{m.admin_col_name()}</th
>
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
<th class="px-6 py-3 text-left text-xs font-bold tracking-wider text-gray-500 uppercase"
>{m.admin_col_permissions()}</th
>
<th
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
class="px-6 py-3 text-right text-xs font-bold tracking-wider text-gray-500 uppercase"
>{m.admin_col_actions()}</th
>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each data.groups as group}
<tbody class="divide-y divide-gray-200 bg-white">
{#each data.groups as group (group.id)}
<tr class="group/row hover:bg-gray-50">
{#if editingGroupId === group.id}
<!-- EDIT MODE -->
@@ -427,7 +430,7 @@
await update();
cancelEditGroup();
}}
class="flex flex-col sm:flex-row items-start gap-4 w-full"
class="flex w-full flex-col items-start gap-4 sm:flex-row"
>
<input type="hidden" name="id" value={group.id} />
@@ -436,13 +439,13 @@
type="text"
name="name"
value={group.name}
class="w-full text-sm border-brand-mint rounded"
class="w-full rounded border-brand-mint text-sm"
required
/>
</div>
<div class="flex-1 flex flex-wrap gap-4 items-center h-full pt-2">
{#each availablePermissions as perm}
<div class="flex h-full flex-1 flex-wrap items-center gap-4 pt-2">
{#each availablePermissions as perm (perm)}
<label
class="inline-flex items-center text-xs font-bold text-gray-600 uppercase"
>
@@ -451,7 +454,7 @@
name="permissions"
value={perm}
checked={group.permissions.includes(perm)}
class="mr-2 text-brand-navy focus:ring-brand-mint rounded border-gray-300"
class="mr-2 rounded border-gray-300 text-brand-navy focus:ring-brand-mint"
/>
{perm.replace('_', ' ')}
</label>
@@ -459,8 +462,12 @@
</div>
<div class="flex gap-2 self-start sm:self-center">
<button type="submit" aria-label={m.btn_save()} class="text-green-600 hover:text-green-800 p-1">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<button
type="submit"
aria-label={m.btn_save()}
class="p-1 text-green-600 hover:text-green-800"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
@@ -473,9 +480,9 @@
type="button"
onclick={cancelEditGroup}
aria-label={m.btn_cancel()}
class="text-gray-400 hover:text-red-500 p-1"
class="p-1 text-gray-400 hover:text-red-500"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
@@ -489,28 +496,28 @@
</td>
{:else}
<!-- VIEW MODE -->
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-brand-navy">
<td class="px-6 py-4 text-sm font-bold whitespace-nowrap text-brand-navy">
{group.name}
</td>
<td class="px-6 py-4 text-sm text-gray-500">
<div class="flex flex-wrap gap-1">
{#each group.permissions as perm}
{#each group.permissions as perm (perm)}
<span
class="px-2 py-0.5 text-[10px] font-bold uppercase rounded-full
class="rounded-full px-2 py-0.5 text-[10px] font-bold uppercase
{perm === 'ADMIN'
? 'bg-red-50 text-red-700 border-red-100'
: 'bg-gray-100 text-gray-600 border-gray-200'}"
? 'border-red-100 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-100 text-gray-600'}"
>
{perm}
</span>
{/each}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<td class="px-6 py-4 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-3">
<button
onclick={() => startEditGroup(group.id)}
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
class="text-sm font-bold tracking-wide text-brand-mint uppercase hover:text-brand-navy"
>
{m.btn_edit()}
</button>
@@ -529,10 +536,10 @@
>
<input type="hidden" name="id" value={group.id} />
<button
class="text-gray-300 hover:text-red-600 p-1 transition-colors"
class="p-1 text-gray-300 transition-colors hover:text-red-600"
title={m.btn_delete()}
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@@ -551,34 +558,34 @@
</table>
<!-- CREATE GROUP FORM -->
<div class="p-6 bg-gray-50 border-t border-gray-200">
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
<div class="border-t border-gray-200 bg-gray-50 p-6">
<h3 class="mb-4 text-xs font-bold tracking-wide text-gray-500 uppercase">
{m.admin_section_new_group()}
</h3>
<form
method="POST"
action="?/createGroup"
use:enhance
class="flex flex-col md:flex-row gap-4 items-start md:items-center"
class="flex flex-col items-start gap-4 md:flex-row md:items-center"
>
<div class="flex-1 w-full">
<div class="w-full flex-1">
<input
type="text"
name="name"
placeholder={m.admin_group_name_placeholder()}
required
class="rounded border-gray-300 text-sm w-full"
class="w-full rounded border-gray-300 text-sm"
/>
</div>
<div class="flex gap-4 items-center">
{#each availablePermissions as perm}
<div class="flex items-center gap-4">
{#each availablePermissions as perm (perm)}
<label class="inline-flex items-center text-xs font-bold text-gray-600 uppercase">
<input
type="checkbox"
name="permissions"
value={perm}
class="mr-2 text-brand-navy focus:ring-brand-mint rounded border-gray-300"
class="mr-2 rounded border-gray-300 text-brand-navy focus:ring-brand-mint"
/>
{perm.replace('_', ' ')}
</label>
@@ -587,7 +594,7 @@
<button
type="submit"
class="bg-brand-navy text-white px-6 py-2 rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full md:w-auto"
class="w-full rounded bg-brand-navy px-6 py-2 text-sm font-bold text-white uppercase hover:bg-brand-mint hover:text-brand-navy md:w-auto"
>
{m.btn_create()}
</button>