diff --git a/packages/site/.babelrc b/packages/site/.babelrc new file mode 100644 index 0000000..e49a7e6 --- /dev/null +++ b/packages/site/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["next/babel"] +} \ No newline at end of file diff --git a/packages/site/jest.config.js b/packages/site/jest.config.js new file mode 100644 index 0000000..a2c2c22 --- /dev/null +++ b/packages/site/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + testPathIgnorePatterns: ['/.next/', '/node_modules/'], + setupFilesAfterEnv: ['/setupTests.js'], + transform: { + '^.+\\.(js|jsx|ts|tsx)$': '/../../node_modules/babel-jest', + }, +}; diff --git a/packages/site/package.json b/packages/site/package.json index 0071bd3..92fb248 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -7,7 +7,8 @@ "dev": "next dev", "build": "next build", "export": "next export -o build", - "deploy": "yarn build && yarn export" + "deploy": "yarn build && yarn export", + "test": "jest" }, "dependencies": { "@chakra-ui/core": "^0.8.0", @@ -18,15 +19,20 @@ "fuse.js": "^6.0.4", "jszip": "^3.4.0", "next": "^9.4.4", - "query-string": "^6.13.0", "react": "^16.13.1", - "react-dom": "^16.13.1", - "use-query-params": "^1.1.3" + "react-dom": "^16.13.1" }, "devDependencies": { + "@testing-library/dom": "^7.24.4", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.0.4", + "@testing-library/react-hooks": "^3.4.2", "@types/node": "^14.0.11", "@types/react": "^16.9.35", "@types/react-dom": "^16.9.8", + "babel-jest": "^26.5.2", + "jest": "^26.5.2", + "react-test-renderer": "^16.13.1", "typescript": "^3.9.5" } } diff --git a/packages/site/setupTests.js b/packages/site/setupTests.js new file mode 100644 index 0000000..cbe7c3d --- /dev/null +++ b/packages/site/setupTests.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom/extend-expect"; \ No newline at end of file diff --git a/packages/site/src/components/Layout.tsx b/packages/site/src/components/Layout.tsx index f3527e4..c7fd0f6 100644 --- a/packages/site/src/components/Layout.tsx +++ b/packages/site/src/components/Layout.tsx @@ -1,11 +1,17 @@ import { Box, Divider, Flex, Text, Link, Icon, useColorMode } from "@chakra-ui/core"; -import { StringParam, useQueryParam } from "use-query-params"; import { useKeyBindings } from "../lib/key"; +import {useRouter} from "next/router"; const Layout = ({ children }) => { - const [, setQuery] = useQueryParam("query", StringParam); + const router = useRouter(); const { colorMode, toggleColorMode } = useColorMode(); + function setQuery(query){ + router.push({ + pathname: '/', + query: { query: query } + }).then(); + } useKeyBindings({ Escape: { fn: () => setQuery(""), diff --git a/packages/site/src/lib/search.tsx b/packages/site/src/lib/search.tsx index b73f8ae..c84757a 100644 --- a/packages/site/src/lib/search.tsx +++ b/packages/site/src/lib/search.tsx @@ -1,21 +1,31 @@ -import Fuse from "fuse.js"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from 'react'; +import { useDebounce } from './useDebounce'; -function useSearch(icons, query) { - const fuse = new Fuse(Object.values(icons), { - threshold: 0.2, - keys: ["name", "tags"], - }); +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 + ); - const [results, setResults] = useState(Object.values(icons)); + 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(() => { - if (query.trim()) { - setResults(fuse.search(query.trim())); - } else { - setResults(Object.values(icons)); - } - }, [query]); + doSearch().then(setResults); + }, [debouncedQuery]); return results; } diff --git a/packages/site/src/lib/useDebounce.tsx b/packages/site/src/lib/useDebounce.tsx new file mode 100644 index 0000000..f318fcf --- /dev/null +++ b/packages/site/src/lib/useDebounce.tsx @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value]); + + return debouncedValue; +} diff --git a/packages/site/src/pages/_app.tsx b/packages/site/src/pages/_app.tsx index f67ef09..5622dbd 100644 --- a/packages/site/src/pages/_app.tsx +++ b/packages/site/src/pages/_app.tsx @@ -1,30 +1,6 @@ -import { CSSReset, ThemeProvider, ColorModeProvider } from "@chakra-ui/core"; -import { useRouter } from "next/router"; -import { QueryParamProvider } from "use-query-params"; -import customTheme from "../lib/theme"; -import Head from "next/head"; - -const QueryProvider = ({ children }) => { - const router = useRouter(); - - const history = { - push: ({ search }: Location) => - router.push({ search, pathname: router.pathname }), - - replace: ({ search }: Location) => - router.replace({ search, pathname: router.pathname }), - }; - - const location = { - search: router.asPath.replace(/[^?]+/u, ""), - } as Location; - - return ( - - {children} - - ); -}; +import { CSSReset, ThemeProvider, ColorModeProvider } from '@chakra-ui/core'; +import customTheme from '../lib/theme'; +import Head from 'next/head'; const App = ({ Component, pageProps }) => { return ( @@ -32,14 +8,12 @@ const App = ({ Component, pageProps }) => { Featherity - - - - - - - - + + + + + + ); }; diff --git a/packages/site/src/pages/index.tsx b/packages/site/src/pages/index.tsx index 9d0d971..ed4e3b9 100644 --- a/packages/site/src/pages/index.tsx +++ b/packages/site/src/pages/index.tsx @@ -9,15 +9,16 @@ import { Stack, Text, useToast, -} from "@chakra-ui/core"; -import copy from "copy-to-clipboard"; -import download from "downloadjs"; -import JSZip from "jszip"; -import { useEffect, useRef, useState } from "react"; -import { StringParam, useQueryParam } from "use-query-params"; -import Layout from "../components/Layout"; -import { getAllData } from "../lib/icons"; -import useSearch from "../lib/search"; +} from '@chakra-ui/core'; +import copy from 'copy-to-clipboard'; +import download from 'downloadjs'; +import JSZip from 'jszip'; +import { useEffect, useRef, useState } from 'react'; +import Layout from '../components/Layout'; +import { getAllData } from '../lib/icons'; +import useSearch from '../lib/search'; +import { useRouter } from 'next/router'; +import { useDebounce } from '../lib/useDebounce'; function generateZip(icons) { const zip = new JSZip(); @@ -25,25 +26,39 @@ function generateZip(icons) { // @ts-ignore zip.file(`${icon.name}.svg`, icon.src) ); - return zip.generateAsync({ type: "blob" }); + return zip.generateAsync({ type: 'blob' }); } const IndexPage = ({ data }) => { - const [query, setQuery] = useQueryParam("query", StringParam); - const results = useSearch(data, query || ""); + const router = useRouter(); + const { query } = router.query; + const [queryText, setQueryText] = useState(query || ''); const toast = useToast(); + const debouncedQuery = useDebounce(queryText, 1000); + const results = useSearch(data, queryText); + + useEffect(() => { + setQueryText(query); + }, [query]); + + useEffect(() => { + router.push({ + pathname: '/', + query: { query: debouncedQuery }, + }); + }, [debouncedQuery]); const inputElement = useRef(null); function handleKeyDown(event) { - if (event.key === "/" && inputElement.current !== document.activeElement) { + if (event.key === '/' && inputElement.current !== document.activeElement) { event.preventDefault(); inputElement.current.focus(); } } useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); }, []); return ( @@ -56,7 +71,7 @@ const IndexPage = ({ data }) => {