Update site (#87)
* Move the site to the root directory * Update Site name Co-authored-by: Eric Fennis <eric.fennis@endurance.com>
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets": ["next/babel"]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
.next
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
|
||||
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/../../node_modules/babel-jest',
|
||||
},
|
||||
};
|
||||
2
packages/site/next-env.d.ts
vendored
2
packages/site/next-env.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "site",
|
||||
"version": "1.0.0",
|
||||
"author": "John Letey",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"export": "next export -o build",
|
||||
"deploy": "yarn build && yarn export",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/core": "^0.8.0",
|
||||
"@emotion/core": "^10.0.28",
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"downloadjs": "^1.4.7",
|
||||
"emotion-theming": "^10.0.27",
|
||||
"fuse.js": "^6.0.4",
|
||||
"jszip": "^3.4.0",
|
||||
"next": "^9.4.4",
|
||||
"react": "^16.13.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Box, Divider, Flex, Text, Link, Icon, useColorMode } from "@chakra-ui/core";
|
||||
import { useKeyBindings } from "../lib/key";
|
||||
import {useRouter} from "next/router";
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const router = useRouter();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
|
||||
function setQuery(query){
|
||||
router.push({
|
||||
pathname: '/',
|
||||
query: { query: query }
|
||||
}).then();
|
||||
}
|
||||
useKeyBindings({
|
||||
Escape: {
|
||||
fn: () => setQuery(""),
|
||||
},
|
||||
KeyT: {
|
||||
fn: () => toggleColorMode(),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box h="100vh">
|
||||
<Flex mb={16} w="full">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
pt={4}
|
||||
pb={4}
|
||||
maxW="1250px"
|
||||
margin="0 auto"
|
||||
w="full"
|
||||
px={8}
|
||||
>
|
||||
<Flex justifyContent="center" alignItems="center">
|
||||
<Text
|
||||
fontSize="4xl"
|
||||
onClick={() => setQuery("")}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Featherity
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="center" alignItems="center">
|
||||
<Link href="https://github.com/lucide-icons/lucide" isExternal style={{ fontSize: "18px", marginRight: '24px' }}>
|
||||
Github
|
||||
</Link>
|
||||
<div onClick={toggleColorMode} style={{ cursor: "pointer" }}>
|
||||
<Icon name={colorMode == "light" ? "moon" : "sun"} size="24px" />
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex margin="0 auto" direction="column" maxW="1250px" px={8}>
|
||||
{children}
|
||||
<Divider marginTop={10} marginBottom={10} />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,32 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import tags from '../../../../tags.json';
|
||||
|
||||
const directory = path.join(process.cwd(), "../../icons");
|
||||
|
||||
export function getAllNames() {
|
||||
const fileNames = fs.readdirSync(directory);
|
||||
|
||||
return fileNames.map((fileName) => {
|
||||
return fileName.replace(/\.svg$/, "");
|
||||
});
|
||||
}
|
||||
|
||||
export function getData(name) {
|
||||
const fullPath = path.join(directory, `${name}.svg`);
|
||||
const fileContents = fs.readFileSync(fullPath, "utf8");
|
||||
|
||||
return {
|
||||
name,
|
||||
tags: tags[name] || [],
|
||||
src: fileContents,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAllData() {
|
||||
const names = getAllNames();
|
||||
|
||||
return names.map((name) => {
|
||||
return getData(name);
|
||||
});
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const isCtrl = (e) => e.metaKey || e.ctrlKey;
|
||||
|
||||
// https://keycode.info
|
||||
export const useKeyBindings = (
|
||||
initialKeyBindings = {},
|
||||
eventListener = "keydown"
|
||||
) => {
|
||||
const [keyBindings] = useState(initialKeyBindings);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener(
|
||||
eventListener,
|
||||
(event) => {
|
||||
const { code } = event;
|
||||
const keyBinding = keyBindings[code];
|
||||
if (keyBinding === undefined) return;
|
||||
const condition = keyBinding.ctrl ? isCtrl(event) : true;
|
||||
if (!condition) return;
|
||||
if (event.target.type != "text" || code == "Escape") {
|
||||
event.preventDefault();
|
||||
keyBinding.fn(event);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return () =>
|
||||
Object.keys(keyBindings).forEach((keyBinding) =>
|
||||
document.removeEventListener(eventListener, keyBindings[keyBinding])
|
||||
);
|
||||
}, []);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -1,74 +0,0 @@
|
||||
import { theme as chakraTheme } from "@chakra-ui/core";
|
||||
|
||||
const theme = {
|
||||
...chakraTheme,
|
||||
fonts: {
|
||||
...chakraTheme.fonts,
|
||||
body: `Jost,-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"`,
|
||||
},
|
||||
icons: {
|
||||
...chakraTheme.icons,
|
||||
sun: {
|
||||
path: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
moon: {
|
||||
path: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
search: {
|
||||
path: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
@@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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>Featherity</title>
|
||||
</Head>
|
||||
<ThemeProvider theme={customTheme}>
|
||||
<ColorModeProvider>
|
||||
<CSSReset />
|
||||
<Component {...pageProps} />
|
||||
</ColorModeProvider>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -1,31 +0,0 @@
|
||||
import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://indestructibletype.com/fonts/Jost.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<style jsx global>{`
|
||||
* {
|
||||
-webkit-transition: none !important;
|
||||
-moz-transition: none !important;
|
||||
-o-transition: none !important;
|
||||
-ms-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
`}</style>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
||||
@@ -1,149 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Grid,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
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 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();
|
||||
Object.values(icons).forEach((icon) =>
|
||||
// @ts-ignore
|
||||
zip.file(`${icon.name}.svg`, icon.src)
|
||||
);
|
||||
return zip.generateAsync({ type: 'blob' });
|
||||
}
|
||||
|
||||
const IndexPage = ({ data }) => {
|
||||
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) {
|
||||
event.preventDefault();
|
||||
inputElement.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Flex direction="column" align="center" justify="center">
|
||||
<Text fontSize="3xl" as="b">
|
||||
Simply beautiful open source icons, community-sourced
|
||||
</Text>
|
||||
<Stack isInline marginTop={3} marginBottom={10}>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const zip = await generateZip(data);
|
||||
download(zip, 'feather.zip');
|
||||
}}
|
||||
>
|
||||
Download all
|
||||
</Button>
|
||||
</Stack>
|
||||
</Flex>
|
||||
<InputGroup position="sticky" top={2} zIndex={1}>
|
||||
<InputLeftElement children={<Icon name="search" />} />
|
||||
<Input
|
||||
ref={inputElement}
|
||||
placeholder={`Search ${Object.keys(data).length} icons (Press "/" to focus)`}
|
||||
value={queryText}
|
||||
onChange={(event) => setQueryText(event.target.value)}
|
||||
marginBottom={5}
|
||||
/>
|
||||
</InputGroup>
|
||||
{results.length > 0 ? (
|
||||
<Grid templateColumns={`repeat(auto-fill, minmax(160px, 1fr))`} gap={5}>
|
||||
{results.map((icon) => {
|
||||
// @ts-ignore
|
||||
const actualIcon = icon.item ? icon.item : icon;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
borderWidth="1px"
|
||||
rounded="lg"
|
||||
padding={16}
|
||||
onClick={(event) => {
|
||||
if (event.shiftKey) {
|
||||
copy(actualIcon.src);
|
||||
toast({
|
||||
title: 'Copied!',
|
||||
description: `Icon "${actualIcon.name}" copied to clipboard.`,
|
||||
status: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
} else {
|
||||
download(actualIcon.src, `${actualIcon.name}.svg`, 'image/svg+xml');
|
||||
}
|
||||
}}
|
||||
key={actualIcon.name}
|
||||
alignItems="center"
|
||||
>
|
||||
<Flex direction="column" align="center" justify="center">
|
||||
<div dangerouslySetInnerHTML={{ __html: actualIcon.src }} />
|
||||
<Text marginTop={5}>{actualIcon.name}</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
) : (
|
||||
<Text
|
||||
fontSize="2xl"
|
||||
fontWeight="bold"
|
||||
textAlign="center"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
No results found for "{query}"
|
||||
</Text>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getStaticProps() {
|
||||
let data = getAllData();
|
||||
return {
|
||||
props: {
|
||||
data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default IndexPage;
|
||||
@@ -1,30 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user