Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
762cf32666 | ||
|
|
216f42cdcb | ||
|
|
b80c2805b2 | ||
|
|
d80a267c81 | ||
|
|
07fa908631 | ||
|
|
1bdf6febac | ||
|
|
78e1057515 | ||
|
|
9d6d0c340d | ||
|
|
77f3f49ce7 | ||
|
|
a55620d6ba | ||
|
|
dad9648e20 | ||
|
|
b3b39afb95 |
2
.gitignore
vendored
@@ -4,7 +4,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
lib
|
/lib
|
||||||
sandbox
|
sandbox
|
||||||
stash
|
stash
|
||||||
coverage
|
coverage
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ stats
|
|||||||
node_modules
|
node_modules
|
||||||
tests
|
tests
|
||||||
scripts
|
scripts
|
||||||
|
site
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
Lucide is a community-run fork of [Feather Icons](https://github.com/feathericons/feather), open for anyone to contribute icons.
|
Lucide is a community-run fork of [Feather Icons](https://github.com/feathericons/feather), open for anyone to contribute icons.
|
||||||
|
|
||||||
Note that we are completely independent from Feather, so **icons submitted here won't get added to Feather Icons or its associated librairies**.
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Usage](#usage)
|
* [Usage](#usage)
|
||||||
|
|||||||
37
docs/INKSCAPE_GUIDE.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Inkscape Setup Guide
|
||||||
|
|
||||||
|
This guide shows the steps to setup Inkscape for creating icons that conform to the Featherity design
|
||||||
|
guidelines.
|
||||||
|
|
||||||
|
## Setting up The Canvas
|
||||||
|
|
||||||
|
When opening a new document, Inkscape will create a canvas of a default size. To change the size to 24x24:
|
||||||
|
|
||||||
|
1. Open the Document Properties dialog (File -> Document Properties).
|
||||||
|
2. On the “Page Size” tab, under “Custom Size” set the Units to `px` and set both Height and Width to 24.
|
||||||
|

|
||||||
|
3. On the “Grid” tab, select `Rectangular Grid` and click “New Grid”.
|
||||||
|

|
||||||
|
4. Set the Grid Units to `px` and set Spacing X and Spacing Y both to 1.
|
||||||
|

|
||||||
|
5. Close the Document Properties dialog.
|
||||||
|
6. To center the canvas in the viewport, select View -> Zoom -> Drawing.
|
||||||
|
|
||||||
|
## Setting up The Paths
|
||||||
|
|
||||||
|
1. Create a path or shape.
|
||||||
|
2. With the path selected, open the Stroke and Fill panel by pressing `Ctrl+Shift+F` on your keyboard.
|
||||||
|

|
||||||
|
3. On the “Stroke Style” tab:
|
||||||
|
* Set Stroke Width to `2px`.
|
||||||
|
* Select the rounded join type.
|
||||||
|
* Select the rounded cap type.
|
||||||
|
4. If the shape is a rectangle, select the rectangle and in the top of the screen below the menu bar, set `Rx` and `Ry` to `2px`.
|
||||||
|

|
||||||
|
|
||||||
|
## Saving A File
|
||||||
|
|
||||||
|
1. When ready to save the file, click Save As and select “Optimized SVG” as the file type.
|
||||||
|

|
||||||
|
2. After clicking Save, to conform with the other icons in the package, set Pretty Printing to use spaces and set the indentation depth to 2.
|
||||||
|

|
||||||
BIN
docs/images/corner-radius.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
docs/images/grid-1.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/images/grid-2.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/images/optimize-settings.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/images/page-size.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/images/save-as.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/images/strokes.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
23
icons/building.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="4" y="2" width="16" height="20" rx="2" ry="2" />
|
||||||
|
<path d="M9 22v-4h6v4" />
|
||||||
|
<path d="M8 6h.01" />
|
||||||
|
<path d="M16 6h.01" />
|
||||||
|
<path d="M12 6h.01" />
|
||||||
|
<path d="M12 10h.01" />
|
||||||
|
<path d="M12 14h.01" />
|
||||||
|
<path d="M16 10h.01" />
|
||||||
|
<path d="M16 14h.01" />
|
||||||
|
<path d="M8 10h.01" />
|
||||||
|
<path d="M8 14h.01" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 524 B |
13
icons/laptop.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 16V7a2 2 0 00-2-2H6a2 2 0 00-2 2v9m16 0H4m16 0l1.28 2.55a1 1 0 01-.9 1.45H3.62a1 1 0 01-.9-1.45L4 16" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 364 B |
17
icons/lasso-select.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M7 22a5 5 0 01-2-4" />
|
||||||
|
<path d="M7 16.93c.96.43 1.96.74 2.99.91" />
|
||||||
|
<path d="M3.34 14A6.8 6.8 0 012 10c0-4.42 4.48-8 10-8s10 3.58 10 8a7.19 7.19 0 01-.33 2" />
|
||||||
|
<path d="M5 18a2 2 0 100-4 2 2 0 000 4z" />
|
||||||
|
<path d="M14.33 22h-.09a.35.35 0 01-.24-.32v-10a.34.34 0 01.33-.34c.08 0 .15.03.21.08l7.34 6a.33.33 0 01-.21.59h-4.49l-2.57 3.85a.35.35 0 01-.28.14v0z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 588 B |
15
icons/lasso.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M7 22a5 5 0 01-2-4" />
|
||||||
|
<path d="M3.3 14A6.8 6.8 0 012 10c0-4.4 4.5-8 10-8s10 3.6 10 8-4.5 8-10 8a12 12 0 01-5-1" />
|
||||||
|
<path d="M5 18a2 2 0 100-4 2 2 0 000 4z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
18
icons/list-checks.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="10" y1="6" x2="21" y2="6" />
|
||||||
|
<line x1="10" y1="12" x2="21" y2="12" />
|
||||||
|
<line x1="10" y1="18" x2="21" y2="18" />
|
||||||
|
<polyline points="3 6 4 7 6 5" />
|
||||||
|
<polyline points="3 12 4 13 6 11" />
|
||||||
|
<polyline points="3 18 4 19 6 17" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 449 B |
18
icons/list-ordered.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="10" y1="6" x2="21" y2="6" />
|
||||||
|
<line x1="10" y1="12" x2="21" y2="12" />
|
||||||
|
<line x1="10" y1="18" x2="21" y2="18" />
|
||||||
|
<path d="M4 6H5V10" />
|
||||||
|
<path d="M4 10H6" />
|
||||||
|
<path d="M6 18H4C4 17 6 16 6 15C6 13.9999 5 13.5 4 14" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 443 B |
4
netlify.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[build]
|
||||||
|
base = "site/"
|
||||||
|
publish = "build/"
|
||||||
|
command = "yarn deploy"
|
||||||
13
package.json
@@ -1,27 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "lucide",
|
"name": "lucide",
|
||||||
"description": "Lucide is a community-run fork of Feather Icons, open for anyone to contribute icons.",
|
"description": "Lucide is a community-run fork of Feather Icons, open for anyone to contribute icons.",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"amdName": "lucide",
|
"amdName": "lucide",
|
||||||
"homepage": "https://featherity.netlify.app",
|
"homepage": "https://lucide.netlify.app",
|
||||||
"url": "https://github.com/owner/project/issues",
|
"url": "https://github.com/owner/project/issues",
|
||||||
"repository": "github:lucide-icons/lucide",
|
"repository": "github:lucide-icons/lucide",
|
||||||
"source": "build/lucide.js",
|
"source": "build/lucide.js",
|
||||||
"main": "dist/cjs/lucide.js",
|
"main": "dist/cjs/lucide.js",
|
||||||
"main:umd": "dist/umd/lucide.js",
|
"main:umd": "dist/umd/lucide.js",
|
||||||
"module": "lib/lucide.js",
|
"module": "dist/esm/lucide.js",
|
||||||
"unpkg": "dist/umd/lucide.min.js",
|
"unpkg": "dist/umd/lucide.min.js",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "babel-watch --watch src",
|
"start": "babel-watch --watch src",
|
||||||
"clean": "rimraf lib && rimraf dist && rimraf build",
|
"clean": "rimraf lib && rimraf dist && rimraf build",
|
||||||
"build": "yarn clean && yarn build:move && yarn build:icons && yarn build:es && yarn build:esbrowser && yarn build:bundles",
|
"build": "yarn clean && yarn build:move && yarn build:icons && yarn build:es && yarn build:bundles",
|
||||||
"build:move": "cp -av src build",
|
"build:move": "cp -av src build",
|
||||||
"build:icons": "npx babel-node ./scripts/buildIcons.js --presets @babel/env",
|
"build:icons": "npx babel-node ./scripts/buildIcons.js --presets @babel/env",
|
||||||
"build:es": "babel build -d lib --source-maps --ignore '**/*.test.js','**/__mocks__'",
|
"build:es": "babel build -d dist/esm --ignore '**/*.test.js','**/__mocks__'",
|
||||||
"build:esbrowser": "BROWSER_COMPAT=true yarn build:es -d dist/esm",
|
"build:bundles": "rollup -c rollup.config.js",
|
||||||
"build:bundles": "BROWSER_COMPAT=true rollup -c rollup.config.js",
|
|
||||||
"optimize": "npx babel-node ./scripts/optimizeSvgs.js --presets @babel/env",
|
"optimize": "npx babel-node ./scripts/optimizeSvgs.js --presets @babel/env",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import Fuse from "fuse.js";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
function useSearch(icons, query) {
|
|
||||||
const fuse = new Fuse(Object.values(icons), {
|
|
||||||
threshold: 0.2,
|
|
||||||
keys: ["name", "tags"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [results, setResults] = useState(Object.values(icons));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (query.trim()) {
|
|
||||||
setResults(fuse.search(query.trim()));
|
|
||||||
} else {
|
|
||||||
setResults(Object.values(icons));
|
|
||||||
}
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useSearch;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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 (
|
|
||||||
<QueryParamProvider history={history} location={location}>
|
|
||||||
{children}
|
|
||||||
</QueryParamProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const App = ({ Component, pageProps }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Featherity</title>
|
|
||||||
</Head>
|
|
||||||
<QueryProvider>
|
|
||||||
<ThemeProvider theme={customTheme}>
|
|
||||||
<ColorModeProvider>
|
|
||||||
<CSSReset />
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</ColorModeProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</QueryProvider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
3
site/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"presets": ["next/babel"]
|
||||||
|
}
|
||||||
7
site/jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/../../node_modules/babel-jest',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -6,8 +6,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"export": "next export",
|
"export": "next export -o build",
|
||||||
"deploy": "yarn build && yarn export"
|
"deploy": "yarn build && yarn export",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/core": "^0.8.0",
|
"@chakra-ui/core": "^0.8.0",
|
||||||
@@ -17,16 +18,21 @@
|
|||||||
"emotion-theming": "^10.0.27",
|
"emotion-theming": "^10.0.27",
|
||||||
"fuse.js": "^6.0.4",
|
"fuse.js": "^6.0.4",
|
||||||
"jszip": "^3.4.0",
|
"jszip": "^3.4.0",
|
||||||
"next": "^9.4.4",
|
"next": "^9.5.4",
|
||||||
"query-string": "^6.13.0",
|
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1"
|
||||||
"use-query-params": "^1.1.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/node": "^14.0.11",
|
||||||
"@types/react": "^16.9.35",
|
"@types/react": "^16.9.35",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@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"
|
"typescript": "^3.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
site/setupTests.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
import { Box, Divider, Flex, Text, Link, Icon, useColorMode } from "@chakra-ui/core";
|
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 { useKeyBindings } from "../lib/key";
|
||||||
|
import {useRouter} from "next/router";
|
||||||
|
|
||||||
const Layout = ({ children }) => {
|
const Layout = ({ children }) => {
|
||||||
const [, setQuery] = useQueryParam("query", StringParam);
|
const router = useRouter();
|
||||||
const { colorMode, toggleColorMode } = useColorMode();
|
const { colorMode, toggleColorMode } = useColorMode();
|
||||||
|
|
||||||
|
function setQuery(query){
|
||||||
|
router.push({
|
||||||
|
pathname: '/',
|
||||||
|
query: { query: query }
|
||||||
|
}).then();
|
||||||
|
}
|
||||||
useKeyBindings({
|
useKeyBindings({
|
||||||
Escape: {
|
Escape: {
|
||||||
fn: () => setQuery(""),
|
fn: () => setQuery(""),
|
||||||
@@ -34,7 +40,7 @@ const Layout = ({ children }) => {
|
|||||||
onClick={() => setQuery("")}
|
onClick={() => setQuery("")}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
Featherity
|
Lucide
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justifyContent="center" alignItems="center">
|
<Flex justifyContent="center" alignItems="center">
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import tags from '../../../../tags.json';
|
import tags from '../../../tags.json';
|
||||||
|
|
||||||
const directory = path.join(process.cwd(), "../../icons");
|
const directory = path.join(process.cwd(), "../icons");
|
||||||
|
|
||||||
export function getAllNames() {
|
export function getAllNames() {
|
||||||
const fileNames = fs.readdirSync(directory);
|
const fileNames = fs.readdirSync(directory);
|
||||||
33
site/src/lib/search.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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;
|
||||||
17
site/src/lib/useDebounce.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
21
site/src/pages/_app.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { CSSReset, ThemeProvider, ColorModeProvider } from '@chakra-ui/core';
|
||||||
|
import customTheme from '../lib/theme';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
const App = ({ Component, pageProps }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Lucide</title>
|
||||||
|
</Head>
|
||||||
|
<ThemeProvider theme={customTheme}>
|
||||||
|
<ColorModeProvider>
|
||||||
|
<CSSReset />
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</ColorModeProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
|
Link,
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
@@ -9,15 +10,16 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/core";
|
} from '@chakra-ui/core';
|
||||||
import copy from "copy-to-clipboard";
|
import copy from 'copy-to-clipboard';
|
||||||
import download from "downloadjs";
|
import download from 'downloadjs';
|
||||||
import JSZip from "jszip";
|
import JSZip from 'jszip';
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { StringParam, useQueryParam } from "use-query-params";
|
import Layout from '../components/Layout';
|
||||||
import Layout from "../components/Layout";
|
import { getAllData } from '../lib/icons';
|
||||||
import { getAllData } from "../lib/icons";
|
import useSearch from '../lib/search';
|
||||||
import useSearch from "../lib/search";
|
import { useRouter } from 'next/router';
|
||||||
|
import { useDebounce } from '../lib/useDebounce';
|
||||||
|
|
||||||
function generateZip(icons) {
|
function generateZip(icons) {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
@@ -25,38 +27,55 @@ function generateZip(icons) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
zip.file(`${icon.name}.svg`, icon.src)
|
zip.file(`${icon.name}.svg`, icon.src)
|
||||||
);
|
);
|
||||||
return zip.generateAsync({ type: "blob" });
|
return zip.generateAsync({ type: 'blob' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const IndexPage = ({ data }) => {
|
const IndexPage = ({ data }) => {
|
||||||
const [query, setQuery] = useQueryParam("query", StringParam);
|
const router = useRouter();
|
||||||
const results = useSearch(data, query || "");
|
const { query } = router.query;
|
||||||
|
const [queryText, setQueryText] = useState(query || '');
|
||||||
const toast = useToast();
|
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);
|
const inputElement = useRef(null);
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.key === "/" && inputElement.current !== document.activeElement) {
|
if (event.key === '/' && inputElement.current !== document.activeElement) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
inputElement.current.focus();
|
inputElement.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Flex direction="column" align="center" justify="center">
|
<Flex direction="column" align="center" justify="center">
|
||||||
<Text fontSize="3xl" as="b">
|
<Text fontSize="3xl" as="b" mb="4">
|
||||||
Simply beautiful open source icons, community-sourced
|
Simply beautiful open source icons, community-sourced
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text fontSize="lg" as="p" textAlign="center" mb="8">
|
||||||
|
An open-source icon library, a fork of Feather Icons. <br/>We're expanding the icon set as much as possible while keeping it nice-looking - <Link href="https://github.com/lucide-icons/lucide" isExternal>join us</Link>!
|
||||||
|
</Text>
|
||||||
<Stack isInline marginTop={3} marginBottom={10}>
|
<Stack isInline marginTop={3} marginBottom={10}>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const zip = await generateZip(data);
|
const zip = await generateZip(data);
|
||||||
download(zip, "feather.zip");
|
download(zip, 'feather.zip');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download all
|
Download all
|
||||||
@@ -67,19 +86,14 @@ const IndexPage = ({ data }) => {
|
|||||||
<InputLeftElement children={<Icon name="search" />} />
|
<InputLeftElement children={<Icon name="search" />} />
|
||||||
<Input
|
<Input
|
||||||
ref={inputElement}
|
ref={inputElement}
|
||||||
placeholder={`Search ${
|
placeholder={`Search ${Object.keys(data).length} icons (Press "/" to focus)`}
|
||||||
Object.keys(data).length
|
value={queryText}
|
||||||
} icons (Press "/" to focus)`}
|
onChange={(event) => setQueryText(event.target.value)}
|
||||||
value={query}
|
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
|
||||||
marginBottom={5}
|
marginBottom={5}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{results.length > 0 ? (
|
{results.length > 0 ? (
|
||||||
<Grid
|
<Grid templateColumns={`repeat(auto-fill, minmax(160px, 1fr))`} gap={5}>
|
||||||
templateColumns={`repeat(auto-fill, minmax(160px, 1fr))`}
|
|
||||||
gap={5}
|
|
||||||
>
|
|
||||||
{results.map((icon) => {
|
{results.map((icon) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const actualIcon = icon.item ? icon.item : icon;
|
const actualIcon = icon.item ? icon.item : icon;
|
||||||
@@ -93,17 +107,13 @@ const IndexPage = ({ data }) => {
|
|||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
copy(actualIcon.src);
|
copy(actualIcon.src);
|
||||||
toast({
|
toast({
|
||||||
title: "Copied!",
|
title: 'Copied!',
|
||||||
description: `Icon "${actualIcon.name}" copied to clipboard.`,
|
description: `Icon "${actualIcon.name}" copied to clipboard.`,
|
||||||
status: "success",
|
status: 'success',
|
||||||
duration: 1500,
|
duration: 1500,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
download(
|
download(actualIcon.src, `${actualIcon.name}.svg`, 'image/svg+xml');
|
||||||
actualIcon.src,
|
|
||||||
`${actualIcon.name}.svg`,
|
|
||||||
"image/svg+xml"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
key={actualIcon.name}
|
key={actualIcon.name}
|
||||||
@@ -122,7 +132,7 @@ const IndexPage = ({ data }) => {
|
|||||||
fontSize="2xl"
|
fontSize="2xl"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
style={{ wordBreak: "break-word" }}
|
style={{ wordBreak: 'break-word' }}
|
||||||
>
|
>
|
||||||
No results found for "{query}"
|
No results found for "{query}"
|
||||||
</Text>
|
</Text>
|
||||||
30
site/src/tests/index.test.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { act, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import Index from '../pages/index';
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from './test-utils';
|
||||||
|
import { getAllData } from '../lib/icons';
|
||||||
|
import App from '../pages/_app';
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import useSearch from '../lib/search';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
let allData = getAllData();
|
||||||
|
render(<App Component={Index} pageProps={{ data: allData }} />);
|
||||||
|
expect(
|
||||||
|
screen.getByText('Simply beautiful open source icons, community-sourced')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('can search filter icons', async () => {
|
||||||
|
let allData = getAllData();
|
||||||
|
|
||||||
|
const { result: result1, waitForNextUpdate: wait1 } = renderHook(() => useSearch(allData, ''));
|
||||||
|
expect(result1.current).toHaveLength(allData.length);
|
||||||
|
|
||||||
|
const { result: result2, waitForNextUpdate: wait2 } = renderHook(() =>
|
||||||
|
useSearch(allData, 'airplay')
|
||||||
|
);
|
||||||
|
await wait2();
|
||||||
|
expect(result2.current).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
51
site/src/tests/test-utils.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render as defaultRender } from '@testing-library/react';
|
||||||
|
import { RouterContext } from 'next/dist/next-server/lib/router-context';
|
||||||
|
import { NextRouter } from 'next/router';
|
||||||
|
|
||||||
|
export * from '@testing-library/react';
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Override the default test render with our own
|
||||||
|
//
|
||||||
|
// You can override the router mock like this:
|
||||||
|
//
|
||||||
|
// const { baseElement } = render(<MyComponent />, {
|
||||||
|
// router: { pathname: '/my-custom-pathname' },
|
||||||
|
// });
|
||||||
|
// --------------------------------------------------
|
||||||
|
type DefaultParams = Parameters<typeof defaultRender>;
|
||||||
|
type RenderUI = DefaultParams[0];
|
||||||
|
type RenderOptions = DefaultParams[1] & { router?: Partial<NextRouter> };
|
||||||
|
|
||||||
|
export function render(ui: RenderUI, { wrapper, router, ...options }: RenderOptions = {}) {
|
||||||
|
if (!wrapper) {
|
||||||
|
wrapper = ({ children }) => (
|
||||||
|
<RouterContext.Provider value={{ ...mockRouter, ...router }}>
|
||||||
|
{children}
|
||||||
|
</RouterContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultRender(ui, { wrapper, ...options });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockRouter: NextRouter = {
|
||||||
|
basePath: '',
|
||||||
|
pathname: '/',
|
||||||
|
route: '/',
|
||||||
|
asPath: '/',
|
||||||
|
query: {},
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
reload: jest.fn(),
|
||||||
|
back: jest.fn(),
|
||||||
|
prefetch: jest.fn(),
|
||||||
|
beforePopState: jest.fn(),
|
||||||
|
events: {
|
||||||
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
},
|
||||||
|
isFallback: false,
|
||||||
|
};
|
||||||
6856
site/yarn.lock
Normal file
@@ -25,6 +25,7 @@
|
|||||||
"bookmark": ["read", "clip", "marker", "tag"],
|
"bookmark": ["read", "clip", "marker", "tag"],
|
||||||
"box": ["cube"],
|
"box": ["cube"],
|
||||||
"briefcase": ["work", "bag", "baggage", "folder"],
|
"briefcase": ["work", "bag", "baggage", "folder"],
|
||||||
|
"building": ["organisation"],
|
||||||
"calendar": ["date"],
|
"calendar": ["date"],
|
||||||
"camera": ["photo"],
|
"camera": ["photo"],
|
||||||
"cast": ["chromecast", "airplay"],
|
"cast": ["chromecast", "airplay"],
|
||||||
@@ -100,6 +101,7 @@
|
|||||||
"inbox": ["email"],
|
"inbox": ["email"],
|
||||||
"instagram": ["logo", "camera"],
|
"instagram": ["logo", "camera"],
|
||||||
"key": ["password", "login", "authentication", "secure"],
|
"key": ["password", "login", "authentication", "secure"],
|
||||||
|
"laptop": ["computer"],
|
||||||
"layers": ["stack"],
|
"layers": ["stack"],
|
||||||
"layout": ["window", "webpage"],
|
"layout": ["window", "webpage"],
|
||||||
"life-bouy": ["help", "life ring", "support"],
|
"life-bouy": ["help", "life ring", "support"],
|
||||||
|
|||||||