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

@@ -17,7 +17,7 @@
"fuse.js": "^6.0.4", "fuse.js": "^6.0.4",
"jszip": "^3.4.0", "jszip": "^3.4.0",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"lucide-react": "^0.1.2-beta.3", "lucide-react": "^0.1.2-beta.4",
"next": "^9.5.4", "next": "^9.5.4",
"react": "^16.13.1", "react": "^16.13.1",
"react-color": "2.17.3", "react-color": "2.17.3",

View File

@@ -152,11 +152,11 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
/> />
</div> </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"> <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:23 }, (_, i) => ( { Array.from({ length:(size - 1) }, (_, i) => (
<g key={`grid-${i}`}> <g key={`grid-${i}`}>
<line key={`horizontal-${i}`} x1={0} y1={i + 1} x2={24} y2={i + 1} /> <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={24} /> <line key={`vertical-${i}`} x1={i + 1} y1={0} x2={i + 1} y2={size} />
</g> </g>
)) } )) }
</svg> </svg>

View File

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

View File

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

View File

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

View File

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

View File

@@ -116,6 +116,15 @@
jsesc "^2.5.1" jsesc "^2.5.1"
source-map "^0.5.0" source-map "^0.5.0"
"@babel/generator@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de"
integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==
dependencies:
"@babel/types" "^7.12.5"
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.7.7": "@babel/generator@^7.7.7":
version "7.11.6" version "7.11.6"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
@@ -300,6 +309,16 @@
"@babel/helper-wrap-function" "^7.10.4" "@babel/helper-wrap-function" "^7.10.4"
"@babel/types" "^7.12.1" "@babel/types" "^7.12.1"
"@babel/helper-replace-supers@^7.10.4":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9"
integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.12.1"
"@babel/helper-optimise-call-expression" "^7.10.4"
"@babel/traverse" "^7.12.5"
"@babel/types" "^7.12.5"
"@babel/helper-replace-supers@^7.12.1": "@babel/helper-replace-supers@^7.12.1":
version "7.12.1" version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.1.tgz#f15c9cc897439281891e11d5ce12562ac0cf3fa9" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.1.tgz#f15c9cc897439281891e11d5ce12562ac0cf3fa9"
@@ -381,6 +400,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd"
integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw== integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==
"@babel/parser@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0"
integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==
"@babel/plugin-proposal-async-generator-functions@^7.10.4": "@babel/plugin-proposal-async-generator-functions@^7.10.4":
version "7.12.1" version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e"
@@ -1132,6 +1156,21 @@
globals "^11.1.0" globals "^11.1.0"
lodash "^4.17.19" lodash "^4.17.19"
"@babel/traverse@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095"
integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/generator" "^7.12.5"
"@babel/helper-function-name" "^7.10.4"
"@babel/helper-split-export-declaration" "^7.11.0"
"@babel/parser" "^7.12.5"
"@babel/types" "^7.12.5"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.19"
"@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4": "@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
version "7.11.5" version "7.11.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
@@ -1159,6 +1198,15 @@
lodash "^4.17.19" lodash "^4.17.19"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@babel/types@^7.12.5":
version "7.12.6"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96"
integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3": "@bcoe/v8-coverage@^0.2.3":
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -5878,10 +5926,10 @@ lru-cache@^5.1.1:
dependencies: dependencies:
yallist "^3.0.2" yallist "^3.0.2"
lucide-react@^0.1.2-beta.3: lucide-react@^0.1.2-beta.4:
version "0.1.2-beta.3" version "0.1.2-beta.4"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.1.2-beta.3.tgz#b42f46876fcf552bc2f739cca0ade3f7a784bff5" resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.1.2-beta.4.tgz#2ac78c788ceb201d80562b64b4317b3fab70e4a0"
integrity sha512-qa8jR2qwoN3cGu/cli6Xov5qI/+U1sHkQF1EyzVyuLYl6RgOtAQLmRWK014H44pG+pB+0xHuWjqen89khKVqyQ== integrity sha512-8EJZ8widhd2gv6FVL5H/8nDT8lr0lHFJi9ePKfGFHXKfESV2ICmQ7FHUnQMSDSYSzbvfe1AkaI/KYfCfu34LAQ==
dependencies: dependencies:
prop-types "^15.7.2" prop-types "^15.7.2"
react "^16.14.0" react "^16.14.0"