+ /baende endpoint fixes

This commit is contained in:
Simon Martens
2026-01-27 09:52:06 +01:00
parent e35f3b19d4
commit cac2b0916c
12 changed files with 2214 additions and 2186 deletions

View File

@@ -240,10 +240,12 @@ func (p *BaendePage) buildResultData(app core.App, ma pagemodels.IApp, e *core.R
// Validate sort field - whitelist approach for security
validSorts := map[string]bool{
"title": true,
"alm": true,
"year": true,
"signatur": true,
"title": true,
"alm": true,
"year": true,
"signatur": true,
"responsibility": true,
"place": true,
}
if !validSorts[sort] {
sort = "title" // default
@@ -325,6 +327,10 @@ func (p *BaendePage) buildResultData(app core.App, ma pagemodels.IApp, e *core.R
dbmodels.Sort_Entries_Year_Title(filteredEntries)
case "signatur":
dbmodels.Sort_Entries_Signatur(filteredEntries, itemsMap)
case "responsibility":
dbmodels.Sort_Entries_Responsibility_Title(filteredEntries)
case "place":
dbmodels.Sort_Entries_Place_Title(filteredEntries)
default: // "title"
dbmodels.Sort_Entries_Title_Year(filteredEntries)
}

View File

@@ -113,3 +113,51 @@ func Sort_Entries_Signatur(entries []*Entry, itemsMap map[string][]*Item) {
return collator.CompareString(iLowestSig, jLowestSig)
})
}
// Sort_Entries_Responsibility_Title sorts entries by responsibility statement, then preferred title.
// Empty responsibility statements sort last.
func Sort_Entries_Responsibility_Title(entries []*Entry) {
collator := collate.New(language.German)
slices.SortFunc(entries, func(i, j *Entry) int {
iResp := i.ResponsibilityStmt()
jResp := j.ResponsibilityStmt()
if iResp == "" && jResp == "" {
return collator.CompareString(i.PreferredTitle(), j.PreferredTitle())
}
if iResp == "" {
return 1
}
if jResp == "" {
return -1
}
if iResp == jResp {
return collator.CompareString(i.PreferredTitle(), j.PreferredTitle())
}
return collator.CompareString(iResp, jResp)
})
}
// Sort_Entries_Place_Title sorts entries by place statement, then preferred title.
// Empty place statements sort last.
func Sort_Entries_Place_Title(entries []*Entry) {
collator := collate.New(language.German)
slices.SortFunc(entries, func(i, j *Entry) int {
iPlace := i.PlaceStmt()
jPlace := j.PlaceStmt()
if iPlace == "" && jPlace == "" {
return collator.CompareString(i.PreferredTitle(), j.PreferredTitle())
}
if iPlace == "" {
return 1
}
if jPlace == "" {
return -1
}
if iPlace == jPlace {
return collator.CompareString(i.PreferredTitle(), j.PreferredTitle())
}
return collator.CompareString(iPlace, jPlace)
})
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -125,8 +125,8 @@ class="container-normal font-sans mt-10">
Alphabet
<i class="ri-arrow-down-s-line transform origin-center transition-transform" :class="{ 'rotate-180': alphabetOpen }"></i>
</summary>
<div class="absolute left-0 mt-2 w-12 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-2 flex flex-col gap-1 text-sm text-gray-700">
<div class="absolute left-0 mt-2 z-10 bg-white rounded-md shadow-lg border border-gray-200">
<div class="p-2 grid grid-cols-13 gap-1 text-sm text-gray-700 w-[26rem]">
{{- range $_, $ch := $model.letters -}}
<a href="/baende/?letter={{ $ch }}&sort={{ $model.sort_field }}&order={{ $model.sort_order }}"
hx-get="/baende/results/?letter={{ $ch }}&sort={{ $model.sort_field }}&order={{ $model.sort_order }}"
@@ -134,33 +134,26 @@ class="container-normal font-sans mt-10">
hx-swap="outerHTML"
hx-indicator="body"
hx-push-url="/baende/?letter={{ $ch }}&sort={{ $model.sort_field }}&order={{ $model.sort_order }}"
@click="offset = 0; hasMore = true; alphabetOpen = false; selectedLetter = '{{ $ch }}'"
@click="offset = 0; hasMore = true; alphabetOpen = false; selectedLetter = '{{ $ch }}'; search = ''"
:class="selectedLetter === '{{ $ch }}' ? 'bg-stone-200 font-bold' : ''"
class="text-center py-1 px-2 rounded hover:bg-gray-100 no-underline transition-colors">
{{ $ch }}
</a>
{{- end -}}
<!-- Clear filter option -->
<a href="/baende/?sort={{ $model.sort_field }}&order={{ $model.sort_order }}"
hx-get="/baende/results/?sort={{ $model.sort_field }}&order={{ $model.sort_order }}"
hx-target="#baenderesults"
hx-swap="outerHTML"
hx-indicator="body"
hx-push-url="/baende/?sort={{ $model.sort_field }}&order={{ $model.sort_order }}"
@click="offset = 0; hasMore = true; alphabetOpen = false; selectedLetter = ''"
class="text-center py-1 px-2 rounded hover:bg-gray-100 no-underline transition-colors border-t mt-1">
@click="offset = 0; hasMore = true; alphabetOpen = false; selectedLetter = ''; search = ''"
class="text-center py-1 px-2 rounded hover:bg-gray-100 no-underline transition-colors col-span-13 border-t mt-1">
Alle
</a>
</div>
</div>
</details>
</div>
</div>
<!-- Right side group: Spalten menu and count/button -->
<div class="flex items-end gap-4">
<!-- Spalten toggle -->
<div class="relative" x-data="{ open: false }">
@@ -173,13 +166,16 @@ class="container-normal font-sans mt-10">
<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="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="language" /> Sprachen</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>
</div>
</div>
</details>
</div>
</div>
<!-- Right side group: count/button -->
<div class="flex items-end gap-4 ml-auto">
<!-- Count and New button -->
<div class="flex items-center gap-3">
@@ -202,6 +198,11 @@ class="container-normal font-sans mt-10">
<div id="baenderesults" class="mt-2" data-next-offset="{{ $model.next_offset }}">
{{ template "_baende_table" $model }}
<!-- Bottom count -->
<div id="baende-count-bottom" class="mt-4 flex justify-center text-base 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>
<!-- Load More Button -->
<div class="mt-6 flex justify-center" x-show="hasMore">
<button

View File

@@ -1,10 +1,10 @@
{{ $model := . }}
<div class="mt-6 overflow-x-auto">
<div class="mt-6 overflow-x-auto overflow-y-visible">
<table class="min-w-full text-sm font-sans baende-text">
<thead class="text-left text-gray-600 border-b">
<tr>
<th class="py-2 pr-4 pl-2 whitespace-nowrap w-[10rem]"
<th class="py-2 pr-4 pl-2 whitespace-nowrap w-[10rem] align-bottom"
:aria-sort="sortField === 'alm' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'">
<button type="button"
class="flex w-full items-center justify-between gap-1 text-left text-sm"
@@ -19,7 +19,7 @@
aria-hidden="true"></i>
</button>
</th>
<th class="py-2 pr-4 whitespace-nowrap w-[44rem]"
<th class="py-2 pr-4 whitespace-nowrap w-[44rem] align-bottom"
:aria-sort="sortField === 'title' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'">
<button type="button"
class="flex w-full items-center justify-between gap-1 text-left text-sm"
@@ -34,8 +34,35 @@
aria-hidden="true"></i>
</button>
</th>
<th class="py-2 pr-4 whitespace-nowrap col-appearance w-[18rem]">Erscheinung</th>
<th class="py-2 pr-4 whitespace-nowrap col-year hidden"
<th class="py-2 pr-4 whitespace-nowrap col-appearance w-[18rem] align-bottom">
<div class="flex flex-col items-start gap-0.5 h-full justify-end">
<button type="button"
class="flex w-full items-center justify-between gap-1 text-left text-sm leading-tight"
@click="changeSort('responsibility')">
<span class="font-semibold tracking-wide">Herausgeber</span>
<i class="text-xs opacity-70 transition-colors"
:class="{
'ri-arrow-up-line text-blue-600': sortField === 'responsibility' && sortOrder === 'asc',
'ri-arrow-down-line text-blue-600': sortField === 'responsibility' && sortOrder === 'desc',
'ri-arrow-up-down-line text-gray-400': sortField !== 'responsibility'
}"
aria-hidden="true"></i>
</button>
<button type="button"
class="flex w-full items-center justify-between gap-1 text-left text-sm leading-tight"
@click="changeSort('place')">
<span class="font-semibold tracking-wide">Ortsangabe</span>
<i class="text-xs opacity-70 transition-colors"
:class="{
'ri-arrow-up-line text-blue-600': sortField === 'place' && sortOrder === 'asc',
'ri-arrow-down-line text-blue-600': sortField === 'place' && sortOrder === 'desc',
'ri-arrow-up-down-line text-gray-400': sortField !== 'place'
}"
aria-hidden="true"></i>
</button>
</div>
</th>
<th class="py-2 pr-4 whitespace-nowrap col-year hidden align-bottom"
:aria-sort="sortField === 'year' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'">
<button type="button"
class="flex w-full items-center justify-between gap-1 text-left text-sm"
@@ -50,9 +77,13 @@
aria-hidden="true"></i>
</button>
</th>
<th class="py-2 pr-4 whitespace-nowrap col-language hidden">Sprachen</th>
<th class="py-2 pr-4 whitespace-nowrap col-extent w-[18rem]">Umfang / Maße</th>
<th class="py-2 pr-4 whitespace-nowrap col-signatures"
<th class="py-2 pr-4 whitespace-nowrap col-extent w-[18rem] align-bottom">
<div class="flex flex-col items-start gap-0.5 leading-tight">
<span class="font-semibold tracking-wide">Umfang&thinsp;/&thinsp;Maße</span>
<span class="font-semibold tracking-wide">Sprache</span>
</div>
</th>
<th class="py-2 pr-4 whitespace-nowrap col-signatures align-bottom"
:aria-sort="sortField === 'signatur' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'">
<button type="button"
class="flex w-full items-center justify-between gap-1 text-left text-sm"
@@ -86,22 +117,22 @@
{{- end -}}
<div class="flex flex-wrap items-center gap-1.5 pt-1">
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-eye-line"></i>
</a>
<div class="data-tip">Ansehen</div>
</tool-tip>
{{- if (IsAdminOrEditor $model.request.user) -}}
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-edit-line"></i>
</a>
<div class="data-tip">Bearbeiten</div>
</tool-tip>
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="return confirm('Band wirklich löschen?');">
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
<tool-tip position="top" class="inline">
<button type="submit" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<i class="ri-delete-bin-line"></i>
</button>
<div class="data-tip">Löschen</div>
@@ -112,7 +143,7 @@
</div>
</td>
<td class="py-2 pr-4">
<div class="font-semibold text-slate-900 text-base leading-snug">
<div class="font-semibold text-slate-900 text-base leading-snug inline-flex items-center gap-1">
{{- if $entry.PreferredTitle -}}
{{ $entry.PreferredTitle }}
{{- else if ne $entry.Year 0 -}}
@@ -120,8 +151,8 @@
{{- else -}}
[o.J.]
{{- end -}}
<tool-tip position="top" class="inline">
<i class="status-icon ml-1 align-middle {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<tool-tip position="top" class="inline-flex items-center">
<i class="status-icon {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<div class="data-tip">
{{- if eq $entry.EditState "Unknown" -}}
Gesucht
@@ -190,21 +221,24 @@
{{ $entry.Year }}
{{- end -}}
</td>
<td class="py-2 pr-4 col-language hidden">
{{- if $entry.Language -}}
{{- range $i, $lang := $entry.Language -}}
{{- if $i }}, {{ end -}}{{ LanguageNameGerman $lang }}
{{- end -}}
{{- end -}}
</td>
<td class="py-2 pr-4 col-extent">
{{- if or $entry.Extent $entry.Dimensions -}}
{{- if or $entry.Extent $entry.Dimensions $entry.Language -}}
<div class="flex flex-col gap-1 text-sm text-gray-700">
{{- if $entry.Extent -}}
<div><span class="font-semibold text-gray-500 block">Struktur</span>{{ $entry.Extent }}</div>
<div>{{ $entry.Extent }}</div>
{{- end -}}
{{- if $entry.Dimensions -}}
<div><span class="font-semibold text-gray-500">Maße:</span> {{ $entry.Dimensions }}</div>
<div>{{ $entry.Dimensions }}</div>
{{- end -}}
{{- if or $entry.Extent $entry.Dimensions -}}
<div class="h-1"></div>
{{- end -}}
{{- if $entry.Language -}}
<div class="flex flex-wrap gap-1.5 pt-0.5">
{{- range $i, $lang := $entry.Language -}}
<span class="inline-flex items-center rounded-xs border border-slate-200 bg-white px-2 py-0.5 text-xs font-semibold text-slate-700">{{ LanguageNameGerman $lang }}</span>
{{- end -}}
</div>
{{- end -}}
</div>
{{- end -}}
@@ -214,12 +248,12 @@
{{- if $items -}}
<div class="flex flex-col gap-2 text-sm text-gray-700">
{{- range $_, $item := $items -}}
<div class="inline-flex flex-col items-center justify-center rounded-xs border border-slate-200 bg-white text-xs">
<div class="inline-flex flex-col items-center justify-center rounded-xs border border-slate-200 bg-white text-sm">
{{- if $item.Identifier -}}
<div class="px-2 py-1 font-semibold text-slate-900 text-center">{{ $item.Identifier }}</div>
{{- end -}}
{{- if $item.Media -}}
<div class="w-full border-t border-slate-200 px-2 py-1 text-gray-600 text-center leading-snug">
<div class="w-full border-t border-slate-200 px-2 py-1 text-gray-600 text-center leading-snug text-sm">
{{- range $i, $media := $item.Media -}}{{- if $i }}, {{ end -}}{{ $media }}{{- end -}}
</div>
{{- end -}}

