fix(frontend): enforce lint locally and in CI, fix all pre-existing violations
## 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:
@@ -1,121 +1,129 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
import './layout.css';
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
|
||||
const locales = ['DE', 'EN', 'ES'] as const;
|
||||
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
|
||||
const activeLocale = $derived(getLocale().toUpperCase());
|
||||
const locales = ['DE', 'EN', 'ES'] as const;
|
||||
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
|
||||
const activeLocale = $derived(getLocale().toUpperCase());
|
||||
|
||||
const isAdmin = $derived(page.data.user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN')));
|
||||
const isAdmin = $derived(
|
||||
page.data.user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'))
|
||||
);
|
||||
|
||||
// Set after client-side hydration completes. Used by E2E tests to know the
|
||||
// page is interactive (event handlers registered) before they interact with it.
|
||||
let hydrated = $state(false);
|
||||
onMount(() => { hydrated = true; });
|
||||
// Set after client-side hydration completes. Used by E2E tests to know the
|
||||
// page is interactive (event handlers registered) before they interact with it.
|
||||
let hydrated = $state(false);
|
||||
onMount(() => {
|
||||
hydrated = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-white" data-hydrated={hydrated || undefined}>
|
||||
{#if !page.url.pathname.startsWith('/login')}
|
||||
<header class="sticky top-0 z-50 border-b border-gray-100 bg-white">
|
||||
<!-- De Gruyter Brill purple accent strip -->
|
||||
<div class="h-1 bg-brand-purple"></div>
|
||||
|
||||
{#if !page.url.pathname.startsWith('/login')}
|
||||
<header class="bg-white border-b border-gray-100 sticky top-0 z-50">
|
||||
<!-- De Gruyter Brill purple accent strip -->
|
||||
<div class="h-1 bg-brand-purple"></div>
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 justify-between">
|
||||
<!-- Logo & Nav -->
|
||||
<div class="flex">
|
||||
<div class="mr-10 flex flex-shrink-0 items-center">
|
||||
<a href="/" class="flex items-center" aria-label="Familienarchiv">
|
||||
<span class="font-sans text-xl font-bold tracking-widest text-brand-navy uppercase"
|
||||
>Familienarchiv</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
|
||||
<!-- Logo & Nav -->
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0 flex items-center mr-10">
|
||||
<a href="/" class="flex items-center" aria-label="Familienarchiv">
|
||||
<span class="font-sans font-bold text-xl tracking-widest text-brand-navy uppercase">Familienarchiv</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="hidden sm:flex sm:space-x-1 items-center">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-bold uppercase tracking-widest font-sans transition-colors
|
||||
<nav class="hidden items-center sm:flex sm:space-x-1">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
|
||||
{page.url.pathname === '/' || page.url.pathname.startsWith('/documents')
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
{m.nav_documents()}
|
||||
</a>
|
||||
? 'rounded bg-brand-purple/15 text-brand-navy'
|
||||
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
|
||||
>
|
||||
{m.nav_documents()}
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/persons"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-bold uppercase tracking-widest font-sans transition-colors
|
||||
<a
|
||||
href="/persons"
|
||||
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
|
||||
{page.url.pathname.startsWith('/persons')
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
{m.nav_persons()}
|
||||
</a>
|
||||
? 'rounded bg-brand-purple/15 text-brand-navy'
|
||||
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
|
||||
>
|
||||
{m.nav_persons()}
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/conversations"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-bold uppercase tracking-widest font-sans transition-colors
|
||||
<a
|
||||
href="/conversations"
|
||||
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
|
||||
{page.url.pathname.startsWith('/conversations')
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
{m.nav_conversations()}
|
||||
</a>
|
||||
{#if isAdmin}
|
||||
<a
|
||||
href="/admin"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-bold uppercase tracking-widest font-sans transition-colors
|
||||
? 'rounded bg-brand-purple/15 text-brand-navy'
|
||||
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
|
||||
>
|
||||
{m.nav_conversations()}
|
||||
</a>
|
||||
{#if isAdmin}
|
||||
<a
|
||||
href="/admin"
|
||||
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
|
||||
{page.url.pathname.startsWith('/admin')
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
{m.nav_admin()}
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
? 'rounded bg-brand-purple/15 text-brand-navy'
|
||||
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
|
||||
>
|
||||
{m.nav_admin()}
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Right Side -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Language selector -->
|
||||
<div class="flex items-center gap-1 border-r border-gray-200 pr-3">
|
||||
{#each locales as locale}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => setLocale(localeMap[locale])}
|
||||
class="text-xs font-sans tracking-widest px-1.5 py-1 transition-colors
|
||||
<!-- Right Side -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Language selector -->
|
||||
<div class="flex items-center gap-1 border-r border-gray-200 pr-3">
|
||||
{#each locales as locale (locale)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => setLocale(localeMap[locale])}
|
||||
class="px-1.5 py-1 font-sans text-xs tracking-widest transition-colors
|
||||
{activeLocale === locale
|
||||
? 'font-bold text-brand-navy'
|
||||
: 'font-normal text-gray-400 hover:text-brand-navy'}"
|
||||
>
|
||||
{locale}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<form action="/logout" method="POST" use:enhance>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center gap-1.5 text-xs text-gray-400 hover:text-brand-navy font-bold uppercase font-sans tracking-widest px-3 py-2 transition-colors"
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Account-SM.svg" alt="" aria-hidden="true" class="w-4 h-4 opacity-50" />
|
||||
{m.nav_logout()}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
{locale}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<form action="/logout" method="POST" use:enhance>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-2 font-sans text-xs font-bold tracking-widest text-gray-400 uppercase transition-colors hover:text-brand-navy"
|
||||
>
|
||||
<img
|
||||
src="/degruyter-icons/Simple/Small-16px/SVG/Action/Account-SM.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 opacity-50"
|
||||
/>
|
||||
{m.nav_logout()}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<main class="py-6">
|
||||
{@render children()}
|
||||
</main>
|
||||
<main class="py-6">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user