Saving success messages betteR

This commit is contained in:
Simon Martens
2026-01-23 20:00:55 +01:00
parent 7ef2611537
commit 0beb5a2c79
20 changed files with 1209 additions and 811 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,19 +16,13 @@
<i class="ri-eye-line"></i> Anschauen
</a>
</div>
&middot;
<div>
<a href="/abkuerzungen/" class="text-gray-700 hover:text-slate-950 block no-underline">
<i class="ri-loop-left-line"></i> Reset
</a>
</div>
</div>
</div>
</div>
</div>
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_usermessage" $model }}
{{/* usermessage moved into action bar */}}
{{- if (IsAdminOrEditor $model.request.user) -}}
{{/* Editable form for admin/editor */}}
@@ -94,16 +88,21 @@
</div>
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
<p id="abk-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
<p
id="abk-save-feedback"
class="form-action-bar-message save-feedback {{ if $model.error }}save-feedback-error text-red-700{{ else if $model.success }}save-feedback-success text-green-700{{ else }}hidden{{ end }}"
aria-live="polite">
{{- if $model.error -}}
{{ $model.error }}
{{- else if $model.success -}}
{{ $model.success }}
{{- end -}}
</p>
<div class="flex items-center gap-3 self-end flex-wrap">
<a href="/abkuerzungen/" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
<a href="/abkuerzungen/" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-loop-left-line"></i>
<span>Reset</span>
</a>
<button type="submit" class="submitbutton w-48 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Alle speichern</span>

View File

@@ -68,12 +68,6 @@
</div>
{{- end -}}
</div>
&middot;
<div>
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/contents/{{ $model.content.MusenalmID }}/edit" class="text-gray-700 no-underline hover:text-slate-950 block">
<i class="ri-loop-left-line"></i> Reset
</a>
</div>
{{- end -}}
</div>
{{- end -}}
@@ -170,10 +164,10 @@
</div>
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_usermessage" $model }}
{{/* usermessage moved into action bar */}}
<form
autocomplete="off"
class="w-full dbform"
class="w-full dbform form-with-action-bar"
method="POST"
enctype="multipart/form-data"
hx-boost="false"
@@ -258,27 +252,55 @@
</div>
</div>
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
<p class="text-sm text-gray-600" aria-live="polite"></p>
<div class="flex items-center gap-3 self-end flex-wrap">
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-arrow-left-line"></i>
<span>Liste</span>
</a>
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/new" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-add-line"></i>
<span>Neuer Beitrag</span>
</a>
{{- if not $model.is_new -}}
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900" data-role="content-delete">
<i class="ri-delete-bin-line"></i>
<span>Eintrag loeschen</span>
</button>
{{- end -}}
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<div class="form-action-bar">
<div class="form-action-bar-inner">
<p
id="user-message"
class="form-action-bar-message save-feedback {{ if $model.error }}save-feedback-error text-red-700{{ else if $model.success }}save-feedback-success text-green-700{{ else }}hidden{{ end }}"
aria-live="polite">
{{- if $model.error -}}
{{ $model.error }}
{{- else if $model.success -}}
{{ $model.success }}
{{- end -}}
</p>
<div class="form-action-bar-actions">
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-arrow-left-line"></i>
<span>Liste</span>
</a>
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/new" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-add-line"></i>
<span>Neuer Beitrag</span>
</a>
{{- if not $model.is_new -}}
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900" data-role="content-delete">
<i class="ri-delete-bin-line"></i>
<span>Eintrag loeschen</span>
</button>
{{- end -}}
{{- if $model.content.MusenalmID -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<button
type="submit"
name="save_action"
value="view"
class="submitbutton flex items-center gap-2 justify-center"
>
<i class="ri-eye-line"></i>
<span>Speichern &amp; Anzeigen</span>
</button>
{{- end -}}
{{- if not $model.content.MusenalmID -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- end -}}
</div>
</div>
</div>
<dialog data-role="content-delete-dialog" class="dbform fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
@@ -388,28 +410,43 @@
});
}
const form = document.querySelector("form.dbform");
const uploadInput = document.querySelector("[data-role='content-images-upload-input']");
const userMessage = document.getElementById("user-message");
if (form && uploadInput && userMessage) {
const showUserMessage = (message, type) => {
const userMessage = document.getElementById("user-message");
if (!userMessage) return;
userMessage.textContent = message;
userMessage.classList.remove("hidden", "save-feedback-error", "save-feedback-success", "text-red-700", "text-green-700");
if (type === "error") {
userMessage.classList.add("save-feedback-error", "text-red-700");
} else if (type === "success") {
userMessage.classList.add("save-feedback-success", "text-green-700");
}
};
const attachFormHandlers = () => {
const form = document.querySelector("form.dbform");
const uploadInput = document.querySelector("[data-role='content-images-upload-input']");
const userMessage = document.getElementById("user-message");
if (!form || !uploadInput || !userMessage) {
return;
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
const submitter = event.submitter;
const files = Array.from(uploadInput.files || []);
if (files.length > 0) {
const hasInvalid = files.some((file) => !file.type || !file.type.startsWith("image/"));
if (hasInvalid) {
userMessage.innerHTML = `
<div class="text-red-800 text-sm mt-2 rounded-xs bg-red-200 p-2 font-bold border-red-700 shadow border mb-3">
<i class="ri-error-warning-fill"></i> Bitte nur Bilddateien auswählen.
</div>
`;
showUserMessage("Bitte nur Bilddateien auswählen.", "error");
return;
}
}
const payload = new FormData(form);
if (submitter && submitter.name) {
payload.set(submitter.name, submitter.value || "");
}
if (payload.has("scans")) {
payload.delete("scans");
}
@@ -440,26 +477,40 @@
body: payload,
credentials: "same-origin",
});
if (response.redirected && response.url) {
window.location.assign(response.url);
return;
}
const html = await response.text();
if (response.redirected && response.url) {
if (response.url !== window.location.href) {
window.location.assign(response.url);
return;
}
}
if (!html) {
return;
}
const doc = new DOMParser().parseFromString(html, "text/html");
const nextForm = doc.querySelector("form.dbform");
if (nextForm) {
form.replaceWith(nextForm);
initMultiSelects();
attachFormHandlers();
return;
}
const nextMessage = doc.getElementById("user-message");
if (nextMessage) {
userMessage.innerHTML = nextMessage.innerHTML;
const liveMessage = document.getElementById("user-message");
if (liveMessage) {
liveMessage.className = nextMessage.className;
liveMessage.textContent = nextMessage.textContent || "";
if (!liveMessage.textContent.trim()) {
liveMessage.classList.add("hidden");
}
}
} else if (!response.ok) {
userMessage.innerHTML = `
<div class="text-red-800 text-sm mt-2 rounded-xs bg-red-200 p-2 font-bold border-red-700 shadow border mb-3">
<i class="ri-error-warning-fill"></i> Speichern fehlgeschlagen.
</div>
`;
showUserMessage("Speichern fehlgeschlagen.", "error");
}
}, true);
}
};
attachFormHandlers();
})();
</script>