View File

@@ -9,22 +9,22 @@
{{- end -}}
<div class="flex flex-wrap items-center gap-1.5 pt-2">
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-eye-line"></i>
</a>
<div class="data-tip">Ansehen</div>
</tool-tip>
{{- if $result.IsAdmin -}}
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-edit-line"></i>
</a>
<div class="data-tip">Bearbeiten</div>
</tool-tip>
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="return confirm('Band wirklich löschen?');">
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
<input type="hidden" name="csrf_token" value="{{ $result.CSRFToken }}" />
<tool-tip position="top" class="inline">
<button type="submit" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<i class="ri-delete-bin-line"></i>
</button>
@@ -35,6 +35,7 @@
<tool-tip position="top" class="inline">
<button
type="button"
onclick="event.stopPropagation();"
class="inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900"
hx-get="/baende/row/{{ $entry.MusenalmID }}"
hx-target="closest tr"
@@ -50,7 +51,7 @@
<td class="py-2 pr-4 pl-2" colspan="6">
<div class="rounded-md border border-slate-200 bg-white shadow-sm p-3 text-base">
<div class="flex items-start justify-between gap-4 mb-3">
<div class="font-sans text-base font-semibold text-slate-900 inline-flex items-center">
<div class="font-sans text-base font-semibold text-slate-900 inline-flex items-center gap-1">
{{- if $entry.PreferredTitle -}}
{{ $entry.PreferredTitle }}
{{- else if ne $entry.Year 0 -}}
@@ -59,7 +60,7 @@
[o.J.]
{{- end -}}
<tool-tip position="top" class="inline-flex items-center">
<i class="status-icon ml-1 {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<i class="status-icon {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<div class="data-tip">
{{- if eq $entry.EditState "Unknown" -}}
Gesucht
@@ -78,7 +79,7 @@
</tool-tip>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_18rem] gap-6 text-base font-sans text-gray-700">
<div class="grid grid-cols-1 lg:grid-cols-[1fr_18rem] gap-6 text-base font-sans text-gray-700 baende-details-card">
<div class="flex flex-col gap-3">
{{- if or $entry.TitleStmt $entry.SubtitleStmt $entry.VariantTitle $entry.ParallelTitle $entry.IncipitStmt -}}
<div>
@@ -105,7 +106,7 @@
{{- if $entry.Annotation -}}
<div>
<div class="text-xs uppercase tracking-wide text-gray-500">Annotation</div>
<div>{{ $entry.Annotation }}</div>
<div class="annotation-content">{{- Safe (ReplaceSlashParen $entry.Annotation) -}}</div>
</div>
{{- end -}}
{{- if $result.SeriesRels -}}
@@ -162,7 +163,7 @@
{{- if $entry.Comment -}}
<div>
<div class="text-xs uppercase tracking-wide text-gray-500">Kommentar</div>
<div>{{ $entry.Comment }}</div>
<div class="annotation-content">{{- Safe (ReplaceSlashParen $entry.Comment) -}}</div>
</div>
{{- end -}}
{{- if ne $entry.Year 0 -}}
@@ -206,33 +207,62 @@
{{- if $result.Items -}}
<div>
<div class="text-xs uppercase tracking-wide text-gray-500">Exemplare</div>
<div class="flex flex-col gap-2">
<div class="grid grid-cols-1 gap-4">
{{- range $_, $item := $result.Items -}}
<div class="rounded-md border border-slate-200 bg-white px-3 py-2 shadow-sm">
<div class="flex items-start gap-3">
<div class="inline-flex flex-col items-center justify-center rounded-xs border border-slate-200 bg-white text-xs min-w-[6rem]">
{{- if $item.Identifier -}}
<div class="px-2 py-1 font-semibold text-slate-900 text-center">{{ $item.Identifier }}</div>
{{- end -}}
{{- if $item.Media -}}
<div class="w-full border-t border-slate-200 px-2 py-1 text-gray-600 text-center leading-snug">
<div class="rounded-sm border border-slate-200 bg-white">
<div class="divide-y divide-slate-200 text-sm text-gray-700">
{{- if $item.Identifier -}}
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 px-3 py-1.5">
<span class="text-xs uppercase tracking-wide text-gray-500 whitespace-nowrap">Signatur</span>
<span class="font-semibold text-slate-900">{{ $item.Identifier }}</span>
</div>
{{- end -}}
{{- if $item.Media -}}
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 px-3 py-1.5">
<span class="text-xs uppercase tracking-wide text-gray-500 whitespace-nowrap">Status</span>
<span class="font-medium text-slate-800">
{{- range $i, $media := $item.Media -}}{{- if $i }}, {{ end -}}{{ $media }}{{- end -}}
</div>
{{- end -}}
</div>
<div class="flex-1">
{{- if or $item.Location $item.Owner -}}
<div class="text-sm text-gray-600">
{{- if $item.Location -}}<span>{{ $item.Location }}</span>{{- end -}}
{{- if $item.Owner -}}<span class="ml-2">{{ $item.Owner }}</span>{{- end -}}
</div>
{{- end -}}
</div>
</span>
</div>
{{- end -}}
{{- if $item.Location -}}
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 px-3 py-1.5">
<span class="text-xs uppercase tracking-wide text-gray-500 whitespace-nowrap">Standort</span>
<span class="font-medium text-slate-800">{{ $item.Location }}</span>
</div>
{{- end -}}
{{- if $item.Owner -}}
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 px-3 py-1.5">
<span class="text-xs uppercase tracking-wide text-gray-500 whitespace-nowrap">Träger</span>
<span class="font-medium text-slate-800">{{ $item.Owner }}</span>
</div>
{{- end -}}
{{- if $item.Condition -}}
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 px-3 py-1.5">
<span class="text-xs uppercase tracking-wide text-gray-500 whitespace-nowrap">Zustand</span>
<span class="font-medium text-slate-800">{{ $item.Condition }}</span>
</div>
{{- end -}}
{{- if $item.Annotation -}}
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 px-3 py-1.5">
<span class="text-xs uppercase tracking-wide text-gray-500 whitespace-nowrap">Anmerkung</span>
<span class="text-slate-700 annotation-content annotation-small">{{- Safe (ReplaceSlashParen $item.Annotation) -}}</span>
</div>
{{- end -}}
{{- if $item.Uri -}}
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 px-3 py-1.5">
<span class="text-xs uppercase tracking-wide text-gray-500 whitespace-nowrap">URI</span>
<a href="{{ $item.Uri }}" class="text-slate-700 hover:text-slate-900 underline underline-offset-2" target="_blank" rel="noopener">Link öffnen</a>
</div>
{{- end -}}
</div>
{{- if or $item.Location $item.Owner -}}
{{- if or $item.Location $item.Owner $item.Condition $item.Annotation $item.Uri -}}
<div class="sr-only">
{{- if $item.Location -}}<span>{{ $item.Location }}</span>{{- end -}}
{{- if $item.Owner -}}<span class="ml-2">{{ $item.Owner }}</span>{{- end -}}
{{- if $item.Condition -}}<span class="ml-2">{{ $item.Condition }}</span>{{- end -}}
{{- if $item.Annotation -}}<span class="ml-2">{{ $item.Annotation }}</span>{{- end -}}
{{- if $item.Uri -}}<span class="ml-2">{{ $item.Uri }}</span>{{- end -}}
</div>
{{- end -}}
</div>
@@ -245,4 +275,3 @@
</div>
</td>
</tr>

