168 lines
5.4 KiB
JavaScript
168 lines
5.4 KiB
JavaScript
|
import React, { useState, useEffect } from 'react'
|
||
|
import { Card } from 'react-bootstrap';
|
||
|
import { getSearchResults } from './TabulatorData';
|
||
|
import '../jqueryloader'
|
||
|
|
||
|
const Search = () => {
|
||
|
const [searchTerm, updateSearchTerm] = useState('');
|
||
|
const [filteredResults, updateFilteredResults] = useState([]);
|
||
|
const [searchResults, updateSearchResults] = useState([]);
|
||
|
const [displayResults, updateDisplayResults] = useState(false);
|
||
|
const [focusIndex, updateFocusIndex] = useState(-1);
|
||
|
const linkRefs = [];
|
||
|
const keys = {
|
||
|
ENTER: 13,
|
||
|
UP: 38,
|
||
|
DOWN: 40
|
||
|
};
|
||
|
|
||
|
useEffect(() => {
|
||
|
const getSearchResult = () => {
|
||
|
// ⚠️ This is where you should pull data in from your server
|
||
|
const searchResultsResponse = getSearchResults();
|
||
|
|
||
|
updateSearchResults(searchResultsResponse);
|
||
|
};
|
||
|
|
||
|
getSearchResult();
|
||
|
}, []);
|
||
|
|
||
|
const updateSearch = e => {
|
||
|
var input = e.target?.value;
|
||
|
updateSearchTerm(e.target.value);
|
||
|
updateFilteredResults(searchResults.filter(result =>{
|
||
|
if(input?.length >= 3){
|
||
|
return result.title.match(new RegExp(e.target.value, 'gi'))
|
||
|
}
|
||
|
}))
|
||
|
};
|
||
|
|
||
|
const hideAutoSuggest = e => {
|
||
|
e.persist();
|
||
|
|
||
|
if (e.relatedTarget && e.relatedTarget.className === 'autosuggest-link') {
|
||
|
return false;
|
||
|
}
|
||
|
updateDisplayResults(true);
|
||
|
updateFocusIndex(-1);
|
||
|
};
|
||
|
|
||
|
const showAutoSuggest = () => {
|
||
|
updateDisplayResults(false);
|
||
|
};
|
||
|
const handleInput = (e) =>{
|
||
|
var input = e.target?.value;
|
||
|
$('.search-suggestions').css('display', 'block');
|
||
|
if(input?.length === 0){
|
||
|
$('.search-suggestions').css('display', 'none');
|
||
|
}
|
||
|
}
|
||
|
const handleNavigation = e => {
|
||
|
switch (e.keyCode) {
|
||
|
case keys.ENTER:
|
||
|
if (focusIndex !== -1) {
|
||
|
window.open(linkRefs[focusIndex].href);
|
||
|
}
|
||
|
hideAutoSuggest(e);
|
||
|
break;
|
||
|
case keys.UP:
|
||
|
|
||
|
if (focusIndex > -1) {
|
||
|
updateFocusIndex(focusIndex - 1);
|
||
|
}
|
||
|
break;
|
||
|
case keys.DOWN:
|
||
|
|
||
|
if (focusIndex < filteredResults.length - 1) {
|
||
|
updateFocusIndex(focusIndex + 1);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const SearchResults = () => {
|
||
|
const Message = ({ text }) => (
|
||
|
<div className="search-results-message">
|
||
|
<h2>{text}</h2>
|
||
|
<hr />
|
||
|
</div>
|
||
|
);
|
||
|
|
||
|
if (!displayResults) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (!searchResults.length) {
|
||
|
return <Message text="Loading search results" />
|
||
|
}
|
||
|
|
||
|
if (!searchTerm) {
|
||
|
return <Message text="Try to search for something..." />
|
||
|
}
|
||
|
|
||
|
if (!filteredResults.length) {
|
||
|
return <Message text="We couldn't find anything for your search term." />
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<ul className="search-results">
|
||
|
{filteredResults.map((article, index) => (
|
||
|
<li key={index}>
|
||
|
{/* ⚠️ You may want to outsource this part to make the component less heavy */}
|
||
|
<Card model={article} />
|
||
|
</li>
|
||
|
))}
|
||
|
</ul>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<section className="search">
|
||
|
<h5>Search {searchTerm.length ? `results for: ${searchTerm}` : null}</h5>
|
||
|
<input type="text"
|
||
|
placeholder="Search for tutorials..."
|
||
|
onKeyUp={updateSearch}
|
||
|
onKeyDown={handleNavigation}
|
||
|
onBlur={hideAutoSuggest}
|
||
|
onFocus={showAutoSuggest}
|
||
|
onChange={handleInput}
|
||
|
className='inputclear'/>
|
||
|
<ul className="search-suggestions">
|
||
|
{(!displayResults && searchTerm) && <li key="-1" className={focusIndex === -1 ? 'active' : null}>{`Search for ${searchTerm}`}</li>}
|
||
|
{!displayResults && filteredResults.map((article, index) => (
|
||
|
<li key={index} className={focusIndex === index ? 'active' : null}>
|
||
|
<div className="autosuggest-link"
|
||
|
ref={link => {linkRefs[index] = link}}>
|
||
|
<Highlight tags={searchTerm}>{article.title}</Highlight>
|
||
|
</div>
|
||
|
</li>
|
||
|
))}
|
||
|
</ul>
|
||
|
<SearchResults />
|
||
|
</section>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function Highlight({ children: text = "", tags }) {
|
||
|
if (!tags?.length) return text;
|
||
|
const matches = [...text.matchAll(new RegExp(tags, "ig"))];
|
||
|
const startText = text.slice(0, matches[0]?.index);
|
||
|
return (
|
||
|
<span>
|
||
|
{startText}
|
||
|
{matches.map((match, i) => {
|
||
|
const startIndex = match.index;
|
||
|
const currentText = match[0];
|
||
|
const endIndex = startIndex + currentText.length;
|
||
|
const nextIndex = matches[i + 1]?.index;
|
||
|
const untilNextText = text.slice(endIndex, nextIndex);
|
||
|
return (
|
||
|
<React.Fragment key={i}>
|
||
|
<p><span className='text-primary'>{currentText}</span>{untilNextText}</p>
|
||
|
</React.Fragment>
|
||
|
);
|
||
|
})}
|
||
|
</span>
|
||
|
);
|
||
|
}
|
||
|
export default Search;
|