feat: Site (#1)

* site: pull data from "icons" dir

* site: display icons

* site: remove redundant code

* site: colour mode support

* site: header

* site: order imports

* site: search

* site: add toast when copying icon

* site: styling

* site: hero

* fix: disable theme toggle transitions

* feat: Use Yarn Workspaces

* refactor: Update site deploy scripts

* refactor: Remove dark mode for now

* feat: Add site title

* refactor: Fix warning and format

* feat: Add dark mode back 👀

* feat: Escape key to reset query

* Fix by aelfric

* Add Github link

* Fix #40

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
This commit is contained in:
John Letey
2020-08-29 22:16:38 +01:00
committed by GitHub
parent 1b726f592a
commit e3e4514dcc
16 changed files with 6674 additions and 20 deletions

View File

@@ -0,0 +1,58 @@
import { Box, Divider, Flex, Text, Link, Icon, useColorMode } from "@chakra-ui/core";
import { StringParam, useQueryParam } from "use-query-params";
import { useKeyBindings } from "../lib/key";
const Layout = ({ children }) => {
const [, setQuery] = useQueryParam("query", StringParam);
const { colorMode, toggleColorMode } = useColorMode();
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/featherity/featherity" 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;

View File

@@ -0,0 +1,32 @@
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);
});
}

View File

@@ -0,0 +1,34 @@
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])
);
}, []);
};

View File

@@ -0,0 +1,23 @@
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;

View File

@@ -0,0 +1,74 @@
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;

View File

@@ -0,0 +1,47 @@
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;

View File

@@ -0,0 +1,31 @@
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;

View File

@@ -0,0 +1,143 @@
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 { StringParam, useQueryParam } from "use-query-params";
import Layout from "../components/Layout";
import { getAllData } from "../lib/icons";
import useSearch from "../lib/search";
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 [query, setQuery] = useQueryParam("query", StringParam);
const results = useSearch(data, query || "");
const toast = useToast();
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={query}
onChange={(event) => setQuery(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;