diff --git a/.gitignore b/.gitignore index 5205805..3f3f6f2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +package-lock.json \ No newline at end of file diff --git a/src/pages/index.js b/src/pages/index.js index 587de8c..901fa99 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,9 +1,10 @@ -import { TbBrandTwitter, TbShare, TbDownload, TbCopy } from "react-icons/tb"; +import { TbBrandTwitter, TbShare, TbDownload, TbCopy, TbChevronDown } from "react-icons/tb"; import React, { useRef, useState, useEffect } from "react"; import { download, fetchData, downloadJSON, + downloadSVG, cleanUsername, share, copyToClipboard @@ -19,17 +20,26 @@ const App = () => { const [theme, setTheme] = useState("standard"); const [data, setData] = useState(null); const [error, setError] = useState(null); + const [showDownloadMenu, setShowDownloadMenu] = useState(false); + const downloadMenuRef = useRef(); useEffect(() => { - if (!data) { - return; - } + if (!data) return; draw(); }, [data, theme]); + useEffect(() => { + const handleClickOutside = (e) => { + if (downloadMenuRef.current && !downloadMenuRef.current.contains(e.target)) { + setShowDownloadMenu(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + const handleSubmit = (e) => { e.preventDefault(); - setUsername(cleanUsername(username)); setLoading(true); setError(null); @@ -52,9 +62,16 @@ const App = () => { }); }; - const onDownload = (e) => { + const onDownloadPNG = (e) => { e.preventDefault(); download(canvasRef.current); + setShowDownloadMenu(false); + }; + + const onDownloadSVG = (e) => { + e.preventDefault(); + downloadSVG(canvasRef.current); + setShowDownloadMenu(false); }; const onCopy = (e) => { @@ -64,9 +81,7 @@ const App = () => { const onDownloadJson = (e) => { e.preventDefault(); - if (data != null) { - downloadJSON(data); - } + if (data != null) downloadJSON(data); }; const onShare = (e) => { @@ -79,131 +94,127 @@ const App = () => { setError("Something went wrong... Check back later."); return; } - const { drawContributions } = await import("github-contributions-canvas"); - drawContributions(canvasRef.current, { data, username: username, themeName: theme, footerText: "Made by @sallar & friends - github-contributions.vercel.app" }); - contentRef.current.scrollIntoView({ - behavior: "smooth" - }); + contentRef.current.scrollIntoView({ behavior: "smooth" }); }; - const _renderGithubButton = () => { - return ( -
- - Star - -
- ); - }; + const _renderGithubButton = () => ( +
+ + Star + +
+ ); - const _renderLoading = () => { - return ( -
-
- Loading... -

Please wait, I’m visiting your profile...

-
+ const _renderLoading = () => ( +
+
+ Loading... +

Please wait, I'm visiting your profile...

- ); - }; +
+ ); - const _renderGraphs = () => { - return ( -
-

Your chart is ready!

+ const _renderGraphs = () => ( +
+

Your chart is ready!

+ {data !== null && ( + <> +
+ - {data !== null && ( - <> -
+
- {global.navigator && "share" in navigator && ( - + {showDownloadMenu && ( +
+ + +
)}
- - - )} -
- ); - }; + {global.navigator && "share" in navigator && ( + + )} +
+ + + )} +
+ ); - const _renderForm = () => { - return ( -
- setUsername(e.target.value)} - value={username} - id="username" - autoCorrect="off" - autoCapitalize="none" - autoFocus - /> - -
- ); - }; + const _renderForm = () => ( +
+ setUsername(e.target.value)} + value={username} + id="username" + autoCorrect="off" + autoCapitalize="none" + autoFocus + /> + +
+ ); - const _renderError = () => { - return ( -
-

{error}

-
- ); - }; + const _renderError = () => ( +
+

{error}

+
+ ); const _renderDownloadAsJSON = () => { if (data === null) return; return ( - - šŸ“Š - {" "} + šŸ“Š{" "} Download data as JSON for your own visualizations ); @@ -228,8 +239,7 @@ const App = () => { Not affiliated with GitHub Inc. Octocat illustration from{" "} GitHub Octodex - - . + .

{_renderDownloadAsJSON()}
@@ -251,4 +261,4 @@ const App = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/src/styles/App.css b/src/styles/App.css index 63b5702..6a59b96 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -19,7 +19,7 @@ --input-border: rgb(55, 68, 75); --button-disabled: rgb(42, 52, 57); --button-disabled-border: var(--input-border); - --loading-text: #rgb(164, 157, 145); + --loading-text: rgb(164, 157, 145); --input-text: #fff; } } @@ -91,6 +91,58 @@ html { height: 3rem; } +.App-download-split { + position: relative; + display: inline-flex; + margin: 0 1rem; +} + +.App-download-split .App-download-button { + margin: 0; +} + +.App-download-button--main { + border-radius: 6px 0 0 6px; + border-right: 1px solid rgba(255, 255, 255, 0.2); +} + +.App-download-button--caret { + border-radius: 0 6px 6px 0; + padding: 0 8px; +} + +.App-download-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + background: #1e1e2e; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + overflow: hidden; + z-index: 100; + min-width: 170px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +.App-download-menu button { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + margin: 0; + padding: 10px 14px; + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 14px; + text-align: left; +} + +.App-download-menu button:hover { + background: rgba(255, 255, 255, 0.08); +} + h1, h4 { font-weight: 300; @@ -284,4 +336,4 @@ footer a { display: flex; flex-wrap: wrap; } -} +} \ No newline at end of file diff --git a/src/utils/export.js b/src/utils/export.js index acca783..ec7d214 100644 --- a/src/utils/export.js +++ b/src/utils/export.js @@ -36,6 +36,27 @@ export function downloadJSON(data) { } } +export function downloadSVG(canvas) { + try { + const width = canvas.width; + const height = canvas.height; + const dataUrl = canvas.toDataURL("image/png"); + const svgContent = ` + +`; + const dataUri = + "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svgContent); + const a = document.createElement("a"); + document.body.insertAdjacentElement("beforeend", a); + a.download = "contributions.svg"; + a.href = dataUri; + a.click(); + document.body.removeChild(a); + } catch (err) { + console.error(err); + } +} + export async function share(canvas) { try { canvas.toBlob(async (blob) => { diff --git a/yarn.lock b/yarn.lock index e6db0ce..15d6d99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,71 +7,11 @@ resolved "https://registry.npmjs.org/@next/env/-/env-13.1.1.tgz" integrity sha512-vFMyXtPjSAiOXOywMojxfKIqE3VWN5RCAx+tT3AS3pcKjMLFTCJFUWsKv8hC+87Z1F4W3r68qTwDFZIFmd5Xkw== -"@next/swc-android-arm-eabi@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.1.tgz#b5c3cd1f79d5c7e6a3b3562785d4e5ac3555b9e1" - integrity sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A== - -"@next/swc-android-arm64@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.1.tgz#e2ca9ccbba9ef770cb19fbe96d1ac00fe4cb330d" - integrity sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA== - -"@next/swc-darwin-arm64@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.1.tgz#4af00877332231bbd5a3703435fdd0b011e74767" - integrity sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg== - -"@next/swc-darwin-x64@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.1.tgz#bf4cb09e7e6ec6d91e031118dde2dd17078bcbbc" - integrity sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA== - -"@next/swc-freebsd-x64@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.1.tgz#6933ea1264328e8523e28818f912cd53824382d4" - integrity sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA== - -"@next/swc-linux-arm-gnueabihf@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.1.tgz#b5896967aaba3873d809c3ad2e2039e89acde419" - integrity sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg== - -"@next/swc-linux-arm64-gnu@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.1.tgz#91b3e9ea8575b1ded421c0ea0739b7bccf228469" - integrity sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw== - -"@next/swc-linux-arm64-musl@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.1.tgz#83149ea05d7d55f3664d608dbe004c0d125f9147" - integrity sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA== - "@next/swc-linux-x64-gnu@13.1.1": version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz#d7d0777b56de0dd82b78055772e13e18594a15ca" + resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz" integrity sha512-nnjuBrbzvqaOJaV+XgT8/+lmXrSCOt1YYZn/irbDb2fR2QprL6Q7WJNgwsZNxiLSfLdv+2RJGGegBx9sLBEzGA== -"@next/swc-linux-x64-musl@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.1.tgz#41655722b127133cd95ab5bc8ca1473e9ab6876f" - integrity sha512-CM9xnAQNIZ8zf/igbIT/i3xWbQZYaF397H+JroF5VMOCUleElaMdQLL5riJml8wUfPoN3dtfn2s4peSr3azz/g== - -"@next/swc-win32-arm64-msvc@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.1.tgz#f10da3dfc9b3c2bbd202f5d449a9b807af062292" - integrity sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ== - -"@next/swc-win32-ia32-msvc@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.1.tgz#4c0102b9b18ece15c818056d07e3917ee9dade78" - integrity sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg== - -"@next/swc-win32-x64-msvc@13.1.1": - version "13.1.1" - resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.1.tgz" - integrity sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg== - "@swc/helpers@0.4.14": version "0.4.14" resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz" @@ -86,7 +26,7 @@ boolbase@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== caniuse-lite@^1.0.30001406: @@ -139,9 +79,14 @@ css-what@^4.0.0: resolved "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz" integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== +csstype@^3.0.10: + version "3.2.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + date-fns@^2.29.3: version "2.29.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz" integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== dom-serializer@^1.0.1, dom-serializer@~1.2.0: @@ -174,26 +119,21 @@ domutils@^2.4.3, domutils@^2.4.4: domelementtype "^2.0.1" domhandler "^4.0.0" -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@~2.1.0: +entities@^2.0.0, entities@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== github-contributions-canvas@^0.8.0: version "0.8.0" - resolved "https://registry.yarnpkg.com/github-contributions-canvas/-/github-contributions-canvas-0.8.0.tgz#629932db914fd3a28cca05260be8a8bf3497a5e3" + resolved "https://registry.npmjs.org/github-contributions-canvas/-/github-contributions-canvas-0.8.0.tgz" integrity sha512-qFwTRW+qnLKjKaVUk4YsVCvh6ymBab1lt4wx2Tg/d7J/eeEaMnbWAsmjFNWs4mjJ1ptD4pqlUA8fUw2s/ZBMjg== dependencies: date-fns "^2.29.3" goober@^2.1.10: version "2.1.12" - resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.12.tgz#6c1645314ac9a68fe76408e1f502c63df8a39042" + resolved "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz" integrity sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q== htmlparser2@^6.0.0: @@ -260,7 +200,7 @@ normalize.css@^8.0.1: nth-check@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== dependencies: boolbase "^1.0.0" @@ -268,7 +208,7 @@ nth-check@^2.0.0: object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== parse5-htmlparser2-tree-adapter@^6.0.0: version "6.0.1" @@ -305,7 +245,7 @@ prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" -react-dom@^18.2.0: +react-dom@^18.2.0, react-dom@>=16: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -322,7 +262,7 @@ react-hot-toast@^2.4.0: react-icons@^4.7.1: version "4.7.1" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.7.1.tgz#0f4b25a5694e6972677cb189d2a72eabea7a8345" + resolved "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz" integrity sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw== react-is@^16.8.1: @@ -330,7 +270,7 @@ react-is@^16.8.1: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react@^18.2.0: +react@*, react@^16.8||^17||^18, react@^18.2.0, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==