@@ -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",
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
36
src/components/Warn/Warn.tsx
Normal file
36
src/components/Warn/Warn.tsx
Normal 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;
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user