@stapel/search-react 0.4.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +1 -1
- package/dist/default/DegradationNotice.d.ts +7 -0
- package/dist/default/DegradationNotice.d.ts.map +1 -1
- package/dist/default/DegradationNotice.js +17 -6
- package/dist/default/DegradationNotice.js.map +1 -1
- package/dist/default/FacetGroupControl.d.ts +40 -0
- package/dist/default/FacetGroupControl.d.ts.map +1 -0
- package/dist/default/FacetGroupControl.js +176 -0
- package/dist/default/FacetGroupControl.js.map +1 -0
- package/dist/default/FacetPanelPane.d.ts +54 -3
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +116 -22
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +28 -0
- package/dist/default/FilterChips.d.ts.map +1 -0
- package/dist/default/FilterChips.js +204 -0
- package/dist/default/FilterChips.js.map +1 -0
- package/dist/default/LanguageSelect.d.ts +9 -0
- package/dist/default/LanguageSelect.d.ts.map +1 -0
- package/dist/default/LanguageSelect.js +72 -0
- package/dist/default/LanguageSelect.js.map +1 -0
- package/dist/default/PageSizeSelect.d.ts +29 -0
- package/dist/default/PageSizeSelect.d.ts.map +1 -0
- package/dist/default/PageSizeSelect.js +25 -0
- package/dist/default/PageSizeSelect.js.map +1 -0
- package/dist/default/RangeFilterRow.d.ts +12 -0
- package/dist/default/RangeFilterRow.d.ts.map +1 -0
- package/dist/default/RangeFilterRow.js +75 -0
- package/dist/default/RangeFilterRow.js.map +1 -0
- package/dist/default/RankingDisclosurePane.d.ts +5 -0
- package/dist/default/RankingDisclosurePane.d.ts.map +1 -1
- package/dist/default/RankingDisclosurePane.js +15 -15
- package/dist/default/RankingDisclosurePane.js.map +1 -1
- package/dist/default/SearchBox.d.ts +13 -0
- package/dist/default/SearchBox.d.ts.map +1 -0
- package/dist/default/SearchBox.js +67 -0
- package/dist/default/SearchBox.js.map +1 -0
- package/dist/default/SearchPage.d.ts +94 -16
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +191 -8
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultCard.d.ts +19 -18
- package/dist/default/SearchResultCard.d.ts.map +1 -1
- package/dist/default/SearchResultCard.js +178 -5
- package/dist/default/SearchResultCard.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +61 -3
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +51 -21
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/SortSelect.d.ts +10 -2
- package/dist/default/SortSelect.d.ts.map +1 -1
- package/dist/default/SortSelect.js +27 -24
- package/dist/default/SortSelect.js.map +1 -1
- package/dist/default/UrlIssueNotice.d.ts.map +1 -1
- package/dist/default/UrlIssueNotice.js +19 -1
- package/dist/default/UrlIssueNotice.js.map +1 -1
- package/dist/default/ViewSwitch.d.ts +66 -0
- package/dist/default/ViewSwitch.d.ts.map +1 -0
- package/dist/default/ViewSwitch.js +49 -0
- package/dist/default/ViewSwitch.js.map +1 -0
- package/dist/default/index.d.ts +38 -15
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +29 -12
- package/dist/default/index.js.map +1 -1
- package/dist/default/sortLabels.d.ts +5 -0
- package/dist/default/sortLabels.d.ts.map +1 -0
- package/dist/default/sortLabels.js +32 -0
- package/dist/default/sortLabels.js.map +1 -0
- package/dist/headless/FacetPanel.d.ts +17 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +23 -2
- package/dist/headless/FacetPanel.js.map +1 -1
- package/dist/headless/useAppliedCount.d.ts +8 -0
- package/dist/headless/useAppliedCount.d.ts.map +1 -0
- package/dist/headless/useAppliedCount.js +30 -0
- package/dist/headless/useAppliedCount.js.map +1 -0
- package/dist/headless/useScorerNames.d.ts +4 -0
- package/dist/headless/useScorerNames.d.ts.map +1 -0
- package/dist/headless/useScorerNames.js +34 -0
- package/dist/headless/useScorerNames.js.map +1 -0
- package/dist/headless/useSearchBox.d.ts +44 -0
- package/dist/headless/useSearchBox.d.ts.map +1 -0
- package/dist/headless/useSearchBox.js +116 -0
- package/dist/headless/useSearchBox.js.map +1 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +38 -4
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +55 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +101 -4
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +42 -4
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/model/queries.d.ts +26 -2
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +38 -1
- package/dist/model/queries.js.map +1 -1
- package/dist/state/limits.d.ts +44 -0
- package/dist/state/limits.d.ts.map +1 -0
- package/dist/state/limits.js +44 -0
- package/dist/state/limits.js.map +1 -0
- package/dist/state/ranges.d.ts +52 -0
- package/dist/state/ranges.d.ts.map +1 -0
- package/dist/state/ranges.js +108 -0
- package/dist/state/ranges.js.map +1 -0
- package/llms.txt +16 -4
- package/manifest.json +268 -22
- package/nav-manifest.json +1 -1
- package/package.json +15 -10
- package/src/analytics/generated/events.json +1 -1
- package/src/default/DegradationNotice.tsx +34 -12
- package/src/default/FacetGroupControl.tsx +312 -0
- package/src/default/FacetPanelPane.tsx +320 -87
- package/src/default/FilterChips.tsx +415 -0
- package/src/default/LanguageSelect.tsx +93 -0
- package/src/default/PageSizeSelect.tsx +80 -0
- package/src/default/RangeFilterRow.tsx +148 -0
- package/src/default/RankingDisclosurePane.tsx +67 -35
- package/src/default/SearchBox.tsx +130 -0
- package/src/default/SearchPage.tsx +454 -44
- package/src/default/SearchResultCard.tsx +231 -24
- package/src/default/SearchResultsPane.tsx +158 -71
- package/src/default/SortSelect.tsx +50 -37
- package/src/default/UrlIssueNotice.tsx +21 -3
- package/src/default/ViewSwitch.tsx +147 -0
- package/src/default/index.ts +55 -15
- package/src/default/sortLabels.ts +34 -0
- package/src/headless/FacetPanel.tsx +30 -2
- package/src/headless/useAppliedCount.ts +41 -0
- package/src/headless/useScorerNames.ts +36 -0
- package/src/headless/useSearchBox.ts +189 -0
- package/src/i18n/es.ts +46 -4
- package/src/i18n/keys.ts +114 -4
- package/src/i18n/ru.ts +50 -4
- package/src/index.ts +22 -2
- package/src/model/queries.ts +51 -1
- package/src/state/limits.ts +50 -0
- package/src/state/ranges.ts +144 -0
- package/dist/default/ErrorAlert.d.ts +0 -9
- package/dist/default/ErrorAlert.d.ts.map +0 -1
- package/dist/default/ErrorAlert.js +0 -27
- package/dist/default/ErrorAlert.js.map +0 -1
- package/dist/default/theme.d.ts +0 -24
- package/dist/default/theme.d.ts.map +0 -1
- package/dist/default/theme.js +0 -39
- package/dist/default/theme.js.map +0 -1
- package/src/default/ErrorAlert.tsx +0 -46
- package/src/default/theme.tsx +0 -66
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The query box, headless — the state machine behind `<SearchBox>`.
|
|
3
|
+
*
|
|
4
|
+
* ── The one legitimate second copy of the state ────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* This pair's rule is that the URL is the state and no component keeps a copy.
|
|
7
|
+
* A text input is the single exception the rule has to make: what a person has
|
|
8
|
+
* TYPED is not yet what they have SEARCHED for, and committing every keystroke
|
|
9
|
+
* to the URL would make the address bar (and the request, and the query cache)
|
|
10
|
+
* chase the keyboard. So the draft lives here, for as long as it takes to stop
|
|
11
|
+
* typing, and the URL is still the only place a SEARCH is recorded.
|
|
12
|
+
*
|
|
13
|
+
* Two properties keep that honest:
|
|
14
|
+
*
|
|
15
|
+
* 1. **The URL wins whenever it moves on its own.** Back, a shared link
|
|
16
|
+
* opening, a host calling `setText` elsewhere — the draft is reset to the
|
|
17
|
+
* committed `q`. The box can never show a word the results are not about.
|
|
18
|
+
* 2. **Committing REPLACES the history entry** (`setText` already does), so a
|
|
19
|
+
* ten-letter word is one entry and Back still removes the last FILTER
|
|
20
|
+
* rather than the last letter.
|
|
21
|
+
*
|
|
22
|
+
* The suggestion list is asked for the DEBOUNCED prefix, not the draft — one
|
|
23
|
+
* request per pause, not per keystroke, on an endpoint the backend throttles.
|
|
24
|
+
*/
|
|
25
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
26
|
+
import { loadStateFromQuery, mapLoad } from "@stapel/core";
|
|
27
|
+
import type { LoadState } from "@stapel/core";
|
|
28
|
+
import { useSearchState } from "./SearchStateProvider.js";
|
|
29
|
+
import { useSuggest } from "../model/queries.js";
|
|
30
|
+
import {
|
|
31
|
+
SEARCH_BOX_DEBOUNCE_MS,
|
|
32
|
+
SEARCH_BOX_SUGGEST_DEBOUNCE_MS,
|
|
33
|
+
SEARCH_QUERY_MAX_CHARS,
|
|
34
|
+
SUGGEST_MIN_CHARS,
|
|
35
|
+
} from "../state/limits.js";
|
|
36
|
+
|
|
37
|
+
/** What a query box needs to read and move the search text. */
|
|
38
|
+
export interface SearchBoxBag {
|
|
39
|
+
/** What is in the input right now. */
|
|
40
|
+
readonly draft: string;
|
|
41
|
+
/** What the URL — and therefore the results — is about. */
|
|
42
|
+
readonly committed: string;
|
|
43
|
+
/** The draft has not reached the URL yet (a search is about to run). */
|
|
44
|
+
readonly pending: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Title prefixes for the typeahead — the READY answer only.
|
|
47
|
+
*
|
|
48
|
+
* A typeahead has no surface for "we could not fetch suggestions": the box
|
|
49
|
+
* must stay typeable and a person is mid-word. So a failed or in-flight
|
|
50
|
+
* suggest shows nothing, and the three answers stay distinguishable in
|
|
51
|
+
* {@link SearchBoxBag.suggestState} for a skin that wants to say more.
|
|
52
|
+
*/
|
|
53
|
+
readonly suggestions: readonly string[];
|
|
54
|
+
/** The suggest read as a state, unflattened. */
|
|
55
|
+
readonly suggestState: LoadState<readonly string[]>;
|
|
56
|
+
/** A suggestion request is in flight for a prefix nothing is shown for yet. */
|
|
57
|
+
readonly suggestLoading: boolean;
|
|
58
|
+
/** Characters a prefix needs before the index is asked. */
|
|
59
|
+
readonly minSuggestChars: number;
|
|
60
|
+
readonly maxLength: number;
|
|
61
|
+
|
|
62
|
+
/** Type. Commits after {@link SEARCH_BOX_DEBOUNCE_MS} of quiet. */
|
|
63
|
+
setDraft(next: string): void;
|
|
64
|
+
/** Commit now — Enter, the Search button, or picking a suggestion. */
|
|
65
|
+
submit(value?: string): void;
|
|
66
|
+
/** Empty the box AND the search (an empty `q` is a valid browse). */
|
|
67
|
+
clear(): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface UseSearchBoxOptions {
|
|
71
|
+
readonly debounceMs?: number;
|
|
72
|
+
/** Ask the index for prefixes. Default `true`. */
|
|
73
|
+
readonly suggest?: boolean;
|
|
74
|
+
/** Quiet time before the index is asked (default
|
|
75
|
+
* {@link SEARCH_BOX_SUGGEST_DEBOUNCE_MS}). */
|
|
76
|
+
readonly suggestDebounceMs?: number;
|
|
77
|
+
/** How many suggestions to ask for (backend clamps to 25). Default 8. */
|
|
78
|
+
readonly suggestLimit?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One frozen empty list, so a render with no suggestions is a stable value. */
|
|
82
|
+
const NO_SUGGESTIONS: readonly string[] = [];
|
|
83
|
+
|
|
84
|
+
export function useSearchBox(options: UseSearchBoxOptions = {}): SearchBoxBag {
|
|
85
|
+
const { state, setText } = useSearchState();
|
|
86
|
+
const committed = state.q;
|
|
87
|
+
const debounceMs = options.debounceMs ?? SEARCH_BOX_DEBOUNCE_MS;
|
|
88
|
+
|
|
89
|
+
const [draft, setDraftState] = useState(committed);
|
|
90
|
+
// What the URL said the last time we looked. When it changes underneath us
|
|
91
|
+
// — Back, a shared link, a host control — the draft follows it; when it
|
|
92
|
+
// changes BECAUSE we committed, `draft === committed` already and the reset
|
|
93
|
+
// is a no-op.
|
|
94
|
+
const lastCommitted = useRef(committed);
|
|
95
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
96
|
+
|
|
97
|
+
if (lastCommitted.current !== committed) {
|
|
98
|
+
lastCommitted.current = committed;
|
|
99
|
+
if (draft !== committed) setDraftState(committed);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cancel = useCallback((): void => {
|
|
103
|
+
if (timer.current !== null) {
|
|
104
|
+
clearTimeout(timer.current);
|
|
105
|
+
timer.current = null;
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
// A pending commit must not fire into an unmounted tree, and a box that is
|
|
110
|
+
// unmounted mid-word has not searched for that word.
|
|
111
|
+
useEffect(() => cancel, [cancel]);
|
|
112
|
+
|
|
113
|
+
const commit = useCallback(
|
|
114
|
+
(value: string): void => {
|
|
115
|
+
cancel();
|
|
116
|
+
if (value !== committed) setText(value);
|
|
117
|
+
},
|
|
118
|
+
[cancel, committed, setText]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const setDraft = useCallback(
|
|
122
|
+
(next: string): void => {
|
|
123
|
+
const clipped = next.slice(0, SEARCH_QUERY_MAX_CHARS);
|
|
124
|
+
setDraftState(clipped);
|
|
125
|
+
cancel();
|
|
126
|
+
timer.current = setTimeout(() => {
|
|
127
|
+
timer.current = null;
|
|
128
|
+
if (clipped !== committed) setText(clipped);
|
|
129
|
+
}, debounceMs);
|
|
130
|
+
},
|
|
131
|
+
[cancel, committed, debounceMs, setText]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const submit = useCallback(
|
|
135
|
+
(value?: string): void => {
|
|
136
|
+
const next = (value ?? draft).slice(0, SEARCH_QUERY_MAX_CHARS);
|
|
137
|
+
setDraftState(next);
|
|
138
|
+
commit(next);
|
|
139
|
+
},
|
|
140
|
+
[commit, draft]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const clear = useCallback((): void => {
|
|
144
|
+
setDraftState("");
|
|
145
|
+
commit("");
|
|
146
|
+
}, [commit]);
|
|
147
|
+
|
|
148
|
+
// The index is asked about the SETTLED prefix, never the keystroke: one
|
|
149
|
+
// request per pause on an endpoint the backend throttles. `useSuggest` then
|
|
150
|
+
// refuses anything below `SUGGEST_MIN_CHARS` and keys the rest by prefix, so
|
|
151
|
+
// backspacing to a prefix already asked about costs nothing.
|
|
152
|
+
const suggestDebounceMs =
|
|
153
|
+
options.suggestDebounceMs ?? SEARCH_BOX_SUGGEST_DEBOUNCE_MS;
|
|
154
|
+
const [suggestPrefix, setSuggestPrefix] = useState(committed);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const handle = setTimeout(() => {
|
|
157
|
+
setSuggestPrefix(draft);
|
|
158
|
+
}, suggestDebounceMs);
|
|
159
|
+
return () => {
|
|
160
|
+
clearTimeout(handle);
|
|
161
|
+
};
|
|
162
|
+
}, [draft, suggestDebounceMs]);
|
|
163
|
+
|
|
164
|
+
const suggest = useSuggest({
|
|
165
|
+
type: state.type,
|
|
166
|
+
q: suggestPrefix,
|
|
167
|
+
...(options.suggestLimit !== undefined ? { limit: options.suggestLimit } : {}),
|
|
168
|
+
enabled: options.suggest !== false,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const suggestState = mapLoad(
|
|
172
|
+
loadStateFromQuery(suggest),
|
|
173
|
+
(data) => data.items as readonly string[]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
draft,
|
|
178
|
+
committed,
|
|
179
|
+
pending: draft !== committed,
|
|
180
|
+
suggestions: suggestState.status === "ready" ? suggestState.data : NO_SUGGESTIONS,
|
|
181
|
+
suggestState,
|
|
182
|
+
suggestLoading: suggest.isLoading && suggest.fetchStatus === "fetching",
|
|
183
|
+
minSuggestChars: SUGGEST_MIN_CHARS,
|
|
184
|
+
maxLength: SEARCH_QUERY_MAX_CHARS,
|
|
185
|
+
setDraft,
|
|
186
|
+
submit,
|
|
187
|
+
clear,
|
|
188
|
+
};
|
|
189
|
+
}
|
package/src/i18n/es.ts
CHANGED
|
@@ -39,6 +39,13 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
39
39
|
"search.results.distance_km": "a {km} km",
|
|
40
40
|
"search.results.untitled": "Sin título",
|
|
41
41
|
"search.results.open": "Abrir",
|
|
42
|
+
"search.results.image_alt": "Foto de {title}",
|
|
43
|
+
|
|
44
|
+
"search.box.label": "Buscar",
|
|
45
|
+
"search.box.placeholder": "¿Qué estás buscando?",
|
|
46
|
+
"search.box.submit": "Buscar",
|
|
47
|
+
"search.box.clear": "Borrar la búsqueda",
|
|
48
|
+
"search.box.suggestions": "Sugerencias",
|
|
42
49
|
|
|
43
50
|
"search.sort.label": "Orden",
|
|
44
51
|
"search.sort.relevance": "Más relevantes",
|
|
@@ -48,6 +55,10 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
48
55
|
"search.sort.distance": "Más cercanos",
|
|
49
56
|
"search.sort.server_chose": "Ordenado por {sort}",
|
|
50
57
|
|
|
58
|
+
"search.view.label": "Vista",
|
|
59
|
+
"search.view.list": "Lista",
|
|
60
|
+
"search.view.grid": "Cuadrícula",
|
|
61
|
+
|
|
51
62
|
"search.facets.title": "Filtros",
|
|
52
63
|
"search.facets.loading": "Cargando filtros…",
|
|
53
64
|
"search.facets.load_failed": "No pudimos cargar los filtros.",
|
|
@@ -64,21 +75,52 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
64
75
|
"search.facets.range_from": "Desde",
|
|
65
76
|
"search.facets.range_to": "Hasta",
|
|
66
77
|
"search.facets.range_apply": "Aplicar",
|
|
78
|
+
"search.facets.range_clear": "Quitar",
|
|
79
|
+
"search.facets.range_from_aria": "{feature}, desde",
|
|
80
|
+
"search.facets.range_to_aria": "{feature}, hasta",
|
|
81
|
+
"search.facets.show_all": "Ver todos ({count})",
|
|
82
|
+
"search.facets.show_less": "Ver menos",
|
|
83
|
+
"search.facets.range_invalid":
|
|
84
|
+
"«Desde» es mayor que «hasta», así que nada podría coincidir. Intercámbialos para aplicar el rango.",
|
|
85
|
+
|
|
86
|
+
"search.filters.open": "Filtros ({count})",
|
|
87
|
+
"search.filters.apply": "Ver resultados",
|
|
88
|
+
"search.filters.show_count.one": "Ver {count} resultado",
|
|
89
|
+
"search.filters.show_count.other": "Ver {count} resultados",
|
|
90
|
+
"search.filters.show_count_at_least.one": "Ver {count}+ resultado",
|
|
91
|
+
"search.filters.show_count_at_least.other": "Ver {count}+ resultados",
|
|
92
|
+
"search.filters.dismiss": "Cerrar los filtros",
|
|
93
|
+
"search.filters.chips_label": "Filtros",
|
|
94
|
+
"search.filters.all": "Todos los filtros",
|
|
95
|
+
"search.filters.chip_more": ", +{count}",
|
|
96
|
+
|
|
97
|
+
"search.category.title": "Categoría",
|
|
98
|
+
"search.category.clear": "Buscar en todo el catálogo",
|
|
99
|
+
"search.category.current": "Buscando dentro de {path}",
|
|
100
|
+
|
|
101
|
+
"search.language.label": "Idioma de la consulta",
|
|
102
|
+
"search.language.any": "Cualquier idioma",
|
|
103
|
+
|
|
104
|
+
"search.limit.label": "Por página",
|
|
105
|
+
"search.limit.option": "{count} por página",
|
|
106
|
+
"search.limit.from_link": "Este enlace fija su propio tamaño de página.",
|
|
67
107
|
|
|
68
108
|
"search.geo.title": "Ubicación",
|
|
69
109
|
"search.geo.radius_km": "A menos de {km} km",
|
|
110
|
+
"search.geo.radius_label": "Radio, km",
|
|
70
111
|
"search.geo.clear": "En cualquier lugar",
|
|
71
112
|
"search.geo.box": "Dentro del área mostrada",
|
|
72
113
|
"search.geo.center": "Cerca de {lat}, {lon}",
|
|
73
114
|
|
|
74
115
|
"search.url.issues_title": "Parte de este enlace no se pudo leer",
|
|
75
|
-
"search.url.issue.not_a_number":
|
|
116
|
+
"search.url.issue.not_a_number":
|
|
117
|
+
"«{param}» en este enlace no es un número, así que se ignoró",
|
|
76
118
|
"search.url.issue.geo_incomplete":
|
|
77
|
-
"la ubicación
|
|
119
|
+
"la ubicación de este enlace está incompleta, así que se ignoró",
|
|
78
120
|
"search.url.issue.bbox_malformed":
|
|
79
|
-
"el área
|
|
121
|
+
"el área del mapa de este enlace está incompleta, así que se ignoró",
|
|
80
122
|
"search.url.issue.range_malformed":
|
|
81
|
-
"{param}
|
|
123
|
+
"al rango «{param}» de este enlace le faltan números, así que se ignoró",
|
|
82
124
|
|
|
83
125
|
"search.degraded.title": "Lo que esta búsqueda no pudo hacer",
|
|
84
126
|
"search.degraded.typo_tolerance":
|
package/src/i18n/keys.ts
CHANGED
|
@@ -54,6 +54,16 @@ export const SEARCH_I18N_KEYS = {
|
|
|
54
54
|
resultsDistanceKm: "search.results.distance_km",
|
|
55
55
|
resultsUntitled: "search.results.untitled",
|
|
56
56
|
resultsOpen: "search.results.open",
|
|
57
|
+
/** Alt text for the card photo — the card's own title, in a sentence. */
|
|
58
|
+
resultsImageAlt: "search.results.image_alt",
|
|
59
|
+
|
|
60
|
+
// ── the query box ────────────────────────────────────────────────────────
|
|
61
|
+
boxLabel: "search.box.label",
|
|
62
|
+
boxPlaceholder: "search.box.placeholder",
|
|
63
|
+
boxSubmit: "search.box.submit",
|
|
64
|
+
boxClear: "search.box.clear",
|
|
65
|
+
/** Accessible name of the suggestion list under the box. */
|
|
66
|
+
boxSuggestions: "search.box.suggestions",
|
|
57
67
|
|
|
58
68
|
// ── sort ─────────────────────────────────────────────────────────────────
|
|
59
69
|
sortLabel: "search.sort.label",
|
|
@@ -64,6 +74,13 @@ export const SEARCH_I18N_KEYS = {
|
|
|
64
74
|
sortDistance: "search.sort.distance",
|
|
65
75
|
sortServerChose: "search.sort.server_chose",
|
|
66
76
|
|
|
77
|
+
// ── how the results are ARRANGED (see ViewSwitch) ────────────────────────
|
|
78
|
+
/** The switch's own accessible name — a `Segmented` renders a radiogroup
|
|
79
|
+
* with no `<label for>` to name it. */
|
|
80
|
+
viewLabel: "search.view.label",
|
|
81
|
+
viewList: "search.view.list",
|
|
82
|
+
viewGrid: "search.view.grid",
|
|
83
|
+
|
|
67
84
|
// ── facets ───────────────────────────────────────────────────────────────
|
|
68
85
|
facetsTitle: "search.facets.title",
|
|
69
86
|
facetsLoading: "search.facets.loading",
|
|
@@ -79,10 +96,60 @@ export const SEARCH_I18N_KEYS = {
|
|
|
79
96
|
facetsRangeFrom: "search.facets.range_from",
|
|
80
97
|
facetsRangeTo: "search.facets.range_to",
|
|
81
98
|
facetsRangeApply: "search.facets.range_apply",
|
|
99
|
+
facetsRangeClear: "search.facets.range_clear",
|
|
100
|
+
/** Accessible names: the visible "From"/"To" repeat on every row, so the
|
|
101
|
+
* field's own name has to carry the feature it belongs to. */
|
|
102
|
+
facetsRangeFromAria: "search.facets.range_from_aria",
|
|
103
|
+
facetsRangeToAria: "search.facets.range_to_aria",
|
|
104
|
+
/** The reason "Apply" is off: the range reads backwards. */
|
|
105
|
+
facetsRangeInvalid: "search.facets.range_invalid",
|
|
106
|
+
/** The fold on a long facet group: "Show all (46)". The number is in
|
|
107
|
+
* brackets and not a counted noun, so this is one message and not a plural
|
|
108
|
+
* family — "(1)" never renders, because a group is only folded when the
|
|
109
|
+
* tail is longer than one row. */
|
|
110
|
+
facetsShowAll: "search.facets.show_all",
|
|
111
|
+
facetsShowLess: "search.facets.show_less",
|
|
112
|
+
|
|
113
|
+
// ── the filter panel as a whole (phone sheet + host slots) ───────────────
|
|
114
|
+
filtersOpen: "search.filters.open",
|
|
115
|
+
/** The sheet's commit button when the count is unknown. */
|
|
116
|
+
filtersApply: "search.filters.apply",
|
|
117
|
+
/** "Show N results" — an EXACT count on the sheet's commit button.
|
|
118
|
+
* A PLURAL FAMILY. */
|
|
119
|
+
filtersShowCount: "search.filters.show_count",
|
|
120
|
+
/** "Show N+ results" — a FLOOR on the same button. A PLURAL FAMILY. */
|
|
121
|
+
filtersShowCountAtLeast: "search.filters.show_count_at_least",
|
|
122
|
+
filtersDismiss: "search.filters.dismiss",
|
|
123
|
+
/** The accessible name of the phone chip ROW — a `role="group"` holding a
|
|
124
|
+
* dozen buttons is announced as nothing at all without one. */
|
|
125
|
+
filtersChipsLabel: "search.filters.chips_label",
|
|
126
|
+
/** The leading, icon-only chip: the whole panel. Icon-only means the name
|
|
127
|
+
* exists ONLY here, so this key is the control's entire accessibility. */
|
|
128
|
+
filtersAll: "search.filters.all",
|
|
129
|
+
/** A chip filtering on more than one value: "Bosch, +2". Not a plural
|
|
130
|
+
* family — nothing is being counted in words. */
|
|
131
|
+
filtersChipMore: "search.filters.chip_more",
|
|
132
|
+
|
|
133
|
+
// ── category (a host slot, plus the control that removes it) ─────────────
|
|
134
|
+
categoryTitle: "search.category.title",
|
|
135
|
+
categoryClear: "search.category.clear",
|
|
136
|
+
categoryCurrent: "search.category.current",
|
|
137
|
+
|
|
138
|
+
// ── language of the query ────────────────────────────────────────────────
|
|
139
|
+
languageLabel: "search.language.label",
|
|
140
|
+
languageAny: "search.language.any",
|
|
141
|
+
|
|
142
|
+
// ── page size ────────────────────────────────────────────────────────────
|
|
143
|
+
limitLabel: "search.limit.label",
|
|
144
|
+
limitOption: "search.limit.option",
|
|
145
|
+
/** Said beside the control when the URL carries a size the ladder does not
|
|
146
|
+
* offer — otherwise the story of "we kept your link's size" is invisible. */
|
|
147
|
+
limitFromLink: "search.limit.from_link",
|
|
82
148
|
|
|
83
149
|
// ── geo ──────────────────────────────────────────────────────────────────
|
|
84
150
|
geoTitle: "search.geo.title",
|
|
85
151
|
geoRadiusKm: "search.geo.radius_km",
|
|
152
|
+
geoRadiusLabel: "search.geo.radius_label",
|
|
86
153
|
geoClear: "search.geo.clear",
|
|
87
154
|
geoBox: "search.geo.box",
|
|
88
155
|
geoCenter: "search.geo.center",
|
|
@@ -140,6 +207,8 @@ export const SEARCH_I18N_PLURAL_KEYS: readonly SearchI18nKey[] = [
|
|
|
140
207
|
SEARCH_I18N_KEYS.resultsCountApproximate,
|
|
141
208
|
SEARCH_I18N_KEYS.resultsCountAtLeast,
|
|
142
209
|
SEARCH_I18N_KEYS.resultsCountExact,
|
|
210
|
+
SEARCH_I18N_KEYS.filtersShowCount,
|
|
211
|
+
SEARCH_I18N_KEYS.filtersShowCountAtLeast,
|
|
143
212
|
];
|
|
144
213
|
|
|
145
214
|
/**
|
|
@@ -176,6 +245,13 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
176
245
|
"search.results.distance_km": "{km} km away",
|
|
177
246
|
"search.results.untitled": "Untitled",
|
|
178
247
|
"search.results.open": "Open",
|
|
248
|
+
"search.results.image_alt": "Photo of {title}",
|
|
249
|
+
|
|
250
|
+
"search.box.label": "Search",
|
|
251
|
+
"search.box.placeholder": "What are you looking for?",
|
|
252
|
+
"search.box.submit": "Search",
|
|
253
|
+
"search.box.clear": "Clear the search",
|
|
254
|
+
"search.box.suggestions": "Suggestions",
|
|
179
255
|
|
|
180
256
|
"search.sort.label": "Sort",
|
|
181
257
|
"search.sort.relevance": "Most relevant",
|
|
@@ -185,6 +261,10 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
185
261
|
"search.sort.distance": "Nearest first",
|
|
186
262
|
"search.sort.server_chose": "Sorted by {sort}",
|
|
187
263
|
|
|
264
|
+
"search.view.label": "View",
|
|
265
|
+
"search.view.list": "List",
|
|
266
|
+
"search.view.grid": "Grid",
|
|
267
|
+
|
|
188
268
|
"search.facets.title": "Filters",
|
|
189
269
|
"search.facets.loading": "Loading filters…",
|
|
190
270
|
"search.facets.load_failed": "We could not load the filters",
|
|
@@ -201,21 +281,51 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
201
281
|
"search.facets.range_from": "From",
|
|
202
282
|
"search.facets.range_to": "To",
|
|
203
283
|
"search.facets.range_apply": "Apply",
|
|
284
|
+
"search.facets.range_clear": "Clear",
|
|
285
|
+
"search.facets.range_from_aria": "{feature}, from",
|
|
286
|
+
"search.facets.range_to_aria": "{feature}, up to",
|
|
287
|
+
"search.facets.show_all": "Show all ({count})",
|
|
288
|
+
"search.facets.show_less": "Show fewer",
|
|
289
|
+
"search.facets.range_invalid":
|
|
290
|
+
"“From” is larger than “to”, so nothing could match. Swap them to apply this range.",
|
|
291
|
+
|
|
292
|
+
"search.filters.open": "Filters ({count})",
|
|
293
|
+
"search.filters.apply": "Show results",
|
|
294
|
+
"search.filters.show_count.one": "Show {count} result",
|
|
295
|
+
"search.filters.show_count.other": "Show {count} results",
|
|
296
|
+
"search.filters.show_count_at_least.one": "Show {count}+ result",
|
|
297
|
+
"search.filters.show_count_at_least.other": "Show {count}+ results",
|
|
298
|
+
"search.filters.dismiss": "Close the filters",
|
|
299
|
+
"search.filters.chips_label": "Filters",
|
|
300
|
+
"search.filters.all": "All filters",
|
|
301
|
+
"search.filters.chip_more": ", +{count}",
|
|
302
|
+
|
|
303
|
+
"search.category.title": "Category",
|
|
304
|
+
"search.category.clear": "Search the whole catalogue",
|
|
305
|
+
"search.category.current": "Searching inside {path}",
|
|
306
|
+
|
|
307
|
+
"search.language.label": "Query language",
|
|
308
|
+
"search.language.any": "Any language",
|
|
309
|
+
|
|
310
|
+
"search.limit.label": "Per page",
|
|
311
|
+
"search.limit.option": "{count} per page",
|
|
312
|
+
"search.limit.from_link": "This link sets its own page size.",
|
|
204
313
|
|
|
205
314
|
"search.geo.title": "Location",
|
|
206
315
|
"search.geo.radius_km": "Within {km} km",
|
|
316
|
+
"search.geo.radius_label": "Radius, km",
|
|
207
317
|
"search.geo.clear": "Anywhere",
|
|
208
318
|
"search.geo.box": "Inside the shown area",
|
|
209
319
|
"search.geo.center": "Around {lat}, {lon}",
|
|
210
320
|
|
|
211
321
|
"search.url.issues_title": "Part of this link could not be read",
|
|
212
|
-
"search.url.issue.not_a_number": "{param} is not a number
|
|
322
|
+
"search.url.issue.not_a_number": "“{param}” in this link is not a number, so it was ignored",
|
|
213
323
|
"search.url.issue.geo_incomplete":
|
|
214
|
-
"the location
|
|
324
|
+
"the location in this link is only half there, so it was ignored",
|
|
215
325
|
"search.url.issue.bbox_malformed":
|
|
216
|
-
"the area
|
|
326
|
+
"the map area in this link is incomplete, so it was ignored",
|
|
217
327
|
"search.url.issue.range_malformed":
|
|
218
|
-
"{param}
|
|
328
|
+
"the range “{param}” in this link needs two numbers, so it was ignored",
|
|
219
329
|
|
|
220
330
|
"search.degraded.title": "What this search could not do",
|
|
221
331
|
"search.degraded.typo_tolerance":
|
package/src/i18n/ru.ts
CHANGED
|
@@ -62,6 +62,13 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
62
62
|
"search.results.distance_km": "{km} км от вас",
|
|
63
63
|
"search.results.untitled": "Без названия",
|
|
64
64
|
"search.results.open": "Открыть",
|
|
65
|
+
"search.results.image_alt": "Фото: {title}",
|
|
66
|
+
|
|
67
|
+
"search.box.label": "Поиск",
|
|
68
|
+
"search.box.placeholder": "Что ищете?",
|
|
69
|
+
"search.box.submit": "Найти",
|
|
70
|
+
"search.box.clear": "Очистить запрос",
|
|
71
|
+
"search.box.suggestions": "Подсказки",
|
|
65
72
|
|
|
66
73
|
"search.sort.label": "Сортировка",
|
|
67
74
|
"search.sort.relevance": "По релевантности",
|
|
@@ -71,6 +78,10 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
71
78
|
"search.sort.distance": "Сначала ближние",
|
|
72
79
|
"search.sort.server_chose": "Отсортировано: {sort}",
|
|
73
80
|
|
|
81
|
+
"search.view.label": "Вид",
|
|
82
|
+
"search.view.list": "Списком",
|
|
83
|
+
"search.view.grid": "Плиткой",
|
|
84
|
+
|
|
74
85
|
"search.facets.title": "Фильтры",
|
|
75
86
|
"search.facets.loading": "Загружаем фильтры…",
|
|
76
87
|
"search.facets.load_failed": "Не удалось загрузить фильтры.",
|
|
@@ -87,21 +98,56 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
87
98
|
"search.facets.range_from": "От",
|
|
88
99
|
"search.facets.range_to": "До",
|
|
89
100
|
"search.facets.range_apply": "Применить",
|
|
101
|
+
"search.facets.range_clear": "Сбросить",
|
|
102
|
+
"search.facets.range_from_aria": "{feature}, от",
|
|
103
|
+
"search.facets.range_to_aria": "{feature}, до",
|
|
104
|
+
"search.facets.show_all": "Показать все ({count})",
|
|
105
|
+
"search.facets.show_less": "Свернуть",
|
|
106
|
+
"search.facets.range_invalid":
|
|
107
|
+
"«От» больше, чем «до», — под такой диапазон ничего не подойдёт. Поменяйте значения местами.",
|
|
108
|
+
|
|
109
|
+
"search.filters.open": "Фильтры ({count})",
|
|
110
|
+
"search.filters.apply": "Показать результаты",
|
|
111
|
+
"search.filters.show_count.one": "Показать {count} объявление",
|
|
112
|
+
"search.filters.show_count.few": "Показать {count} объявления",
|
|
113
|
+
"search.filters.show_count.many": "Показать {count} объявлений",
|
|
114
|
+
"search.filters.show_count.other": "Показать {count} объявления",
|
|
115
|
+
"search.filters.show_count_at_least.one": "Показать {count}+ объявление",
|
|
116
|
+
"search.filters.show_count_at_least.few": "Показать {count}+ объявления",
|
|
117
|
+
"search.filters.show_count_at_least.many": "Показать {count}+ объявлений",
|
|
118
|
+
"search.filters.show_count_at_least.other": "Показать {count}+ объявления",
|
|
119
|
+
"search.filters.dismiss": "Закрыть фильтры",
|
|
120
|
+
"search.filters.chips_label": "Фильтры",
|
|
121
|
+
"search.filters.all": "Все фильтры",
|
|
122
|
+
"search.filters.chip_more": ", +{count}",
|
|
123
|
+
|
|
124
|
+
"search.category.title": "Категория",
|
|
125
|
+
"search.category.clear": "Искать по всему каталогу",
|
|
126
|
+
"search.category.current": "Ищем внутри {path}",
|
|
127
|
+
|
|
128
|
+
"search.language.label": "Язык запроса",
|
|
129
|
+
"search.language.any": "Любой язык",
|
|
130
|
+
|
|
131
|
+
"search.limit.label": "На странице",
|
|
132
|
+
"search.limit.option": "{count} на странице",
|
|
133
|
+
"search.limit.from_link": "Размер страницы задан этой ссылкой.",
|
|
90
134
|
|
|
91
135
|
"search.geo.title": "Где искать",
|
|
92
136
|
"search.geo.radius_km": "В радиусе {km} км",
|
|
137
|
+
"search.geo.radius_label": "Радиус, км",
|
|
93
138
|
"search.geo.clear": "Везде",
|
|
94
139
|
"search.geo.box": "В показанной области",
|
|
95
140
|
"search.geo.center": "Рядом с {lat}, {lon}",
|
|
96
141
|
|
|
97
142
|
"search.url.issues_title": "Часть этой ссылки прочитать не удалось",
|
|
98
|
-
"search.url.issue.not_a_number":
|
|
143
|
+
"search.url.issue.not_a_number":
|
|
144
|
+
"«{param}» в этой ссылке — не число, поэтому параметр пропущен",
|
|
99
145
|
"search.url.issue.geo_incomplete":
|
|
100
|
-
"
|
|
146
|
+
"точка в этой ссылке указана наполовину, поэтому она пропущена",
|
|
101
147
|
"search.url.issue.bbox_malformed":
|
|
102
|
-
"
|
|
148
|
+
"область на карте в этой ссылке указана не полностью, поэтому она пропущена",
|
|
103
149
|
"search.url.issue.range_malformed":
|
|
104
|
-
"{param}
|
|
150
|
+
"диапазону «{param}» в этой ссылке не хватает чисел, поэтому он пропущен",
|
|
105
151
|
|
|
106
152
|
"search.degraded.title": "Что этот поиск не смог сделать",
|
|
107
153
|
"search.degraded.typo_tolerance":
|
package/src/index.ts
CHANGED
|
@@ -102,6 +102,24 @@ export type {
|
|
|
102
102
|
FacetOption,
|
|
103
103
|
} from "./state/facets.js";
|
|
104
104
|
|
|
105
|
+
export {
|
|
106
|
+
RANGE_FEATURE_TYPES,
|
|
107
|
+
buildRangeGroups,
|
|
108
|
+
isRangeFeature,
|
|
109
|
+
isRangeUsable,
|
|
110
|
+
} from "./state/ranges.js";
|
|
111
|
+
export type { BuildRangeGroupsInput, RangeGroup } from "./state/ranges.js";
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
SEARCH_BOX_DEBOUNCE_MS,
|
|
115
|
+
SEARCH_BOX_SUGGEST_DEBOUNCE_MS,
|
|
116
|
+
SEARCH_DEFAULT_PAGE_SIZE,
|
|
117
|
+
SEARCH_MAX_PAGE_SIZE,
|
|
118
|
+
SEARCH_QUERY_MAX_CHARS,
|
|
119
|
+
SUGGEST_MAX_LIMIT,
|
|
120
|
+
SUGGEST_MIN_CHARS,
|
|
121
|
+
} from "./state/limits.js";
|
|
122
|
+
|
|
105
123
|
// ── model (runtime wiring, query keys, context, hooks) ───────────────────────
|
|
106
124
|
export { createSearchRuntime } from "./model/runtime.js";
|
|
107
125
|
export type {
|
|
@@ -117,7 +135,7 @@ export {
|
|
|
117
135
|
export { searchQueryKeys } from "./model/queryKeys.js";
|
|
118
136
|
export type { SearchQueryKeyParams } from "./model/queryKeys.js";
|
|
119
137
|
|
|
120
|
-
export { useRankingDisclosure, useSearchQuery } from "./model/queries.js";
|
|
138
|
+
export { useRankingDisclosure, useSearchQuery, useSuggest } from "./model/queries.js";
|
|
121
139
|
|
|
122
140
|
// ── headless (renderless components) ─────────────────────────────────────────
|
|
123
141
|
export { SearchProvider } from "./headless/SearchProvider.js";
|
|
@@ -129,9 +147,11 @@ export type {
|
|
|
129
147
|
} from "./headless/SearchStateProvider.js";
|
|
130
148
|
export { SearchResults } from "./headless/SearchResults.js";
|
|
131
149
|
export type { SearchPageInfo, SearchResultsBag } from "./headless/SearchResults.js";
|
|
132
|
-
export { FacetPanel } from "./headless/FacetPanel.js";
|
|
150
|
+
export { FacetPanel, useFacetPanel } from "./headless/FacetPanel.js";
|
|
133
151
|
export type { FacetPanelBag } from "./headless/FacetPanel.js";
|
|
134
152
|
export { useAppliedSort } from "./headless/useAppliedSort.js";
|
|
153
|
+
export { useSearchBox } from "./headless/useSearchBox.js";
|
|
154
|
+
export type { SearchBoxBag, UseSearchBoxOptions } from "./headless/useSearchBox.js";
|
|
135
155
|
export { RankingDisclosure } from "./headless/RankingDisclosure.js";
|
|
136
156
|
export type { RankingDisclosureBag } from "./headless/RankingDisclosure.js";
|
|
137
157
|
|
package/src/model/queries.ts
CHANGED
|
@@ -6,7 +6,9 @@ import type {
|
|
|
6
6
|
RankingResponse,
|
|
7
7
|
SearchQueryState,
|
|
8
8
|
SearchResponse,
|
|
9
|
+
SuggestResponse,
|
|
9
10
|
} from "../api/types.js";
|
|
11
|
+
import { SUGGEST_MAX_LIMIT, SUGGEST_MIN_CHARS } from "../state/limits.js";
|
|
10
12
|
import { useSearchApi } from "./context.js";
|
|
11
13
|
import { searchQueryKeys } from "./queryKeys.js";
|
|
12
14
|
|
|
@@ -60,6 +62,52 @@ export function useSearchQuery(
|
|
|
60
62
|
});
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Title prefixes from the index, for the search box's typeahead.
|
|
67
|
+
*
|
|
68
|
+
* Not from a query log: stapel-search keeps none, which is a privacy decision
|
|
69
|
+
* before it is a product one (`services.suggest`). So the list is what exists
|
|
70
|
+
* in the catalogue, and choosing one is always a search that has results.
|
|
71
|
+
*
|
|
72
|
+
* `enabled` is the debounce's partner, not its replacement: the CALLER holds a
|
|
73
|
+
* debounced prefix (see `useSearchBox`) and this hook refuses to ask about a
|
|
74
|
+
* prefix too short to mean anything. `placeholderData: keepPreviousData` keeps
|
|
75
|
+
* the previous list under the cursor while the next one is in flight — a menu
|
|
76
|
+
* that empties and refills on every keystroke is a menu nobody can click.
|
|
77
|
+
*
|
|
78
|
+
* `staleTime` is a minute: the index changes when something is published, not
|
|
79
|
+
* between two letters of one word.
|
|
80
|
+
*/
|
|
81
|
+
export function useSuggest(params: {
|
|
82
|
+
readonly type: string;
|
|
83
|
+
readonly q: string;
|
|
84
|
+
readonly limit?: number;
|
|
85
|
+
readonly enabled?: boolean;
|
|
86
|
+
}): UseQueryResult<SuggestResponse, StapelApiError> {
|
|
87
|
+
const api = useSearchApi();
|
|
88
|
+
const q = params.q.trim();
|
|
89
|
+
const limit = params.limit;
|
|
90
|
+
return useQuery({
|
|
91
|
+
queryKey: searchQueryKeys.suggest(params.type, q, limit),
|
|
92
|
+
queryFn: ({ signal }) =>
|
|
93
|
+
api.suggest(
|
|
94
|
+
{
|
|
95
|
+
type: params.type,
|
|
96
|
+
q,
|
|
97
|
+
...(limit !== undefined ? { limit: Math.min(limit, SUGGEST_MAX_LIMIT) } : {}),
|
|
98
|
+
},
|
|
99
|
+
{ signal }
|
|
100
|
+
),
|
|
101
|
+
enabled:
|
|
102
|
+
(params.enabled ?? true) &&
|
|
103
|
+
params.type.length > 0 &&
|
|
104
|
+
q.length >= SUGGEST_MIN_CHARS,
|
|
105
|
+
placeholderData: keepPreviousData,
|
|
106
|
+
staleTime: 60_000,
|
|
107
|
+
retry: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
63
111
|
/**
|
|
64
112
|
* The P2B Art. 5 ranking disclosure.
|
|
65
113
|
*
|
|
@@ -67,12 +115,14 @@ export function useSearchQuery(
|
|
|
67
115
|
* not while a person reads the page it explains.
|
|
68
116
|
*/
|
|
69
117
|
export function useRankingDisclosure(
|
|
70
|
-
type?: string
|
|
118
|
+
type?: string,
|
|
119
|
+
options?: { readonly enabled?: boolean }
|
|
71
120
|
): UseQueryResult<RankingResponse, StapelApiError> {
|
|
72
121
|
const api = useSearchApi();
|
|
73
122
|
return useQuery({
|
|
74
123
|
queryKey: searchQueryKeys.ranking(type),
|
|
75
124
|
queryFn: ({ signal }) => api.ranking(type, { signal }),
|
|
125
|
+
enabled: options?.enabled ?? true,
|
|
76
126
|
staleTime: Number.POSITIVE_INFINITY,
|
|
77
127
|
retry: false,
|
|
78
128
|
});
|