feat(domains): Inline-Edit + Favicon in Settings + Filter IN Suchmaske
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s

Domain-Admin-Seite bekommt jetzt ein Favicon-Icon vor jedem Eintrag,
einen Pencil-Button zum Inline-Editieren von Domain und Anzeigename,
und Save/Cancel-Buttons. Beim Ändern des Domain-Namens wird das Favicon
zurückgesetzt und beim Speichern frisch nachgeladen (den Filter-Dropdown-
Icons reicht der neue favicon_path automatisch zu).

Der Filter-Button auf der Hauptseite sitzt jetzt IM weißen Suchfeld-
Container (neuer .search-box-Wrapper mit Border) statt daneben, analog
zum Referenz-Screenshot von rezeptwelt.de. Neue inline-Prop an
SearchFilter schaltet eigenen Border/Background ab und setzt stattdessen
einen vertikalen Divider nach rechts.

- Neuer PATCH /api/domains/[id] mit zod-Schema.
- Repository: updateDomain(id, patch) + getDomainById(id).
  domain-Change nullt favicon_path → Caller lädt neu.
- Tests für updateDomain-Fälle und getDomainById.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-18 08:28:02 +02:00
parent 6c2b24d060
commit 15c15c8494
6 changed files with 312 additions and 28 deletions

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Pencil, Check, X, Globe } from 'lucide-svelte';
import type { AllowedDomain } from '$lib/types';
import { confirmAction } from '$lib/client/confirm.svelte';
import { confirmAction, alertAction } from '$lib/client/confirm.svelte';
let domains = $state<AllowedDomain[]>([]);
let loading = $state(true);
@@ -10,6 +11,11 @@
let adding = $state(false);
let errored = $state<string | null>(null);
let editingId = $state<number | null>(null);
let editDomain = $state('');
let editLabel = $state('');
let saving = $state(false);
async function load() {
const res = await fetch('/api/domains');
domains = await res.json();
@@ -39,6 +45,45 @@
await load();
}
function startEdit(d: AllowedDomain) {
editingId = d.id;
editDomain = d.domain;
editLabel = d.display_name ?? '';
}
function cancelEdit() {
editingId = null;
editDomain = '';
editLabel = '';
}
async function saveEdit(d: AllowedDomain) {
if (!editDomain.trim()) return;
saving = true;
try {
const res = await fetch(`/api/domains/${d.id}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
domain: editDomain.trim(),
display_name: editLabel.trim() || null
})
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
await alertAction({
title: 'Speichern fehlgeschlagen',
message: body.message ?? `HTTP ${res.status}`
});
return;
}
cancelEdit();
await load();
} finally {
saving = false;
}
}
async function remove(d: AllowedDomain) {
const ok = await confirmAction({
title: 'Domain entfernen?',
@@ -83,11 +128,59 @@
<ul class="list">
{#each domains as d (d.id)}
<li>
<div>
<div class="dom">{d.domain}</div>
{#if d.display_name}<div class="label">{d.display_name}</div>{/if}
</div>
<button class="btn danger" onclick={() => remove(d)}>Löschen</button>
{#if d.favicon_path}
<img class="fav" src={`/images/${d.favicon_path}`} alt="" loading="lazy" />
{:else}
<span class="fav fallback" aria-hidden="true"><Globe size={18} strokeWidth={1.8} /></span>
{/if}
{#if editingId === d.id}
<div class="edit-fields">
<input
type="text"
bind:value={editDomain}
placeholder="chefkoch.de"
aria-label="Domain"
/>
<input
type="text"
bind:value={editLabel}
placeholder="Anzeigename (optional)"
aria-label="Anzeigename"
/>
</div>
<div class="actions">
<button
class="btn primary icon-btn"
aria-label="Speichern"
disabled={saving}
onclick={() => saveEdit(d)}
>
<Check size={18} strokeWidth={2} />
</button>
<button
class="btn icon-btn"
aria-label="Abbrechen"
onclick={cancelEdit}
>
<X size={18} strokeWidth={2} />
</button>
</div>
{:else}
<div class="info">
<div class="dom">{d.domain}</div>
{#if d.display_name}<div class="label">{d.display_name}</div>{/if}
</div>
<div class="actions">
<button
class="btn icon-btn"
aria-label="Bearbeiten"
onclick={() => startEdit(d)}
>
<Pencil size={16} strokeWidth={2} />
</button>
<button class="btn danger" onclick={() => remove(d)}>Löschen</button>
</div>
{/if}
</li>
{/each}
</ul>
@@ -150,11 +243,15 @@
.list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
background: white;
border: 1px solid #e4eae7;
border-radius: 12px;
padding: 0.75rem 1rem;
padding: 0.7rem 0.85rem;
}
.info {
flex: 1;
min-width: 0;
}
.dom {
font-weight: 600;
@@ -163,6 +260,48 @@
color: #888;
font-size: 0.85rem;
}
.fav {
width: 24px;
height: 24px;
border-radius: 4px;
object-fit: contain;
flex-shrink: 0;
}
.fav.fallback {
background: #eef3ef;
color: #8fb097;
display: inline-flex;
align-items: center;
justify-content: center;
}
.edit-fields {
flex: 1;
display: flex;
gap: 0.5rem;
min-width: 0;
flex-wrap: wrap;
}
.edit-fields input {
flex: 1;
min-width: 120px;
padding: 0.5rem 0.7rem;
border: 1px solid #cfd9d1;
border-radius: 8px;
font-size: 0.95rem;
min-height: 40px;
}
.actions {
display: flex;
gap: 0.4rem;
flex-shrink: 0;
}
.icon-btn {
min-width: 40px;
padding: 0.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
.error {
color: #c53030;
margin-bottom: 1rem;