SearchResults.jsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. "use client";
  2. import React from "react";
  3. import { Loader2 } from "lucide-react";
  4. import ExplorerLoading from "@/components/explorer/states/ExplorerLoading";
  5. import ExplorerEmpty from "@/components/explorer/states/ExplorerEmpty";
  6. import ExplorerError from "@/components/explorer/states/ExplorerError";
  7. import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
  8. import { Button } from "@/components/ui/button";
  9. import {
  10. sortSearchItems,
  11. SEARCH_RESULTS_SORT,
  12. } from "@/lib/frontend/search/resultsSorting";
  13. import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
  14. import SearchResultsToolbar from "@/components/search/SearchResultsToolbar";
  15. import SearchResultsTable from "@/components/search/SearchResultsTable";
  16. const LOADING_DELAY_MS = 300;
  17. export default function SearchResults({
  18. branch,
  19. status,
  20. items,
  21. total,
  22. error,
  23. onRetry,
  24. nextCursor,
  25. onLoadMore,
  26. isLoadingMore,
  27. loadMoreError,
  28. needsBranchSelection = false,
  29. }) {
  30. const [sortMode, setSortMode] = React.useState(SEARCH_RESULTS_SORT.RELEVANCE);
  31. const showLoadingUi = useDebouncedVisibility(status === "loading", {
  32. delayMs: LOADING_DELAY_MS,
  33. minVisibleMs: 0,
  34. });
  35. const sortedItems = React.useMemo(() => {
  36. return sortSearchItems(items, sortMode);
  37. }, [items, sortMode]);
  38. if (status === "idle") {
  39. if (needsBranchSelection) {
  40. return (
  41. <ExplorerEmpty
  42. title="Niederlassungen auswählen"
  43. description="Bitte wählen Sie mindestens eine Niederlassung aus, um die Suche zu starten."
  44. upHref={null}
  45. />
  46. );
  47. }
  48. return (
  49. <ExplorerEmpty
  50. title="Suche starten"
  51. description="Bitte geben Sie einen Suchbegriff ein und klicken Sie auf „Suchen“."
  52. upHref={null}
  53. />
  54. );
  55. }
  56. if (status === "error" && error) {
  57. // Validation errors are rendered in the SearchForm (near the inputs).
  58. if (error.kind === "validation") {
  59. return (
  60. <ExplorerEmpty
  61. title="Eingaben prüfen"
  62. description="Bitte prüfen Sie Ihre Filter oben."
  63. upHref={null}
  64. />
  65. );
  66. }
  67. return (
  68. <ExplorerError
  69. title={error.title}
  70. description={error.description}
  71. onRetry={onRetry}
  72. />
  73. );
  74. }
  75. // Debounced loading UI:
  76. // - If loading is very fast, do not show skeletons (prevents flicker).
  77. if (showLoadingUi) {
  78. return <ExplorerLoading variant="table" count={8} />;
  79. }
  80. if (status === "loading") {
  81. return <div className="h-16" aria-hidden="true" />;
  82. }
  83. const list = Array.isArray(sortedItems) ? sortedItems : [];
  84. if (list.length === 0) {
  85. return (
  86. <ExplorerEmpty
  87. title="Keine Treffer"
  88. description="Für Ihre Suche wurden keine Treffer gefunden."
  89. upHref={null}
  90. />
  91. );
  92. }
  93. return (
  94. <div className="space-y-4">
  95. <SearchResultsToolbar
  96. countLoaded={list.length}
  97. total={total}
  98. sortMode={sortMode}
  99. onSortModeChange={setSortMode}
  100. />
  101. <SearchResultsTable routeBranch={branch} items={list} />
  102. {loadMoreError ? (
  103. <Alert variant="destructive">
  104. <AlertTitle>{loadMoreError.title}</AlertTitle>
  105. <AlertDescription>{loadMoreError.description}</AlertDescription>
  106. </Alert>
  107. ) : null}
  108. {nextCursor ? (
  109. <div className="flex justify-center">
  110. <Button
  111. type="button"
  112. variant="outline"
  113. onClick={onLoadMore}
  114. disabled={isLoadingMore}
  115. title="Weitere Ergebnisse laden"
  116. >
  117. {isLoadingMore ? (
  118. <>
  119. <Loader2 className="h-4 w-4 animate-spin" />
  120. Lädt…
  121. </>
  122. ) : (
  123. "Mehr laden"
  124. )}
  125. </Button>
  126. </div>
  127. ) : null}
  128. </div>
  129. );
  130. }