From a7e8b3bcb71a1cc5ebe466264dc8d7658f6c301a Mon Sep 17 00:00:00 2001 From: Eric Fennis Date: Fri, 12 Feb 2021 20:38:47 +0100 Subject: [PATCH] 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 --- site/package.json | 3 +- site/src/components/Header.tsx | 32 ++++-- site/src/components/IconDetailOverlay.tsx | 100 +++++++++++------ site/src/components/IconList.tsx | 7 +- site/src/components/Layout.tsx | 4 +- site/src/components/ModifiedTooltip.tsx | 31 ++++++ site/src/lib/fetchAllContributors.ts | 129 ++++++++++++++++++++++ site/src/lib/icons.tsx | 12 +- site/src/pages/_app.tsx | 5 +- site/src/pages/icon/[iconName].tsx | 15 +-- site/src/pages/index.tsx | 19 ++-- site/yarn.lock | 58 ++++------ 12 files changed, 305 insertions(+), 110 deletions(-) create mode 100644 site/src/components/ModifiedTooltip.tsx create mode 100644 site/src/lib/fetchAllContributors.ts diff --git a/site/package.json b/site/package.json index 0c01f7c..d87d238 100644 --- a/site/package.json +++ b/site/package.json @@ -13,7 +13,7 @@ "dependencies": { "@chakra-ui/core": "^1.0.0-rc.8", "downloadjs": "^1.4.7", - "framer-motion": "^2.9.4", + "framer-motion": "^3.3.0", "fuse.js": "^6.0.4", "jszip": "^3.4.0", "lodash": "^4.17.20", @@ -23,7 +23,6 @@ "react": "^16.13.1", "react-color": "2.17.3", "react-dom": "^16.13.1", - "react-spring": "^8.0.27", "react-svg-loader": "^3.0.3" }, "devDependencies": { diff --git a/site/src/components/Header.tsx b/site/src/components/Header.tsx index 4d1dfaa..e8cd6c4 100644 --- a/site/src/components/Header.tsx +++ b/site/src/components/Header.tsx @@ -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 }) => { An open-source icon library, a fork of Feather Icons.
We're expanding the icon set as much as possible while keeping it nice-looking - join us!
- - + + + + + + + - + + ) }; diff --git a/site/src/components/IconDetailOverlay.tsx b/site/src/components/IconDetailOverlay.tsx index c2f751c..5f5b95d 100644 --- a/site/src/components/IconDetailOverlay.tsx +++ b/site/src/components/IconDetailOverlay.tsx @@ -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(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} > + { w="full" px={8} > - + { - - - - {icon.name} - + + + + + + {icon.name} + + { icon?.contributors?.length ? ( ) : null} + + { tags?.length ? ( { Edit Tags */} - - - - - + + + + + + + + { icon?.contributors?.length ? ( + <> + + Contributors: + + + { icon.contributors.map((commit, index) => ( + + + + + + )) } + + + ) : null } - + + ); }; diff --git a/site/src/components/IconList.tsx b/site/src/components/IconList.tsx index 0b27c8a..f4f5ba3 100644 --- a/site/src/components/IconList.tsx +++ b/site/src/components/IconList.tsx @@ -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 ( { icons.map((icon) => { const actualIcon = icon.item ? icon.item : icon; - const { name, content } = actualIcon; + const { name, content, contributors } = actualIcon; return ( { 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 ? ( ) : null} { maxW="1250px" margin="0 auto" w="full" - px={8} + px={5} > @@ -77,7 +77,7 @@ const Layout = ({ children }) => { - + {children}

diff --git a/site/src/components/ModifiedTooltip.tsx b/site/src/components/ModifiedTooltip.tsx new file mode 100644 index 0000000..9c8206b --- /dev/null +++ b/site/src/components/ModifiedTooltip.tsx @@ -0,0 +1,31 @@ +import { Box, Tooltip, useColorMode } from "@chakra-ui/core"; +import theme from '../lib/theme'; + +const ModifiedTooltip = ({}) => { + const { colorMode } = useColorMode(); + + return ( + + + + ) +} + +export default ModifiedTooltip; diff --git a/site/src/lib/fetchAllContributors.ts b/site/src/lib/fetchAllContributors.ts new file mode 100644 index 0000000..27ad1b4 --- /dev/null +++ b/site/src/lib/fetchAllContributors.ts @@ -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); + } +} diff --git a/site/src/lib/icons.tsx b/site/src/lib/icons.tsx index 8ed75c0..547c52f 100644 --- a/site/src/lib/icons.tsx +++ b/site/src/lib/icons.tsx @@ -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))); } diff --git a/site/src/pages/_app.tsx b/site/src/pages/_app.tsx index d2bfb1b..17b119d 100644 --- a/site/src/pages/_app.tsx +++ b/site/src/pages/_app.tsx @@ -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 }) => { Lucide - + + + ); diff --git a/site/src/pages/icon/[iconName].tsx b/site/src/pages/icon/[iconName].tsx index 988c265..59191b8 100644 --- a/site/src/pages/icon/[iconName].tsx +++ b/site/src/pages/icon/[iconName].tsx @@ -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 }) => {

@@ -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, diff --git a/site/src/pages/index.tsx b/site/src/pages/index.tsx index 945823b..91e8054 100644 --- a/site/src/pages/index.tsx +++ b/site/src/pages/index.tsx @@ -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 ( - - router.push('/')} - /> -
- - + router.push('/')} + /> +
+ ); }; export async function getStaticProps() { - let data = getAllData(); + let data = await getAllData(); return { props: { diff --git a/site/yarn.lock b/site/yarn.lock index 023e681..3d2c4a7 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -356,7 +356,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@7.12.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.9.2": +"@babel/runtime@7.12.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.9.2": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== @@ -3774,25 +3774,23 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -framer-motion@^2.9.4: - version "2.9.5" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-2.9.5.tgz#bbb185325d531c57f494cf3f6cf7719fc2c225c7" - integrity sha512-epSX4Co1YbDv0mjfHouuY0q361TpHE7WQzCp/xMTilxy4kXd+Z23uJzPVorfzbm1a/9q1Yu8T5bndaw65NI4Tg== +framer-motion@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-3.3.0.tgz#e355824369f03c8cd07a87ef59b7a8348c33fafd" + integrity sha512-bjUrwXfMJZ6D+HSMDiXbMGKmlWGnUux8HotWgORTZkdPTgKAndlRXjeC2ikCgNVo2ifmRvEla5ckP9JaZc7JKA== dependencies: - framesync "^4.1.0" + framesync "^5.0.0" hey-listen "^1.0.8" - popmotion "9.0.0-rc.20" - style-value-types "^3.1.9" + popmotion "^9.1.0" + style-value-types "^4.0.1" tslib "^1.10.0" optionalDependencies: "@emotion/is-prop-valid" "^0.8.2" -framesync@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/framesync/-/framesync-4.1.0.tgz#69a8db3ca432dc70d6a76ba882684a1497ef068a" - integrity sha512-MmgZ4wCoeVxNbx2xp5hN/zPDCbLSKiDt4BbbslK7j/pM2lg5S0vhTNv1v8BCVb99JPIo6hXBFdwzU7Q4qcAaoQ== - dependencies: - hey-listen "^1.0.5" +framesync@5.0.0, framesync@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.0.0.tgz#7de8caedf53ac441118e79680f1beb7391c328b6" + integrity sha512-wd8t+JsQGisluSv1twiEeDv0aNGpavGb9q7xgIk9fGbcIWkNXF/KVtrjnOrCwBWJuiXxlJfNkcvGudsI32FxYA== from2@^2.1.0: version "2.3.0" @@ -4064,7 +4062,7 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -hey-listen@^1.0.5, hey-listen@^1.0.8: +hey-listen@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== @@ -6049,14 +6047,14 @@ pnp-webpack-plugin@1.6.4: dependencies: ts-pnp "^1.1.6" -popmotion@9.0.0-rc.20: - version "9.0.0-rc.20" - resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.0.0-rc.20.tgz#f3550042ae31957b5416793ae8723200951ad39d" - integrity sha512-f98sny03WuA+c8ckBjNNXotJD4G2utG/I3Q23NU69OEafrXtxxSukAaJBxzbtxwDvz3vtZK69pu9ojdkMoBNTg== +popmotion@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.1.0.tgz#4360d06bd18ce8baa8f9284ecec7d55344af6325" + integrity sha512-+J7pzzBy5kk2qsP8ilowKs/CH+HoZa3kOGEBNCleCvsPXEF3nKHdfAR3SboMyPvdpIrofaT7ZIy/xWgz446Azw== dependencies: - framesync "^4.1.0" + framesync "5.0.0" hey-listen "^1.0.8" - style-value-types "^3.1.9" + style-value-types "^4.0.1" tslib "^1.10.0" posix-character-classes@^0.1.0: @@ -6215,7 +6213,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6421,14 +6419,6 @@ react-remove-scroll@2.4.0: use-callback-ref "^1.2.3" use-sidecar "^1.0.1" -react-spring@^8.0.27: - version "8.0.27" - resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a" - integrity sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g== - dependencies: - "@babel/runtime" "^7.3.1" - prop-types "^15.5.8" - react-style-singleton@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.1.tgz#ce7f90b67618be2b6b94902a30aaea152ce52e66" @@ -7343,10 +7333,10 @@ style-loader@1.2.1: loader-utils "^2.0.0" schema-utils "^2.6.6" -style-value-types@^3.1.9: - version "3.2.0" - resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-3.2.0.tgz#eb89cab1340823fa7876f3e289d29d99c92111bb" - integrity sha512-ih0mGsrYYmVvdDi++/66O6BaQPRPRMQHoZevNNdMMcPlP/cH28Rnfsqf1UEba/Bwfuw9T8BmIMwbGdzsPwQKrQ== +style-value-types@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.0.1.tgz#23f05dd03e8a850654defc22cf03ebac572aaa00" + integrity sha512-aOV/HHyynIyTmU27qfs0oAHhFde6BFIvV4+nMerE2MAPZMwYOeQk1/F3S6djxF2u4HdbiieCPs3ZzWsbNUoc9A== dependencies: hey-listen "^1.0.8" tslib "^1.10.0"