Anpassung Menü
This commit is contained in:
20
README.md
20
README.md
@@ -1196,6 +1196,26 @@ aber weiterhin benutzte Liste verschwinden.
|
|||||||
Gelöscht wird per Soft Delete: Offline gebliebene Geräte erfahren noch davon,
|
Gelöscht wird per Soft Delete: Offline gebliebene Geräte erfahren noch davon,
|
||||||
endgültig entfernt der Aufräumlauf nach `CLEANUP_DELETED_DAYS`.
|
endgültig entfernt der Aufräumlauf nach `CLEANUP_DELETED_DAYS`.
|
||||||
|
|
||||||
|
## Kopfleiste
|
||||||
|
|
||||||
|
Alle Ansichten teilen sich eine Leiste (`web/html/js/appbar.js`): links der Weg
|
||||||
|
zurück, in der Mitte der Kontext (Listenname), rechts ein Menü mit den
|
||||||
|
Einträgen **untereinander**.
|
||||||
|
|
||||||
|
Vorher hatte jede Ansicht ihre eigene Zeile aus Textlinks. Bei sechs Einträgen
|
||||||
|
unterschiedlicher Länge – „Drucken", „Märkte & Gruppen", „Teilen (2)" – brach
|
||||||
|
das auf schmalen Geräten unregelmäßig um, und die Grundlinien lagen versetzt.
|
||||||
|
|
||||||
|
Zwei Feinheiten:
|
||||||
|
|
||||||
|
- **Eine einzelne Aktion erscheint direkt**, ohne Menü. Ein Menü mit einem
|
||||||
|
Eintrag wäre ein Klick zu viel.
|
||||||
|
- **Verweise bleiben Verweise.** „Drucken" ist ein `<a>`, damit „In neuem Tab
|
||||||
|
öffnen" und das Kopieren der Adresse funktionieren.
|
||||||
|
|
||||||
|
Das Menü schließt bei Klick daneben, mit Escape und beim Ansichtswechsel. Im
|
||||||
|
Ausdruck ist die gesamte Leiste ausgeblendet.
|
||||||
|
|
||||||
## Einträge bearbeiten
|
## Einträge bearbeiten
|
||||||
|
|
||||||
Im „⋯"-Menü eines Eintrags lässt sich alles ändern: **Artikel**, Anzahl,
|
Im „⋯"-Menü eines Eintrags lässt sich alles ändern: **Artikel**, Anzahl,
|
||||||
|
|||||||
156
web/html/app.css
156
web/html/app.css
@@ -129,15 +129,156 @@ button.primary:disabled { opacity: .55; cursor: progress; }
|
|||||||
.switch { margin: 1rem 0 0; text-align: center; font-size: .92rem; }
|
.switch { margin: 1rem 0 0; text-align: center; font-size: .92rem; }
|
||||||
a { color: var(--accent); }
|
a { color: var(--accent); }
|
||||||
|
|
||||||
.bar {
|
/* ===================================================================
|
||||||
|
Kopfleiste mit Menü
|
||||||
|
=================================================================== */
|
||||||
|
|
||||||
|
.app-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
align-items: baseline;
|
gap: .75rem;
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
font-size: .92rem;
|
min-height: 2.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Drei Bereiche mit festem Verhalten: links zurück, Mitte der Titel
|
||||||
|
(darf gekürzt werden), rechts das Menü. Vorher standen alle Einträge
|
||||||
|
nebeneinander und brachen unregelmäßig um. */
|
||||||
|
.bar-back {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: .4rem 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: .95rem;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-title {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: .95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
/* Ein langer Listenname darf die Leiste nicht sprengen. */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-back + .bar-title { text-align: right; }
|
||||||
|
.app-bar > span.bar-back + .bar-title { text-align: left; }
|
||||||
|
|
||||||
|
.bar-menu { position: relative; flex: 0 0 auto; }
|
||||||
|
|
||||||
|
button.burger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .4rem;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: .45rem .7rem;
|
||||||
|
font: inherit;
|
||||||
|
font-size: .9rem;
|
||||||
|
color: var(--fg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.burger[aria-expanded="true"] {
|
||||||
|
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Die drei Striche aus einem Element - spart drei weitere Knoten. */
|
||||||
|
.burger-lines {
|
||||||
|
width: 1.05rem;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-lines::before,
|
||||||
|
.burger-lines::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-lines::before { top: -.34rem; }
|
||||||
|
.burger-lines::after { top: .34rem; }
|
||||||
|
|
||||||
|
/* Auf sehr schmalen Geräten nur das Symbol. */
|
||||||
|
@media (max-width: 24rem) {
|
||||||
|
.burger-text { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-single {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: .4rem 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: .95rem;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.menu-panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 30;
|
||||||
|
top: calc(100% + .3rem);
|
||||||
|
right: 0;
|
||||||
|
min-width: 15rem;
|
||||||
|
max-width: min(20rem, calc(100vw - 2rem));
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: .25rem;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.menu-panel button,
|
||||||
|
ul.menu-panel a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: calc(var(--radius) - 2px);
|
||||||
|
/* Große Trefferfläche: Das Menü wird am Telefon mit dem Daumen
|
||||||
|
bedient. */
|
||||||
|
padding: .6rem .7rem;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.menu-panel button:hover,
|
||||||
|
ul.menu-panel a:hover,
|
||||||
|
ul.menu-panel button:focus-visible,
|
||||||
|
ul.menu-panel a:focus-visible {
|
||||||
|
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.menu-panel .danger { color: var(--error); }
|
||||||
|
|
||||||
|
.entry-label { display: block; font-weight: 600; }
|
||||||
|
.entry-hint { display: block; font-size: .78rem; color: var(--muted); }
|
||||||
|
|
||||||
.who { color: var(--muted); overflow-wrap: anywhere; }
|
.who { color: var(--muted); overflow-wrap: anywhere; }
|
||||||
|
|
||||||
.lists { list-style: none; margin: 0 0 1rem; padding: 0; }
|
.lists { list-style: none; margin: 0 0 1rem; padding: 0; }
|
||||||
@@ -460,7 +601,7 @@ li.sortable-row.dragging button.grip { color: var(--accent); }
|
|||||||
body { background: #fff; color: #000; font-size: 11pt; }
|
body { background: #fff; color: #000; font-size: 11pt; }
|
||||||
#app { max-width: none; padding: 0; }
|
#app { max-width: none; padding: 0; }
|
||||||
|
|
||||||
.bar, .add-row, .summary button, button.menu-toggle,
|
.app-bar, .bar, .add-row, .summary button, button.menu-toggle,
|
||||||
.item-menu, .offline-hint, .footnote, label, .row { display: none !important; }
|
.item-menu, .offline-hint, .footnote, label, .row { display: none !important; }
|
||||||
|
|
||||||
.card, .market, .summary {
|
.card, .market, .summary {
|
||||||
@@ -554,7 +695,8 @@ li.invite.expired .who-block .name {
|
|||||||
li.invite.accepted .who-block .name { color: var(--muted); }
|
li.invite.accepted .who-block .name { color: var(--muted); }
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.bar-actions, .member-actions, .share-list { display: none !important; }
|
.app-bar, .bar-actions, .member-actions, .share-list,
|
||||||
|
ul.menu-panel { display: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { adminView } from "./views/admin.js";
|
|||||||
import { settingsView } from "./views/settings.js";
|
import { settingsView } from "./views/settings.js";
|
||||||
import { welcomeView } from "./views/welcome.js";
|
import { welcomeView } from "./views/welcome.js";
|
||||||
import { manageView } from "./views/manage.js";
|
import { manageView } from "./views/manage.js";
|
||||||
|
import { closeMenu } from "./appbar.js";
|
||||||
import * as db from "./db.js";
|
import * as db from "./db.js";
|
||||||
import { installTriggers, stopWatching } from "./sync.js";
|
import { installTriggers, stopWatching } from "./sync.js";
|
||||||
import { publicView } from "./views/public.js";
|
import { publicView } from "./views/public.js";
|
||||||
@@ -60,6 +61,8 @@ function currentRoute() {
|
|||||||
/** Beim Verlassen der Detailansicht den Ereigniskanal schließen -
|
/** Beim Verlassen der Detailansicht den Ereigniskanal schließen -
|
||||||
* sonst bleibt pro besuchter Liste eine offene Verbindung stehen. */
|
* sonst bleibt pro besuchter Liste eine offene Verbindung stehen. */
|
||||||
function navigate(hash) {
|
function navigate(hash) {
|
||||||
|
// Sonst bliebe ein offenes Menü über den Wechsel hinweg stehen.
|
||||||
|
closeMenu();
|
||||||
stopWatching();
|
stopWatching();
|
||||||
if (location.hash === hash) {
|
if (location.hash === hash) {
|
||||||
route();
|
route();
|
||||||
|
|||||||
128
web/html/js/appbar.js
Normal file
128
web/html/js/appbar.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// Gemeinsame Kopfleiste für alle Ansichten.
|
||||||
|
//
|
||||||
|
// Vorher hatte jede Ansicht ihre eigene Zeile aus Textlinks. Bei sechs
|
||||||
|
// Einträgen unterschiedlicher Länge - "Drucken", "Märkte & Gruppen",
|
||||||
|
// "Teilen (2)" - brach das auf schmalen Geräten unregelmäßig um, und die
|
||||||
|
// Grundlinien lagen versetzt.
|
||||||
|
//
|
||||||
|
// Jetzt: links der Weg zurück, rechts ein Menü, dessen Einträge
|
||||||
|
// untereinander stehen. Eine einzelne Aktion wird direkt angezeigt -
|
||||||
|
// ein Menü mit einem Eintrag wäre ein Klick zu viel.
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { el } from "./dom.js";
|
||||||
|
|
||||||
|
let openPanel = null;
|
||||||
|
|
||||||
|
/** Schließt ein offenes Menü. Wird von aussen aufgerufen, wenn die
|
||||||
|
* Ansicht wechselt - sonst bliebe das Menü über den Wechsel hinweg
|
||||||
|
* stehen. */
|
||||||
|
export function closeMenu() {
|
||||||
|
if (!openPanel) return;
|
||||||
|
openPanel.panel.hidden = true;
|
||||||
|
openPanel.button.setAttribute("aria-expanded", "false");
|
||||||
|
document.removeEventListener("click", openPanel.onOutside, true);
|
||||||
|
document.removeEventListener("keydown", openPanel.onKey);
|
||||||
|
openPanel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} options
|
||||||
|
* @param {{label: string, onClick?: Function}} [options.back]
|
||||||
|
* @param {string} [options.title] Kontext, etwa der Listenname
|
||||||
|
* @param {Array<{label: string, onClick?: Function, href?: string,
|
||||||
|
* danger?: boolean, hint?: string}>} [options.actions]
|
||||||
|
*/
|
||||||
|
export function appBar({ back, title, actions = [] } = {}) {
|
||||||
|
closeMenu();
|
||||||
|
const usable = actions.filter(Boolean);
|
||||||
|
|
||||||
|
const left = back
|
||||||
|
? el("button.bar-back", { type: "button", onclick: back.onClick },
|
||||||
|
"\u2039 " + back.label)
|
||||||
|
: el("span.bar-back", {});
|
||||||
|
|
||||||
|
const middle = title ? el("span.bar-title", { title }, title) : el("span", {});
|
||||||
|
|
||||||
|
function entry(action) {
|
||||||
|
const content = [
|
||||||
|
el("span.entry-label", {}, action.label),
|
||||||
|
action.hint ? el("span.entry-hint", {}, action.hint) : null,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ein Verweis bleibt ein Verweis: So funktionieren "In neuem Tab
|
||||||
|
// öffnen" und das Kopieren der Adresse wie erwartet.
|
||||||
|
const node = action.href
|
||||||
|
? el("a", {
|
||||||
|
href: action.href,
|
||||||
|
target: action.target || "_blank",
|
||||||
|
rel: "noopener",
|
||||||
|
className: action.danger ? "danger" : "",
|
||||||
|
onclick: () => closeMenu(),
|
||||||
|
}, content)
|
||||||
|
: el("button", {
|
||||||
|
type: "button",
|
||||||
|
className: action.danger ? "danger" : "",
|
||||||
|
onclick: () => { closeMenu(); action.onClick?.(); },
|
||||||
|
}, content);
|
||||||
|
|
||||||
|
return el("li", {}, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let right;
|
||||||
|
if (usable.length === 0) {
|
||||||
|
right = el("span", {});
|
||||||
|
} else if (usable.length === 1) {
|
||||||
|
const only = usable[0];
|
||||||
|
right = only.href
|
||||||
|
? el("a.bar-single", {
|
||||||
|
href: only.href, target: only.target || "_blank", rel: "noopener",
|
||||||
|
}, only.label)
|
||||||
|
: el("button.bar-single", { type: "button", onclick: only.onClick },
|
||||||
|
only.label);
|
||||||
|
} else {
|
||||||
|
const panel = el("ul.menu-panel", { hidden: true }, usable.map(entry));
|
||||||
|
const button = el("button.burger", {
|
||||||
|
type: "button",
|
||||||
|
"aria-haspopup": "true",
|
||||||
|
"aria-expanded": "false",
|
||||||
|
"aria-label": "Menü",
|
||||||
|
title: "Menü",
|
||||||
|
},
|
||||||
|
el("span.burger-lines", { "aria-hidden": "true" }),
|
||||||
|
el("span.burger-text", {}, "Menü"));
|
||||||
|
|
||||||
|
button.addEventListener("click", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (openPanel && openPanel.panel === panel) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
|
||||||
|
panel.hidden = false;
|
||||||
|
button.setAttribute("aria-expanded", "true");
|
||||||
|
|
||||||
|
const onOutside = (event) => {
|
||||||
|
if (!panel.contains(event.target) && event.target !== button) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKey = (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeMenu();
|
||||||
|
button.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// In der Erfassungsphase, damit auch Klicks auf Elemente greifen,
|
||||||
|
// die ihrerseits die Weiterleitung stoppen.
|
||||||
|
document.addEventListener("click", onOutside, true);
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
openPanel = { panel, button, onOutside, onKey };
|
||||||
|
});
|
||||||
|
|
||||||
|
right = el("div.bar-menu", {}, button, panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return el("header.app-bar", {}, left, middle, right);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
import { del, get, post, put } from "../api.js";
|
import { del, get, post, put } from "../api.js";
|
||||||
import { el, mount } from "../dom.js";
|
import { el, mount } from "../dom.js";
|
||||||
import { state } from "../store.js";
|
import { state } from "../store.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
let openId = null;
|
let openId = null;
|
||||||
|
|
||||||
@@ -361,8 +362,10 @@ export async function adminView(root, { back }) {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({
|
||||||
el("button.linklike", { type: "button", onclick: back }, "‹ Zurück")),
|
back: { label: "Einstellungen", onClick: back },
|
||||||
|
title: "Benutzerverwaltung",
|
||||||
|
}),
|
||||||
|
|
||||||
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { del, get, patch, post } from "../api.js";
|
|||||||
import { el, mount } from "../dom.js";
|
import { el, mount } from "../dom.js";
|
||||||
import { set, state } from "../store.js";
|
import { set, state } from "../store.js";
|
||||||
import { cameraAvailable, scanBarcode } from "./scanner.js";
|
import { cameraAvailable, scanBarcode } from "./scanner.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
let editingId = null;
|
let editingId = null;
|
||||||
let query = "";
|
let query = "";
|
||||||
@@ -261,9 +262,7 @@ export async function articlesView(root, { listId, back }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({ back: { label: "Zurück", onClick: back }, title: "Artikel" }),
|
||||||
el("button.linklike", { type: "button", onclick: back },
|
|
||||||
"‹ Zurück zur Liste")),
|
|
||||||
|
|
||||||
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { set, state } from "../store.js";
|
|||||||
import { composeView, flush, pull, syncNow, watch } from "../sync.js";
|
import { composeView, flush, pull, syncNow, watch } from "../sync.js";
|
||||||
import { confirmScan } from "./scan-result.js";
|
import { confirmScan } from "./scan-result.js";
|
||||||
import { cameraAvailable, scanBarcode } from "./scanner.js";
|
import { cameraAvailable, scanBarcode } from "./scanner.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
let openMenuId = null;
|
let openMenuId = null;
|
||||||
|
|
||||||
@@ -639,26 +640,30 @@ export async function listDetailView(
|
|||||||
(n, c) => n + c.items.filter((i) => i.status === "bought").length, 0), 0);
|
(n, c) => n + c.items.filter((i) => i.status === "bought").length, 0), 0);
|
||||||
|
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({
|
||||||
el("button.linklike", { type: "button", onclick: back }, "‹ Listen"),
|
back: { label: "Listen", onClick: back },
|
||||||
el("span.bar-actions", {},
|
title: view.list_name,
|
||||||
|
actions: [
|
||||||
|
{ label: "Märkte & Gruppen", onClick: () => manage(listId),
|
||||||
|
hint: "Reihenfolge im Laden" },
|
||||||
|
{ label: "Artikel", onClick: () => articles(listId),
|
||||||
|
hint: "Stammdaten, Strichcodes" },
|
||||||
|
{ label: "Preise", onClick: () => prices(listId),
|
||||||
|
hint: "Vergleich zwischen Märkten" },
|
||||||
|
meta && meta.may_share_public
|
||||||
|
? {
|
||||||
|
label: meta.member_count > 1
|
||||||
|
? `Teilen (${meta.member_count})` : "Teilen",
|
||||||
|
onClick: () => share(listId),
|
||||||
|
hint: "Personen und öffentliche Links",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
// Serverseitiger Ausdruck: eigene Seite, damit auch ohne
|
// Serverseitiger Ausdruck: eigene Seite, damit auch ohne
|
||||||
// geöffnete App gedruckt werden kann.
|
// geöffnete App gedruckt werden kann.
|
||||||
el("a.linklike", {
|
{ label: "Drucken", href: `/api/lists/${listId}/print`,
|
||||||
href: `/api/lists/${listId}/print`,
|
hint: "A4, nach Märkten gegliedert" },
|
||||||
target: "_blank",
|
],
|
||||||
rel: "noopener",
|
}),
|
||||||
}, "Drucken"),
|
|
||||||
el("button.linklike", { type: "button", onclick: () => prices(listId) },
|
|
||||||
"Preise"),
|
|
||||||
el("button.linklike", { type: "button", onclick: () => articles(listId) },
|
|
||||||
"Artikel"),
|
|
||||||
el("button.linklike", { type: "button", onclick: () => manage(listId) },
|
|
||||||
"Märkte & Gruppen"),
|
|
||||||
meta && meta.may_share_public
|
|
||||||
? el("button.linklike", { type: "button", onclick: () => share(listId) },
|
|
||||||
meta.member_count > 1 ? `Teilen (${meta.member_count})` : "Teilen")
|
|
||||||
: null)),
|
|
||||||
|
|
||||||
statusLine(),
|
statusLine(),
|
||||||
banner ? el("p.error", {}, banner) : null,
|
banner ? el("p.error", {}, banner) : null,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { del, get, patch, post } from "../api.js";
|
|||||||
import * as db from "../db.js";
|
import * as db from "../db.js";
|
||||||
import { clear, el, mount } from "../dom.js";
|
import { clear, el, mount } from "../dom.js";
|
||||||
import { set, state } from "../store.js";
|
import { set, state } from "../store.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
// Welche Zeile hat gerade ihr Menü offen
|
// Welche Zeile hat gerade ihr Menü offen
|
||||||
let openMenuId = null;
|
let openMenuId = null;
|
||||||
@@ -244,11 +245,14 @@ export async function listsView(root, { openList, onLogout, settings }) {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({
|
||||||
el("span.who", {}, state.user.display_name || state.user.email),
|
title: state.user.display_name || state.user.email,
|
||||||
el("span.bar-actions", {},
|
actions: [
|
||||||
el("button.linklike", { type: "button", onclick: settings }, "Einstellungen"),
|
{ label: "Einstellungen", onClick: settings,
|
||||||
el("button.linklike", { type: "button", onclick: onLogout }, "Abmelden"))),
|
hint: "Benachrichtigungen, Passwort" },
|
||||||
|
{ label: "Abmelden", onClick: onLogout, danger: true },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { del, get, post, put } from "../api.js";
|
|||||||
import { clear, el, mount } from "../dom.js";
|
import { clear, el, mount } from "../dom.js";
|
||||||
import { makeSortable } from "../sortable.js";
|
import { makeSortable } from "../sortable.js";
|
||||||
import { set, state } from "../store.js";
|
import { set, state } from "../store.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
const STEP = 10;
|
const STEP = 10;
|
||||||
|
|
||||||
@@ -148,10 +149,7 @@ export async function manageView(root, { listId, back }) {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({ back: { label: "Zurück", onClick: back }, title: listName }),
|
||||||
el("button.linklike", { type: "button", onclick: back },
|
|
||||||
"‹ Zurück zur Liste"),
|
|
||||||
listName ? el("span.who", {}, listName) : null),
|
|
||||||
|
|
||||||
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { get } from "../api.js";
|
import { get } from "../api.js";
|
||||||
import { el, euro, formatQuantity, mount } from "../dom.js";
|
import { el, euro, formatQuantity, mount } from "../dom.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
let expandedId = null;
|
let expandedId = null;
|
||||||
|
|
||||||
@@ -127,9 +128,7 @@ export async function pricesView(root, { listId, back }) {
|
|||||||
const savings = withSpread.reduce((sum, r) => sum + r.spread_cents, 0);
|
const savings = withSpread.reduce((sum, r) => sum + r.spread_cents, 0);
|
||||||
|
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({ back: { label: "Zurück", onClick: back }, title: "Preise" }),
|
||||||
el("button.linklike", { type: "button", onclick: back },
|
|
||||||
"‹ Zurück zur Liste")),
|
|
||||||
|
|
||||||
banner ? el("p.error", {}, banner) : null,
|
banner ? el("p.error", {}, banner) : null,
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { del, patch, post } from "../api.js";
|
|||||||
import { el, mount } from "../dom.js";
|
import { el, mount } from "../dom.js";
|
||||||
import * as push from "../push.js";
|
import * as push from "../push.js";
|
||||||
import { set, state } from "../store.js";
|
import { set, state } from "../store.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
function formatDate(iso) {
|
function formatDate(iso) {
|
||||||
return new Date(iso).toLocaleDateString("de-DE", {
|
return new Date(iso).toLocaleDateString("de-DE", {
|
||||||
@@ -205,22 +206,24 @@ export async function settingsView(root, { back, admin }) {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({
|
||||||
el("button.linklike", { type: "button", onclick: back }, "‹ Zurück"),
|
back: { label: "Listen", onClick: back },
|
||||||
el("span.who", {}, state.user.display_name || state.user.email)),
|
title: "Einstellungen",
|
||||||
|
actions: state.user.is_admin
|
||||||
|
? [{ label: "Benutzerverwaltung", onClick: admin,
|
||||||
|
hint: "Konten, Einladungen, Bereinigung" }]
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
|
|
||||||
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
||||||
|
|
||||||
|
// Die Benutzerverwaltung steht im Menü der Kopfleiste - nur für
|
||||||
|
// Administratoren, und der Endpunkt prüft zusätzlich.
|
||||||
el("section.card", {},
|
el("section.card", {},
|
||||||
el("h1", {}, "Einstellungen"),
|
el("h1", {}, state.user.display_name || state.user.email),
|
||||||
// Nur für Administratoren sichtbar - der Endpunkt prüft
|
el("p.lead", {},
|
||||||
// zusätzlich, die Oberfläche allein wäre kein Schutz.
|
state.user.email,
|
||||||
state.user.is_admin
|
state.user.is_admin ? " · Administrator" : "")),
|
||||||
? el("div", {},
|
|
||||||
el("p.lead", {}, "Du hast Administratorrechte."),
|
|
||||||
el("button.secondary", { type: "button", onclick: admin },
|
|
||||||
"Benutzerverwaltung"))
|
|
||||||
: null),
|
|
||||||
pushSection(),
|
pushSection(),
|
||||||
profileSection(),
|
profileSection(),
|
||||||
passwordSection()
|
passwordSection()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { del, get, post, put } from "../api.js";
|
import { del, get, post, put } from "../api.js";
|
||||||
import { clear, el, mount } from "../dom.js";
|
import { clear, el, mount } from "../dom.js";
|
||||||
|
import { appBar } from "../appbar.js";
|
||||||
|
|
||||||
const ROLE_LABEL = { owner: "Eigentümer", editor: "Bearbeiter", viewer: "Nur lesen" };
|
const ROLE_LABEL = { owner: "Eigentümer", editor: "Bearbeiter", viewer: "Nur lesen" };
|
||||||
|
|
||||||
@@ -352,9 +353,10 @@ export async function shareView(root, { listId, back }) {
|
|||||||
const openInvites = invites.filter((i) => i.status === "pending");
|
const openInvites = invites.filter((i) => i.status === "pending");
|
||||||
|
|
||||||
mount(root,
|
mount(root,
|
||||||
el("header.bar", {},
|
appBar({
|
||||||
el("button.linklike", { type: "button", onclick: back }, "‹ Zurück zur Liste")
|
back: { label: "Zurück", onClick: back },
|
||||||
),
|
title: list ? list.name : "Teilen",
|
||||||
|
}),
|
||||||
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
||||||
|
|
||||||
el("section.card", {},
|
el("section.card", {},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
// Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet,
|
// Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet,
|
||||||
// wann alte Zwischenspeicher verworfen werden.
|
// wann alte Zwischenspeicher verworfen werden.
|
||||||
const VERSION = "v24";
|
const VERSION = "v25";
|
||||||
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
|
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
|
||||||
|
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
@@ -15,6 +15,7 @@ const SHELL = [
|
|||||||
"/js/api.js",
|
"/js/api.js",
|
||||||
"/js/dom.js",
|
"/js/dom.js",
|
||||||
"/js/store.js",
|
"/js/store.js",
|
||||||
|
"/js/appbar.js",
|
||||||
"/js/db.js",
|
"/js/db.js",
|
||||||
"/js/sync.js",
|
"/js/sync.js",
|
||||||
"/js/push.js",
|
"/js/push.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user