|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { Link } from "@techsio/ui-kit/atoms/link"; |
| 4 | +import NextLink from "next/link"; |
| 5 | +import type { MouseEvent } from "react"; |
| 6 | +import type { |
| 7 | + SearchAutocompleteSuggestion, |
| 8 | + SearchAutocompleteSuggestionType, |
| 9 | +} from "@/lib/search-autocomplete/search-autocomplete-types"; |
| 10 | +import { SearchAutocompleteMedia } from "./search-autocomplete-media"; |
| 11 | + |
| 12 | +export type SearchAutocompletePanelSection = { |
| 13 | + key: SearchAutocompleteSuggestionType; |
| 14 | + title: string; |
| 15 | + items: SearchAutocompleteSuggestion[]; |
| 16 | +}; |
| 17 | + |
| 18 | +type SearchAutocompletePanelProps = { |
| 19 | + activeItemId?: string; |
| 20 | + id: string; |
| 21 | + onItemClick: () => void; |
| 22 | + onItemMouseEnter: (item: SearchAutocompleteSuggestion) => void; |
| 23 | + onMouseDown: (event: MouseEvent<HTMLDivElement>) => void; |
| 24 | + query: string; |
| 25 | + sections: SearchAutocompletePanelSection[]; |
| 26 | + status: "idle" | "loading" | "success" | "error"; |
| 27 | +}; |
| 28 | + |
| 29 | +const joinClassNames = (...classNames: Array<string | false | undefined>) => |
| 30 | + classNames.filter(Boolean).join(" "); |
| 31 | + |
| 32 | +export const getSearchAutocompleteOptionId = ( |
| 33 | + panelId: string, |
| 34 | + item: SearchAutocompleteSuggestion, |
| 35 | +) => `${panelId}-${item.type}-${item.id}`; |
| 36 | + |
| 37 | +function SearchAutocompleteRow({ |
| 38 | + activeItemId, |
| 39 | + item, |
| 40 | + onItemClick, |
| 41 | + onItemMouseEnter, |
| 42 | + panelId, |
| 43 | +}: Pick< |
| 44 | + SearchAutocompletePanelProps, |
| 45 | + "activeItemId" | "onItemClick" | "onItemMouseEnter" |
| 46 | +> & { |
| 47 | + item: SearchAutocompleteSuggestion; |
| 48 | + panelId: string; |
| 49 | +}) { |
| 50 | + const optionId = getSearchAutocompleteOptionId(panelId, item); |
| 51 | + const isActive = activeItemId === optionId; |
| 52 | + |
| 53 | + return ( |
| 54 | + <li role="presentation"> |
| 55 | + <Link |
| 56 | + aria-selected={isActive} |
| 57 | + as={NextLink} |
| 58 | + className={joinClassNames( |
| 59 | + "flex min-w-0 items-center gap-300 px-300 py-200 text-fg-primary transition-colors", |
| 60 | + isActive ? "bg-fill-secondary" : "hover:bg-fill-secondary", |
| 61 | + )} |
| 62 | + href={item.href} |
| 63 | + id={optionId} |
| 64 | + onClick={onItemClick} |
| 65 | + onMouseEnter={() => onItemMouseEnter(item)} |
| 66 | + role="option" |
| 67 | + > |
| 68 | + <SearchAutocompleteMedia item={item} /> |
| 69 | + <span className="min-w-0 flex-1"> |
| 70 | + <span className="block truncate text-sm font-semibold leading-snug"> |
| 71 | + {item.title} |
| 72 | + </span> |
| 73 | + {item.subtitle ? ( |
| 74 | + <span className="block truncate text-xs leading-snug text-fg-secondary"> |
| 75 | + {item.subtitle} |
| 76 | + </span> |
| 77 | + ) : null} |
| 78 | + </span> |
| 79 | + {item.priceLabel || typeof item.inStock === "boolean" ? ( |
| 80 | + <span className="shrink-0 text-right text-xs leading-snug"> |
| 81 | + {item.priceLabel ? ( |
| 82 | + <span className="block font-bold text-primary"> |
| 83 | + {item.priceLabel} |
| 84 | + </span> |
| 85 | + ) : null} |
| 86 | + {typeof item.inStock === "boolean" ? ( |
| 87 | + <span |
| 88 | + className={item.inStock ? "text-success" : "text-fg-secondary"} |
| 89 | + > |
| 90 | + {item.inStock ? "Skladom" : "Vypredané"} |
| 91 | + </span> |
| 92 | + ) : null} |
| 93 | + </span> |
| 94 | + ) : null} |
| 95 | + </Link> |
| 96 | + </li> |
| 97 | + ); |
| 98 | +} |
| 99 | + |
| 100 | +export function SearchAutocompletePanel({ |
| 101 | + activeItemId, |
| 102 | + id, |
| 103 | + onItemClick, |
| 104 | + onItemMouseEnter, |
| 105 | + onMouseDown, |
| 106 | + query, |
| 107 | + sections, |
| 108 | + status, |
| 109 | +}: SearchAutocompletePanelProps) { |
| 110 | + const hasItems = sections.some((section) => section.items.length > 0); |
| 111 | + |
| 112 | + return ( |
| 113 | + <div |
| 114 | + className="absolute left-0 right-0 top-full z-50 mt-100 max-h-screen overflow-y-auto rounded-xs border border-border-secondary bg-surface py-200 shadow-md" |
| 115 | + id={id} |
| 116 | + onMouseDown={onMouseDown} |
| 117 | + role="listbox" |
| 118 | + > |
| 119 | + {status === "loading" && !hasItems ? ( |
| 120 | + <p className="px-300 py-250 text-sm text-fg-secondary"> |
| 121 | + Hľadáme návrhy... |
| 122 | + </p> |
| 123 | + ) : null} |
| 124 | + |
| 125 | + {status === "error" ? ( |
| 126 | + <p className="px-300 py-250 text-sm text-danger"> |
| 127 | + Návrhy sa nepodarilo načítať. |
| 128 | + </p> |
| 129 | + ) : null} |
| 130 | + |
| 131 | + {status === "success" && !hasItems ? ( |
| 132 | + <p className="px-300 py-250 text-sm text-fg-secondary"> |
| 133 | + Pre výraz "{query}" nemáme rýchle návrhy. |
| 134 | + </p> |
| 135 | + ) : null} |
| 136 | + |
| 137 | + {sections.map((section) => |
| 138 | + section.items.length > 0 ? ( |
| 139 | + <div className="border-b border-border-secondary py-100 last:border-b-0" key={section.key}> |
| 140 | + <div className="px-300 py-100 text-xs font-semibold uppercase tracking-normal text-fg-secondary"> |
| 141 | + {section.title} |
| 142 | + </div> |
| 143 | + <ul aria-label={section.title} role="group"> |
| 144 | + {section.items.map((item) => ( |
| 145 | + <SearchAutocompleteRow |
| 146 | + activeItemId={activeItemId} |
| 147 | + item={item} |
| 148 | + key={`${item.type}-${item.id}`} |
| 149 | + onItemClick={onItemClick} |
| 150 | + onItemMouseEnter={onItemMouseEnter} |
| 151 | + panelId={id} |
| 152 | + /> |
| 153 | + ))} |
| 154 | + </ul> |
| 155 | + </div> |
| 156 | + ) : null, |
| 157 | + )} |
| 158 | + </div> |
| 159 | + ); |
| 160 | +} |
0 commit comments