| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- "use client";
- import React from "react";
- import Link from "next/link";
- import { Eye, FolderOpen, Loader2, SlidersHorizontal } from "lucide-react";
- import { SEARCH_SCOPE } from "@/lib/frontend/search/urlState";
- import { dayPath } from "@/lib/frontend/routes";
- import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl";
- import ExplorerLoading from "@/components/explorer/states/ExplorerLoading";
- import ExplorerEmpty from "@/components/explorer/states/ExplorerEmpty";
- import ExplorerError from "@/components/explorer/states/ExplorerError";
- import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
- import { Button } from "@/components/ui/button";
- import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuLabel,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
- } from "@/components/ui/dropdown-menu";
- import {
- Table,
- TableBody,
- TableCaption,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- } from "@/components/ui/table";
- const SORT = Object.freeze({
- RELEVANCE: "relevance",
- DATE_DESC: "date_desc",
- FILENAME_ASC: "filename_asc",
- });
- function toDateKey(it) {
- const y = String(it?.year || "");
- const m = String(it?.month || "").padStart(2, "0");
- const d = String(it?.day || "").padStart(2, "0");
- return `${y}-${m}-${d}`;
- }
- function formatDateDe(it) {
- const y = String(it?.year || "");
- const m = String(it?.month || "").padStart(2, "0");
- const d = String(it?.day || "").padStart(2, "0");
- return `${d}.${m}.${y}`;
- }
- export default function SearchResults({
- branch,
- scope,
- status,
- items,
- error,
- onRetry,
- nextCursor,
- onLoadMore,
- isLoadingMore,
- loadMoreError,
- }) {
- const showBranchColumn =
- scope === SEARCH_SCOPE.ALL || scope === SEARCH_SCOPE.MULTI;
- const [sortMode, setSortMode] = React.useState(SORT.RELEVANCE);
- const sortedItems = React.useMemo(() => {
- const arr = Array.isArray(items) ? [...items] : [];
- if (sortMode === SORT.RELEVANCE) return arr;
- if (sortMode === SORT.DATE_DESC) {
- return arr.sort((a, b) => {
- const da = toDateKey(a);
- const db = toDateKey(b);
- if (da !== db) return da < db ? 1 : -1;
- const fa = String(a?.filename || "");
- const fb = String(b?.filename || "");
- return fa.localeCompare(fb, "de");
- });
- }
- if (sortMode === SORT.FILENAME_ASC) {
- return arr.sort((a, b) =>
- String(a?.filename || "").localeCompare(String(b?.filename || ""), "de")
- );
- }
- return arr;
- }, [items, sortMode]);
- if (status === "idle") {
- return (
- <ExplorerEmpty
- title="Suche starten"
- description="Bitte geben Sie einen Suchbegriff ein und klicken Sie auf „Suchen“."
- upHref={null}
- />
- );
- }
- if (status === "loading") {
- return <ExplorerLoading variant="table" count={8} />;
- }
- if (status === "error" && error) {
- return (
- <ExplorerError
- title={error.title}
- description={error.description}
- onRetry={onRetry}
- />
- );
- }
- const list = Array.isArray(sortedItems) ? sortedItems : [];
- if (list.length === 0) {
- return (
- <ExplorerEmpty
- title="Keine Treffer"
- description="Für Ihre Suche wurden keine Treffer gefunden."
- upHref={null}
- />
- );
- }
- return (
- <div className="space-y-4">
- <div className="flex flex-wrap items-center justify-between gap-2">
- <div className="text-xs text-muted-foreground">
- {list.length} Treffer (aktuell geladen)
- </div>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="outline" size="sm" type="button">
- <SlidersHorizontal className="h-4 w-4" />
- Sortierung
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="min-w-[16rem]">
- <DropdownMenuLabel>Sortierung</DropdownMenuLabel>
- <DropdownMenuSeparator />
- <DropdownMenuRadioGroup
- value={sortMode}
- onValueChange={(value) => setSortMode(value)}
- >
- <DropdownMenuRadioItem value={SORT.RELEVANCE}>
- Relevanz
- </DropdownMenuRadioItem>
- <DropdownMenuRadioItem value={SORT.DATE_DESC}>
- Datum (neueste zuerst)
- </DropdownMenuRadioItem>
- <DropdownMenuRadioItem value={SORT.FILENAME_ASC}>
- Dateiname (A–Z)
- </DropdownMenuRadioItem>
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- <Table>
- <TableCaption>
- Hinweis: PDFs werden in einem neuen Tab geöffnet.
- </TableCaption>
- <TableHeader>
- <TableRow>
- {showBranchColumn ? <TableHead>Niederlassung</TableHead> : null}
- <TableHead>Datum</TableHead>
- <TableHead>Datei</TableHead>
- <TableHead className="hidden md:table-cell">Pfad</TableHead>
- <TableHead className="text-right">Aktion</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {list.map((it) => {
- const itemBranch = String(it?.branch || branch);
- const year = String(it?.year || "");
- const month = String(it?.month || "");
- const day = String(it?.day || "");
- const filename = String(it?.filename || "");
- const relativePath = String(it?.relativePath || "");
- const snippet =
- typeof it?.snippet === "string" && it.snippet.trim()
- ? it.snippet.trim()
- : null;
- const pdfUrl = buildPdfUrl({
- branch: itemBranch,
- year,
- month,
- day,
- filename,
- });
- const dayHref = dayPath(itemBranch, year, month, day);
- return (
- <TableRow
- key={
- relativePath ||
- `${itemBranch}/${year}/${month}/${day}/${filename}`
- }
- >
- {showBranchColumn ? (
- <TableCell>
- <span className="text-sm">{itemBranch}</span>
- </TableCell>
- ) : null}
- <TableCell>
- <span className="text-sm">{formatDateDe(it)}</span>
- </TableCell>
- <TableCell className="min-w-0">
- <div className="min-w-0">
- <p className="truncate font-medium">{filename}</p>
- {snippet ? (
- <p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
- {snippet}
- </p>
- ) : null}
- <p className="truncate text-xs text-muted-foreground md:hidden">
- {relativePath}
- </p>
- </div>
- </TableCell>
- <TableCell className="hidden md:table-cell">
- <span className="text-xs text-muted-foreground">
- {relativePath}
- </span>
- </TableCell>
- <TableCell className="text-right">
- <div className="flex justify-end gap-2">
- <Button variant="outline" size="sm" asChild>
- <a
- href={pdfUrl}
- target="_blank"
- rel="noopener noreferrer"
- aria-label={`PDF öffnen: ${filename}`}
- title={`PDF öffnen: ${filename}`}
- >
- <Eye className="h-4 w-4" />
- Öffnen
- </a>
- </Button>
- <Button variant="outline" size="sm" asChild>
- <Link href={dayHref} title="Zum Tag">
- <FolderOpen className="h-4 w-4" />
- Zum Tag
- </Link>
- </Button>
- </div>
- </TableCell>
- </TableRow>
- );
- })}
- </TableBody>
- </Table>
- {loadMoreError ? (
- <Alert variant="destructive">
- <AlertTitle>{loadMoreError.title}</AlertTitle>
- <AlertDescription>{loadMoreError.description}</AlertDescription>
- </Alert>
- ) : null}
- {nextCursor ? (
- <div className="flex justify-center">
- <Button
- type="button"
- variant="outline"
- onClick={onLoadMore}
- disabled={isLoadingMore}
- title="Weitere Ergebnisse laden"
- >
- {isLoadingMore ? (
- <>
- <Loader2 className="h-4 w-4 animate-spin" />
- Lädt…
- </>
- ) : (
- "Mehr laden"
- )}
- </Button>
- </div>
- ) : null}
- </div>
- );
- }
|