@searchstax-inc/searchstudio-ux-react
v4.1.49
Published
Library to build Site Search page
Downloads
390
Keywords
Readme
sitesearch-ux-react
Library to build Site Search page
Installation
npm install following package
npm install --save @searchstax-inc/searchstudio-ux-react
Usage
Add the following code to :
<script type="text/javascript">
var _msq = _msq || []; //declare object
var analyticsBaseUrl = '<https://analytics-us-east.searchstax.co>';
(function () {
var ms = document.createElement('script');
ms.type = 'text/javascript';
ms.src = '<https://static.searchstax.co/studio-js/v3/js/studio-analytics.js>';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ms, s);
})();
</script>Initialization
sampleConfig object needs to be of type: ISearchstaxConfig
const sampleConfig = {
language: 'en',
searchURL: '',
suggesterURL: '',
trackApiKey: '',
searchAuth: '',
authType: 'basic',
router: {
enabled: true,
routeName: 'searchstax',
title: (result) => `Search results for: ${result.query}`,
ignoredKeys: []
},
hooks: {
beforeSearch: (props) => {
// modify props
return props;
},
afterSearch: (results) => {
// modify results
return results;
}
}
};
<SearchstaxWrapper
language={sampleConfig.language}
searchURL={sampleConfig.searchURL}
suggesterURL={sampleConfig.suggesterURL}
trackApiKey={sampleConfig.trackApiKey}
searchAuth={sampleConfig.searchAuth}
authType={sampleConfig.authType}
router={sampleConfig.router}
beforeSearch={sampleConfig.hooks.beforeSearch}
afterSearch={sampleConfig.hooks.afterSearch}
>
</SearchstaxWrapper>
Initial layout
Add any other Site Search components as needed:
<SearchstaxWrapper>
<SearchstaxInputWidget />
<SearchstaxResultsWidget />
{/* Other components */}
</SearchstaxWrapper>widgets
Following widgets are available:
Answer Widget
SearchStax Site Search solution offers React and Next.js widgets to assist in building your custom search page.
The SearchstaxAnswerWidget component provides a React and Next.js AI answer widget for your searches.
Usage
<SearchstaxAnswerWidget
showMoreAfterWordCount={100}
feedbackwidget={feedbackConfig}
></SearchstaxAnswerWidget>Props
- showMoreAfterWordCount - number(default 100) determining after how many words UI will show “Read More” view.
- feedbackWidget – an optional object that configures thumbs-up and thumbs-down feedback functionality.
- searchAnswerTemplate - template override for answers widget
Example of feedbackWidget config:
const feedbackConfig = {
renderFeedbackWidget: true,
emailOverride: searchstaxEmailOverride,
thumbsUpValue: 10,
thumbsDownValue: 0
}searchAnswerTemplate
The templates prop allows customizing the answer UI.
It receives the following props:
- shouldShowAnswer – boolean
- answerErrorMessage – string
- fullAnswerFormatted – string
- showMoreButtonVisible – boolean
- answerLoading – boolean
Example
function answerTemplate(
answerData: null | ISearchstaxAnswerData,
showMore: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
) {
return (
<>
{answerData && (
<div className="searchstax-answer-wrap">
<div className="searchstax-answer-icon"></div>
<div>
<div
className={
"searchstax-answer-container " +
(answerData.showMoreButtonVisible === true ? "searchstax-answer-show-more" : "")
}
>
<div className="searchstax-answer-title">Smart Answers</div>
{answerData.shouldShowAnswerError && (
<div className="searchstax-answer-error" dangerouslySetInnerHTML={{__html:answerData.answerErrorMessage}}></div>
)}
<div className="searchstax-answer-description" dangerouslySetInnerHTML={{__html:answerData.fullAnswerFormatted}}>
</div>
{answerData.answerLoading && (
<div className="searchstax-answer-loading"></div>
)}
</div>
{answerData.showMoreButtonVisible === true && (
<div className="searchstax-answer-load-more-button-container">
<button
className="searchstax-answer-load-more-button"
onClick={(e) => {
showMore(e);
}}
>
Read more
</button>
</div>
)}
</div>
<div className="searchstax-answer-footer">
<div id="feedbackWidgetContainer"></div>
<div className="searchstax-lightweight-widget-separator-inline"></div>
<p className="searchstax-disclaimer">Generative AI is Experimental</p>
</div>
</div>
)}
</>
);
}
<SearchstaxAnswerWidget
searchAnswerTemplate={answerTemplate}
showMoreAfterWordCount={100}
></SearchstaxAnswerWidget>Input Widget
SearchStax Site Search solution provides React and Next.js widgets to help you build your custom search page.
The SearchstaxInputWidget component provides a search input with autosuggest/autocomplete functionality.
Usage
<SearchstaxInputWidget
suggestAfterMinChars={3}
afterAutosuggest={afterAutosuggest}
beforeAutosuggest={beforeAutosuggest}
></SearchstaxInputWidget>Props
- suggestAfterMinChars - default 3. Number of characters needed for autosuggest to start triggering
- beforeAutosuggest - callback function that gets called before firing autosuggest. autosuggestProps are being passed as a property and can be modified, if passed along further search will execute with modified properties, if null is returned then event gets canceled and search never fires.
- afterAutosuggest - callback function that gets called after autosuggest has values but before rendering. It needs to return same type of data but it can be modified.
- inputTemplate - template override. look at examples below
inputWidgetTemplate
The inputTemplate prop allows customizing the input UI.
It receives the following props:
- locationEnabled – boolean
example
const config = {
appId: "APP_ID",
relatedSearchesAPIKey: "KEY"
}
const locationWidgetConfig = {
locationDecode: (term: string): Promise<ISearchstaxLocation> => {
return new Promise((resolve) => {
// make a request to google geocoding API to retrieve lat, lon and address
const geocodingAPIKey = "GEOCODING_API_KEY";
const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
term
)}&key=${geocodingAPIKey}`;
fetch(geocodingURL)
.then((response) => response.json())
.then((data) => {
if (data.status === "OK" && data.results.length > 0) {
const result = data.results[0];
const location = {
lat: result.geometry.location.lat,
lon: result.geometry.location.lng,
address: result.formatted_address,
};
resolve(location);
} else {
resolve({
address: undefined,
lat: undefined,
lon: undefined,
error: true,
});
}
})
.catch(() => {
resolve({
address: undefined,
lat: undefined,
lon: undefined,
error: true,
});
});
});
},
locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
return new Promise((resolve) => {
fetch(
`https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
{
method: "GET",
headers: {
Authorization: `Token ${config.relatedSearchesAPIKey}`,
},
}
)
.then((response) => response.json())
.then((data) => {
if (data.status === "OK" && data.results.length > 0) {
const result = data.results[0];
resolve({
address: result.formatted_address,
lat: lat,
lon: lon,
error: false,
});
} else {
resolve({
address: undefined,
lat: lat,
lon: lon,
error: true,
});
}
})
.catch(() => {
resolve({
address: undefined,
lat: lat,
lon: lon,
error: true,
});
});
});
},
locationSearchEnabled: false,
locationValuesOverride: {
locationDistanceEnabled: true,
filterValues: ["any", "1000"],
filterUnit: "miles",
locationFilterDefaultValue: "any"
},
}
function LocationTemplate(
locationData: null | ISearchstaxLocationRenderData,
locationChange: (value: string) => void,
locationBlur: (value: string) => void,
radiusChange: (value: string | number) => void,
getCurrentLocation: () => void,
inputValue?: string | null,
selectValue?: string | number | undefined,
locationError?: boolean
): React.ReactElement {
return (
<>
{locationData && (
<div
className="searchstax-location-input-container"
data-test-id="searchstax-location-input-container"
>
<div className="searchstax-location-input-wrapper">
<span className="searchstax-location-input-label">NEAR</span>
<div className="searchstax-location-input-wrapper-inner">
<input
type="text"
id="searchstax-location-input"
className={"searchstax-location-input" + (locationError ? " searchstax-input-location-error" : "")}
placeholder="Zip, Postal Code or City..."
aria-label="Search Location Input"
data-test-id="searchstax-location-input"
value={inputValue ?? ""}
onChange={(e) => locationChange(e.target.value)}
onBlur={(e) => locationBlur(e.target.value)}
/>
<button onClick={getCurrentLocation} className="searchstax-get-current-location-button">Use my current location</button>
</div>
{locationData.shouldShowLocationDistanceDropdown && (
<span className="searchstax-location-input-label">WITHIN</span>
)}
{locationData.shouldShowLocationDistanceDropdown && (
<select
id="searchstax-location-radius-select"
className="searchstax-location-radius-select"
aria-label="Search Location Radius Select"
data-test-id="searchstax-location-radius-select"
onChange={(e) => radiusChange(e.target.value)}
value={selectValue}
>
{locationData.locationSearchDistanceValues.map(
({ value, label }) => (
<option value={value} key={value}>
{label}
</option>
)
)}
</select>
)}
</div>
</div>
)}
</>)
}
function InputTemplate(
suggestions: ISearchstaxSuggestion[],
onMouseLeave: () => void,
onMouseOver: (suggestion: ISearchstaxSuggestion) => void,
onMouseClick: () => void
): React.ReactElement {
return (
<>
<div className="searchstax-search-input-wrapper">
<input
type="text"
id="searchstax-search-input"
className="searchstax-search-input"
placeholder="SEARCH FOR..."
aria-label="search"
/>
<div
className={`searchstax-autosuggest-container ${
suggestions.length === 0 ? "hidden" : ""
}`}
onMouseLeave={onMouseLeave}
>
{suggestions.map(function (suggestion) {
return (
<div
className="searchstax-autosuggest-item"
key={suggestion.term}
>
<div
className="searchstax-autosuggest-item-term-container"
tabIndex={0}
dangerouslySetInnerHTML={{ __html: suggestion.term }}
onMouseOver={() => {
onMouseOver(suggestion);
}}
onClick={() => {
onMouseClick();
}}
></div>
</div>
);
})}
</div>
</div>
<SearchstaxLocationWidget
searchLocationTemplate={LocationTemplate}
hooks={{
locationDecode: locationWidgetConfig.locationDecode,
locationDecodeCoordinatesToAddress:
locationWidgetConfig.locationDecodeCoordinatesToAddress,
}}
locationSearchEnabled={locationWidgetConfig.locationSearchEnabled}
locationValuesOverride={locationWidgetConfig.locationValuesOverride}
/>
<button
className="searchstax-spinner-icon"
id="searchstax-search-input-action-button"
aria-label="search"
></button>
</>
);
}
<SearchstaxInputWidget
afterAutosuggest={afterAutosuggest}
beforeAutosuggest={beforeAutosuggest}
inputTemplate={InputTemplate}
></SearchstaxInputWidget>Location Widget
SearchStax Site Search solution offers a React search-location widget to assist with your custom search page.
The SearchstaxLocationWidget provides a location search input with location-based search functionality.
Usage
const config = {
appId: "APP_ID",
relatedSearchesAPIKey: "KEY"
}
const locationWidgetConfig = {
locationDecode: (term: string): Promise<ISearchstaxLocation> => {
return new Promise((resolve) => {
// make a request to google geocoding API to retrieve lat, lon and address
const geocodingAPIKey = "GEOCODING_API_KEY";
const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
term
)}&key=${geocodingAPIKey}`;
fetch(geocodingURL)
.then((response) => response.json())
.then((data) => {
if (data.status === "OK" && data.results.length > 0) {
const result = data.results[0];
const location = {
lat: result.geometry.location.lat,
lon: result.geometry.location.lng,
address: result.formatted_address,
};
resolve(location);
} else {
resolve({
address: undefined,
lat: undefined,
lon: undefined,
error: true,
});
}
})
.catch(() => {
resolve({
address: undefined,
lat: undefined,
lon: undefined,
error: true,
});
});
});
},
locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
return new Promise((resolve) => {
fetch(
`https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
{
method: "GET",
headers: {
Authorization: `Token ${config.relatedSearchesAPIKey}`,
},
}
)
.then((response) => response.json())
.then((data) => {
if (data.status === "OK" && data.results.length > 0) {
const result = data.results[0];
resolve({
address: result.formatted_address,
lat: lat,
lon: lon,
error: false,
});
} else {
resolve({
address: undefined,
lat: lat,
lon: lon,
error: true,
});
}
})
.catch(() => {
resolve({
address: undefined,
lat: lat,
lon: lon,
error: true,
});
});
});
},
locationSearchEnabled: false,
locationValuesOverride: {
locationDistanceEnabled: true,
filterValues: ["any", "1000"],
filterUnit: "miles",
locationFilterDefaultValue: "any"
},
}
<SearchstaxLocationWidget hooks={ {locationDecode: locationDecodeFunction, locationDecodeCoordinatesToAddress: locationDecodeCoordinatesToAddress} } />Props
- locationDecode - callback function to override location decoding
- locationDecodeCoordinatesToAddress - callback function to override location decoding
- searchLocationTemplate - template override for location widget
Template Override
The searchLocationTemplate prop allows customizing the location input UI.
It receives the following props:
- locationSearchDistanceValues – location distance values
- shouldShowLocationDistanceDropdown – boolean determining if distance dropdown should be shown
Example
const config = {
appId: "APP_ID",
relatedSearchesAPIKey: "KEY"
}
const locationWidgetConfig = {
locationDecode: (term: string): Promise<ISearchstaxLocation> => {
return new Promise((resolve) => {
// make a request to google geocoding API to retrieve lat, lon and address
const geocodingAPIKey = "GEOCODING_API_KEY";
const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
term
)}&key=${geocodingAPIKey}`;
fetch(geocodingURL)
.then((response) => response.json())
.then((data) => {
if (data.status === "OK" && data.results.length > 0) {
const result = data.results[0];
const location = {
lat: result.geometry.location.lat,
lon: result.geometry.location.lng,
address: result.formatted_address,
};
resolve(location);
} else {
resolve({
address: undefined,
lat: undefined,
lon: undefined,
error: true,
});
}
})
.catch(() => {
resolve({
address: undefined,
lat: undefined,
lon: undefined,
error: true,
});
});
});
},
locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
return new Promise((resolve) => {
fetch(
`https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
{
method: "GET",
headers: {
Authorization: `Token ${config.relatedSearchesAPIKey}`,
},
}
)
.then((response) => response.json())
.then((data) => {
if (data.status === "OK" && data.results.length > 0) {
const result = data.results[0];
resolve({
address: result.formatted_address,
lat: lat,
lon: lon,
error: false,
});
} else {
resolve({
address: undefined,
lat: lat,
lon: lon,
error: true,
});
}
})
.catch(() => {
resolve({
address: undefined,
lat: lat,
lon: lon,
error: true,
});
});
});
},
locationSearchEnabled: false,
locationValuesOverride: {
locationDistanceEnabled: true,
filterValues: ["any", "1000"],
filterUnit: "miles",
locationFilterDefaultValue: "any"
},
}
function LocationTemplate(
locationData: null | ISearchstaxLocationRenderData,
locationChange: (value: string) => void,
locationBlur: (value: string) => void,
radiusChange: (value: string | number) => void,
getCurrentLocation: () => void,
inputValue?: string | null,
selectValue?: string | number | undefined,
locationError?: boolean
): React.ReactElement {
return (
<>
{locationData && (
<div
className="searchstax-location-input-container"
data-test-id="searchstax-location-input-container"
>
<div className="searchstax-location-input-wrapper">
<span className="searchstax-location-input-label">NEAR</span>
<div className="searchstax-location-input-wrapper-inner">
<input
type="text"
id="searchstax-location-input"
className={"searchstax-location-input" + (locationError ? " searchstax-input-location-error" : "")}
placeholder="Zip, Postal Code or City..."
aria-label="Search Location Input"
data-test-id="searchstax-location-input"
value={inputValue ?? ""}
onChange={(e) => locationChange(e.target.value)}
onBlur={(e) => locationBlur(e.target.value)}
/>
<button onClick={getCurrentLocation} className="searchstax-get-current-location-button">Use my current location</button>
</div>
{locationData.shouldShowLocationDistanceDropdown && (
<span className="searchstax-location-input-label">WITHIN</span>
)}
{locationData.shouldShowLocationDistanceDropdown && (
<select
id="searchstax-location-radius-select"
className="searchstax-location-radius-select"
aria-label="Search Location Radius Select"
data-test-id="searchstax-location-radius-select"
onChange={(e) => radiusChange(e.target.value)}
value={selectValue}
>
{locationData.locationSearchDistanceValues.map(
({ value, label }) => (
<option value={value} key={value}>
{label}
</option>
)
)}
</select>
)}
</div>
</div>
)}
</>)
}
<SearchstaxLocationWidget searchLocationTemplate={LocationTemplate} hooks={ {locationDecode:locationDecode, locationDecodeCoordinatesToAddress: locationDecodeCoordinatesToAddress} } />Result Widget
The SearchStax Site Search solution provides a React and Next.js results widget to support your custom search page.
The SearchstaxResultsWidget displays the search results.
Usage
<SearchstaxResultWidget
afterLinkClick={afterLinkClick}
renderMethod={'pagination'}
resultsPerPage={10}
></SearchstaxResultWidget>Props
- renderMethod – either “pagination” or “infiniteScroll”.
- resultsPerPage – number of results on a page.
- afterLinkClick – Callback function invoked when a result link is clicked. Allows modifying the result object.
- resultsTemplate - template override for result view
- noResultTemplate - template override for no results view
Result Template Override
The resultsTemplate prop allows customizing the result UI.
It receives no props:
- custom?: any - custom properties that may be added through aftersearchHook
- ribbon: string | null
- paths: string | null;
- url: string | null;
- title: string | null;
- titleTracking: string | null;
- promoted: boolean | null;
- thumbnail: string | null;
- date: string | null;
- snippet: string | null;
- description: string | null;
- uniqueId: string;
- position: number;
- distance: number | null;
- unit: string | null;
- unmappedFields: { key: string; value: string | string[] | boolean; isImage?: boolean; }[] - fields that were not mapped.
- allFields: { key: string; value: string | string[] | boolean }[] - all fields
No Results Template Override
The noResultTemplate prop allows customizing the result UI.
It receives following props:
- spellingSuggestion - suggestion of corrected spelling
- searchExecuted - boolean if search was executed
- searchTerm - term that was searched
Example of default render method
Example of infinite scroll and pagination render methods
function noResultTemplate(
searchTerm: string,
metaData: ISearchstaxSearchMetadata | null,
executeSearch: (searchTerm: string) => void
): React.ReactElement {
return (
<div>
<div className="searchstax-no-results">
{" "}
Showing <strong>no results</strong> for <strong>"{searchTerm}"</strong>
<br />
{metaData?.spellingSuggestion && (
<span>
Did you mean{" "}
<a
href="#"
aria-label={`Did you mean ${metaData?.spellingSuggestion}`}
className="searchstax-suggestion-term"
onClick={(e) => {
e.preventDefault();
executeSearch(metaData?.spellingSuggestion);
}}
>
{metaData?.spellingSuggestion}
</a>
?
</span>
)}
</div>
<ul>
<li>
{" "}
Try searching for search related terms or topics. We offer a wide
variety of content to help you get the information you need.{" "}
</li>
<li>Lost? Click on the ‘X” in the Search Box to reset your search.</li>
</ul>
</div>
);
}
function resultsTemplate(
searchResults: ISearchstaxParsedResult[],
resultClicked: (
results: ISearchstaxParsedResult,
event: any
) => void
): React.ReactElement {
return (
<>
{searchResults && searchResults.length && (
<div className="searchstax-search-results" aria-live="polite">
{searchResults !== null &&
searchResults.map(function (searchResult) {
return (
<a
href={searchResult.url ?? ""}
onClick={(event) => {
resultClicked(searchResult, event);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
resultClicked(searchResult, e);
}
}}
data-searchstax-unique-result-id={searchResult.uniqueId}
key={searchResult.uniqueId}
aria-labelledby={`title-${searchResult.uniqueId}`}
className="searchstax-result-item-link searchstax-result-item-link-wrapping"
tabIndex={0}
>
<div
className={`searchstax-search-result ${
searchResult.thumbnail ? "has-thumbnail" : ""
}`}
key={searchResult.uniqueId}
>
{searchResult.promoted && (
<div className="searchstax-search-result-promoted"></div>
)}
{searchResult.ribbon && (
<div
className="searchstax-search-result-ribbon"
dangerouslySetInnerHTML={{
__html: searchResult.ribbon,
}}
></div>
)}
{searchResult.thumbnail && (
<img
alt=""
src={searchResult.thumbnail}
className="searchstax-thumbnail"
/>
)}
<div className="searchstax-search-result-title-container">
<span
className="searchstax-search-result-title"
id={`title-${searchResult.uniqueId}`}
dangerouslySetInnerHTML={{
__html: searchResult.title ?? "",
}}
></span>
</div>
{searchResult.paths && (
<p
className="searchstax-search-result-common"
tabIndex={0}
dangerouslySetInnerHTML={{ __html: searchResult.paths }}
></p>
)}
{searchResult.description && (
<p
className="searchstax-search-result-description searchstax-search-result-common"
tabIndex={0}
dangerouslySetInnerHTML={{
__html: searchResult.description,
}}
></p>
)}
{searchResult.unmappedFields.map(function (
unmappedField: any
) {
return (
<div key={unmappedField.key}>
{unmappedField.isImage &&
typeof unmappedField.value === "string" && (
<div className="searchstax-search-result-image-container">
<img
alt=""
src={unmappedField.value}
className="searchstax-result-image"
/>
</div>
)}
{!unmappedField.isImage && (
<div>
<p
tabIndex={0}
className="searchstax-search-result-common"
dangerouslySetInnerHTML={{
__html: unmappedField.value,
}}
></p>
</div>
)}
</div>
);
})}
</div>
</a>
);
})}
</div>
)}
</>
);
}
<SearchstaxResultWidget
afterLinkClick={afterLinkClick}
noResultTemplate={noResultTemplate}
resultsTemplate={resultsTemplate}
renderMethod={'pagination'}
></SearchstaxResultWidget>
// Infinite scroll
<SearchstaxResultWidget
afterLinkClick={afterLinkClick}
noResultTemplate={noResultTemplate}
resultsTemplate={resultsTemplate}
renderMethod={'infiniteScroll'}
></SearchstaxResultWidget>
Pagination Widget
The SearchStax Site Search solution offers a pagination widget for React and Next.js search pages.
The SearchstaxPaginationWidget displays pagination controls for search results.
Usage
<SearchstaxPaginationWidget></SearchstaxPaginationWidget>Props
- paginationTemplate - template override for pagination widget
Main Template Override
Main template for the pagination controls.
It receives following props:
- nextPageLink - link of next page;
- previousPageLink - link of previous page;
Infinite Scroll Template Override
Main template for the pagination controls in infinite scroll mode.
It receives following props:
- isLastPage - boolean, true if its last page
- results - results.length can be used if there are results
Example
function paginationTemplate(
paginationData: IPaginationData | null,
nextPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void,
previousPage: (
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) => void
) {
return (
<>
{paginationData && paginationData?.totalResults !== 0 && (
<div className="searchstax-pagination-container">
<div className="searchstax-pagination-content">
<a
className={`searchstax-pagination-previous ${paginationData.isFirstPage ? "disabled" : ""}`}
tabIndex={0}
style={
paginationData?.isFirstPage ? { pointerEvents: "none" } : {}
}
onClick={(e) => {
previousPage(e);
}}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
previousPage(e as any);
}
}}
id="searchstax-pagination-previous"
>
{" "}
< Previous{" "}
</a>
<div className="searchstax-pagination-details">
{" "}
{paginationData?.startResultIndex} -{" "}
{paginationData?.endResultIndex} of{" "}
{paginationData?.totalResults}
</div>
<a
className={`searchstax-pagination-next ${paginationData.isLastPage ? "disabled" : ""}`}
tabIndex={0}
style={
paginationData?.isLastPage ? { pointerEvents: "none" } : {}
}
onClick={(e) => {
nextPage(e);
}}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
nextPage(e as any);
}
}}
id="searchstax-pagination-next"
>
Next >
</a>
</div>
</div>
)}
</>
);
}
function infiniteScrollTemplate(
paginationData: IPaginationData | null,
nextPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
) {
return (
<>
{paginationData &&
paginationData.isInfiniteScroll &&
paginationData?.totalResults !== 0 &&
!paginationData?.isLastPage && (
<div className="searchstax-pagination-container">
<a
className="searchstax-pagination-load-more"
tabIndex={0}
onClick={(e) => {
nextPage(e);
}}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
nextPage(e as any);
}
}}
>
Show More >
</a>
</div>
)}
</>
);
}
<SearchstaxPaginationWidget paginationTemplate={paginationTemplate}></SearchstaxPaginationWidget>
// example of pagination widget for infinite scroll
<SearchstaxPaginationWidget infiniteScrollTemplate={infiniteScrollTemplate}></SearchstaxPaginationWidget>Facets Widget
The SearchStax Site Search solution offers a SearchstaxFacetsWidget component for React and Next.js. This widget displays the search facets.
Facet Selection and Order
Facet lists are configured and ordered on the Site Search Faceting Tab.
Usage
<SearchstaxFacetsWidget
facetingType="and"
itemsPerPageDesktop={2}
itemsPerPageMobile={3}
specificFacets={undefined}
></SearchstaxFacetsWidget>Props
- facetingType: "and" | "or" | "showUnavailable" | "tabs"; // type that determines how facets will behave
- specificFacets?: string[]; // optional array of facet names that if provided will only render those facets
- itemsPerPageDesktop: number; // default expanded facets for desktop
- itemsPerPageMobile: number; // default expanded facets for mobile
- facetsTemplateDesktop - template override for desktop view
- facetsTemplateMobile - template override for mobile view
Main Template Desktop Override
Main wrapper template for desktop facets display.
It receives following props from IFacetsTemplateData
Main Template Mobile Override
Main wrapper template for mobile facets display.
It receives following props from IFacetsTemplateData
Example
function facetsTemplateDesktop(
facetsTemplateDataDesktop: IFacetsTemplateData | null,
facetContainers: {
[key: string]: React.LegacyRef<HTMLDivElement> | undefined;
},
isNotDeactivated: (name: string) => boolean,
toggleFacetGroup: (name: string) => void,
selectFacet: (
index: string,
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetValueData,
isInput: boolean
) => void,
isChecked: (facetValue: IFacetValueData) => boolean | undefined,
showMoreLessDesktop: (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetData
) => void
) {
return (
<div className="searchstax-facets-container-desktop">
{facetsTemplateDataDesktop?.facets.map((facet) => {
return (
<div
className={`searchstax-facet-container ${
isNotDeactivated(facet.name) ? "active" : ""
}`}
key={facet.name + "desktop"}
>
<div>
<div
className="searchstax-facet-title-container"
onClick={() => {
toggleFacetGroup(facet.name);
}}
>
<div className="searchstax-facet-title">
{" "}
{facet.label}
</div>
<div className="searchstax-facet-title-arrow active"></div>
</div>
<div className="searchstax-facet-values-container" aria-live="polite">
{facet.values.map(
//@ts-ignore
(facetValue: IFacetValueData, key) => {
facetContainers[key + facet.name] = createRef();
return (
<div
key={facetValue.value + facetValue.parentName}
className={`searchstax-facet-value-container ${
facetValue.disabled
? "searchstax-facet-value-disabled"
: ""
}`}
ref={facetContainers[key + facet.name]}
>
<div
className={
"searchstax-facet-input" +
" desktop-" +
key +
facet.name
}
>
<input
type="checkbox"
className="searchstax-facet-input-checkbox"
checked={isChecked(facetValue)}
readOnly={true}
aria-label={facetValue.value + ' ' + facetValue.count}
disabled={facetValue.disabled}
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
true
);
}}
/>
</div>
<div
className="searchstax-facet-value-label"
onClick={(e) => {
selectFacet(key + facet.name, e, facetValue, false);
}}
>
{facetValue.value}
</div>
<div
className="searchstax-facet-value-count"
onClick={(e) => {
selectFacet(key + facet.name, e, facetValue, false);
}}
>
({facetValue.count})
</div>
</div>
);
}
)}
{facet.hasMoreFacets && (
<div className="searchstax-facet-show-more-container">
<div
className="searchstax-facet-show-more-container"
onClick={(e) => {
showMoreLessDesktop(e, facet);
}}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
showMoreLessDesktop(e as any, facet);
}
}}
tabIndex={0}
>
{facet.showingAllFacets && (
<div className="searchstax-facet-show-less-button searchstax-facet-show-button">
less
</div>
)}
{!facet.showingAllFacets && (
<div className="searchstax-facet-show-more-button searchstax-facet-show-button">
more
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
);
}
function facetsTemplateMobile(
facetsTemplateDataMobile: IFacetsTemplateData | null,
selectedFacetsCheckboxes: IFacetValue[],
facetContainers: {
[key: string]: React.LegacyRef<HTMLDivElement> | undefined;
},
isNotDeactivated: (name: string) => boolean,
toggleFacetGroup: (name: string) => void,
selectFacet: (
index: string,
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetValueData,
isInput: boolean,
isMobile?: boolean
) => void,
isChecked: (facetValue: IFacetValueData) => boolean | undefined,
unselectFacet: (facet: IFacetValue) => void,
showMoreLessMobile: (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetData
) => void,
openOverlay: () => void,
closeOverlay: () => void,
unselectAll: () => void
) {
return facetsTemplateDataMobile ? (
<div className="searchstax-facets-container-mobile">
<div className="searchstax-facets-pills-container">
<div
className="searchstax-facets-pill searchstax-facets-pill-filter-by"
onClick={() => {
openOverlay();
}}
>
<div className="searchstax-facets-pill-label">Filter By</div>
</div>
<div className="searchstax-facets-pills-selected">
{selectedFacetsCheckboxes.map((facet) => {
return (
<div
className="searchstax-facets-pill searchstax-facets-pill-facets"
key={facet.value}
onClick={() => {
unselectFacet(facet);
}}
>
<div className="searchstax-facets-pill-label">
{facet.value} ({facet.count})
</div>
<div className="searchstax-facets-pill-icon-close"></div>
</div>
);
})}
{selectedFacetsCheckboxes.length !== 0 && (
<div
className="searchstax-facets-pill searchstax-clear-filters searchstax-facets-pill-clear-all"
onClick={() => {
unselectAll();
}}
>
<div className="searchstax-facets-pill-label">Clear Filters</div>
</div>
)}
</div>
<div
className={`searchstax-facets-mobile-overlay ${
//@ts-ignore
facetsTemplateDataMobile.overlayOpened ? "searchstax-show" : ""
}`}
>
<div className="searchstax-facets-mobile-overlay-header">
<div className="searchstax-facets-mobile-overlay-header-title">
Filter By
</div>
<div
className="searchstax-search-close"
onClick={() => {
closeOverlay();
}}
></div>
</div>
<div className="searchstax-facets-container-mobile">
{facetsTemplateDataMobile?.facets.map((facet) => {
return (
<div
key={facet.name + "mobile"}
className={`searchstax-facet-container ${
isNotDeactivated(facet.name) ? `active` : ``
}`}
>
<div>
<div
className="searchstax-facet-title-container"
onClick={() => {
toggleFacetGroup(facet.name);
}}
>
<div className="searchstax-facet-title">
{" "}
{facet.label}{" "}
</div>
<div className="searchstax-facet-title-arrow active"></div>
</div>
<div className="searchstax-facet-values-container" aria-live="polite">
{facet.values.map(
//@ts-ignore
(facetValue: IFacetValueData, key) => {
facetContainers[key + facet.name] = createRef();
return (
<div
key={facetValue.value + facetValue.parentName}
className={`searchstax-facet-value-container ${
facetValue.disabled
? `searchstax-facet-value-disabled`
: ``
}`}
ref={facetContainers[key + facet.name]}
>
<div
className={
"searchstax-facet-input" +
" mobile-" +
key +
facet.name
}
>
<input
type="checkbox"
className="searchstax-facet-input-checkbox"
checked={isChecked(facetValue)}
readOnly={true}
aria-label={facetValue.value + ' ' + facetValue.count}
disabled={facetValue.disabled}
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
true,
true
);
}}
/>
</div>
<div
className="searchstax-facet-value-label"
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
false
);
}}
>
{facetValue.value}
</div>
<div
className="searchstax-facet-value-count"
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
false
);
}}
>
({facetValue.count})
</div>
</div>
);
}
)}
<div
className="searchstax-facet-show-more-container"
v-if="facet.hasMoreFacets"
>
<div
className="searchstax-facet-show-more-container"
onClick={(e) => {
showMoreLessMobile(e, facet);
}}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
showMoreLessMobile(e as any, facet);
}
}}
tabIndex={0}
role="button"
>
{facet.showingAllFacets && (
<div className="searchstax-facet-show-less-button searchstax-facet-show-button">
less
</div>
)}
{!facet.showingAllFacets && (
<div className="searchstax-facet-show-more-button searchstax-facet-show-button">
more
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
<button
className="searchstax-facets-mobile-overlay-done"
onClick={() => {
closeOverlay();
}}
>
Done
</button>
</div>
</div>
</div>
) : (
<></>
);
}
<SearchstaxFacetsWidget
facetingType="and"
itemsPerPageDesktop={2}
itemsPerPageMobile={3}
specificFacets={undefined}
facetsTemplateDesktop={facetsTemplateDesktop}
facetsTemplateMobile={facetsTemplateMobile}
></SearchstaxFacetsWidget>SearchFeedback Widget
The SearchStax Site Search solution provides a search feedback widget to support your React and Next.js search pages.
The SearchstaxFeedbackWidget displays search feedback and stats.
Usage
<SearchstaxOverviewWidget></SearchstaxOverviewWidget>Props
- searchOverviewTemplate - template override for search overview
Main Template Override
Main template for the search feedback message.
It receives following props:
- searchExecuted - boolean, true if search was executed
- hasResults - boolean, true if search has results
- startResultIndex - number, start of page results
- endResultIndex - number, end of page results
- totalResults - number, total results
- searchTerm - term that was searched
- autoCorrectedQuery - query that search was autocorrected to
- originalQuery - original query
Example
function searchOverviewTemplate(
searchFeedbackData: null | ISearchstaxSearchFeedbackData,
onOriginalQueryClick: (
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) => void
) {
return (
<>
{searchFeedbackData &&
searchFeedbackData?.searchExecuted &&
searchFeedbackData?.totalResults > 0 && (
<>
Showing{" "}
<b>
{searchFeedbackData.startResultIndex} -{" "}
{searchFeedbackData.endResultIndex}
</b>{" "}
of
<b> {searchFeedbackData.totalResults}</b> results
{searchFeedbackData.searchTerm && (
<span>
for "<b>{searchFeedbackData.searchTerm}</b>"{" "}
</span>
)}
<div className="searchstax-feedback-container-suggested">
{searchFeedbackData.autoCorrectedQuery && (
<div>
{" "}
Search instead for{" "}
<a
href="#"
onClick={(e) => {
onOriginalQueryClick(e);
}}
className="searchstax-feedback-original-query"
aria-label={`Search instead for ${searchFeedbackData.originalQuery}`}
>
{searchFeedbackData.originalQuery}
</a>
</div>
)}
</div>
</>
)}
</>
);
}
<SearchstaxOverviewWidget searchOverviewTemplate={searchOverviewTemplate}></SearchstaxOverviewWidget>RelatedSearches widget
The SearchStax Site Search solution offers a related-searches widget for React and Next.js search pages.
The SearchstaxRelatedSearchesWidget displays related searches.
Usage
<SearchstaxRelatedSearchesWidget
relatedSearchesURL={config.relatedSearchesURL}
relatedSearchesAPIKey={config.relatedSearchesAPIKey}
></SearchstaxRelatedSearchesWidget>Props
- relatedSearchesURL: // : An endpoint from the Discovery tab of the App Settings > All APls screen.
- relatedSearchesAPIKey?: The Discovery API key from the Discovery tab of the App Settings > All
- searchRelatedSearchesTemplate - template override for related searches
Main Template Override
Main template for related searches.
It receives following props:
- hasRelatedSearches - boolean, true if has related searches
- relatedSearches - array of related searches
- related_search - string, related searc
- last - boolean, true if last related search
Example
function searchRelatedSearchesTemplate(
relatedData: null | ISearchstaxRelatedSearchesData,
executeSearch: (result: ISearchstaxRelatedSearchResult) => void
) {
return (
<>
{relatedData &&
relatedData?.searchExecuted &&
relatedData?.hasRelatedSearches && (
<div
className="searchstax-related-searches-container"
id="searchstax-related-searches-container"
>
{" "}
Related searches: <span id="searchstax-related-searches"></span>
{relatedData.relatedSearches && (
<span className="searchstax-related-search">
{relatedData.relatedSearches.map((related) => (
<span
key={related.related_search}
onClick={() => {
executeSearch(related);
}}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
executeSearch(related);
}
}}
tabIndex={0}
className="searchstax-related-search searchstax-related-search-item"
aria-label={'Related search: ' + related.related_search}
>
{" "}
{related.related_search}
{!related.last && <span>,</span>}
</span>
))}
</span>
)}
</div>
)}
</>
);
}
<SearchstaxRelatedSearchesWidget
relatedSearchesURL={config.relatedSearchesURL}
relatedSearchesAPIKey={config.relatedSearchesAPIKey}
searchRelatedSearchesTemplate={searchRelatedSearchesTemplate}
></SearchstaxRelatedSearchesWidget>ExternalPromotions widget
The SearchStax Site Search solution offers an external-promotions widget to assist with your React and Next.js custom search pages.
The SearchstaxExternalPromotionsWidget displays external promotions fetched from the API.
Usage
<SearchstaxExternalPromotionsWidget></SearchstaxExternalPromotionsWidget>Props
- searchExternalPromotionsTemplate - template override for external promotions widget
Main Template Override
Main template for external promotions.
It receives following props:
- hasExternalPromotions - boolean, true if has external promotions
- url - url of external promotion
- uniqueId - unique id
- name - name of promotion
- description - description
Example
function searchExternalPromotionsTemplate(
externalPromotionsData: null | ISearchstaxExternalPromotionsData,
trackClick: (
externalPromotion: IExternalPromotion,
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) => void
) {
return (
<>
{externalPromotionsData &&
externalPromotionsData?.searchExecuted &&
externalPromotionsData?.hasExternalPromotions &&
externalPromotionsData.externalPromotions.map((externalPromotion) => (
<div
className="searchstax-external-promotion searchstax-search-result"
key={externalPromotion.id}
>
<div className="icon-elevated"></div>
{externalPromotion.url && (
<a
href={externalPromotion.url}
onClick={(event) => {
trackClick(externalPromotion, event);
}}
className="searchstax-result-item-link"
></a>
)}
<div className="searchstax-search-result-title-container">
<span className="searchstax-search-result-title">
{externalPromotion.name}
</span>
</div>
{externalPromotion.description && (
<p className="searchstax-search-result-description searchstax-search-result-common">
{" "}
{externalPromotion.description}{" "}
</p>
)}
{externalPromotion.url && (
<p className="searchstax-search-result-description searchstax-search-result-common">
{" "}
{externalPromotion.url}{" "}
</p>
)}
</div>
))}
</>
);
}
<SearchstaxExternalPromotionsWidget
searchExternalPromotionsTemplate={searchExternalPromotionsTemplate}
></SearchstaxExternalPromotionsWidget>Sorting Widget
The SearchStax Site Search solution offers a sorting widget for your React or Next.js custom search page.
The SearchstaxSortingWidget displays sorting options for search results.
Usage
<SearchstaxSortingWidget></SearchstaxSortingWidget>Props
- searchSortingTemplate - template override for sorting widget
Main Template Override
Main template for sorting widget.
It receives following props:
- searchExecuted - boolean, true if search was executed
- hasResultsOrExternalPromotions - boolean, true if there are results or external promotions
- sortOptions - array of sort options, has key/value
Example
function searchSortingTemplate(
sortingData: null | ISearchstaxSearchSortingData,
orderChange: (value: string) => void,
selectedSorting: string
) {
return (
<>
{sortingData &&
sortingData?.searchExecuted &&
sortingData?.hasResultsOrExternalPromotions && (
<div className="searchstax-sorting-container">
<label className="searchstax-sorting-label" htmlFor="searchstax-search-order-select">
Sort By
</label>
<select
id="searchstax-search-order-select"
className="searchstax-search-order-select"
value={selectedSorting}
onChange={(e) => {
orderChange(e.target.value);
}}
>
{sortingData.sortOptions.map(function (sortOption) {
return (
<option key={sortOption.key} value={sortOption.key}>
{sortOption.value}
</option>
);
})}
</select>
</div>
)}
</>
);
}
<SearchstaxSortingWidget searchSortingTemplate={searchSortingTemplate}></SearchstaxSortingWidget>Template overrides
Templates use relatedsearches templating.
STYLING
scss styles can be imported from searchstudio-ux-js
@import './../node_modules/@searchstax-inc/searchstudio-ux-js/dist/styles/scss/mainTheme.scss';css can be taken from
./../node_modules/@searchstax-inc/searchstudio-ux-js/dist/styles/mainTheme.css