View File

@@ -94,13 +94,6 @@ type AlmanachResult struct {
</div>
{{- end -}}
</div>
&middot;
<div>
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/edit" class="text-gray-700
no-underline hover:text-slate-950 block ">
<i class="ri-loop-left-line"></i> Reset
</a>
</div>
</div>
{{- end -}}
</div>
@@ -181,10 +174,10 @@ type AlmanachResult struct {
</div>
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_usermessage" $model }}
{{/* usermessage moved into action bar */}}
<form
autocomplete="off"
class="w-full dbform"
class="w-full dbform form-with-action-bar"
id="changealmanachform"
x-target="changealmanachform user-message almanach-header-data"
hx-boost="false"
@@ -1209,30 +1202,55 @@ type AlmanachResult struct {
</div>
<!-- End Right Column -->
</div>
<div class="w-full flex flex-col gap-3 mt-6 items-end">
<p id="almanach-save-feedback" class="save-feedback hidden text-right" aria-live="polite"></p>
<div class="flex items-center gap-3 flex-wrap justify-end">
<a href="{{ if $model.is_new }}/suche/baende{{ else }}/almanach/{{ $model.result.Entry.MusenalmID }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
{{- if not $model.is_new -}}
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center" data-role="almanach-reset">
<i class="ri-loop-left-line"></i>
<span>Reset</span>
</button>
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="almanach-delete">
<i class="ri-delete-bin-line"></i>
<span>Eintrag löschen</span>
</button>
{{- end -}}
<button type="button" class="submitbutton w-40 flex items-center gap-2 justify-center" data-role="almanach-save">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<div class="form-action-bar">
<div class="form-action-bar-inner">
<div id="user-message" class="form-action-bar-message">
<p
id="almanach-save-feedback"
class="save-feedback {{ if $model.error }}save-feedback-error text-red-700{{ else if $model.success }}save-feedback-success text-green-700{{ else }}hidden{{ end }}"
aria-live="polite">
{{- if $model.error -}}
{{ $model.error }}
{{- else if $model.success -}}
{{ $model.success }}
{{- end -}}
</p>
</div>
<div class="form-action-bar-actions">
<a href="{{ if $model.is_new }}/suche/baende{{ else }}/almanach/{{ $model.result.Entry.MusenalmID }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
{{- if not $model.is_new -}}
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="almanach-delete">
<i class="ri-delete-bin-line"></i>
<span>Eintrag löschen</span>
</button>
{{- end -}}
{{- if not $model.is_new -}}
<button type="button" class="submitbutton w-40 flex items-center gap-2 justify-center" data-role="almanach-save">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<button
type="button"
class="submitbutton flex items-center gap-2 justify-center"
data-role="almanach-save-view"
data-redirect-url="/almanach/{{ $model.result.Entry.MusenalmID }}">
<i class="ri-eye-line"></i>
<span>Speichern &amp; Anzeigen</span>
</button>
{{- end -}}
{{- if $model.is_new -}}
<button type="button" class="submitbutton w-40 flex items-center gap-2 justify-center" data-role="almanach-save">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- end -}}
</div>
</div>
</div>
{{- if not $model.is_new -}}

View File

@@ -13,7 +13,7 @@
<div data-role="content-edit" class="mt-2">
<form
autocomplete="off"
class="w-full dbform"
class="w-full dbform form-with-action-bar"
method="POST"
action="/almanach/{{ $entry.MusenalmID }}/contents/edit">
<input type="hidden" name="csrf_token" value="{{ $csrf }}" />
@@ -33,19 +33,40 @@
"content_agents" $contentAgents
"agent_relations" $agentRelations
) -}}
<div class="w-full flex items-center justify-end gap-3 mt-4 flex-wrap">
<a href="/almanach/{{ $entry.MusenalmID }}/contents/edit" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Zurueck</span>
</a>
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900" data-role="content-delete">
<i class="ri-delete-bin-line"></i>
<span>Eintrag loeschen</span>
</button>
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<div class="form-action-bar">
<div class="form-action-bar-inner">
<div class="form-action-bar-actions">
<a href="/almanach/{{ $entry.MusenalmID }}/contents/edit" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Zurueck</span>
</a>
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900" data-role="content-delete">
<i class="ri-delete-bin-line"></i>
<span>Eintrag loeschen</span>
</button>
{{- if $content.MusenalmID -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<button
type="submit"
name="save_action"
value="view"
class="submitbutton flex items-center gap-2 justify-center"
>
<i class="ri-eye-line"></i>
<span>Speichern &amp; Anzeigen</span>
</button>
{{- end -}}
{{- if not $content.MusenalmID -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- end -}}
</div>
</div>
</div>
</form>
<dialog data-role="content-delete-dialog" class="dbform fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">

View File

@@ -18,17 +18,18 @@
</h1>
{{- if not $model.is_new -}}
<div class="flex flex-row gap-x-3">
<div>
<a href="/reihen/?place={{ $place.Id }}" class="text-gray-700 hover:text-slate-950 block no-underline">
<i class="ri-eye-line"></i> Anschauen
</a>
</div>
&middot;
<div>
<a href="/orte/" class="text-gray-700 hover:text-slate-950 block no-underline">
<i class="ri-eye-line"></i> Orte
</a>
</div>
&middot;
<div>
<a href="/ort/{{ $place.Id }}/edit" class="text-gray-700 no-underline hover:text-slate-950 block">
<i class="ri-loop-left-line"></i> Reset
</a>
</div>
</div>
{{- end -}}
</div>
@@ -105,10 +106,10 @@
</div>
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_usermessage" $model }}
{{/* usermessage moved into action bar */}}
<form
autocomplete="off"
class="w-full dbform"
autocomplete="off"
class="w-full dbform form-with-action-bar"
id="changeplaceform"
method="POST"
action="{{ if $model.is_new }}/orte/new/{{ else }}/ort/{{ $place.Id }}/edit{{ end }}"
@@ -191,30 +192,54 @@
</div>
</div>
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
<p id="place-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
<div class="flex items-center gap-3 self-end flex-wrap">
<a href="/orte/" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
{{- if not $model.is_new -}}
<a href="/ort/{{ $place.Id }}/edit" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-loop-left-line"></i>
<span>Reset</span>
<div class="form-action-bar">
<div class="form-action-bar-inner">
<p
id="place-save-feedback"
class="form-action-bar-message save-feedback {{ if $model.error }}save-feedback-error text-red-700{{ else if $model.success }}save-feedback-success text-green-700{{ else }}hidden{{ end }}"
aria-live="polite">
{{- if $model.error -}}
{{ $model.error }}
{{- else if $model.success -}}
{{ $model.success }}
{{- end -}}
</p>
<div class="form-action-bar-actions">
<a href="/orte/" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="edit-delete">
<i class="ri-delete-bin-line"></i>
<span>Ort löschen</span>
</button>
{{- end -}}
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- if not $model.is_new -}}
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="edit-delete">
<i class="ri-delete-bin-line"></i>
<span>Ort löschen</span>
</button>
{{- end -}}
{{- if not $model.is_new -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<button
type="submit"
name="save_action"
value="view"
class="submitbutton flex items-center gap-2 justify-center"
>
<i class="ri-eye-line"></i>
<span>Speichern &amp; Anzeigen</span>
</button>
{{- end -}}
{{- if $model.is_new -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- end -}}
</div>
</div>
</div>
{{- if not $model.is_new -}}

View File

@@ -26,11 +26,6 @@
</a>
</div>
&middot;
<div>
<a href="/person/{{ $agent.Id }}/edit" class="text-gray-700 no-underline hover:text-slate-950 block">
<i class="ri-loop-left-line"></i> Reset
</a>
</div>
</div>
{{- end -}}
</div>
@@ -107,10 +102,10 @@
</div>
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_usermessage" $model }}
{{/* usermessage moved into action bar */}}
<form
autocomplete="off"
class="w-full dbform"
class="w-full dbform form-with-action-bar"
id="changepersonform"
method="POST"
action="{{ if $model.is_new }}/personen/new/{{ else }}/person/{{ $agent.Id }}/edit{{ end }}"
@@ -248,30 +243,54 @@
</div>
</div>
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
<p id="person-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
<div class="flex items-center gap-3 self-end flex-wrap">
<a href="{{ if $model.is_new }}/personen/{{ else }}/person/{{ $agent.Id }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
{{- if not $model.is_new -}}
<a href="/person/{{ $agent.Id }}/edit" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-loop-left-line"></i>
<span>Reset</span>
<div class="form-action-bar">
<div class="form-action-bar-inner">
<p
id="person-save-feedback"
class="form-action-bar-message save-feedback {{ if $model.error }}save-feedback-error text-red-700{{ else if $model.success }}save-feedback-success text-green-700{{ else }}hidden{{ end }}"
aria-live="polite">
{{- if $model.error -}}
{{ $model.error }}
{{- else if $model.success -}}
{{ $model.success }}
{{- end -}}
</p>
<div class="form-action-bar-actions">
<a href="{{ if $model.is_new }}/personen/{{ else }}/person/{{ $agent.Id }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="edit-delete">
<i class="ri-delete-bin-line"></i>
<span>Person löschen</span>
</button>
{{- end -}}
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- if not $model.is_new -}}
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="edit-delete">
<i class="ri-delete-bin-line"></i>
<span>Person löschen</span>
</button>
{{- end -}}
{{- if not $model.is_new -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<button
type="submit"
name="save_action"
value="view"
class="submitbutton flex items-center gap-2 justify-center"
>
<i class="ri-eye-line"></i>
<span>Speichern &amp; Anzeigen</span>
</button>
{{- end -}}
{{- if $model.is_new -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- end -}}
</div>
</div>
</div>
{{- if not $model.is_new -}}

View File

@@ -26,11 +26,6 @@
</a>
</div>
&middot;
<div>
<a href="/reihe/{{ $series.MusenalmID }}/edit" class="text-gray-700 no-underline hover:text-slate-950 block">
<i class="ri-loop-left-line"></i> Reset
</a>
</div>
</div>
{{- end -}}
</div>
@@ -107,10 +102,10 @@
</div>
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_usermessage" $model }}
{{/* usermessage moved into action bar */}}
<form
autocomplete="off"
class="w-full dbform"
class="w-full dbform form-with-action-bar"
id="changeseriesform"
method="POST"
action="{{ if $model.is_new }}/reihen/new/{{ else }}/reihe/{{ $series.MusenalmID }}/edit{{ end }}"
@@ -183,30 +178,54 @@
</div>
</div>
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
<p id="series-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
<div class="flex items-center gap-3 self-end flex-wrap">
<a href="{{ if $model.is_new }}/reihen/{{ else }}/reihe/{{ $series.MusenalmID }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
{{- if not $model.is_new -}}
<a href="/reihe/{{ $series.MusenalmID }}/edit" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-loop-left-line"></i>
<span>Reset</span>
<div class="form-action-bar">
<div class="form-action-bar-inner">
<p
id="series-save-feedback"
class="form-action-bar-message save-feedback {{ if $model.error }}save-feedback-error text-red-700{{ else if $model.success }}save-feedback-success text-green-700{{ else }}hidden{{ end }}"
aria-live="polite">
{{- if $model.error -}}
{{ $model.error }}
{{- else if $model.success -}}
{{ $model.success }}
{{- end -}}
</p>
<div class="form-action-bar-actions">
<a href="{{ if $model.is_new }}/reihen/{{ else }}/reihe/{{ $series.MusenalmID }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="edit-delete">
<i class="ri-delete-bin-line"></i>
<span>Reihe löschen</span>
</button>
{{- end -}}
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- if not $model.is_new -}}
<button
type="button"
class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900"
data-role="edit-delete">
<i class="ri-delete-bin-line"></i>
<span>Reihe löschen</span>
</button>
{{- end -}}
{{- if not $model.is_new -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
<button
type="submit"
name="save_action"
value="view"
class="submitbutton flex items-center gap-2 justify-center"
>
<i class="ri-eye-line"></i>
<span>Speichern &amp; Anzeigen</span>
</button>
{{- end -}}
{{- if $model.is_new -}}
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
</button>
{{- end -}}
</div>
</div>
</div>
{{- if not $model.is_new -}}

View File

@@ -18,6 +18,7 @@ export class AlmanachEditPage extends HTMLElement {
this._preferredSeriesRelationId = "";
this._preferredSeriesSeriesId = "";
this._handleSaveClick = this._handleSaveClick.bind(this);
this._handleSaveViewClick = this._handleSaveViewClick.bind(this);
this._handleResetClick = this._handleResetClick.bind(this);
this._handleDeleteClick = this._handleDeleteClick.bind(this);
this._handleDeleteConfirmClick = this._handleDeleteConfirmClick.bind(this);
@@ -145,6 +146,7 @@ export class AlmanachEditPage extends HTMLElement {
this._teardownSaveHandling();
this._form = this.querySelector("#changealmanachform");
this._saveButton = this.querySelector("[data-role='almanach-save']");
this._saveViewButton = this.querySelector("[data-role='almanach-save-view']");
this._resetButton = this.querySelector("[data-role='almanach-reset']");
this._deleteButton = this.querySelector("[data-role='almanach-delete']");
this._deleteDialog = this.querySelector("[data-role='almanach-delete-dialog']");
@@ -157,6 +159,9 @@ export class AlmanachEditPage extends HTMLElement {
this._saveEndpoint = this._form.getAttribute("data-save-endpoint") || this._deriveSaveEndpoint();
this._deleteEndpoint = this._form.getAttribute("data-delete-endpoint") || "";
this._saveButton.addEventListener("click", this._handleSaveClick);
if (this._saveViewButton) {
this._saveViewButton.addEventListener("click", this._handleSaveViewClick);
}
if (this._resetButton) {
this._resetButton.addEventListener("click", this._handleResetClick);
}
@@ -188,6 +193,9 @@ export class AlmanachEditPage extends HTMLElement {
if (this._saveButton) {
this._saveButton.removeEventListener("click", this._handleSaveClick);
}
if (this._saveViewButton) {
this._saveViewButton.removeEventListener("click", this._handleSaveViewClick);
}
if (this._resetButton) {
this._resetButton.removeEventListener("click", this._handleResetClick);
}
@@ -204,6 +212,7 @@ export class AlmanachEditPage extends HTMLElement {
this._deleteDialog.removeEventListener("cancel", this._handleDeleteCancelClick);
}
this._saveButton = null;
this._saveViewButton = null;
this._resetButton = null;
this._deleteButton = null;
this._deleteDialog = null;
@@ -258,13 +267,54 @@ export class AlmanachEditPage extends HTMLElement {
throw new Error(message);
}
if (data?.redirect) {
window.location.assign(data.redirect);
return;
await this._reloadForm(data?.message || "Änderungen gespeichert.");
} catch (error) {
this._showStatus(error instanceof Error ? error.message : "Speichern fehlgeschlagen.", "error");
} finally {
this._setSavingState(false);
}
}
async _handleSaveViewClick(event) {
event.preventDefault();
if (this._isSaving) {
return;
}
const redirectUrl = this._saveViewButton?.getAttribute("data-redirect-url");
if (!redirectUrl) {
return;
}
this._clearStatus();
let payload;
try {
payload = this._buildPayload();
} catch (error) {
this._showStatus(error instanceof Error ? error.message : String(error), "error");
return;
}
this._setSavingState(true);
try {
const response = await fetch(this._saveEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
let message = `Speichern fehlgeschlagen (${response.status}).`;
try {
const data = await response.clone().json();
message = data?.error || message;
} catch {
// ignore parsing error
}
throw new Error(message);
}
await this._reloadForm(data?.message || "Änderungen gespeichert.");
this._clearStatus();
window.location.assign(redirectUrl);
} catch (error) {
this._showStatus(error instanceof Error ? error.message : "Speichern fehlgeschlagen.", "error");
} finally {

View File

@@ -134,6 +134,44 @@
@apply w-full inline-flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-gray-800 bg-stone-200 hover:bg-stone-300 cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 no-underline;
}
.form-with-action-bar {
@apply pb-0;
}
.form-action-bar {
@apply sticky bottom-0 z-30 border-t border-transparent bg-stone-50;
box-shadow: none;
width: calc(100% + 120px);
margin-left: -60px;
margin-right: -60px;
}
.form-action-bar-inner {
@apply w-full max-w-(--breakpoint-xl) mx-auto px-8 py-3 flex items-center justify-between gap-4 flex-wrap;
}
.form-action-bar-actions {
@apply ml-auto flex items-center gap-3 flex-wrap justify-end w-full md:w-auto;
}
.form-action-bar-message {
@apply text-left mr-auto w-full md:w-auto;
}
.form-action-bar .submitbutton,
.form-action-bar .resetbutton {
@apply !w-auto px-4;
}
.form-action-bar.is-stuck {
@apply border-t border-l border-r border-transparent;
background-color: rgb(250 250 249);
border-color: rgba(15, 23, 42, 0.06);
box-shadow: 0 -1px 6px rgba(15, 23, 42, 0.06);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.content-action-button {
@apply inline-flex items-center justify-center gap-2 rounded-xs border border-slate-300 bg-white px-3 py-1.5 text-base font-semibold text-gray-800 shadow-sm transition-all duration-75 hover:bg-stone-50 focus:outline-none focus:ring-2 focus:ring-slate-400/30;
}

View File

@@ -616,6 +616,32 @@ function FormLoad(form) {
// Update on change
checkbox.addEventListener("change", updateHiddenInput);
});
}
function InitStickyActionBars() {
if (InitStickyActionBars._initialized) {
return;
}
InitStickyActionBars._initialized = true;
const update = () => {
const bars = document.querySelectorAll(".form-action-bar");
if (!bars.length) {
return;
}
const viewportBottom = window.innerHeight || document.documentElement.clientHeight;
bars.forEach((bar) => {
const rect = bar.getBoundingClientRect();
const stuck = rect.bottom >= viewportBottom - 1;
bar.classList.toggle("is-stuck", stuck);
});
};
update();
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
document.addEventListener("htmx:afterSwap", update);
}
document.addEventListener("keydown", (event) => {
@@ -639,5 +665,6 @@ window.HookupRBChange = HookupRBChange;
window.FormLoad = FormLoad;
window.TextareaAutoResize = TextareaAutoResize;
InitGlobalHtmxNotice();
InitStickyActionBars();
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor, EditPage, FabMenu, LookupField };

View File

@@ -11,7 +11,7 @@ export class ScrollButton extends HTMLElement {
<button
class="
scroll-to-top
fixed bottom-5 right-5
fixed bottom-12 right-8 z-50
hidden
bg-gray-800 text-white
p-2