From b8660d3200de934c780fce2793d90edf8c1003e6 Mon Sep 17 00:00:00 2001 From: Jakob Moser Date: Thu, 13 Nov 2025 20:02:25 +0100 Subject: [PATCH] Add remaining files (with AI taint) --- .../components/pieces/ClearSearchLink.js | 27 ++ .../components/pieces/PaginationControls.js | 122 ++++++++ .../ui/static/components/pieces/Tab.js | 16 + karaokatalog/ui/static/style.css | 275 ++++++++++++++++++ 4 files changed, 440 insertions(+) create mode 100644 karaokatalog/ui/static/components/pieces/ClearSearchLink.js create mode 100644 karaokatalog/ui/static/components/pieces/PaginationControls.js create mode 100644 karaokatalog/ui/static/components/pieces/Tab.js create mode 100644 karaokatalog/ui/static/style.css diff --git a/karaokatalog/ui/static/components/pieces/ClearSearchLink.js b/karaokatalog/ui/static/components/pieces/ClearSearchLink.js new file mode 100644 index 0000000..12ddb27 --- /dev/null +++ b/karaokatalog/ui/static/components/pieces/ClearSearchLink.js @@ -0,0 +1,27 @@ +/* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. + I should do this at some point in the future and change things I don't like. */ +import search from "../../model/search.js" + +export default { + view: function (vnode) { + return ["Möchtest du ", m( + "a.clear-search-link", + { + onclick: e => { + e.preventDefault() + search.query = null + }, + href: "#", + role: "button", + tabindex: 0, + onkeypress: e => { + if ((e.key === "Enter" || e.key === " ")) { + e.preventDefault() + search.query = null + } + }, + }, + `alle ${vnode.attrs.totalFavoriteCount} Favoriten anzeigen`, + ), "?"] + }, +} diff --git a/karaokatalog/ui/static/components/pieces/PaginationControls.js b/karaokatalog/ui/static/components/pieces/PaginationControls.js new file mode 100644 index 0000000..5c2a662 --- /dev/null +++ b/karaokatalog/ui/static/components/pieces/PaginationControls.js @@ -0,0 +1,122 @@ +/* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. + I should do this at some point in the future and change things I don't like. */ +const PaginationControls = { + view: function (vnode) { + const { currentPage, totalPages, onPageChange } = vnode.attrs + + if (totalPages <= 1) { + return null // Keine Steuerelemente anzeigen, wenn nicht benötigt + } + + const pages = [] + const delta = 2 // Anzahl der Seiten, die um die aktuelle Seite herum angezeigt werden + + // Hilfsfunktion zum Hinzufügen einer Seitenzahl + const addPage = pageNumber => { + pages.push( + m( + "span.page-number", + { + class: + pageNumber === currentPage ? "active" : "clickable", + onclick: () => + pageNumber !== currentPage + ? onPageChange(pageNumber) + : null, + role: "button", + tabindex: 0, // Für Tastaturnavigation + onkeypress: e => { + // Für Tastaturnavigation (Enter/Space) + if ( + (e.key === "Enter" || e.key === " ") && + pageNumber !== currentPage + ) { + e.preventDefault() + onPageChange(pageNumber) + } + }, + "aria-label": `Gehe zu Seite ${pageNumber}`, + "aria-current": + pageNumber === currentPage ? "page" : undefined, + }, + pageNumber, + ), + ) + } + + // "Zurück"-Button + const prevButton = m( + "button.pagination-button", + { + onclick: () => onPageChange(currentPage - 1), + disabled: currentPage === 1, + "aria-label": "Vorherige Seite", + }, + "Zurück", + ) + + // "Weiter"-Button + const nextButton = m( + "button.pagination-button", + { + onclick: () => onPageChange(currentPage + 1), + disabled: currentPage === totalPages, + "aria-label": "Nächste Seite", + }, + "Weiter", + ) + + // Logik zur Seitenzahlanzeige + if (totalPages <= 7) { + // Alle Seiten anzeigen, wenn es 7 oder weniger sind + for (let i = 1; i <= totalPages; i++) { + addPage(i) + } + } else { + addPage(1) // Immer die erste Seite anzeigen + + if (currentPage > delta + 2) { + // Auslassungspunkte anzeigen, wenn die aktuelle Seite weit vom Anfang entfernt ist + pages.push(m("span.dots", "...")) + } + + // Bereich der Seiten um die aktuelle Seite bestimmen + let start = Math.max(2, currentPage - delta) + let end = Math.min(totalPages - 1, currentPage + delta) + + if (currentPage <= delta + 1) { + // Aktuelle Seite ist nahe am Anfang + start = 2 + end = Math.min(totalPages - 1, 2 * delta + 1) + } else if (currentPage >= totalPages - delta) { + // Aktuelle Seite ist nahe am Ende + start = Math.max(2, totalPages - 2 * delta) + end = totalPages - 1 + } + + // Korrektur für den Fall, dass start und end sich überschneiden oder ungültig werden + if (start > end && currentPage < totalPages / 2) { + // Wenn start > end und wir sind eher am Anfang + end = start // Zeige zumindest die Startseite + } else if (start > end && currentPage > totalPages / 2) { + // Wenn start > end und wir sind eher am Ende + start = end // Zeige zumindest die Endseite + } + + for (let i = start; i <= end; i++) { + if (i > 0) addPage(i) // Sicherstellen, dass i positiv ist + } + + if (currentPage < totalPages - delta - 1 && end < totalPages - 1) { + // Auslassungspunkte anzeigen, wenn die aktuelle Seite weit vom Ende entfernt ist + pages.push(m("span.dots", "...")) + } + + if (totalPages > 1) addPage(totalPages) // Immer die letzte Seite anzeigen (wenn nicht 1) + } + + return m("div.pagination-controls", [prevButton, ...pages, nextButton]) + }, +} + +export default PaginationControls diff --git a/karaokatalog/ui/static/components/pieces/Tab.js b/karaokatalog/ui/static/components/pieces/Tab.js new file mode 100644 index 0000000..bc94c35 --- /dev/null +++ b/karaokatalog/ui/static/components/pieces/Tab.js @@ -0,0 +1,16 @@ +/* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. + I should do this at some point in the future and change things I don't like. */ +export const Tab = { + view(vnode) { + return m( + m.route.Link, + { + class: m.route.get() === vnode.attrs.href ? "active" : "", + href: vnode.attrs.href, + role: "button", // Für Barrierefreiheit + tabindex: 0, // Für Tastaturnavigation + }, + vnode.attrs.label, + ) + }, +} diff --git a/karaokatalog/ui/static/style.css b/karaokatalog/ui/static/style.css new file mode 100644 index 0000000..b512b6a --- /dev/null +++ b/karaokatalog/ui/static/style.css @@ -0,0 +1,275 @@ +/* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. + I should do this at some point in the future and change things I don't like. */ + +/* Globale Resets und Basis-Styling (wie vorher) */ +body, +html { + margin: 0; + padding: 0; + font-family: "Roboto", sans-serif; + background-color: #f4f4f9; + color: #333; + line-height: 1.6; +} + +main > p { + text-align: center; + padding: 1em; + font-size: 1.1em; + color: #757575; +} + +/* Suchleiste (Höhe ca. 60px) */ +search { + position: fixed; + top: 0; + left: 0; + right: 0; + background-color: #3f51b5; /* Indigo - Material Primary */ + padding: 12px 16px; + z-index: 1000; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + box-sizing: border-box; /* Stellt sicher, dass Padding die Höhe nicht sprengt */ + height: 60px; /* Feste Höhe für genaue Positionierung */ +} + +input[type="search"] { + width: 100%; + padding: 10px 12px; + border-radius: 4px; + border: none; + font-size: 16px; + box-sizing: border-box; + background-color: #fff; + color: #333; +} + +input[type="search"]::placeholder { + color: #888; +} + +/* Tab Bar (Höhe ca. 48px) */ +nav { + display: flex; + position: fixed; + top: 60px; /* Direkt unter der Suchleiste */ + left: 0; + right: 0; + background-color: #3f51b5; /* Gleiche Farbe wie Suchleiste für einen einheitlichen Header-Block */ + z-index: 999; /* Unter der Suchleiste, falls sie überlappen könnten */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); /* Dezenter Schatten */ + height: 48px; /* Feste Höhe */ + box-sizing: border-box; +} + +nav a { + flex-grow: 1; + display: flex; /* Für vertikales Zentrieren des Texts */ + align-items: center; + justify-content: center; + padding: 0 8px; /* Horizontal Padding */ + text-align: center; + color: rgba( + 255, + 255, + 255, + 0.75 + ); /* Etwas helleres, aber gedämpftes Weiß für inaktive Tabs */ + cursor: pointer; + font-weight: 500; + font-size: 0.9em; /* Leicht kleiner als Haupttext */ + text-transform: uppercase; + border-bottom: 3px solid transparent; /* Platzhalter für den aktiven Indikator */ + transition: + color 0.2s ease-in-out, + border-bottom-color 0.2s ease-in-out; + user-select: none; + text-decoration: none; +} + +nav a:hover { + background-color: rgba(255, 255, 255, 0.1); /* Leichter Hover-Effekt */ +} + +nav a.active { + color: #ffffff; /* Reines Weiß für aktiven Tab */ + border-bottom-color: #ff4081; /* Material Accent Pink für den Indikator */ +} + +/* song .Liste Container */ +main { + padding-top: calc(60px + 48px); /* Höhe Suchleiste + Höhe TabBar */ + padding-bottom: 20px; /* Platz für Paginierung, falls am Ende */ +} + +/* Link zum Zurücksetzen der Suche in der main > p */ +main > p a.clear-search-link, +main > p .clear-search-link { + /* Zweite Regel für den Fall, dass Mithril das 'a' nicht direkt rendert */ + color: #3f51b5; /* Material Indigo, passend zum Header */ + text-decoration: underline; + cursor: pointer; + font-weight: 500; /* Etwas hervorheben */ + background: none; /* Sicherstellen, dass keine Button-Styles übernommen werden */ + border: none; + padding: 0; + display: inline; /* Damit es sich wie ein normaler Link im Text verhält */ +} + +main > p a.clear-search-link:hover, +main > p a.clear-search-link:focus, +main > p .clear-search-link:hover, +main > p .clear-search-link:focus { + color: #303f9f; /* Dunkleres Indigo für Hover/Focus */ + text-decoration: none; /* Optional: Unterstreichung bei Hover entfernen */ +} + +/* Restliche Styles für Artist-Group, song .Item etc. bleiben wie zuvor */ +section.artist { + margin: 12px 8px; + background-color: #ffffff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +section.artist > h2 { + margin: 0; + padding: 12px 16px; + background-color: #e8eaf6; + color: #303f9f; + font-size: 1.1em; + font-weight: 500; + border-bottom: 1px solid #c5cae9; +} + +.song { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + transition: background-color 0.2s ease-in-out; +} + +.song:last-child { + border-bottom: none; +} + +.song:hover { + background-color: #f9f9f9; +} + +.song .info { + flex-grow: 1; + margin-right: 8px; /* Abstand zum Icon */ + overflow: hidden; /* Verhindert Textüberlauf */ +} + +.song .title { + font-size: 1em; + font-weight: 400; + color: #212121; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; /* Text abschneiden bei Überlauf */ +} + +.song .artist { + font-size: 0.85em; + color: #757575; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.song .actions { + margin-left: auto; + flex-shrink: 0; +} + +.heart-icon { + cursor: pointer; + color: #bdbdbd; + font-size: 24px; + user-select: none; + transition: + color 0.2s ease-in-out, + transform 0.1s ease-out; + vertical-align: middle; +} + +.heart-icon:hover { + transform: scale(1.1); +} + +.heart-icon.is-favorite { + color: #ff4081; + font-variation-settings: "FILL" 1; +} + +.material-symbols-outlined { + font-variation-settings: + "FILL" 0, + "wght" 400, + "GRAD" 0, + "opsz" 24; +} + +/* NEU: Pagination Controls Styles */ +.pagination-controls { + display: flex; + justify-content: center; + align-items: center; + padding: 20px 0; /* Mehr Padding für bessere Sichtbarkeit */ + margin-top: 10px; + user-select: none; + flex-wrap: wrap; /* Erlaubt Umbruch bei vielen Seitenzahlen auf kleinen Bildschirmen */ +} + +.pagination-controls button.pagination-button, /* Klasse für Buttons hinzugefügt */ +.pagination-controls span.page-number { + margin: 4px; /* Etwas mehr Margin für Umbruch */ + padding: 8px 12px; + border: 1px solid #ddd; + background-color: #fff; + color: #3f51b5; /* Material Indigo */ + cursor: pointer; + border-radius: 4px; + transition: + background-color 0.2s, + color 0.2s, + border-color 0.2s; + font-size: 0.9em; + min-width: 36px; /* Mindestbreite für bessere Klickbarkeit */ + text-align: center; + box-sizing: border-box; +} + +.pagination-controls button.pagination-button:hover:not(:disabled), +.pagination-controls span.page-number.clickable:hover { + background-color: #e8eaf6; /* Heller Indigo-Ton für Hover */ + border-color: #c5cae9; +} + +.pagination-controls button.pagination-button:disabled { + color: #aaa; + cursor: not-allowed; + background-color: #f9f9f9; + border-color: #eee; +} + +.pagination-controls span.page-number.active { + background-color: #3f51b5; + color: #fff; + border-color: #3f51b5; + font-weight: bold; + cursor: default; +} + +.pagination-controls span.dots { + padding: 8px 6px; + color: #777; + margin: 4px; + font-size: 0.9em; +}