This commit is contained in:
seb
2026-10-04 21:04:40 +02:00
parent 669954b496
commit f765b87336
59 changed files with 1491 additions and 2045 deletions

View File

@@ -0,0 +1,108 @@
import { Automat } from 'react-automat';
import articleIndexAutomat from './articleIndexAutomat';
/**
* @typedef {Object} ArticleSearchResult
* @property {Record<string,object>} categoryFacets Facet counts per category id.
* @property {number} totalCount Number of matching records.
* @property {Array<object>} recordset Matching article index records.
* @property {Error|null} error Search failure, or `null`.
*/
/**
* @typedef {Object} ArticleSearchState
* @property {ArticleSearchResult} result Latest search outcome.
* @property {string} target Active search term.
* @property {boolean} loading Whether a search is in flight.
*/
/** @type {ArticleSearchResult} */
const emptyResult = {
categoryFacets: {},
totalCount: 0,
recordset: [],
error: null,
};
/** @type {ArticleSearchState} */
const initialState = {
result: emptyResult,
target: '',
loading: false,
};
let articleSearchAutomat;
/**
* Matches one index record against a lowercased search term.
*
* @param {object} row Article index record.
* @param {string} query Lowercased search term.
* @returns {boolean} Whether the record matches.
*/
function matches(row, query) {
if (String(row.name ?? '').toLowerCase().includes(query)) return true;
if (String(row.sku ?? '').toLowerCase().includes(query)) return true;
return (row.eans ?? []).some((ean) => String(ean).includes(query));
}
const actions = {
/**
* Runs a client-side search over the article index.
*
* The full index is already cached by `articleIndexAutomat`, so this does not
* hit the network once the index has loaded.
*
* @param {string} target Search term; an empty string returns everything.
* @returns {Promise<ArticleSearchResult>} Search outcome.
*/
async search(target) {
articleSearchAutomat.setState({ target, loading: true });
try {
const articles = await articleIndexAutomat.actions.ensureLoaded();
const query = String(target ?? '').trim().toLowerCase();
const recordset = query
? articles.filter((row) => matches(row, query))
: articles;
const result = {
categoryFacets: {},
totalCount: recordset.length,
recordset,
error: null,
};
articleSearchAutomat.setState({ result, loading: false });
return result;
} catch (error) {
const result = { ...emptyResult, error };
articleSearchAutomat.setState({ result, loading: false });
throw error;
}
},
/** Reset the result set. */
reset() {
articleSearchAutomat.setState(initialState);
},
};
/**
* Article search state for the `/artikel` overview page.
*
* Replaces `StorageManager.doSearch()`; components subscribe to `result`
* instead of receiving a callback.
*
* @type {Automat<ArticleSearchState>}
*/
articleSearchAutomat = new Automat(initialState, actions, { name: 'articleSearch' });
/**
* Selects the latest search result.
*
* @param {ArticleSearchState} state Automat state.
* @returns {ArticleSearchResult} Search outcome.
*/
export function selectSearchResult(state) {
return state.result;
}
export default articleSearchAutomat;