UX Block 1: Flash-Messages, Screen-Status, Responsive-Tabellen, Navbar-Burger
- Flash-Messages nach allen Manage-Aktionen (Upload, Löschen, Speichern, Hinzufügen) - Screen-Online/Offline-Status als farbiger Punkt in Admin-Tabelle - overflow-x Wrapper für alle Tabellen (Admin, Playlist, Medienbibliothek) - Navbar-Burger für mobile Viewports in Admin und Manage - UX-Gestaltungsplan als Sektion in TODO.md eingetragen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f11bd4f6c4
commit
883a8146c5
3 changed files with 158 additions and 9 deletions
25
TODO.md
25
TODO.md
|
|
@ -134,6 +134,31 @@
|
|||
- [ ] Update- und Release-Prozess festlegen
|
||||
- [ ] Langfristige Wayland-Neubewertung fuer spaetere Version vormerken
|
||||
|
||||
## UX-Verbesserungen (Gestaltungsplan)
|
||||
|
||||
### Hohe Prioritaet
|
||||
|
||||
- [ ] Flash-Messages nach Aktionen in Manage-UI (Upload, Loeschen, Speichern) — Feedback fuer den Nutzer
|
||||
- [ ] Screen-Online/Offline-Status in Admin-Tabelle anzeigen (aus /status-Endpoint befuellen)
|
||||
- [ ] Playlist-Tabelle in overflow-x Wrapper einwickeln (Responsive auf kleinen Screens)
|
||||
|
||||
### Mittlere Prioritaet
|
||||
|
||||
- [ ] Loesch-Bestaetigung: Bulma-Modal statt browser-nativer confirm()-Dialog
|
||||
- [ ] Status-Page: Sprache von Englisch auf Deutsch vereinheitlichen
|
||||
- [ ] Status-Page: Relative Zeitstempel statt RFC3339 ("vor 2 Minuten")
|
||||
- [ ] Querlinks zwischen Admin-UI und Status-Page (Navigation)
|
||||
- [ ] Bulma und SortableJS als lokale Assets einbetten statt CDN
|
||||
- [ ] Player-UI: CSS-Transitions fuer sanfte Content-Wechsel (Fade statt abrupt)
|
||||
- [ ] Player-UI: Erweitertes Sysinfo-Overlay (aktueller Titel, Playlist-Laenge)
|
||||
- [ ] Aria-Labels fuer Loesch-Buttons und Drag-Handles (Accessibility)
|
||||
|
||||
### Niedrige Prioritaet
|
||||
|
||||
- [ ] Upload-Fortschrittsbalken in Manage-UI
|
||||
- [ ] vars.yml Download-Button in Provision-UI statt Copy-Paste
|
||||
- [ ] Toggle-Switch statt Ja/Nein-Select fuer Enabled-Feld
|
||||
|
||||
## Querschnittsthemen
|
||||
|
||||
- [ ] Datensicherung fuer Datenbank und Medien einplanen
|
||||
|
|
|
|||
|
|
@ -141,23 +141,74 @@ const adminTmpl = `<!DOCTYPE html>
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-dark" role="navigation">
|
||||
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<span class="navbar-item"><strong>📺 MORZ Infoboard</strong></span>
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="adminNavbar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="adminNavbar" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<script>
|
||||
(function() {
|
||||
var burger = document.querySelector('.navbar-burger[data-target="adminNavbar"]');
|
||||
if (burger) {
|
||||
burger.addEventListener('click', function() {
|
||||
var target = document.getElementById(burger.dataset.target);
|
||||
burger.classList.toggle('is-active');
|
||||
target.classList.toggle('is-active');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
var msg = new URLSearchParams(window.location.search).get('msg');
|
||||
if (!msg) return;
|
||||
var texts = {
|
||||
'uploaded': '✓ Medium erfolgreich hochgeladen.',
|
||||
'deleted': '✓ Erfolgreich gelöscht.',
|
||||
'saved': '✓ Änderungen gespeichert.',
|
||||
'added': '✓ Erfolgreich hinzugefügt.'
|
||||
};
|
||||
var text = texts[msg] || '✓ Aktion erfolgreich.';
|
||||
var n = document.createElement('div');
|
||||
n.className = 'notification is-success';
|
||||
n.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;max-width:380px;box-shadow:0 4px 12px rgba(0,0,0,.15)';
|
||||
n.innerHTML = '<button class="delete"></button>' + text;
|
||||
n.querySelector('.delete').addEventListener('click', function() { n.remove(); });
|
||||
document.body.appendChild(n);
|
||||
setTimeout(function() {
|
||||
n.style.transition = 'opacity .5s';
|
||||
n.style.opacity = '0';
|
||||
setTimeout(function() { n.remove(); }, 500);
|
||||
}, 3000);
|
||||
// Clean URL without reloading
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.delete('msg');
|
||||
history.replaceState(null, '', url.toString());
|
||||
})();
|
||||
</script>
|
||||
<section class="section pt-0">
|
||||
<div class="container">
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Bildschirme</h2>
|
||||
{{if .Screens}}
|
||||
<div style="overflow-x: auto">
|
||||
<table class="table is-fullwidth is-hoverable is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Format</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -167,6 +218,7 @@ const adminTmpl = `<!DOCTYPE html>
|
|||
<td><strong>{{.Name}}</strong></td>
|
||||
<td><code>{{.Slug}}</code></td>
|
||||
<td>{{orientationLabel .Orientation}}</td>
|
||||
<td id="status-{{.Slug}}"><span class="has-text-grey">⚪</span></td>
|
||||
<td>
|
||||
<a class="button is-small is-link" href="/manage/{{.Slug}}">Playlist verwalten</a>
|
||||
|
||||
|
|
@ -179,6 +231,7 @@ const adminTmpl = `<!DOCTYPE html>
|
|||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="has-text-grey">Noch keine Bildschirme angelegt.</p>
|
||||
{{end}}
|
||||
|
|
@ -294,6 +347,24 @@ const adminTmpl = `<!DOCTYPE html>
|
|||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
fetch('/api/v1/screens/status')
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.then(function(data) {
|
||||
if (!data || !data.screens) return;
|
||||
var dots = { 'online': '🟢', 'degraded': '🟡', 'offline': '🔴' };
|
||||
data.screens.forEach(function(s) {
|
||||
var cell = document.getElementById('status-' + s.screen_id);
|
||||
if (cell) {
|
||||
cell.innerHTML = (dots[s.derived_state] || '⚪') + ' <small>' + s.derived_state + '</small>';
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function() {});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
|
|
@ -319,7 +390,7 @@ const manageTmpl = `<!DOCTYPE html>
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar is-dark" role="navigation">
|
||||
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/admin">← Admin</a>
|
||||
<span class="navbar-item">
|
||||
|
|
@ -327,9 +398,46 @@ const manageTmpl = `<!DOCTYPE html>
|
|||
|
||||
<span class="tag is-info is-light">{{orientationLabel .Screen.Orientation}}</span>
|
||||
</span>
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="manageNavbar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="manageNavbar" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var msg = new URLSearchParams(window.location.search).get('msg');
|
||||
if (!msg) return;
|
||||
var texts = {
|
||||
'uploaded': '✓ Medium erfolgreich hochgeladen.',
|
||||
'deleted': '✓ Erfolgreich gelöscht.',
|
||||
'saved': '✓ Änderungen gespeichert.',
|
||||
'added': '✓ Erfolgreich hinzugefügt.'
|
||||
};
|
||||
var text = texts[msg] || '✓ Aktion erfolgreich.';
|
||||
var n = document.createElement('div');
|
||||
n.className = 'notification is-success';
|
||||
n.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;max-width:380px;box-shadow:0 4px 12px rgba(0,0,0,.15)';
|
||||
n.innerHTML = '<button class="delete"></button>' + text;
|
||||
n.querySelector('.delete').addEventListener('click', function() { n.remove(); });
|
||||
document.body.appendChild(n);
|
||||
setTimeout(function() {
|
||||
n.style.transition = 'opacity .5s';
|
||||
n.style.opacity = '0';
|
||||
setTimeout(function() { n.remove(); }, 500);
|
||||
}, 3000);
|
||||
// Clean URL without reloading
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.delete('msg');
|
||||
history.replaceState(null, '', url.toString());
|
||||
})();
|
||||
</script>
|
||||
<section class="section pt-4">
|
||||
<div class="container">
|
||||
|
||||
|
|
@ -337,6 +445,7 @@ const manageTmpl = `<!DOCTYPE html>
|
|||
<div class="box">
|
||||
<h2 class="title is-5 mb-3">Aktuelle Playlist</h2>
|
||||
{{if .Items}}
|
||||
<div style="overflow-x: auto">
|
||||
<table class="table is-fullwidth" id="playlist-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -423,6 +532,7 @@ const manageTmpl = `<!DOCTYPE html>
|
|||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="help has-text-grey mt-2">Einträge per Drag & Drop in der Reihenfolge verschieben.</p>
|
||||
{{else}}
|
||||
<div class="notification is-light">
|
||||
|
|
@ -435,6 +545,7 @@ const manageTmpl = `<!DOCTYPE html>
|
|||
<div class="box">
|
||||
<h2 class="title is-5 mb-3">Medienbibliothek</h2>
|
||||
{{if .Assets}}
|
||||
<div style="overflow-x: auto">
|
||||
<table class="table is-fullwidth is-hoverable is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -474,6 +585,7 @@ const manageTmpl = `<!DOCTYPE html>
|
|||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="has-text-grey">Noch keine Medien hochgeladen. Lade unten eine Datei hoch oder füge eine Webseite hinzu.</p>
|
||||
{{end}}
|
||||
|
|
@ -563,6 +675,18 @@ const manageTmpl = `<!DOCTYPE html>
|
|||
</section>
|
||||
|
||||
<script>
|
||||
// Navbar burger toggle
|
||||
(function() {
|
||||
var burger = document.querySelector('.navbar-burger[data-target="manageNavbar"]');
|
||||
if (burger) {
|
||||
burger.addEventListener('click', function() {
|
||||
var target = document.getElementById(burger.dataset.target);
|
||||
burger.classList.toggle('is-active');
|
||||
target.classList.toggle('is-active');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
function toggleEdit(id) {
|
||||
var row = document.getElementById('edit-' + id);
|
||||
if (row) {
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ func HandleCreateScreenUI(tenants *store.TenantStore, screens *store.ScreenStore
|
|||
http.Error(w, "Fehler: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin?msg=added", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +223,7 @@ func HandleDeleteScreenUI(screens *store.ScreenStore) http.HandlerFunc {
|
|||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin?msg=deleted", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +289,7 @@ func HandleUploadMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
|
|||
http.Error(w, "DB-Fehler: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/manage/"+screenSlug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/manage/"+screenSlug+"?msg=uploaded", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,7 +353,7 @@ func HandleAddItemUI(playlists *store.PlaylistStore, media *store.MediaStore, sc
|
|||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/manage/"+screenSlug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/manage/"+screenSlug+"?msg=added", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,7 +366,7 @@ func HandleDeleteItemUI(playlists *store.PlaylistStore) http.HandlerFunc {
|
|||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/manage/"+screenSlug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/manage/"+screenSlug+"?msg=deleted", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,7 +419,7 @@ func HandleUpdateItemUI(playlists *store.PlaylistStore) http.HandlerFunc {
|
|||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/manage/"+screenSlug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/manage/"+screenSlug+"?msg=saved", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +435,6 @@ func HandleDeleteMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
|
|||
}
|
||||
media.Delete(r.Context(), mediaID) //nolint:errcheck
|
||||
|
||||
http.Redirect(w, r, "/manage/"+screenSlug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/manage/"+screenSlug+"?msg=deleted", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue