Fix site search (#133)

This commit is contained in:
Eric Fennis
2020-11-16 12:05:34 +01:00
committed by GitHub
parent 0dd10483c9
commit 8f1c7eb737
10 changed files with 165 additions and 72 deletions

View File

@@ -152,11 +152,11 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
/>
</div>
<svg className="icon-grid" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={colorMode == "light" ? '#E2E8F0' : theme.colors.gray[600]} strokeWidth="0.1" xmlns="http://www.w3.org/2000/svg">
{ Array.from({ length:23 }, (_, i) => (
<svg className="icon-grid" width="24" height="24" viewBox={`0 0 ${size} ${size}`} fill="none" stroke={colorMode == "light" ? '#E2E8F0' : theme.colors.gray[600]} strokeWidth="0.1" xmlns="http://www.w3.org/2000/svg">
{ Array.from({ length:(size - 1) }, (_, i) => (
<g key={`grid-${i}`}>
<line key={`horizontal-${i}`} x1={0} y1={i + 1} x2={24} y2={i + 1} />
<line key={`vertical-${i}`} x1={i + 1} y1={0} x2={i + 1} y2={24} />
<line key={`horizontal-${i}`} x1={0} y1={i + 1} x2={size} y2={i + 1} />
<line key={`vertical-${i}`} x1={i + 1} y1={0} x2={i + 1} y2={size} />
</g>
)) }
</svg>

View File

@@ -2,13 +2,18 @@ import { Button, Flex, Grid, Text, useToast } from "@chakra-ui/core";
import download from 'downloadjs';
import Link from 'next/link'
import copy from "copy-to-clipboard";
import {useContext} from "react";
import {useContext, useMemo} from "react";
import {IconStyleContext} from "./CustomizeIconContext";
import {IconWrapper} from "./IconWrapper";
import { useRouter } from "next/router";
const IconList = ({icons}) => {
const router = useRouter()
const toast = useToast();
const {color, size, strokeWidth} = useContext(IconStyleContext);
const { search } = router.query;
const query = useMemo(()=> search !== undefined ? { search } : {},[search])
return (
<Grid
@@ -17,12 +22,21 @@ const IconList = ({icons}) => {
marginBottom="320px"
>
{ icons.map((icon) => {
// @ts-ignore
const actualIcon = icon.item ? icon.item : icon;
const { name, content } = actualIcon;
return (
<Link key={name} href={`/?iconName=${name}`} as={`/icon/${name}`} scroll={false}>
<Link
key={name}
scroll={false}
href={{
pathname: '/icon/[iconName]',
query: {
...query,
iconName: name,
},
}}
>
<Button
variant="ghost"
borderWidth="1px"

View File

@@ -8,19 +8,20 @@ import {
Icon,
} from '@chakra-ui/core';
import IconList from './IconList';
import { useEffect, useRef, useState } from 'react';
import useSearch from '../lib/search';
import { useEffect, useMemo, useRef, useState } from 'react';
import useSearch from '../lib/useSearch';
import { useRouter } from 'next/router';
import { useDebounce } from '../lib/useDebounce';
import theme from '../lib/theme';
import { Search as SearchIcon } from 'lucide-react';
import debounce from 'lodash/debounce';
const isFilledString = (string) => string !== undefined && string !== null && string !== '';
const IconOverview = ({ data }) => {
const router = useRouter();
const { query } = router.query;
const [queryText, setQueryText] = useState(query || '');
const debouncedQuery = useDebounce(queryText, 1000);
const results = useSearch(data, queryText);
const { search } = router.query;
const [queryText, setQueryText] = useState(search);
const { colorMode } = useColorMode();
const inputElement = useRef(null);
@@ -32,20 +33,47 @@ const IconOverview = ({ data }) => {
}
}
useEffect(() => {
setQueryText(query || '');
}, [query]);
const setQueryParam = (searchString) => {
const { query, asPath } = router;
if(isFilledString(searchString)) {
let route = {
pathname: '',
query
}
if(query.iconName) {
route.query.iconName = query.iconName;
route.pathname = '/icon/[iconName]';
}
route.query.search = searchString;
router.replace(route);
}
else {
if (query?.search) {
delete query.search;
router.replace({
query
})
}
}
}
// @ts-ignore
const searchResults = useMemo(() => useSearch(data, queryText), [data, queryText])
const handleSearchInput = debounce((event) => {
event.persist();
const { value = '' } = inputElement?.current;
setQueryText(value)
setQueryParam(value)
}, 400)
useEffect(() => {
const { query } = router;
router.push({
query: {
...query,
query: debouncedQuery,
},
});
}, [debouncedQuery]);
setQueryText(search)
}, [search]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
@@ -65,14 +93,14 @@ const IconOverview = ({ data }) => {
<Input
ref={inputElement}
placeholder={`Search ${Object.keys(data).length} icons (Press "/" to focus)`}
value={queryText}
onChange={(event) => setQueryText(event.target.value)}
onChange={handleSearchInput}
defaultValue={queryText}
bg={colorMode == 'light' ? theme.colors.white : theme.colors.gray[700]}
/>
</InputGroup>
<Box marginTop={5}>
{results.length > 0 ? (
<IconList icons={results} />
{searchResults.length > 0 ? (
<IconList icons={searchResults} />
) : (
<Text
fontSize="2xl"
@@ -80,7 +108,7 @@ const IconOverview = ({ data }) => {
textAlign="center"
style={{ wordBreak: 'break-word' }}
>
No results found for "{query}"
No results found for "{queryText}"
</Text>
)}
</Box>

View File

@@ -1,33 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { useDebounce } from './useDebounce';
function useSearch(icons: Object, query: string | string[]) {
let iconList = useMemo(() => Object.values(icons), [icons]);
const [results, setResults] = useState(iconList);
// query can be an array because this is a valid query string ?query=xyz&query=abc
const debouncedQuery = useDebounce(
typeof query === 'string' ? query.trim() : typeof query === 'undefined' ? '' : query[0].trim(),
300
);
async function doSearch() {
if (debouncedQuery) {
const Fuse = (await import('fuse.js')).default;
const fuse = new Fuse(iconList, {
threshold: 0.2,
keys: ['name', 'tags'],
});
return fuse.search(debouncedQuery);
} else {
return iconList;
}
}
useEffect(() => {
doSearch().then(setResults);
}, [debouncedQuery]);
return results;
}
export default useSearch;

View File

@@ -0,0 +1,20 @@
import { useEffect, useMemo, useState } from 'react';
import { useDebounce } from './useDebounce';
function useSearch(icons: Array<any>, query:string) {
if(!query) return icons;
const searchString = query.toLowerCase()
return icons.filter(({ name, tags }) => {
const icon = { name, tags };
return Object.keys(icon).some(
key => String(icon[key])
.toLowerCase()
.includes(searchString)
)
});
}
export default useSearch;

View File

@@ -1,4 +1,4 @@
import { CSSReset, ChakraProvider, ColorModeProvider } from '@chakra-ui/core';
import { ChakraProvider } from '@chakra-ui/core';
import customTheme from '../lib/theme';
import '../assets/styling.css';
import Head from 'next/head';

View File

@@ -9,11 +9,27 @@ import Header from '../../components/Header';
const IconPage = ({ icon, data }) => {
const router = useRouter()
const onClose = () => {
let query = {};
if(router.query.search) {
query = {
search: router.query.search
};
}
router.push({
pathname: '/',
query,
})
}
return (
<Layout>
<IconDetailOverlay
key={icon.name}
icon={icon}
onClose={() => router.push('/')}
onClose={onClose}
/>
<Header {...{data}}/>
<IconOverview {...{data}}/>

View File

@@ -1,6 +1,6 @@
import { getAllData } from '../lib/icons';
import { renderHook } from '@testing-library/react-hooks';
import useSearch from '../lib/search';
import useSearch from '../lib/useSearch';
describe('Icon Overview', () => {
it('can search filter icons', async () => {