Merge pull request #254 from cy/customize-github-stats
[add]: feature to customize the GitHub stats and top languages card
This commit is contained in:
+182
-40
@@ -5,35 +5,54 @@ import links from "../constants/page-links"
|
||||
import { isMediumUsernameValid, isGitHubUsernameValid } from "../utils/validation"
|
||||
import { ToolsIcon, XCircleIcon } from "@primer/octicons-react";
|
||||
|
||||
const AddonsItem = ({inputId, inputChecked, onInputChange, Icon, onIconClick, ...props}) => {
|
||||
const AddonsItem = ({ inputId, inputChecked, onInputChange, Options, onIconClick, ...props }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const Icon = open ? XCircleIcon : ToolsIcon;
|
||||
|
||||
return (
|
||||
<div className="py-2 flex justify-start items-center text-sm sm:text-lg">
|
||||
<label htmlFor={inputId} className="cursor-pointer flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={inputId}
|
||||
checked={inputChecked}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<span className="pl-4">{props.children}</span>
|
||||
</label>
|
||||
{
|
||||
Icon?
|
||||
<button onClick={onIconClick} className="flex ml-3 focus:bg-gray-400" style={{outline: "none"}}>
|
||||
<>
|
||||
<div className="py-2 flex justify-start items-center text-sm sm:text-lg">
|
||||
<label htmlFor={inputId} className="cursor-pointer flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={inputId}
|
||||
checked={inputChecked}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<span className="pl-4">{props.children}</span>
|
||||
</label>
|
||||
{Options && (
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex ml-3 focus:bg-gray-400"
|
||||
style={{ outline: "none" }}
|
||||
>
|
||||
<Icon className="transform scale-100 md:scale-125" />
|
||||
</button>
|
||||
:''
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{Options && open && Options}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomizeOptions = ({ title, CustomizationOptions }) => (
|
||||
<div
|
||||
className={`border-2 border-solid border-gray-900 bg-gray-100 p-2 ml-8`}
|
||||
style={{ maxWidth: "21rem" }}
|
||||
>
|
||||
<header className="text-base sm:text-lg">{title}</header>
|
||||
<hr className="border-gray-500" />
|
||||
<div className="text-sm sm:text-lg flex flex-col mt-2 ml-0 md:ml-4">
|
||||
{CustomizationOptions}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const CustomizeBadge = ({githubName, badgeOptions, onBadgeUpdate}) => {
|
||||
return (
|
||||
<div className={`border-2 border-solid border-gray-900 bg-gray-100 p-2 ml-8`} style={{maxWidth: '21rem'}}>
|
||||
<header className="text-base sm:text-lg">Customize Badge</header>
|
||||
<hr className="border-gray-500"/>
|
||||
<div className="text-sm sm:text-lg flex flex-col mt-2 ml-0 md:ml-4">
|
||||
<>
|
||||
<label htmlFor="badge-style">Style:
|
||||
<select
|
||||
id="badge-style"
|
||||
@@ -83,13 +102,90 @@ const CustomizeBadge = ({githubName, badgeOptions, onBadgeUpdate}) => {
|
||||
}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomizeGithubStatsBase = ({ prefix, options, onUpdate }) =>
|
||||
<>
|
||||
<label htmlFor={`${prefix}-theme`}>Theme:
|
||||
<select
|
||||
id={`${prefix}-theme`}
|
||||
onChange={({target: { value }}) => onUpdate("theme", value)}
|
||||
defaultValue={options.theme}
|
||||
>
|
||||
<option value="none">none</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="radical">Radical</option>
|
||||
<option value="merko">Merko</option>
|
||||
<option value="gruvbox">Gruvbox</option>
|
||||
<option value="tokyonight">Tokyonight</option>
|
||||
<option value="onedark">Onedark</option>
|
||||
<option value="cobalt">Cobalt</option>
|
||||
<option value="synthwave">Synthwave</option>
|
||||
<option value="highcontrast">Highcontrast</option>
|
||||
<option value="dracula">Dracula</option>
|
||||
</select>
|
||||
</label>
|
||||
<label htmlFor={`${prefix}-title-color`}>Title Color:
|
||||
<input
|
||||
type="color"
|
||||
id={`${prefix}-title-color`}
|
||||
defaultValue={`#${options.titleColor}`}
|
||||
className="w-6"
|
||||
onChange={(e) => onUpdate('titleColor', e.target.value.replace('#', ''))}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor={`${prefix}-text-color`}>Text Color:
|
||||
<input
|
||||
type="color"
|
||||
id={`${prefix}-text-color`}
|
||||
defaultValue={`#${options.textColor}`}
|
||||
className="w-6"
|
||||
onChange={(e) => onUpdate('textColor', e.target.value.replace('#', ''))}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor={`${prefix}-bg-color`}>Background Color:
|
||||
<input
|
||||
type="color"
|
||||
id={`${prefix}-bg-color`}
|
||||
defaultValue={`#${options.bgColor}`}
|
||||
className="w-6"
|
||||
onChange={(e) => onUpdate('bgColor', e.target.value.replace('#', ''))}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor={`${prefix}-hide-border`}>Hide border:
|
||||
<input
|
||||
id={`${prefix}-hide-border`}
|
||||
type="checkbox"
|
||||
checked={options.hideBorder}
|
||||
onChange={(e) => onUpdate('hideBorder', e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor={`${prefix}-cache-seconds`}>Cache Seconds:
|
||||
<input
|
||||
id={`${prefix}-cache-seconds`}
|
||||
type="number"
|
||||
min={1800}
|
||||
max={86400}
|
||||
placeholder={1800}
|
||||
defaultValue={options.cacheSeconds}
|
||||
onChange={(e) => onUpdate('cacheSeconds', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor={`${prefix}-locale`}>Locale:
|
||||
<input
|
||||
id={`${prefix}-locale`}
|
||||
type="text"
|
||||
placeholder="en"
|
||||
defaultValue={options.locale}
|
||||
onChange={(e) => onUpdate('locale', e.target.value)}
|
||||
size="2"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
|
||||
const Addons = props => {
|
||||
const [customizeBadgeOpen, setCustomizeOpen] = useState(false);
|
||||
const [debounce, setDebounce] = useState(undefined);
|
||||
const [badgeOptions, setBadgeOptions] = useState({
|
||||
badgeStyle: props.data.badgeStyle,
|
||||
@@ -105,6 +201,26 @@ const Addons = props => {
|
||||
})
|
||||
}, [props.data.badgeStyle, props.data.badgeColor, props.data.badgeLabel])
|
||||
|
||||
const [githubStatsOptions, setGithubStatsOptions] = useState({
|
||||
...props.data.githubStatsOptions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setGithubStatsOptions({
|
||||
...props.data.githubStatsOptions
|
||||
})
|
||||
}, [props.data.githubStatsOptions])
|
||||
|
||||
const [topLanguagesOptions, setTopLanguagesOptions] = useState({
|
||||
...props.data.topLanguagesOptions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setTopLanguagesOptions({
|
||||
...props.data.topLanguagesOptions
|
||||
})
|
||||
}, [props.data.topLanguagesOptions])
|
||||
|
||||
const blogPostPorkflow = () => {
|
||||
let payload = {
|
||||
dev: {
|
||||
@@ -133,10 +249,6 @@ const Addons = props => {
|
||||
document.body.removeChild(tempElement)
|
||||
}
|
||||
|
||||
const onCustomizeClick = () => {
|
||||
setCustomizeOpen(!customizeBadgeOpen);
|
||||
}
|
||||
|
||||
const onBadgeUpdate = (option, value) => {
|
||||
const callback = () => {
|
||||
let newVal = (option==='badgeLabel' && value==='')?'Profile views':value;
|
||||
@@ -146,6 +258,19 @@ const Addons = props => {
|
||||
clearTimeout(debounce);
|
||||
setDebounce(setTimeout(callback, 300));
|
||||
}
|
||||
|
||||
const onStatsUpdate = (option, value) => {
|
||||
const newStatsOptions = {...githubStatsOptions, [option]: value}
|
||||
setGithubStatsOptions(newStatsOptions)
|
||||
props.handleDataChange("githubStatsOptions", {target: {value: newStatsOptions}})
|
||||
}
|
||||
|
||||
const onTopLangUpdate = (option, value) => {
|
||||
const newLangOptions = {...topLanguagesOptions, [option]: value}
|
||||
setTopLanguagesOptions(newLangOptions)
|
||||
props.handleDataChange("topLanguagesOptions", {target: {value: newLangOptions}})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
|
||||
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">
|
||||
@@ -155,20 +280,21 @@ const Addons = props => {
|
||||
inputId="visitors-count"
|
||||
inputChecked={props.data.visitorsBadge}
|
||||
onInputChange={() => props.handleCheckChange("visitorsBadge")}
|
||||
Icon={ customizeBadgeOpen ? XCircleIcon : ToolsIcon }
|
||||
onIconClick={onCustomizeClick}
|
||||
Options={
|
||||
<CustomizeOptions
|
||||
title="Customize Badge"
|
||||
CustomizationOptions={
|
||||
<CustomizeBadge
|
||||
githubName={props.social.github}
|
||||
badgeOptions={badgeOptions}
|
||||
onBadgeUpdate={onBadgeUpdate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
display visitors count badge
|
||||
</AddonsItem>
|
||||
{
|
||||
customizeBadgeOpen?
|
||||
<CustomizeBadge
|
||||
githubName={props.social.github}
|
||||
badgeOptions={badgeOptions}
|
||||
onBadgeUpdate={onBadgeUpdate}
|
||||
/>
|
||||
: ''
|
||||
}
|
||||
<AddonsItem
|
||||
inputId="github-profile-trophy"
|
||||
inputChecked={props.data.githubProfileTrophy}
|
||||
@@ -180,6 +306,14 @@ const Addons = props => {
|
||||
inputId="github-stats"
|
||||
inputChecked={props.data.githubStats}
|
||||
onInputChange={() => props.handleCheckChange("githubStats")}
|
||||
Options={
|
||||
<CustomizeOptions
|
||||
title="Customize Github Stats Card"
|
||||
CustomizationOptions={
|
||||
<CustomizeGithubStatsBase prefix="stats" options={githubStatsOptions} onUpdate={onStatsUpdate}/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
display github profile stats card
|
||||
</AddonsItem>
|
||||
@@ -187,6 +321,14 @@ const Addons = props => {
|
||||
inputId="top-languages"
|
||||
inputChecked={props.data.topLanguages}
|
||||
onInputChange={() => props.handleCheckChange("topLanguages")}
|
||||
Options={
|
||||
<CustomizeOptions
|
||||
title="Customize Top Skills Card"
|
||||
CustomizationOptions={
|
||||
<CustomizeGithubStatsBase prefix="top-lang" options={topLanguagesOptions} onUpdate={onTopLangUpdate}/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
display top skills
|
||||
</AddonsItem>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react"
|
||||
import { isMediumUsernameValid } from "../utils/validation"
|
||||
import { icons, skills, skillWebsites } from "../constants/skills"
|
||||
import { githubStatsLinkGenerator, topLanguagesLinkGenerator } from "../utils/link-generators"
|
||||
|
||||
const Markdown = props => {
|
||||
const Title = props => {
|
||||
@@ -123,15 +124,11 @@ const Markdown = props => {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
const GitHubStats = props => {
|
||||
let link =
|
||||
"https://github-readme-stats.vercel.app/api?username=" +
|
||||
props.github +
|
||||
"&show_icons=true"
|
||||
if (props.show) {
|
||||
const GitHubStats = ({ show, github, options }) => {
|
||||
if (show) {
|
||||
return (
|
||||
<>
|
||||
{`<p> <img align="center" src="${link}" alt="${props.github}" /></p>`}
|
||||
{`<p> <img align="center" src="${githubStatsLinkGenerator({github: github, options})}" alt="${github}" /></p>`}
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
@@ -204,15 +201,11 @@ const Markdown = props => {
|
||||
return ""
|
||||
}
|
||||
const DisplayTopLanguages = props => {
|
||||
let link =
|
||||
"https://github-readme-stats.vercel.app/api/top-langs/?username=" +
|
||||
props.github +
|
||||
"&layout=compact"
|
||||
if (props.show) {
|
||||
if (!props.showStats) {
|
||||
return (
|
||||
<>
|
||||
{`<p><img align="center" src="${link}" alt="${props.github}" /></p>`}
|
||||
{`<p><img align="center" src="${topLanguagesLinkGenerator({github: props.github, options: props.options})}" alt="${props.github}" /></p>`}
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
@@ -220,7 +213,7 @@ const Markdown = props => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{`<p><img align="left" src="${link}" alt="${props.github}" /></p>`}
|
||||
{`<p><img align="left" src="${topLanguagesLinkGenerator({github: props.github, options: props.options })}" alt="${props.github}" /></p>`}
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
@@ -491,16 +484,17 @@ const Markdown = props => {
|
||||
show={props.data.topLanguages}
|
||||
showStats={props.data.githubStats}
|
||||
github={props.social.github}
|
||||
options={props.data.topLanguagesOptions}
|
||||
/>
|
||||
</>
|
||||
<>
|
||||
<GitHubStats
|
||||
show={props.data.githubStats}
|
||||
github={props.social.github}
|
||||
options={props.data.githubStatsOptions}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Markdown
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react"
|
||||
import { icons, skills, skillWebsites } from "../constants/skills"
|
||||
import { githubStatsLinkGenerator, topLanguagesLinkGenerator } from "../utils/link-generators"
|
||||
|
||||
const MarkdownPreview = props => {
|
||||
const TitlePreview = props => {
|
||||
@@ -267,29 +268,22 @@ const MarkdownPreview = props => {
|
||||
}
|
||||
return null
|
||||
}
|
||||
const GitHubStatsPreview = props => {
|
||||
let link =
|
||||
"https://github-readme-stats.vercel.app/api?username=" +
|
||||
props.github +
|
||||
"&show_icons=true"
|
||||
if (props.show) {
|
||||
|
||||
const GitHubStatsPreview = ({github, options, show })=> {
|
||||
if (show) {
|
||||
return (
|
||||
<div className="text-center mx-4 mb-4">
|
||||
<img src={link} alt={props.github} />
|
||||
<img src={githubStatsLinkGenerator({github, options})} alt={github} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
const TopLanguagesPreview = props => {
|
||||
let link =
|
||||
"https://github-readme-stats.vercel.app/api/top-langs/?username=" +
|
||||
props.github +
|
||||
"&layout=compact"
|
||||
if (props.show) {
|
||||
const TopLanguagesPreview = ({github, options, show})=> {
|
||||
if (show) {
|
||||
return (
|
||||
<div className="text-center mx-4 mb-4">
|
||||
<img src={link} alt={props.github} />
|
||||
<img src={topLanguagesLinkGenerator({github, options})} alt={props.github} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -348,10 +342,12 @@ const MarkdownPreview = props => {
|
||||
<TopLanguagesPreview
|
||||
show={props.data.topLanguages}
|
||||
github={props.social.github}
|
||||
options={props.data.topLanguagesOptions}
|
||||
/>
|
||||
<GitHubStatsPreview
|
||||
show={props.data.githubStats}
|
||||
github={props.social.github}
|
||||
options={props.data.githubStatsOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-1
@@ -63,7 +63,25 @@ const DEFAULT_DATA = {
|
||||
badgeLabel: "Profile views",
|
||||
githubProfileTrophy: false,
|
||||
githubStats: false,
|
||||
githubStatsOptions: {
|
||||
theme: "",
|
||||
titleColor: "",
|
||||
textColor: "",
|
||||
bgColor: "",
|
||||
hideBorder: false,
|
||||
cacheSeconds: null,
|
||||
locale: "en",
|
||||
},
|
||||
topLanguages: false,
|
||||
topLanguagesOptions: {
|
||||
theme: "",
|
||||
titleColor: "",
|
||||
textColor: "",
|
||||
bgColor: "",
|
||||
hideBorder: false,
|
||||
cacheSeconds: null,
|
||||
locale: "en",
|
||||
},
|
||||
devDynamicBlogs: false,
|
||||
mediumDynamicBlogs: false,
|
||||
rssDynamicBlogs: false,
|
||||
@@ -455,7 +473,6 @@ const IndexPage = () => {
|
||||
setRestore("")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="m-4 sm:p-4">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const githubStatsStylingQueryString = options => {
|
||||
const params = {
|
||||
show_icons: true,
|
||||
...(options.theme && options.theme !== "none") && { theme: options.theme },
|
||||
...options.titleColor && { "title_color": options.titleColor },
|
||||
...options.textColor && { "text_color": options.textColor},
|
||||
...options.bgColor && { "bg_color": options.bgColor},
|
||||
...options.hideBorder && { "hide_border": options.hideBorder},
|
||||
...options.cacheSeconds && { "cache_seconds": options.cacheSeconds},
|
||||
...options.locale && { "locale": options.locale},
|
||||
}
|
||||
const query_string = Object.entries(params).map(([key, value]) => `${key}=${value}`).join("&")
|
||||
return query_string
|
||||
}
|
||||
|
||||
export const githubStatsLinkGenerator = ({github, options}) =>
|
||||
`https://github-readme-stats.vercel.app/api?username=${github}&${githubStatsStylingQueryString(options)}`
|
||||
|
||||
export const topLanguagesLinkGenerator = ({github, options}) =>
|
||||
`https://github-readme-stats.vercel.app/api/top-langs?username=${github}&${githubStatsStylingQueryString(options)}&layout=compact`
|
||||
Reference in New Issue
Block a user