Add contributors to icon overlay and add dot (#223)

* add contributers

* Add icon fetcher

* add contributing json

* Fix fetch call

* Add contributers to site

* Add caching for github api

* Fix build

* Move context provider

* Revert packages changes

* Fix mobile layout

* remove react-spring

* remove incorrect type prop
This commit is contained in:
Eric Fennis
2021-02-12 20:38:47 +01:00
committed by GitHub
parent c4dfe6b8cb
commit a7e8b3bcb7
12 changed files with 305 additions and 110 deletions

View File

@@ -1,4 +1,4 @@
import {Button, Flex, Link, Stack, Text,} from "@chakra-ui/core";
import {Button, Flex, Link, WrapItem, Text, Wrap,} from "@chakra-ui/core";
import download from "downloadjs";
import JSZip from "jszip";
import { Download, Github } from 'lucide-react';
@@ -30,15 +30,26 @@ const Header = ({ data }) => {
<Text fontSize="lg" as="p" textAlign="center" mb="8">
An open-source icon library, a fork of <Link href="https://github.com/feathericons/feather" isExternal>Feather Icons</Link>. <br/>We're expanding the icon set as much as possible while keeping it nice-looking - <Link href={repositoryUrl} isExternal>join us</Link>!
</Text>
<Stack isInline marginTop={3} marginBottom={10}>
<Button
leftIcon={<Download/>}
size="lg"
onClick={downloadAllIcons}
>
Download all
</Button>
<Wrap
isInline
marginTop={3}
marginBottom={10}
spacing="15px"
justify="center"
>
<WrapItem>
<Button
leftIcon={<Download/>}
size="lg"
onClick={downloadAllIcons}
>
Download all
</Button>
</WrapItem>
<WrapItem>
<IconCustomizerDrawer/>
</WrapItem>
<WrapItem>
<Button
as="a"
leftIcon={<Github/>}
@@ -49,7 +60,8 @@ const Header = ({ data }) => {
>
Github
</Button>
</Stack>
</WrapItem>
</Wrap>
</Flex>
)
};

View File

@@ -1,41 +1,37 @@
import { useSpring, animated } from "react-spring";
import { Box, Text, IconButton, useColorMode, Flex, ButtonGroup, Button, useToast } from "@chakra-ui/core";
import { Box, Text, IconButton, useColorMode, Flex, Slide, ButtonGroup, Button, useToast, Heading, Avatar, AvatarGroup, Link, Tooltip, useMediaQuery, useDisclosure } from "@chakra-ui/core";
import theme from "../lib/theme";
import download from 'downloadjs';
import copy from "copy-to-clipboard";
import { X as Close } from 'lucide-react';
import {useContext, useRef} from "react";
import {useContext, useEffect, useRef} from "react";
import {IconStyleContext} from "./CustomizeIconContext";
import {IconWrapper} from "./IconWrapper";
import ModifiedTooltip from "./ModifiedTooltip";
type IconDownload = {
src: string;
name: string;
};
const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
const IconDetailOverlay = ({ open = true, close, icon }) => {
const toast = useToast();
const { colorMode } = useColorMode();
const { tags = [], name } = icon;
const {color, strokeWidth, size} = useContext(IconStyleContext);
const iconRef = useRef<SVGSVGElement>(null);
const { transform, opacity } = useSpring({
opacity: isOpen ? 1 : 0,
transform: `translateY(${isOpen ? -120 : 0}%)`,
config: { mass: 5, tension: 500, friction: 80 },
});
const [isMobile] = useMediaQuery("(max-width: 560px)")
const { isOpen, onOpen, onClose } = useDisclosure()
const handleClose = () => {
onClose();
close();
};
const panelStyling = {
transform: transform.interpolate(t => t),
opacity: opacity.interpolate(o => o),
width: "100%",
willChange: "transform"
}
useEffect(() => {
if(open) {
onOpen()
}
}, [open])
const iconStyling = (isLight) => ({
height: "25vw",
@@ -88,6 +84,7 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
height={0}
key={name}
>
<Slide direction="bottom" in={isOpen} style={{ zIndex: 10 }}>
<Flex
alignItems="center"
justifyContent="space-between"
@@ -98,9 +95,7 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
w="full"
px={8}
>
<animated.div
style={panelStyling}
>
<Box
borderWidth="1px"
rounded="lg"
@@ -163,11 +158,25 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
</svg>
</Box>
</Flex>
<Flex marginLeft={[0, 8]}>
<Box>
<Text fontSize="3xl" style={{ cursor: "pointer" }} mb={1}>
{icon.name}
</Text>
<Flex marginLeft={[0, 8]} w="100%">
<Box w="100%">
<Flex
justify={isMobile ? 'center' : 'flex-start'}
marginTop={isMobile ? 10 : 0}
>
<Box
position="relative"
mb={1}
display="inline-block"
style={{ cursor: "pointer" }}
pr={6}
>
<Text fontSize="3xl">
{icon.name}
</Text>
{ icon?.contributors?.length ? ( <ModifiedTooltip/> ) : null}
</Box>
</Flex>
<Box mb={4}>
{ tags?.length ? (
<Text
@@ -187,23 +196,42 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
Edit Tags
</Button> */}
</Box>
<ButtonGroup spacing={4}>
<Button variant="solid" onClick={() => downloadIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Download SVG
</Button>
<Button variant="solid" onClick={() => copyIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Copy SVG
</Button>
<Button variant="solid" onClick={() => downloadPNG({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Download PNG
</Button>
</ButtonGroup>
<Box overflowY="auto" w="100%" pt={1} pb={1}>
<ButtonGroup spacing={4}>
<Button variant="solid" onClick={() => downloadIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Download SVG
</Button>
<Button variant="solid" onClick={() => copyIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Copy SVG
</Button>
<Button variant="solid" onClick={() => downloadPNG({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Download PNG
</Button>
</ButtonGroup>
</Box>
{ icon?.contributors?.length ? (
<>
<Heading as="h5" size="sm" marginTop={4} marginBottom={2}>
Contributors:
</Heading>
<AvatarGroup size="md">
{ icon.contributors.map((commit, index) => (
<Link href={`https://github.com/${commit.author}`} isExternal key={`${index}_${commit.sha}`}>
<Tooltip label={commit.author} key={commit.sha}>
<Avatar name={commit.author} showBorder={false} src={`https://github.com/${commit.author}.png?size=88`} />
</Tooltip>
</Link>
)) }
</AvatarGroup>
</>
) : null }
</Box>
</Flex>
</Flex>
</Box>
</animated.div>
</Flex>
</Slide>
</Box>
);
};

View File

@@ -6,6 +6,7 @@ import {useContext, useMemo} from "react";
import {IconStyleContext} from "./CustomizeIconContext";
import {IconWrapper} from "./IconWrapper";
import { useRouter } from "next/router";
import ModifiedTooltip from './ModifiedTooltip';
const IconList = ({icons}) => {
const router = useRouter()
@@ -17,13 +18,13 @@ const IconList = ({icons}) => {
return (
<Grid
templateColumns={`repeat(auto-fill, minmax(160px, 1fr))`}
templateColumns={`repeat(auto-fill, minmax(150px, 1fr))`}
gap={5}
marginBottom="320px"
>
{ icons.map((icon) => {
const actualIcon = icon.item ? icon.item : icon;
const { name, content } = actualIcon;
const { name, content, contributors } = actualIcon;
return (
<Link
@@ -42,6 +43,7 @@ const IconList = ({icons}) => {
borderWidth="1px"
rounded="lg"
padding={16}
position="relative"
onClick={(event) => {
if (event.shiftKey) {
copy(actualIcon.src);
@@ -63,6 +65,7 @@ const IconList = ({icons}) => {
key={name}
alignItems="center"
>
{ contributors?.length ? ( <ModifiedTooltip/> ) : null}
<Flex direction="column" align="center" justify="center">
<IconWrapper
content={content}

View File

@@ -38,7 +38,7 @@ const Layout = ({ children }) => {
maxW="1250px"
margin="0 auto"
w="full"
px={8}
px={5}
>
<Flex justifyContent="center" alignItems="center">
<NextLink href="/" passHref>
@@ -77,7 +77,7 @@ const Layout = ({ children }) => {
</Flex>
</Flex>
</Flex>
<Flex margin="0 auto" direction="column" maxW="1250px" px={8}>
<Flex margin="0 auto" direction="column" maxW="1250px" px={5}>
{children}
<Divider marginBottom={8} />
<p style={{ alignSelf: "center" }}>

View File

@@ -0,0 +1,31 @@
import { Box, Tooltip, useColorMode } from "@chakra-ui/core";
import theme from '../lib/theme';
const ModifiedTooltip = ({}) => {
const { colorMode } = useColorMode();
return (
<Tooltip
hasArrow
label="This is new or modified icon"
bg={colorMode === 'light' ? theme.colors.white : theme.colors.gray[700]}
color={colorMode === 'dark' ? theme.colors.white : null}
>
<Box
{
...{
position: 'absolute',
height: '8px',
width: '8px',
background: '#F56565',
top: '8px',
right: '8px',
borderRadius: '4px'
}
}
/>
</Tooltip>
)
}
export default ModifiedTooltip;

View File

@@ -0,0 +1,129 @@
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
const IGNORE_COMMIT_MESSAGES = ['fork', 'optimize'];
function getContentHashOfFile(path) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md4');
const stream = fs.createReadStream(path);
stream.on('error', err => reject(err));
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
const fetchCommitsOfIcon = (name) =>
new Promise(async (resolve, reject) => {
try {
const headers = new Headers();
const username = 'ericfennis';
const password = process.env.GITHUB_API_KEY;
headers.set(
'Authorization',
`Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
);
const res = await fetch(
`https://api.github.com/repos/lucide-icons/lucide/commits?path=icons/${name}.svg`,
{
method: 'GET',
headers,
},
);
const data = await res.json();
resolve({
name,
commits: data,
});
} catch (error) {
reject(error);
}
});
export const filterCommits = (commits) =>
commits.filter(({ commit }) =>
!IGNORE_COMMIT_MESSAGES.some(ignoreItem =>
commit.message.toLowerCase().includes(ignoreItem),
))
.map(({ sha, author, commit }) => ({
author: author && author.login ? author.login : null,
commit: sha,
}));
const getIconHash = async (icon) => await getContentHashOfFile(path.join(process.cwd(), "../icons", `${icon}.svg`))
const iconCacheDir = path.join(process.cwd(),'.next/cache/github-api');
const iconCache = (hash) => path.join(iconCacheDir, `${hash}.json`);
export async function checkIconCache(icon) {
const hash = await getIconHash(icon);
const cachePath = iconCache(hash);
if(fs.existsSync( cachePath )) {
const iconCache = fs.readFileSync(cachePath, "utf8");
return JSON.parse(iconCache)
}
return false
}
async function writeIconCache(icon, content) {
const hash = await getIconHash(icon);
const iconCachePath = iconCache(hash);
if (!fs.existsSync(iconCacheDir)){
fs.mkdirSync(iconCacheDir);
}
fs.writeFileSync(iconCachePath, JSON.stringify(content), 'utf-8');
}
export async function getContributors(icon) {
try {
let iconCommits
const iconCache = await checkIconCache(icon);
if (iconCache) {
iconCommits = iconCache
} else {
const { commits } : any = await fetchCommitsOfIcon(icon);
writeIconCache(icon, commits)
iconCommits = commits
}
if (iconCommits && iconCommits.length) {
return filterCommits(iconCommits);
}
return [];
} catch (error) {
throw new Error(error);
}
}
export async function getAllContributors(icons) {
try {
const AllIconCommits = await Promise.all(icons.map(fetchCommitsOfIcon));
const filteredCommits = AllIconCommits.reduce((acc, { name, commits }) => {
if (commits && commits.length) {
acc[name] = filterCommits(commits)
}
return acc;
}, {});
return filteredCommits
} catch (error) {
console.error(error);
}
}

View File

@@ -2,6 +2,7 @@ import fs from "fs";
import path from "path";
import cheerio from 'cheerio';
import tags from '../../../tags.json';
import { getContributors } from "./fetchAllContributors";
const directory = path.join(process.cwd(), "../icons");
@@ -13,25 +14,26 @@ export function getAllNames() {
});
}
export function getData(name) {
export async function getData(name:string) {
const fullPath = path.join(directory, `${name}.svg`);
const fileContents = fs.readFileSync(fullPath, "utf8");
const $ = cheerio.load(fileContents);
const content = $("svg").html();
const contributors = await getContributors(name);
return {
name,
tags: tags[name] || [],
contributors,
src: fileContents,
content: content
};
}
export function getAllData() {
export async function getAllData() {
const names = getAllNames();
return names.map((name) => {
return getData(name);
});
return Promise.all(names.map((name) => getData(name)));
}

View File

@@ -2,6 +2,7 @@ import { ChakraProvider } from '@chakra-ui/core';
import customTheme from '../lib/theme';
import '../assets/styling.css';
import Head from 'next/head';
import { CustomizeIconContext } from "../components/CustomizeIconContext";
const App = ({ Component, pageProps }) => {
return (
@@ -10,7 +11,9 @@ const App = ({ Component, pageProps }) => {
<title>Lucide</title>
</Head>
<ChakraProvider theme={customTheme}>
<Component {...pageProps} />
<CustomizeIconContext>
<Component {...pageProps} />
</CustomizeIconContext>
</ChakraProvider>
</>
);

View File

@@ -1,4 +1,3 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import IconDetailOverlay from '../../components/IconDetailOverlay'
import { getAllData, getData } from '../../lib/icons';
@@ -29,7 +28,7 @@ const IconPage = ({ icon, data }) => {
<IconDetailOverlay
key={icon.name}
icon={icon}
onClose={onClose}
close={onClose}
/>
<Header {...{data}}/>
<IconOverview {...{data}}/>
@@ -39,15 +38,17 @@ const IconPage = ({ icon, data }) => {
export default IconPage
export function getStaticProps({ params: { iconName } }) {
const data = getAllData();
const icon = getData(iconName);
export async function getStaticProps({ params: { iconName } }) {
const data = await getAllData();
const icon = await getData(iconName);
return { props: { icon, data } }
}
export function getStaticPaths() {
export async function getStaticPaths() {
const data = await getAllData();
return {
paths: getAllData().map(({name: iconName }) => ({
paths: data.map(({ name: iconName }) => ({
params: { iconName },
})),
fallback: false,

View File

@@ -5,7 +5,6 @@ import IconOverview from "../components/IconOverview";
import IconDetailOverlay from "../components/IconDetailOverlay";
import { useRouter } from "next/router";
import Header from "../components/Header";
import {CustomizeIconContext} from "../components/CustomizeIconContext";
const IndexPage = ({ data }) => {
const router = useRouter();
@@ -13,21 +12,19 @@ const IndexPage = ({ data }) => {
return (
<Layout>
<CustomizeIconContext>
<IconDetailOverlay
isOpen={!!router.query.iconName}
icon={getIcon(router.query.iconName)}
onClose={() => router.push('/')}
/>
<Header {...{data}}/>
<IconOverview {...{data}}/>
</CustomizeIconContext>
<IconDetailOverlay
open={!!router.query.iconName}
icon={getIcon(router.query.iconName)}
close={() => router.push('/')}
/>
<Header {...{data}}/>
<IconOverview {...{data}}/>
</Layout>
);
};
export async function getStaticProps() {
let data = getAllData();
let data = await getAllData();
return {
props: {