From 02d493dedeb79a259f3c5c1ad1197c7d5c229d06 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Mon, 14 Sep 2026 20:37:01 +0200 Subject: [PATCH] fix: verbleibende Lint-Befunde ausraeumen (Lint jetzt ohne Beanstandung) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vier offene Befunde standen einem sauberen Produktionslauf im Weg: - recently-viewed-section und site-chrome riefen setState direkt im Effektrumpf auf, um einen Leerzustand herzustellen ("keine angesehenen Produkte" bzw. "Eingabe kuerzer als zwei Zeichen"). Beides ergibt sich unmittelbar aus vorhandenen Werten und wird jetzt beim Rendern abgeleitet — das spart je einen zusaetzlichen Renderdurchlauf. Bei der Suche verschwindet die Vorschlagsliste dadurch sofort beim Leeren des Feldes statt erst nach dem naechsten Effektlauf. - STAT_KEYS im Admin-Dashboard wurde ausschliesslich als Typ verwendet, landete als `as const`-Array aber trotzdem im Bundle. Ersetzt durch einen Typ-Alias. - Ein DHL-Test importierte DhlApiError, ohne es zu benutzen, und pruefte nur den Meldungstext — er haette damit jeden beliebigen anderen Fehler mit passendem Text durchgehen lassen. Er prueft jetzt zusaetzlich den Fehlertyp, wie seine beiden Geschwistertests. Stand danach: eslint ohne Beanstandung (vorher 2 Fehler, 2 Warnungen), tsc ohne Fehler, 201 Tests gruen, Produktions-Build erfolgreich. --- src/app/admin/page.tsx | 29 +++++++++++++--------- src/components/recently-viewed-section.tsx | 25 ++++++++++++------- src/components/site-chrome.tsx | 17 ++++++++----- src/lib/dhl.test.ts | 21 ++++++++++------ 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 381ebc3..f9249ed 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -52,17 +52,22 @@ function PeopleIcon() { ); } -const STAT_KEYS = [ - "revenueToday", - "revenueMonth", - "openOrders", - "inventory", - "soldGlasses", - "avgCart", - "repeatCustomers", -] as const; +/** + * Die Kennzahlen-Kacheln des Dashboards. Bewusst ein Typ und keine + * Wert-Konstante: die Liste wurde nie zur Laufzeit durchlaufen, sondern + * ausschliesslich benutzt, um die Record-/Array-Typen unten vollstaendig zu + * halten — als `as const`-Array landete sie trotzdem im Bundle. + */ +type StatKey = + | "revenueToday" + | "revenueMonth" + | "openOrders" + | "inventory" + | "soldGlasses" + | "avgCart" + | "repeatCustomers"; -const statIcons: Record<(typeof STAT_KEYS)[number], () => ReturnType> = { +const statIcons: Record ReturnType> = { revenueToday: CoinIcon, revenueMonth: CoinIcon, openOrders: OrdersIcon, @@ -161,7 +166,7 @@ export default async function AdminDashboardPage() { } const repeatCustomerCount = [...ordersPerCustomer.values()].filter((count) => count > 1).length; - const statLabels: Record<(typeof STAT_KEYS)[number], string> = { + const statLabels: Record = { revenueToday: t.adminDashboardPage.statRevenueToday, revenueMonth: t.adminDashboardPage.statRevenueMonth, openOrders: t.adminDashboardPage.statOpenOrders, @@ -171,7 +176,7 @@ export default async function AdminDashboardPage() { repeatCustomers: t.adminDashboardPage.statRepeatCustomers, }; - const stats: { key: (typeof STAT_KEYS)[number]; value: string }[] = [ + const stats: { key: StatKey; value: string }[] = [ { key: "revenueToday", value: formatPrice(revenueToday._sum.totalCents ?? 0) }, { key: "revenueMonth", value: formatPrice(revenueMonth._sum.totalCents ?? 0) }, { key: "openOrders", value: String(openOrders) }, diff --git a/src/components/recently-viewed-section.tsx b/src/components/recently-viewed-section.tsx index 0247b37..1bf6d76 100644 --- a/src/components/recently-viewed-section.tsx +++ b/src/components/recently-viewed-section.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRecentlyViewedStore } from "@/store/recently-viewed-store"; import { getRecentlyViewedProducts } from "@/app/shop/actions"; import { ProductCard } from "@/components/product-card"; @@ -15,22 +15,29 @@ import type { ProductWithRelations } from "@/lib/data"; export function RecentlyViewedSection({ excludeProductId }: { excludeProductId?: string }) { const { t } = useI18n(); const viewedIds = useRecentlyViewedStore((s) => s.viewedIds); - const [products, setProducts] = useState([]); + const ids = useMemo( + () => viewedIds.filter((id) => id !== excludeProductId), + [viewedIds, excludeProductId] + ); + const [fetched, setFetched] = useState([]); useEffect(() => { - const ids = viewedIds.filter((id) => id !== excludeProductId); - if (ids.length === 0) { - setProducts([]); - return; - } + // Der Leerfall wird bewusst NICHT mehr per setState im Effekt behandelt: + // "keine angesehenen Produkte" ergibt sich unmittelbar aus ids und laesst + // sich beim Rendern ableiten. Ein setState direkt im Effektrumpf loest + // dagegen einen zusaetzlichen Renderdurchlauf aus, nur um einen Zustand + // herzustellen, der ohnehin schon feststand. + if (ids.length === 0) return; let cancelled = false; getRecentlyViewedProducts(ids).then((result) => { - if (!cancelled) setProducts(result); + if (!cancelled) setFetched(result); }); return () => { cancelled = true; }; - }, [viewedIds, excludeProductId]); + }, [ids]); + + const products = ids.length === 0 ? [] : fetched; if (products.length === 0) return null; diff --git a/src/components/site-chrome.tsx b/src/components/site-chrome.tsx index 1336d78..9136081 100644 --- a/src/components/site-chrome.tsx +++ b/src/components/site-chrome.tsx @@ -101,12 +101,17 @@ function SearchButton() { const [suggestions, setSuggestions] = useState([]); const toggleButtonRef = useRef(null); + // "Eingabe zu kurz" ergibt sich unmittelbar aus query und wird beim + // Rendern abgeleitet, statt im Effekt per setState hergestellt zu werden — + // ein setState direkt im Effektrumpf loest einen zusaetzlichen + // Renderdurchlauf aus, nur um einen ohnehin feststehenden Zustand zu + // erzeugen. Nebeneffekt: beim Leeren des Feldes verschwindet die Liste + // sofort, statt erst nach dem naechsten Effektlauf. + const visibleSuggestions = query.trim().length < 2 ? [] : suggestions; + useEffect(() => { const trimmed = query.trim(); - if (trimmed.length < 2) { - setSuggestions([]); - return; - } + if (trimmed.length < 2) return; const controller = new AbortController(); const timeout = setTimeout(() => { fetch(`/api/shop/search-suggestions?q=${encodeURIComponent(trimmed)}`, { @@ -153,9 +158,9 @@ function SearchButton() { placeholder={t.nav.searchPlaceholder} className="w-full border-b border-forest bg-transparent px-2 py-1 text-sm focus:outline-none" /> - {suggestions.length > 0 ? ( + {visibleSuggestions.length > 0 ? (
    - {suggestions.map((s) => ( + {visibleSuggestions.map((s) => (
  • { vi.stubGlobal("fetch", fetchMock); const { createDhlShipmentLabel, DhlApiError } = await import("./dhl"); - await expect( - createDhlShipmentLabel({ - reference: "WH-2026-0004", - weightGrams: 500, - shipper: validShipper, - receiver: validAddress, - }) - ).rejects.toThrow(/Ungültige Anfrage/); + const attempt = createDhlShipmentLabel({ + reference: "WH-2026-0004", + weightGrams: 500, + shipper: validShipper, + receiver: validAddress, + }); + + // Neben der Meldung auch den Fehlertyp pruefen — wie in den beiden Tests + // darueber. Ohne diese Zusage blieb das bereits importierte DhlApiError + // hier ungenutzt, und der Test haette jeden beliebigen anderen Fehler mit + // passendem Text durchgehen lassen. + await expect(attempt).rejects.toThrow(DhlApiError); + await expect(attempt).rejects.toThrow(/Ungültige Anfrage/); }); });