View File

@@ -15,22 +15,22 @@
{{- end -}}
<div class="flex flex-wrap items-center gap-1.5 pt-1">
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-eye-line"></i>
</a>
<div class="data-tip">Ansehen</div>
</tool-tip>
{{- if (IsAdminOrEditor $model.request.user) -}}
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-edit-line"></i>
</a>
<div class="data-tip">Bearbeiten</div>
</tool-tip>
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="return confirm('Band wirklich löschen?');">
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
<tool-tip position="top" class="inline">
<button type="submit" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<i class="ri-delete-bin-line"></i>
</button>
<div class="data-tip">Löschen</div>
@@ -41,7 +41,7 @@
</div>
</td>
<td class="py-2 pr-4">
<div class="font-semibold text-slate-900 text-base leading-snug">
<div class="font-semibold text-slate-900 text-base leading-snug inline-flex items-center gap-1">
{{- if $entry.PreferredTitle -}}
{{ $entry.PreferredTitle }}
{{- else if ne $entry.Year 0 -}}
@@ -49,8 +49,8 @@
{{- else -}}
[o.J.]
{{- end -}}
<tool-tip position="top" class="inline">
<i class="status-icon ml-1 align-middle {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<tool-tip position="top" class="inline-flex items-center">
<i class="status-icon {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<div class="data-tip">
{{- if eq $entry.EditState "Unknown" -}}
Gesucht
@@ -119,21 +119,24 @@
{{ $entry.Year }}
{{- end -}}
</td>
<td class="py-2 pr-4 col-language hidden">
{{- if $entry.Language -}}
{{- range $i, $lang := $entry.Language -}}
{{- if $i }}, {{ end -}}{{ LanguageNameGerman $lang }}
{{- end -}}
{{- end -}}
</td>
<td class="py-2 pr-4 col-extent">
{{- if or $entry.Extent $entry.Dimensions -}}
{{- if or $entry.Extent $entry.Dimensions $entry.Language -}}
<div class="flex flex-col gap-1 text-sm text-gray-700">
{{- if $entry.Extent -}}
<div><span class="font-semibold text-gray-500 block">Struktur</span>{{ $entry.Extent }}</div>
<div>{{ $entry.Extent }}</div>
{{- end -}}
{{- if $entry.Dimensions -}}
<div><span class="font-semibold text-gray-500">Maße:</span> {{ $entry.Dimensions }}</div>
<div>{{ $entry.Dimensions }}</div>
{{- end -}}
{{- if or $entry.Extent $entry.Dimensions -}}
<div class="h-1"></div>
{{- end -}}
{{- if $entry.Language -}}
<div class="flex flex-wrap gap-1.5 pt-0.5">
{{- range $i, $lang := $entry.Language -}}
<span class="inline-flex items-center rounded-xs border border-slate-200 bg-white px-2 py-0.5 text-xs font-semibold text-slate-700">{{ LanguageNameGerman $lang }}</span>
{{- end -}}
</div>
{{- end -}}
</div>
{{- end -}}
@@ -143,12 +146,12 @@
{{- if $items -}}
<div class="flex flex-col gap-2 text-sm text-gray-700">
{{- range $_, $item := $items -}}
<div class="inline-flex flex-col items-center justify-center rounded-xs border border-slate-200 bg-white text-xs">
<div class="inline-flex flex-col items-center justify-center rounded-xs border border-slate-200 bg-white text-sm">
{{- if $item.Identifier -}}
<div class="px-2 py-1 font-semibold text-slate-900 text-center">{{ $item.Identifier }}</div>
{{- end -}}
{{- if $item.Media -}}
<div class="w-full border-t border-slate-200 px-2 py-1 text-gray-600 text-center leading-snug">
<div class="w-full border-t border-slate-200 px-2 py-1 text-gray-600 text-center leading-snug text-sm">
{{- range $i, $media := $item.Media -}}{{- if $i }}, {{ end -}}{{ $media }}{{- end -}}
</div>
{{- end -}}

