import{ useState, useEffect } from "react"; import styles from "../styles/search.module.scss"; import { DebounceInput } from "react-debounce-input"; import Fuse from "fuse.js"; import SingleApp from "../components/SingleApp"; import ListPackages from "../components/ListPackages"; import {FiSearch, FiHelpCircle} from "react-icons/fi"; import { forceVisible } from 'react-lazyload'; import { useRouter } from "next/router"; function Search({ apps, onSearch, label, placeholder, preventGlobalSelect, isPackView, alreadySelected=[], limit=-1}) { const [results, setResults] = useState([]) const [searchInput, setSearchInput] = useState(); const defaultKeys = [{ name: "name", weight: 2 }, "path", "desc", "publisher", "tags"]; const [keys, setKeys] = useState(defaultKeys); const router = useRouter(); const [urlQuery, setUrlQuery] = useState(); const options = (keys) => { return { minMatchCharLength: 3, threshold: 0.3, keys } } let fuse = new Fuse(apps, options(defaultKeys)); useEffect(() => { // if we have a ?q param on the url, we deal with it if (apps.length !== 0 && router.query && router.query.q && urlQuery !== router.query.q){ handleSearchInput(null, router.query.q) setUrlQuery(router.query.q) } else if(results != 0 && urlQuery && router.query && !router.query.q){ // if we previously had a query, going back should reset it. setSearchInput(""); setResults([]); onSearch(); } }) const handleSearchInput = (e, q) => { const inputVal = e ? e.target.value : q; if(onSearch) onSearch(inputVal); let query = inputVal; if(query === ""){ forceVisible(); // for some reason lazy load doesn't detect when the new elements roll in, so we force visible to all imgs } let prefixes = ["name", "tags", "publisher", "desc"]; let checkPrefix = prefixes.filter(prefix => query.startsWith(`${prefix}:`)); if(checkPrefix.length !== 0){ setKeys(checkPrefix); query = query.replace(`${checkPrefix[0]}:`, "") fuse = new Fuse(apps, options(checkPrefix)); } else if(keys !== defaultKeys){ setKeys(defaultKeys) fuse = new Fuse(apps, options(defaultKeys)); } setSearchInput(inputVal); if (query<= 3) return; let results = fuse.search(query.toLowerCase().replace(/\s/g, "")); setResults([...results.map((r) => r.item).slice(0, (limit ? limit : results.length))]); }; if (!apps) return <></>; return ( <div> <label htmlFor="search" className={styles.searchLabel}>{label || `${Math.floor(apps.length / 50) * 50}+ apps and growing.`}</label> <div className={styles.searchBox}> <div className={styles.searchInner}> <FiSearch /> <DebounceInput minLength={2} debounceTimeout={300} onChange={(e) => handleSearchInput(e)} id="search" value={searchInput} autoComplete="off" autoFocus={true} placeholder={placeholder || "Search for apps here"} /> </div> <div className={styles.tip}> <a href="#" title="Search tips"><FiHelpCircle /></a> <div className={styles.tipData}> <p>Use search prefixes to target a specific field in searches!</p> <ul> <li><code>name:</code> search for an app's name</li> <li><code>publisher:</code> search for apps by a publisher</li> <li><code>tags:</code> search for apps by a tag</li> <li><code>desc:</code> search the description of apps</li> </ul> </div> </div> {results.length > 0 && searchInput && <p className={styles.searchHint}>Showing {results.length} {results.length === 1 ? "result" : "results"}.</p>} </div> {searchInput && results.length !== 0 ? ( <ListPackages> {results.map((app, i) => <SingleApp app={app} showDesc={true} preventGlobalSelect={preventGlobalSelect} pack={isPackView} hideBorder={true} key={`${app._id}`} preSelected={alreadySelected.findIndex(a => a._id === app._id) != -1 ? true : false} /> )} </ListPackages> ) : ( <>{searchInput ? <p className={styles.noresults}>Could not find any apps.</p> : ""}</> )} </div> ); } export default Search;