Fix site search (#133)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
20
site/src/lib/useSearch.tsx
Normal file
20
site/src/lib/useSearch.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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}}/>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user