View File

@@ -17,6 +17,10 @@
{{ template "_baende_table" $model }}
<div id="baende-count-bottom" class="mt-4 flex justify-center text-base 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>
<!-- Load More Button -->
<div class="mt-6 flex justify-center" x-show="hasMore">
<button

View File

@@ -15,22 +15,22 @@
{{- end -}}
<div class="flex flex-wrap items-center gap-1.5 pt-1">
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-eye-line"></i>
</a>
<div class="data-tip">Ansehen</div>
</tool-tip>
{{- if .is_admin -}}
<tool-tip position="top" class="inline">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<a href="/almanach/{{ $entry.MusenalmID }}/edit" onclick="event.stopPropagation();" class="no-underline inline-flex items-center gap-1 rounded-xs bg-stone-100 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-stone-200 hover:text-slate-900">
<i class="ri-edit-line"></i>
</a>
<div class="data-tip">Bearbeiten</div>
</tool-tip>
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="return confirm('Band wirklich löschen?');">
<form method="POST" action="/almanach/{{ $entry.MusenalmID }}/edit/delete" class="inline" onsubmit="event.stopPropagation(); return confirm('Band wirklich löschen?');">
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
<tool-tip position="top" class="inline">
<button type="submit" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<button type="submit" onclick="event.stopPropagation();" class="inline-flex items-center gap-1 rounded-xs bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100 hover:text-red-900">
<i class="ri-delete-bin-line"></i>
</button>
<div class="data-tip">Löschen</div>
@@ -41,7 +41,7 @@
</div>
</td>
<td class="py-2 pr-4">
<div class="font-semibold text-slate-900 text-base leading-snug">
<div class="font-semibold text-slate-900 text-base leading-snug inline-flex items-center gap-1">
{{- if $entry.PreferredTitle -}}
{{ $entry.PreferredTitle }}
{{- else if ne $entry.Year 0 -}}
@@ -49,8 +49,8 @@
{{- else -}}
[o.J.]
{{- end -}}
<tool-tip position="top" class="inline">
<i class="status-icon ml-1 align-middle {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<tool-tip position="top" class="inline-flex items-center">
<i class="status-icon {{- if eq $entry.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $entry.EditState "Seen" }} ri-information-line{{- else if eq $entry.EditState "Review" }} ri-search-line{{- else if eq $entry.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}" data-status="{{ $entry.EditState }}"></i>
<div class="data-tip">
{{- if eq $entry.EditState "Unknown" -}}
Gesucht
@@ -119,21 +119,24 @@
{{ $entry.Year }}
{{- end -}}
</td>
<td class="py-2 pr-4 col-language hidden">
{{- if $entry.Language -}}
{{- range $i, $lang := $entry.Language -}}
{{- if $i }}, {{ end -}}{{ LanguageNameGerman $lang }}
{{- end -}}
{{- end -}}
</td>
<td class="py-2 pr-4 col-extent">
{{- if or $entry.Extent $entry.Dimensions -}}
{{- if or $entry.Extent $entry.Dimensions $entry.Language -}}
<div class="flex flex-col gap-1 text-sm text-gray-700">
{{- if $entry.Extent -}}
<div><span class="font-semibold text-gray-500 block">Struktur</span>{{ $entry.Extent }}</div>
<div>{{ $entry.Extent }}</div>
{{- end -}}
{{- if $entry.Dimensions -}}
<div><span class="font-semibold text-gray-500">Maße:</span> {{ $entry.Dimensions }}</div>
<div>{{ $entry.Dimensions }}</div>
{{- end -}}
{{- if or $entry.Extent $entry.Dimensions -}}
<div class="h-1"></div>
{{- end -}}
{{- if $entry.Language -}}
<div class="flex flex-wrap gap-1.5 pt-0.5">
{{- range $i, $lang := $entry.Language -}}
<span class="inline-flex items-center rounded-xs border border-slate-200 bg-white px-2 py-0.5 text-xs font-semibold text-slate-700">{{ LanguageNameGerman $lang }}</span>
{{- end -}}
</div>
{{- end -}}
</div>
{{- end -}}
@@ -143,12 +146,12 @@
{{- if $items -}}
<div class="flex flex-col gap-2 text-sm text-gray-700">
{{- range $_, $item := $items -}}
<div class="inline-flex flex-col items-center justify-center rounded-xs border border-slate-200 bg-white text-xs">
<div class="inline-flex flex-col items-center justify-center rounded-xs border border-slate-200 bg-white text-sm">
{{- if $item.Identifier -}}
<div class="px-2 py-1 font-semibold text-slate-900 text-center">{{ $item.Identifier }}</div>
{{- end -}}
{{- if $item.Media -}}
<div class="w-full border-t border-slate-200 px-2 py-1 text-gray-600 text-center leading-snug">
<div class="w-full border-t border-slate-200 px-2 py-1 text-gray-600 text-center leading-snug text-sm">
{{- range $i, $media := $item.Media -}}{{- if $i }}, {{ end -}}{{ $media }}{{- end -}}
</div>
{{- end -}}

