Files
musenalm/views/routes/baende/body.gohtml
2026-01-30 19:39:02 +01:00

800 lines
35 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{ $model := . }}
<div x-data="{
search: '{{ $model.search }}',
offset: {{ if $model.next_offset }}{{ $model.next_offset }}{{ else }}0{{ end }},
hasMore: {{ if $model.has_more }}true{{ else }}false{{ end }},
loading: false,
alphabetOpen: false,
selectedLetter: '{{ $model.letter }}',
status: '{{ $model.status }}',
person: '{{ $model.person }}',
user: '{{ $model.user }}',
year: '{{ $model.year }}',
place: '{{ $model.place }}',
sortField: '{{ if $model.sort_field }}{{ $model.sort_field }}{{ else }}title{{ end }}',
sortOrder: '{{ if $model.sort_order }}{{ $model.sort_order }}{{ else }}asc{{ end }}',
statusLabels: {
{{- range $k, $v := $model.filter_status_labels -}}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end -}}
},
personLabels: {
{{- range $k, $v := $model.filter_agent_labels -}}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end -}}
},
userLabels: {
{{- range $k, $v := $model.filter_user_labels -}}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end -}}
},
placeLabels: {
{{- range $k, $v := $model.filter_place_labels -}}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end -}}
},
yearLabels: {
{{- range $k, $v := $model.filter_year_labels -}}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end -}}
},
appendFilters(params) {
if (this.status) params.set('status', this.status);
if (this.person) params.set('person', this.person);
if (this.user) params.set('user', this.user);
if (this.year) params.set('year', this.year);
if (this.place) params.set('place', this.place);
},
buildResultsUrl(overrides = {}) {
const params = new URLSearchParams();
const next = {
search: this.search,
letter: this.selectedLetter,
status: this.status,
person: this.person,
user: this.user,
year: this.year,
place: this.place,
sort: this.sortField,
order: this.sortOrder,
offset: 0,
...overrides,
};
Object.entries(next).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
params.set(key, value);
}
});
return `/baende/results/?${params.toString()}`;
},
buildPageUrl(overrides = {}) {
const params = new URLSearchParams();
const next = {
search: this.search,
letter: this.selectedLetter,
status: this.status,
person: this.person,
user: this.user,
year: this.year,
place: this.place,
sort: this.sortField,
order: this.sortOrder,
offset: this.offset,
...overrides,
};
Object.entries(next).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
params.set(key, value);
}
});
const query = params.toString();
return query ? `/baende/?${query}` : '/baende/';
},
applyFilter(overrides = {}, indicator = 'body', sourceEl = null) {
Object.entries(overrides).forEach(([key, value]) => {
this[key] = value;
});
this.offset = 0;
this.hasMore = true;
const url = this.buildResultsUrl();
htmx.ajax('GET', url, {
target: '#baenderesults',
swap: 'outerHTML',
indicator,
source: sourceEl || (indicator && document.querySelector(indicator)) || undefined,
});
},
clearFilters() {
this.status = '';
this.person = '';
this.user = '';
this.year = '';
this.place = '';
},
closeOtherDropdowns(current) {
document.querySelectorAll('details').forEach((d) => {
if (d !== current) d.open = false;
});
},
changeSort(field, event = null) {
if (this.sortField === field) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortOrder = 'asc';
}
this.offset = 0;
this.hasMore = true;
const params = new URLSearchParams();
params.set('sort', this.sortField);
params.set('order', this.sortOrder);
params.set('offset', 0);
this.appendFilters(params);
if (this.search) {
params.set('search', this.search);
}
if (this.selectedLetter) {
params.set('letter', this.selectedLetter);
}
const queryString = params.toString();
htmx.ajax('GET', `/baende/results/?${queryString}`, {
target: '#baenderesults',
swap: 'outerHTML',
source: event?.currentTarget || undefined,
});
},
updateUrl() {
const params = new URLSearchParams();
params.set('offset', this.offset);
params.set('sort', this.sortField);
params.set('order', this.sortOrder);
this.appendFilters(params);
if (this.search) {
params.set('search', this.search);
}
if (this.selectedLetter) {
params.set('letter', this.selectedLetter);
}
const query = params.toString();
const newUrl = query ? `/baende/?${query}` : '/baende/';
window.history.replaceState(null, '', newUrl);
},
loadMoreUrl() {
const params = new URLSearchParams();
params.set('offset', this.offset);
params.set('sort', this.sortField);
params.set('order', this.sortOrder);
this.appendFilters(params);
if (this.search) {
params.set('search', this.search);
}
if (this.selectedLetter) {
params.set('letter', this.selectedLetter);
}
return `/baende/more/?${params.toString()}`;
}
}"
@htmx:after-swap.window="
if ($event.detail.target && $event.detail.target.id === 'baenderesults') {
const responseUrl = $event.detail.xhr?.responseURL || window.location.href;
const params = new URL(responseUrl).searchParams;
sortField = params.get('sort') || sortField;
sortOrder = params.get('order') || sortOrder;
status = params.get('status') || '';
person = params.get('person') || '';
user = params.get('user') || '';
year = params.get('year') || '';
place = params.get('place') || '';
search = params.get('search') || '';
selectedLetter = params.get('letter') || '';
updateUrl();
}
"
class="container-normal font-sans mt-10">
<div id="pageheading" class="headingcontainer">
<h1 class="heading">Bände AZ</h1>
<div class="mt-2">
<div class="border-b px-3 border-zinc-300">
<!-- Row 1: Filters -->
<div class="flex flex-wrap items-end justify-center min-h-14 gap-x-2 gap-y-3 pb-3" hx-boost="false">
<!-- Alphabet navigation toggle -->
<div class="relative">
<details class="font-sans text-base list-none" data-role="alphabet-toggle" @toggle="alphabetOpen = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
:class="selectedLetter ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
<span x-text="selectedLetter ? `Alphabet: ${selectedLetter}` : 'Alphabet'"></span>
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': alphabetOpen }"></i>
<span id="baende-alphabet-spinner" class="htmx-indicator text-slate-900">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
</span>
</summary>
<div class="absolute left-0 mt-2 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-2 w-[26rem]">
<a :href="buildPageUrl({ letter: '', offset: 0 })"
hx-indicator="#baende-alphabet-spinner"
@click.prevent="alphabetOpen = false; $el.closest('details').open = false; applyFilter({ selectedLetter: '' }, '#baende-alphabet-spinner', $el)"
x-show="selectedLetter"
class="mb-2 inline-flex w-full items-center justify-center gap-2 rounded bg-orange-100 px-2 py-1 text-sm font-semibold text-orange-800 hover:bg-orange-200 no-underline transition-colors">
<i class="ri-filter-off-line text-base"></i>
<span>Alle</span>
</a>
<div class="grid grid-cols-13 gap-1 text-sm text-gray-700" x-effect="
const active = selectedLetter || '';
$el.querySelectorAll('[data-letter]').forEach((el) => {
const isActive = el.dataset.letter === active;
el.classList.toggle('bg-stone-200', isActive);
el.classList.toggle('font-bold', isActive);
});
">
{{- range $_, $ch := $model.letters -}}
<a :href="buildPageUrl({ letter: '{{ $ch }}', offset: 0 })"
hx-indicator="#baende-alphabet-spinner"
@click.prevent="alphabetOpen = false; $el.closest('details').open = false; applyFilter({ selectedLetter: '{{ $ch }}' }, '#baende-alphabet-spinner', $el)"
data-letter="{{ $ch }}"
class="text-center py-1 px-2 rounded hover:bg-gray-100 no-underline transition-colors">
{{ $ch }}
</a>
{{- end -}}
</div>
</div>
</div>
</details>
</div>
<!-- Status filter -->
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
:class="status ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
<span x-text="status ? `Status: ${statusLabels[status] || status}` : 'Status'"></span>
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
<span id="baende-status-spinner" class="htmx-indicator text-slate-900">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
</span>
</summary>
<div class="absolute left-0 mt-2 w-72 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-3">
<div class="max-h-64 overflow-auto flex flex-col gap-1 text-sm text-gray-700 border border-stone-100 rounded-sm" data-role="filter-list">
<a data-role="filter-item" data-label="Alle" :href="buildPageUrl({ status: '', offset: 0 })"
hx-indicator="#baende-status-spinner"
@click.prevent="open = false; $el.closest('details').open = false; applyFilter({ status: '' }, '#baende-status-spinner', $el)"
x-show="status"
class="mb-2 inline-flex w-full items-center justify-center gap-2 rounded bg-orange-100 px-2 py-1 text-sm font-semibold text-orange-800 hover:bg-orange-200 no-underline transition-colors">
<i class="ri-filter-off-line text-base"></i>
<span>Alle</span>
</a>
{{- range $_, $s := $model.filter_statuses -}}
<a data-role="filter-item" data-label="{{ $s.label }}" :href="buildPageUrl({ status: '{{ $s.value }}', offset: 0 })"
hx-indicator="#baende-status-spinner"
@click.prevent="open = false; $el.closest('details').open = false; applyFilter({ status: '{{ $s.value }}' }, '#baende-status-spinner', $el)"
:class="status === '{{ $s.value }}' ? 'bg-stone-100 font-semibold' : ''"
class="filter-list-row px-2 py-1 rounded-sm hover:bg-stone-100 no-underline transition-colors">
{{ $s.label }}
</a>
{{- end -}}
</div>
</div>
</div>
</details>
</div>
<!-- Person filter -->
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
:class="person ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
<span x-text="person ? `Person: ${personLabels[person] || person}` : 'Person'"></span>
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
</summary>
<div class="absolute left-0 mt-2 w-80 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-3">
<a data-role="filter-item" data-label="Alle" :href="buildPageUrl({ person: '', offset: 0 })"
hx-indicator="#baende-person-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ person: '' }, '#baende-person-spinner', $el);
"
x-show="person"
class="mb-2 inline-flex w-full items-center justify-center gap-2 rounded bg-orange-100 px-2 py-1 text-sm font-semibold text-orange-800 hover:bg-orange-200 no-underline transition-colors">
<i class="ri-filter-off-line text-base"></i>
<span>Alle</span>
</a>
<div class="relative">
<input data-role="filter-search" type="search" autocomplete="off" placeholder="Personen filtern..." class="w-full px-2 py-1 pr-7 border border-stone-200 rounded text-sm" />
<span id="baende-person-spinner" class="htmx-indicator absolute right-2 top-1/2 -translate-y-1/2 text-slate-900">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
</span>
</div>
<div class="mt-2 max-h-80 overflow-auto flex flex-col gap-0.5 text-sm text-gray-700 border border-stone-100 rounded-sm" data-role="filter-list">
{{- range $_, $a := $model.filter_agents -}}
<a data-role="filter-item" data-label="{{ $a.Name }}" :href="buildPageUrl({ person: '{{ $a.Id }}', offset: 0 })"
hx-indicator="#baende-person-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ person: '{{ $a.Id }}' }, '#baende-person-spinner', $el);
"
:class="person === '{{ $a.Id }}' ? 'bg-stone-100 font-semibold' : ''"
class="filter-list-row px-2 py-1 rounded-sm hover:bg-stone-100 no-underline transition-colors">
<span class="filter-list-searchable mr-1">{{ $a.Name }}</span>
{{- if $a.CorporateBody -}}
<span class="inline-flex items-center rounded-xs bg-stone-100 px-1.5 py-0.5 text-[0.7rem] font-semibold text-slate-600">ORG</span>
{{- else if $a.BiographicalData -}}
<span class="text-[0.7rem] text-stone-500 whitespace-nowrap">{{ $a.BiographicalData }}</span>
{{- end -}}
</a>
{{- end -}}
</div>
</div>
</div>
</details>
</div>
<!-- Benutzer filter -->
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
:class="user ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
<span x-text="user ? `Benutzer: ${userLabels[user] || user}` : 'Benutzer'"></span>
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
</summary>
<div class="absolute left-0 mt-2 w-80 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-3">
<a data-role="filter-item" data-label="Alle" :href="buildPageUrl({ user: '', offset: 0 })"
hx-indicator="#baende-user-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ user: '' }, '#baende-user-spinner', $el);
"
x-show="user"
class="mb-2 inline-flex w-full items-center justify-center gap-2 rounded bg-orange-100 px-2 py-1 text-sm font-semibold text-orange-800 hover:bg-orange-200 no-underline transition-colors">
<i class="ri-filter-off-line text-base"></i>
<span>Alle</span>
</a>
<div class="relative">
<input data-role="filter-search" type="search" autocomplete="off" placeholder="Benutzer filtern..." class="w-full px-2 py-1 pr-7 border border-stone-200 rounded text-sm" />
<span id="baende-user-spinner" class="htmx-indicator absolute right-2 top-1/2 -translate-y-1/2 text-slate-900">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
</span>
</div>
<div class="mt-2 max-h-80 overflow-auto flex flex-col gap-0.5 text-sm text-gray-700 border border-stone-100 rounded-sm" data-role="filter-list">
{{- range $_, $u := $model.filter_users -}}
<a data-role="filter-item" data-label="{{ $u.Name }}" :href="buildPageUrl({ user: '{{ $u.Id }}', offset: 0 })"
hx-indicator="#baende-user-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ user: '{{ $u.Id }}' }, '#baende-user-spinner', $el);
"
:class="user === '{{ $u.Id }}' ? 'bg-stone-100 font-semibold' : ''"
class="filter-list-row px-2 py-1 rounded-sm hover:bg-stone-100 no-underline transition-colors">
<span class="filter-list-searchable mr-1">{{ $u.Name }}</span>
</a>
{{- end -}}
</div>
</div>
</div>
</details>
</div>
<!-- Jahr filter -->
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
:class="year ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
<span x-text="year ? `Jahr: ${yearLabels[year] || year}` : 'Jahr'"></span>
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
</summary>
<div class="absolute left-0 mt-2 w-72 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-3">
<a data-role="filter-item" data-label="Alle" :href="buildPageUrl({ year: '', offset: 0 })"
hx-indicator="#baende-year-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ year: '' }, '#baende-year-spinner', $el);
"
x-show="year"
class="mb-2 inline-flex w-full items-center justify-center gap-2 rounded bg-orange-100 px-2 py-1 text-sm font-semibold text-orange-800 hover:bg-orange-200 no-underline transition-colors">
<i class="ri-filter-off-line text-base"></i>
<span>Alle</span>
</a>
<div class="relative">
<input data-role="filter-search" type="search" autocomplete="off" placeholder="Jahre filtern..." class="w-full px-2 py-1 pr-7 border border-stone-200 rounded text-sm" />
<span id="baende-year-spinner" class="htmx-indicator absolute right-2 top-1/2 -translate-y-1/2 text-slate-900">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
</span>
</div>
<div class="mt-2 max-h-80 overflow-auto flex flex-col gap-0.5 text-sm text-gray-700 border border-stone-100 rounded-sm" data-role="filter-list">
{{- range $_, $y := $model.filter_years -}}
{{- $label := $y -}}
{{- if eq $y 0 -}}{{- $label = "ohne Jahr" -}}{{- end -}}
<a data-role="filter-item" data-label="{{ $label }}" :href="buildPageUrl({ year: '{{ $y }}', offset: 0 })"
hx-indicator="#baende-year-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ year: '{{ $y }}' }, '#baende-year-spinner', $el);
"
:class="year === '{{ $y }}' ? 'bg-stone-100 font-semibold' : ''"
class="filter-list-row px-2 py-1 rounded-sm hover:bg-stone-100 no-underline transition-colors">
{{ $label }}
</a>
{{- end -}}
</div>
</div>
</div>
</details>
</div>
<!-- Ort filter -->
<div class="relative" x-data="{ open: false }" data-role="baende-filter">
<details class="font-sans text-base list-none" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
<summary class="cursor-pointer select-none text-gray-700 hover:text-slate-900 bg-gray-100 px-3 py-1.5 rounded-md flex items-center gap-2"
:class="place ? 'font-semibold text-slate-900 ring-1 ring-slate-300' : ''">
<span x-text="place ? `Ort: ${placeLabels[place] || place}` : 'Ort'"></span>
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
</summary>
<div class="absolute left-0 mt-2 w-80 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-3">
<a data-role="filter-item" data-label="Alle" :href="buildPageUrl({ place: '', offset: 0 })"
hx-indicator="#baende-place-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ place: '' }, '#baende-place-spinner', $el);
"
x-show="place"
class="mb-2 inline-flex w-full items-center justify-center gap-2 rounded bg-orange-100 px-2 py-1 text-sm font-semibold text-orange-800 hover:bg-orange-200 no-underline transition-colors">
<i class="ri-filter-off-line text-base"></i>
<span>Alle</span>
</a>
<div class="relative">
<input data-role="filter-search" type="search" autocomplete="off" placeholder="Orte filtern..." class="w-full px-2 py-1 pr-7 border border-stone-200 rounded text-sm" />
<span id="baende-place-spinner" class="htmx-indicator absolute right-2 top-1/2 -translate-y-1/2 text-slate-900">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
</span>
</div>
<div class="mt-2 max-h-80 overflow-auto flex flex-col gap-0.5 text-sm text-gray-700 border border-stone-100 rounded-sm" data-role="filter-list">
{{- range $_, $p := $model.filter_places -}}
<a data-role="filter-item" data-label="{{ $p.Name }}" :href="buildPageUrl({ place: '{{ $p.Id }}', offset: 0 })"
hx-indicator="#baende-place-spinner"
@click.prevent="
open = false;
const root = $el.closest('details');
const input = root?.querySelector('[data-role=filter-search]');
if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }
root.open = false;
applyFilter({ place: '{{ $p.Id }}' }, '#baende-place-spinner', $el);
"
:class="place === '{{ $p.Id }}' ? 'bg-stone-100 font-semibold' : ''"
class="filter-list-row px-2 py-1 rounded-sm hover:bg-stone-100 no-underline transition-colors">
{{ $p.Name }}
</a>
{{- end -}}
</div>
</div>
</div>
</details>
</div>
</div>
<!-- Row 2: Search + Spalten + count -->
<div class="flex flex-wrap items-center justify-between gap-3 pb-0 mt-3">
<!-- Search box -->
<div class="min-w-[22.5rem] max-w-96 flex flex-row bg-stone-50 relative font-sans text-lg items-center">
<div>
<i class="ri-search-line"></i><i class="-ml-0.5 inline-block ri-arrow-right-s-line"></i>
</div>
<div class="border-b-4 border-zinc-300 grow">
<form
method="GET"
action="/baende/"
hx-get="/baende/results/"
hx-indicator="#baende-search-spinner"
hx-push-url="false"
hx-swap="outerHTML"
hx-target="#baenderesults"
role="search"
@submit="offset = 0; hasMore = true"
aria-label="Bändesuche">
<input type="hidden" name="sort" :value="sortField" />
<input type="hidden" name="order" :value="sortOrder" />
<input type="hidden" name="status" :value="status" />
<input type="hidden" name="person" :value="person" />
<input type="hidden" name="user" :value="user" />
<input type="hidden" name="year" :value="year" />
<input type="hidden" name="place" :value="place" />
<input type="hidden" name="letter" :value="selectedLetter" />
<div class="relative">
<input
class="px-2 py-0.5 pr-7 font-sans placeholder:italic w-full text-lg"
type="search"
name="search"
value="{{ $model.search }}"
placeholder="Signatur oder Suchbegriff"
x-model="search"
@input.debounce.500="((search.trim().length >= 3) || /^[0-9]+$/.test(search.trim()) || search === '') && $el.form.requestSubmit()"
@search.debounce.500="((search.trim().length >= 3) || /^[0-9]+$/.test(search.trim()) || search === '') && $el.form.requestSubmit()"
autocomplete="off" />
<span id="baende-search-spinner" class="htmx-indicator absolute right-1 top-1/2 -translate-y-1/2 text-slate-900">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
</span>
</div>
<button x-show="false">Suchen</button>
</form>
</div>
</div>
<div class="flex flex-wrap items-center justify-end gap-3">
<div class="relative" x-data="{ open: false }">
<details class="font-sans text-base list-none" data-role="baende-column-toggle" @toggle="open = $el.open; if ($el.open) { closeOtherDropdowns($el); }">
<summary class="cursor-pointer select-none text-gray-600 hover:text-slate-900 px-2 py-1 rounded-xs flex items-center gap-2 text-lg font-semibold font-sans">
<i class="ri-eye-line"></i>
<span>Spalten</span>
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': open }"></i>
</summary>
<div class="absolute right-0 mt-2 w-56 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-4 flex flex-col gap-2 text-sm text-gray-700">
<label class="inline-flex items-center gap-2"><input type="checkbox" data-col="title" checked /> Titel</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" data-col="appearance" checked /> Erscheinung</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" data-col="year" /> Jahr</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" data-col="extent" checked /> Umfang / Maße</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" data-col="signatures" checked /> Signaturen</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" data-col="modified" /> Bearbeitet / Benutzer</label>
</div>
</div>
</details>
</div>
<div id="baende-count" class="text-lg font-semibold font-sans text-gray-600 whitespace-nowrap">
{{ if $model.current_count }}{{ $model.current_count }}&thinsp;/&thinsp;{{ end }}{{ if $model.total_count }}{{ $model.total_count }}{{ else }}{{ len $model.result.Entries }}{{ end }} Bände
</div>
</div>
</div>
</div>
</div>
</div>
<div id="baenderesults" class="mt-2" data-next-offset="{{ $model.next_offset }}">
{{ template "_baende_table" $model }}
<!-- Load More Button -->
<div class="mt-6 flex justify-center" x-show="hasMore">
<button
type="button"
class="content-action-button"
:hx-get="loadMoreUrl()"
hx-target="#baende-tbody"
hx-swap="beforeend"
hx-indicator="this"
@htmx:before-request="loading = true"
@htmx:after-request="
loading = false;
hasMore = $event.detail.xhr.getResponseHeader('X-Has-More') === 'true';
const nextOffsetHeader = Number($event.detail.xhr.getResponseHeader('X-Next-Offset'));
if (!Number.isNaN(nextOffsetHeader)) {
offset = nextOffsetHeader;
}
updateUrl();
"
:disabled="loading">
<i class="ri-arrow-down-line" :class="{ 'spinning': loading }"></i>
<span x-text="loading ? 'Lädt...' : 'Weitere 100 laden'"></span>
</button>
</div>
</div>
</div>
<dialog data-role="baende-delete-dialog" class="dbform fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
<div class="p-5 w-[26rem]">
<div class="text-base font-bold text-gray-900">Band löschen?</div>
<div data-role="baende-delete-title" class="text-sm font-bold text-gray-900 mt-1"></div>
<div data-role="baende-delete-impacts" class="mt-2 text-sm text-gray-700">
Lade Informationen …
</div>
<div data-role="baende-delete-error" class="mt-2 text-sm text-red-700 hidden"></div>
<div class="flex items-center justify-end gap-3 mt-4">
<button type="button" class="resetbutton w-auto px-3 py-1 text-sm" data-role="baende-delete-cancel">Abbrechen</button>
<button
type="button"
class="submitbutton w-auto bg-red-700 hover:bg-red-800 px-3 py-1 text-sm"
data-role="baende-delete-confirm">
Löschen
</button>
</div>
<input type="hidden" data-role="baende-delete-csrf" value="{{ $model.csrf_token }}" />
</div>
</dialog>
<script>
(() => {
const filterRoots = document.querySelectorAll('[data-role="baende-filter"]');
filterRoots.forEach((root) => {
const input = root.querySelector('[data-role="filter-search"]');
const list = root.querySelector('[data-role="filter-list"]');
if (!list || !input) return;
input.addEventListener('input', () => {
const query = input.value.trim().toLowerCase();
list.querySelectorAll('[data-role="filter-item"]').forEach((item) => {
const label = (item.getAttribute('data-label') || '').toLowerCase();
if (item.getAttribute('data-label') === 'Alle') {
return;
}
item.classList.toggle('hidden', query !== '' && !label.includes(query));
});
});
});
})();
(() => {
const dialog = document.querySelector("[data-role='baende-delete-dialog']");
if (!dialog) return;
const titleEl = dialog.querySelector("[data-role='baende-delete-title']");
const impactsEl = dialog.querySelector("[data-role='baende-delete-impacts']");
const errorEl = dialog.querySelector("[data-role='baende-delete-error']");
const cancelBtn = dialog.querySelector("[data-role='baende-delete-cancel']");
const confirmBtn = dialog.querySelector("[data-role='baende-delete-confirm']");
const csrfInput = dialog.querySelector("[data-role='baende-delete-csrf']");
let currentEntryId = "";
let currentDeleteEndpoint = "";
const closeDialog = (event) => {
if (event) {
event.preventDefault();
}
if (dialog.open) {
dialog.close();
}
};
const openDialog = () => {
if (typeof dialog.showModal === "function") {
dialog.showModal();
}
};
const setError = (message) => {
if (!errorEl) return;
if (message) {
errorEl.textContent = message;
errorEl.classList.remove("hidden");
} else {
errorEl.textContent = "";
errorEl.classList.add("hidden");
}
};
const handleDeleteClick = async (event) => {
let button = null;
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
for (const node of path) {
if (node && node.getAttribute && node.getAttribute("data-role") === "baende-delete") {
button = node;
break;
}
}
if (!button && event.target && event.target.closest) {
button = event.target.closest("[data-role='baende-delete']");
}
if (!button) return;
event.preventDefault();
event.stopPropagation();
currentEntryId = button.getAttribute("data-entry-id") || "";
currentDeleteEndpoint = button.getAttribute("data-delete-endpoint") || "";
const entryTitle = button.getAttribute("data-entry-title") || "";
if (titleEl) {
titleEl.textContent = entryTitle ? entryTitle : "Unbekannter Eintrag";
}
if (impactsEl) {
impactsEl.textContent = "Lade Informationen …";
}
setError("");
openDialog();
if (!currentEntryId || !impactsEl) return;
try {
const response = await fetch(`/baende/delete-info/${encodeURIComponent(currentEntryId)}`);
if (!response.ok) {
throw new Error("Infos konnten nicht geladen werden.");
}
const html = await response.text();
impactsEl.innerHTML = html;
} catch (error) {
impactsEl.textContent = "Infos konnten nicht geladen werden.";
}
};
document.addEventListener("click", handleDeleteClick, true);
if (cancelBtn) {
cancelBtn.addEventListener("click", closeDialog);
}
dialog.addEventListener("cancel", closeDialog);
if (confirmBtn) {
confirmBtn.addEventListener("click", async (event) => {
event.preventDefault();
if (!currentDeleteEndpoint || !csrfInput) return;
setError("");
try {
const response = await fetch(currentDeleteEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
csrf_token: csrfInput.value || "",
}),
});
let data = null;
try {
data = await response.clone().json();
} catch {
data = null;
}
if (!response.ok) {
throw new Error(data?.error || "Löschen fehlgeschlagen.");
}
closeDialog();
if (currentEntryId) {
const rows = document.querySelectorAll(`tr[data-entry-id='${CSS.escape(currentEntryId)}']`);
rows.forEach((row) => row.remove());
const countEl = document.getElementById("baende-count");
if (countEl) {
const raw = countEl.textContent || "";
const hasSlash = raw.includes("/");
const nums = raw.match(/\d+/g) || [];
if (nums.length > 0) {
let current = parseInt(nums[0], 10);
let total = nums.length > 1 ? parseInt(nums[1], 10) : null;
if (!Number.isNaN(current) && current > 0) current -= 1;
if (total !== null && !Number.isNaN(total) && total > 0) total -= 1;
if (hasSlash && total !== null) {
countEl.textContent = `${current} / ${total} Bände`;
} else {
countEl.textContent = `${current} Bände`;
}
}
}
}
} catch (error) {
setError(error instanceof Error ? error.message : "Löschen fehlgeschlagen.");
}
});
}
})();
</script>