Meine Codes: Liste nach Art einschraenken
Seit statische Codes ablegbar sind, mischen sich zwei Arten in derselben Liste. Eine Auswahl schraenkt sie auf dynamische oder statische ein — und erscheint nur, wenn ueberhaupt beide vorkommen. Zeigt die Liste nur statische Codes, faellt die Scan-Spalte weg und die Sortierung nach Scans mit ihr: Statische zaehlen keine Scans, die Spalte waere eine Reihe aus Strichen. Stand die Sortierung darauf, springt sie auf "Neueste zuerst". Die Beschreibung der Seite behauptete noch, statische Codes wuerden nicht gespeichert. Das stimmt seit #2 nicht mehr. Refs #5
This commit is contained in:
@@ -618,13 +618,18 @@ Dazu kommen
|
||||
|
||||
- ein **Suchfeld** über Name, Ziel, Kurz-URL und Typ,
|
||||
- eine **Sortierung** nach Datum, Name oder Scan-Anzahl,
|
||||
- ein **Gruppenfilter**, sobald Codes in mehr als einer Gruppe liegen.
|
||||
- ein **Gruppenfilter**, sobald Codes in mehr als einer Gruppe liegen,
|
||||
- ein **Filter nach Art**, sobald dynamische und statische Codes
|
||||
nebeneinander liegen.
|
||||
|
||||
Auf schmalen Bildschirmen blendet die Liste die hinteren Spalten aus; die
|
||||
Kurz-URL rückt dann unter den Namen. Zum Herunterladen dient dort die
|
||||
Kartenansicht.
|
||||
|
||||
Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden.
|
||||
Abgelegte **statische Codes** stehen mit in der Liste, tragen aber ein
|
||||
Abzeichen „statisch“ statt einer Kurz-URL und zählen keine Scans. Steht der
|
||||
Filter auf „Nur statische“, fällt die Scan-Spalte weg und die Sortierung nach
|
||||
Scans mit ihr — bei einer Liste aus lauter Nullen sagt sie nichts.
|
||||
|
||||
Bei einem **Feedback-Code** führt aus der Zeile ein Stern-Symbol zur
|
||||
Auswertung: Durchschnitt, Verteilung der Noten und die abgegebenen Kommentare.
|
||||
|
||||
@@ -27,9 +27,9 @@ export default async function MyCodesPage() {
|
||||
Meine Codes
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Alle dynamischen QR-Codes mit ihrem aktuellen Ziel und der Anzahl der
|
||||
Scans. Statische Codes erscheinen hier nicht — sie werden nicht
|
||||
gespeichert.
|
||||
Dynamische Codes mit ihrem aktuellen Ziel und der Anzahl der Scans,
|
||||
dazu abgelegte statische Codes. Über die Auswahl „Nach Art filtern“
|
||||
lässt sich die Liste auf eine der beiden Arten einschränken.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ function formatShortDate(value: string | null): string {
|
||||
|
||||
type View = "list" | "cards";
|
||||
type SortKey = "neueste" | "aelteste" | "name" | "scans";
|
||||
/** Betriebsart, auf die die Liste eingeschränkt ist. */
|
||||
type ModeFilter = "alle" | "dynamic" | "static";
|
||||
|
||||
const SORT_LABELS: Record<SortKey, string> = {
|
||||
neueste: "Neueste zuerst",
|
||||
@@ -51,6 +53,12 @@ const SORT_LABELS: Record<SortKey, string> = {
|
||||
scans: "Meiste Scans",
|
||||
};
|
||||
|
||||
const MODE_LABELS: Record<ModeFilter, string> = {
|
||||
alle: "Alle Arten",
|
||||
dynamic: "Nur dynamische",
|
||||
static: "Nur statische",
|
||||
};
|
||||
|
||||
/** Merkt sich die gewählte Ansicht über Seitenwechsel hinweg. */
|
||||
const VIEW_STORAGE_KEY = "qrgen.codes.view";
|
||||
|
||||
@@ -94,6 +102,7 @@ export function CodesList({
|
||||
const [query, setQuery] = useState("");
|
||||
const [sort, setSort] = useState<SortKey>("neueste");
|
||||
const [groupFilter, setGroupFilter] = useState("alle");
|
||||
const [modeFilter, setModeFilter] = useState<ModeFilter>("alle");
|
||||
|
||||
// Erst nach dem Einhängen lesen — auf dem Server gibt es keinen
|
||||
// localStorage, und ein abweichender Startwert führte zu Hydrationsfehlern.
|
||||
@@ -107,6 +116,14 @@ export function CodesList({
|
||||
window.localStorage.setItem(VIEW_STORAGE_KEY, next);
|
||||
};
|
||||
|
||||
const changeModeFilter = (next: ModeFilter) => {
|
||||
setModeFilter(next);
|
||||
// Statische Codes zählen keine Scans. Bleibt die Sortierung danach
|
||||
// stehen, sortiert die Liste nach lauter Nullen — und die Auswahl bietet
|
||||
// eine Reihenfolge an, die es dort gar nicht gibt.
|
||||
if (next === "static" && sort === "scans") setSort("neueste");
|
||||
};
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const result = await apiJson<{ codes: QrCodeDto[] }>("/api/codes");
|
||||
@@ -241,11 +258,25 @@ export function CodesList({
|
||||
);
|
||||
}, [codes]);
|
||||
|
||||
// Welche Betriebsarten überhaupt vorkommen. Ein Filter, der nichts zu
|
||||
// filtern hat, steht nur im Weg — deshalb erscheint er erst, wenn beide
|
||||
// Arten in der Liste stehen.
|
||||
const kinds = useMemo(() => {
|
||||
let dynamisch = false;
|
||||
let statisch = false;
|
||||
for (const code of codes ?? []) {
|
||||
if (code.mode === "static") statisch = true;
|
||||
else dynamisch = true;
|
||||
}
|
||||
return { dynamisch, statisch, beide: dynamisch && statisch };
|
||||
}, [codes]);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const needle = query.trim().toLowerCase();
|
||||
|
||||
const filtered = (codes ?? []).filter((code) => {
|
||||
if (groupFilter !== "alle" && code.groupId !== groupFilter) return false;
|
||||
if (modeFilter !== "alle" && code.mode !== modeFilter) return false;
|
||||
if (!needle) return true;
|
||||
return [
|
||||
displayName(code),
|
||||
@@ -273,7 +304,7 @@ export function CodesList({
|
||||
}
|
||||
});
|
||||
return sorted;
|
||||
}, [codes, query, sort, groupFilter]);
|
||||
}, [codes, query, sort, groupFilter, modeFilter]);
|
||||
|
||||
/* ---------------------------------------------------------------- View */
|
||||
|
||||
@@ -287,6 +318,10 @@ export function CodesList({
|
||||
const svgHref = (code: QrCodeDto) =>
|
||||
`/api/codes/${code.id}/qr?format=svg&download=1`;
|
||||
|
||||
// Zeigt die Liste ausschließlich statische Codes, hat die Scan-Spalte
|
||||
// nichts zu sagen — dann fällt sie weg, samt der Sortierung danach.
|
||||
const nurStatische = modeFilter === "static" || (kinds.statisch && !kinds.dynamisch);
|
||||
|
||||
const toolbar = codes.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative min-w-0 flex-1 basis-56">
|
||||
@@ -320,13 +355,32 @@ export function CodesList({
|
||||
</Select>
|
||||
) : null}
|
||||
|
||||
{kinds.beide ? (
|
||||
<Select
|
||||
aria-label="Nach Art filtern"
|
||||
className="!w-auto"
|
||||
value={modeFilter}
|
||||
onChange={(event) =>
|
||||
changeModeFilter(event.target.value as ModeFilter)
|
||||
}
|
||||
>
|
||||
{(Object.keys(MODE_LABELS) as ModeFilter[]).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{MODE_LABELS[key]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
) : null}
|
||||
|
||||
<Select
|
||||
aria-label="Sortierung"
|
||||
className="!w-auto"
|
||||
value={sort}
|
||||
onChange={(event) => setSort(event.target.value as SortKey)}
|
||||
>
|
||||
{(Object.keys(SORT_LABELS) as SortKey[]).map((key) => (
|
||||
{(Object.keys(SORT_LABELS) as SortKey[])
|
||||
.filter((key) => key !== "scans" || !nurStatische)
|
||||
.map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{SORT_LABELS[key]}
|
||||
</option>
|
||||
@@ -406,12 +460,13 @@ export function CodesList({
|
||||
{codes.length > 0 && visible.length === 0 ? (
|
||||
<Card className="text-center">
|
||||
<p className="text-sm text-slate-600">
|
||||
Kein Code passt zur Suche.{" "}
|
||||
Kein Code passt zu Suche und Filtern.{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setQuery("");
|
||||
setGroupFilter("alle");
|
||||
setModeFilter("alle");
|
||||
}}
|
||||
className="font-medium text-accent-text underline"
|
||||
>
|
||||
@@ -446,9 +501,11 @@ export function CodesList({
|
||||
Gruppe
|
||||
</th>
|
||||
) : null}
|
||||
{nurStatische ? null : (
|
||||
<th scope="col" className="w-16 px-3 py-2.5 text-right sm:w-20">
|
||||
Scans
|
||||
</th>
|
||||
)}
|
||||
<th
|
||||
scope="col"
|
||||
className="hidden w-24 px-3 py-2.5 whitespace-nowrap sm:table-cell"
|
||||
@@ -571,15 +628,20 @@ export function CodesList({
|
||||
</td>
|
||||
) : null}
|
||||
|
||||
{nurStatische ? null : (
|
||||
<td className="px-3 py-2.5 text-right font-semibold text-slate-900 tabular-nums">
|
||||
{code.mode === "static" ? (
|
||||
<span className="font-normal text-slate-400" title="Statische Codes werden nicht gezählt">
|
||||
<span
|
||||
className="font-normal text-slate-400"
|
||||
title="Statische Codes werden nicht gezählt"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
) : (
|
||||
code.scans
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
|
||||
<td
|
||||
className="hidden px-3 py-2.5 whitespace-nowrap text-slate-500 tabular-nums sm:table-cell"
|
||||
|
||||
Reference in New Issue
Block a user