View File

@@ -300,6 +300,15 @@
@apply font-serif;
}
/* Card view (baende details) should use sans annotations */
.baende-details-card .annotation-content {
@apply font-sans text-base;
}
.baende-details-card .annotation-small {
@apply text-sm;
}
/* Headings - bold with spacing, same font size */
.annotation-content h1 {
@apply font-bold mt-2 mb-1 leading-normal;

View File

@@ -81,7 +81,7 @@ export class ToolTip extends HTMLElement {
"tooltip-box",
"opacity-0",
"hidden",
"absolute",
"fixed",
"px-2",
"py-1",
"text-sm",
@@ -89,7 +89,7 @@ export class ToolTip extends HTMLElement {
"bg-gray-900",
"rounded",
"shadow",
"z-10",
"z-50",
"whitespace-nowrap",
"transition-all",
"duration-200",
@@ -169,6 +169,7 @@ export class ToolTip extends HTMLElement {
clearTimeout(this._hideTimeout);
clearTimeout(this._hiddenTimeout);
this._tooltipBox.classList.remove("hidden");
this._updatePosition();
setTimeout(() => {
this._tooltipBox.classList.remove("opacity-0");
this._tooltipBox.classList.add("opacity-100");
@@ -186,61 +187,40 @@ export class ToolTip extends HTMLElement {
}
_updatePosition() {
this._tooltipBox.classList.remove(
"bottom-full",
"left-1/2",
"-translate-x-1/2",
"mb-2", // top
"top-full",
"mt-2", // bottom
"right-full",
"-translate-y-1/2",
"mr-2",
"top-1/2", // left
"left-full",
"ml-2", // right
);
const anchorRect = this.getBoundingClientRect();
const tipRect = this._tooltipBox.getBoundingClientRect();
const gap = 6;
let top = 0;
let left = 0;
const pos = this.getAttribute("position") || "top";
switch (pos) {
case "bottom":
this._tooltipBox.classList.add(
"top-full",
"left-1/2",
"transform",
"-translate-x-1/2",
"mt-0.5",
);
top = anchorRect.bottom + gap;
left = anchorRect.left + (anchorRect.width - tipRect.width) / 2;
break;
case "left":
this._tooltipBox.classList.add(
"right-full",
"top-1/2",
"transform",
"-translate-y-1/2",
"mr-0.5",
);
top = anchorRect.top + (anchorRect.height - tipRect.height) / 2;
left = anchorRect.left - tipRect.width - gap;
break;
case "right":
this._tooltipBox.classList.add(
"left-full",
"top-1/2",
"transform",
"-translate-y-1/2",
"ml-0.5",
);
top = anchorRect.top + (anchorRect.height - tipRect.height) / 2;
left = anchorRect.right + gap;
break;
case "top":
default:
// top as default
this._tooltipBox.classList.add(
"bottom-full",
"left-1/2",
"transform",
"-translate-x-1/2",
"mb-0.5",
);
top = anchorRect.top - tipRect.height - gap;
left = anchorRect.left + (anchorRect.width - tipRect.width) / 2;
}
const padding = 4;
const maxLeft = window.innerWidth - tipRect.width - padding;
const maxTop = window.innerHeight - tipRect.height - padding;
left = Math.max(padding, Math.min(left, maxLeft));
top = Math.max(padding, Math.min(top, maxTop));
this._tooltipBox.style.left = `${left}px`;
this._tooltipBox.style.top = `${top}px`;
}
}