Merge pull request #5 from phosphor-icons/fuzzy-search

Fuzzy search
This commit is contained in:
Tobias Fried
2020-09-15 01:16:04 -04:00
committed by GitHub
6 changed files with 62 additions and 44 deletions

View File

@@ -23,6 +23,7 @@
"dependencies": {
"file-saver": "^2.0.2",
"framer-motion": "^2.1.0",
"fuse.js": "^6.4.1",
"phosphor-react": "^0.2.2",
"react": "^17.0.0-rc.0",
"react-dom": "^17.0.0-rc.0",

View File

@@ -5,6 +5,8 @@ import Header from "../Header/Header";
import Toolbar from "../Toolbar/Toolbar";
import IconGrid from "../IconGrid/IconGrid";
import Footer from "../Footer/Footer";
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
import Warn from "../Warn/Warn";
const App: React.FC<any> = () => {
return (
@@ -12,9 +14,11 @@ const App: React.FC<any> = () => {
<Header />
<main>
<Toolbar />
<ErrorBoundary fallback={<Warn message="Search error"/>}>
<Suspense fallback={<div>Loading...</div>}>
<IconGrid />
</Suspense>
</ErrorBoundary>
</main>
<Footer />
</>

View File

@@ -2,19 +2,15 @@ import React, { useRef, useEffect } from "react";
import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion";
import { useWindowSize } from "react-use";
import { IconContext, Warning } from "phosphor-react";
import { IconContext } from "phosphor-react";
import {
iconStyleAtom,
iconSizeAtom,
iconColorAtom,
searchQueryAtom,
} from "../../state/atoms";
import { iconStyleAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
import {
filteredQueryResultsSelector,
isDarkThemeSelector,
} from "../../state/selectors";
import GridItem from "./IconGridItem";
import Warn from "../Warn/Warn";
import "./IconGrid.css";
type IconGridProps = {};
@@ -23,7 +19,6 @@ const IconGrid: React.FC<IconGridProps> = () => {
const weight = useRecoilValue(iconStyleAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const query = useRecoilValue(searchQueryAtom);
const isDark = useRecoilValue(isDarkThemeSelector);
const { width } = useWindowSize();
@@ -38,22 +33,7 @@ const IconGrid: React.FC<IconGridProps> = () => {
controls.start("visible");
}, [controls, filteredQueryResults]);
if (!filteredQueryResults.length)
return (
<div style={isDark ? { backgroundColor: "#35313D", color: "white" } : {}}>
<motion.div
className="empty-list"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<Warning size={128} color="currentColor" weight="fill" />
<p>
No results for '<code>{query}</code>'
</p>
</motion.div>
</div>
);
if (!filteredQueryResults.length) return <Warn />;
return (
<IconContext.Provider value={{ weight, size, color, mirrored: false }}>

View File

@@ -0,0 +1,36 @@
import React from "react";
import { motion } from "framer-motion";
import { useRecoilValue } from "recoil";
import { isDarkThemeSelector } from "../../state/selectors";
import { searchQueryAtom } from "../../state/atoms";
import { Warning } from "phosphor-react";
interface WarnProps {
message?: string;
}
const Warn: React.FC<WarnProps> = ({ message }) => {
const isDark = useRecoilValue(isDarkThemeSelector);
const query = useRecoilValue(searchQueryAtom);
return (
<div style={isDark ? { backgroundColor: "#35313D", color: "white" } : {}}>
<motion.div
className="empty-list"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<Warning size={128} color="currentColor" weight="fill" />
{message ?? (
<p>
No results for '<code>{query}</code>'
</p>
)}
</motion.div>
</div>
);
};
export default Warn;

View File

@@ -3086,13 +3086,13 @@ export const icons: Readonly<IconEntry[]> = [
{
name: "trash",
categories: [IconCategory.DOCUMENT, IconCategory.SYSTEM],
tags: ["garbage", "delete", "destroy", "recycle", "recycling"],
tags: ["garbage", "remove", "delete", "destroy", "recycle", "recycling"],
Icon: Icon.Trash,
},
{
name: "trash-handle",
categories: [IconCategory.DOCUMENT, IconCategory.SYSTEM],
tags: ["garbage", "delete", "destroy", "recycle", "recycling"],
tags: ["garbage", "remove", "delete", "destroy", "recycle", "recycling"],
Icon: Icon.TrashHandle,
},
{

View File

@@ -1,28 +1,25 @@
import { selector, selectorFamily } from "recoil";
import TinyColor from "tinycolor2";
import Fuse from "fuse.js";
import { searchQueryAtom, iconStyleAtom, iconColorAtom } from "./atoms";
import { searchQueryAtom, iconColorAtom } from "./atoms";
import { IconEntry, IconCategory } from "../lib";
import { icons } from "../lib/icons";
const isQueryMatch = (icon: IconEntry, query: string): boolean => {
return (
icon.name.includes(query) ||
icon.tags.some((tag) => tag.toLowerCase().includes(query)) ||
icon.categories.some((category) => category.toLowerCase().includes(query))
);
};
const fuse = new Fuse(icons, {
keys: [{ name: "name", weight: 2 }, "tags", "categories"],
threshold: 0.2, // Tweak this to what feels like the right number of results
// shouldSort: false, // We may want to sort if we find too many results?
});
export const filteredQueryResultsSelector = selector<Readonly<IconEntry[]>>({
key: "filteredQueryResultsSelector",
get: async ({ get }) => {
get: ({ get }) => {
const query = get(searchQueryAtom).trim().toLowerCase();
const style = get(iconStyleAtom);
if (!query && !style) return icons;
if (!query) return icons;
return new Promise((resolve) =>
resolve(icons.filter((icon) => isQueryMatch(icon, query)))
resolve(fuse.search(query).map((value) => value.item))
);
},
});
@@ -33,7 +30,7 @@ export const categorizedQueryResultsSelector = selector<
Readonly<CategorizedIcons>
>({
key: "categorizedQueryResultsSelector",
get: async ({ get }) => {
get: ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
resolve(
@@ -54,7 +51,7 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
IconCategory
>({
key: "singleCategoryQueryResultsSelector",
get: (category: IconCategory) => async ({ get }) => {
get: (category: IconCategory) => ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
resolve(