import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { withPrefix } from 'gatsby'; import { ToolsIcon, XCircleIcon } from '@primer/octicons-react'; import latestBlogs from '../utils/workflows'; import links from '../constants/page-links'; import { isMediumUsernameValid, isGitHubUsernameValid } from '../utils/validation'; const AddonsItem = (props) => { const { inputId, inputChecked, onInputChange, Options, children } = 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="checkbox-label flex items-center"> <input id={inputId} type="checkbox" className="checkbox-label__input" checked={inputChecked} onChange={onInputChange} /> <span className="checkbox-label__control" /> <span className="pl-4">{children}</span> </label> {Options && ( <button type="button" id={`${inputId}-open-btn`} 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} </> ); }; AddonsItem.propTypes = { inputId: PropTypes.string.isRequired, inputChecked: PropTypes.bool.isRequired, onInputChange: PropTypes.func.isRequired, Options: PropTypes.element.isRequired, children: PropTypes.element.isRequired, }; 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> ); CustomizeOptions.propTypes = { title: PropTypes.string.isRequired, CustomizationOptions: PropTypes.element.isRequired, }; const CustomizeBadge = ({ githubName, badgeOptions, onBadgeUpdate }) => ( <> <label htmlFor="badge-style"> Style: <select id="badge-style" onChange={(e) => onBadgeUpdate('badgeStyle', e.target.value)} value={badgeOptions.badgeStyle} > <option value="flat">Flat</option> <option value="flat-square">Flat Square</option> <option value="plastic">Plastic</option> </select> </label> <label htmlFor="badge-color"> Color: <input type="color" id="badge-color" defaultValue={`#${badgeOptions.badgeColor}`} className="w-6" onChange={(e) => onBadgeUpdate('badgeColor', e.target.value.replace('#', ''))} /> </label> <label htmlFor="badge-label-text"> Label Text: <input type="text" id="badge-label-text" placeholder="Profile views" className="w-2/4 bg-gray-300 pl-2" onChange={(e) => onBadgeUpdate('badgeLabel', e.target.value.trim())} defaultValue={badgeOptions.badgeLabel} /> </label> <span className="mt-2 flex items-center"> Preview: {isGitHubUsernameValid(githubName) ? ( <img src={`https://komarev.com/ghpvc/?username=${githubName}&label=${encodeURI(badgeOptions.badgeLabel)}&color=${ badgeOptions.badgeColor }&style=${badgeOptions.badgeStyle}`} alt="profile-visitors-count" /> ) : ( <span className="text-xxs md:text-sm text-red-600">Invalid GitHub username</span> )} </span> </> ); CustomizeBadge.propTypes = { githubName: PropTypes.string.isRequired, badgeOptions: PropTypes.object.isRequired, onBadgeUpdate: PropTypes.func.isRequired, }; 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`} className="checkbox-label"> Hide border: <input id={`${prefix}-hide-border`} type="checkbox" className="checkbox-label__input" checked={options.hideBorder} onChange={(e) => onUpdate('hideBorder', e.target.checked)} /> <span className="checkbox-label__control" /> </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> </> ); CustomizeGithubStatsBase.propTypes = { prefix: PropTypes.string.isRequired, options: PropTypes.object.isRequired, onUpdate: PropTypes.func.isRequired, }; const CustomizeStreakStats = ({ prefix, options, onUpdate }) => ( <> <label htmlFor={`${prefix}-theme`}> Theme: <select id={`${prefix}-theme`} onChange={({ target: { value } }) => onUpdate('theme', value)} defaultValue={options.theme} > <option value="default">default</option> <option value="dark">dark</option> <option value="highcontrast">highcontrast</option> </select> </label> </> ); CustomizeStreakStats.propTypes = { prefix: PropTypes.string.isRequired, options: PropTypes.object.isRequired, onUpdate: PropTypes.func.isRequired, }; const Addons = (props) => { const { data, social, handleDataChange, handleCheckChange } = props; const [debounce, setDebounce] = useState(undefined); const [badgeOptions, setBadgeOptions] = useState({ badgeStyle: data.badgeStyle, badgeColor: data.badgeColor, badgeLabel: data.badgeLabel, }); useEffect(() => { setBadgeOptions({ badgeStyle: data.badgeStyle, badgeColor: data.badgeColor, badgeLabel: data.badgeLabel, }); }, [data.badgeStyle, data.badgeColor, data.badgeLabel]); const [githubStatsOptions, setGithubStatsOptions] = useState({ ...data.githubStatsOptions, }); useEffect(() => { setGithubStatsOptions({ ...data.githubStatsOptions, }); }, [data.githubStatsOptions]); const [topLanguagesOptions, setTopLanguagesOptions] = useState({ ...data.topLanguagesOptions, }); useEffect(() => { setTopLanguagesOptions({ ...data.topLanguagesOptions, }); }, [data.topLanguagesOptions]); const [streakStatsOptions, setStreakStatsOptions] = useState({ ...data.streakStatsOptions, }); useEffect(() => { setStreakStatsOptions({ ...data.streakStatsOptions, }); }, [data.streakStatsOptions]); const blogPostPorkflow = () => { const payload = { dev: { show: data.devDynamicBlogs, username: social.dev, }, medium: { show: data.mediumDynamicBlogs, username: social.medium, }, rssurl: { show: data.rssDynamicBlogs, username: social.rssurl, }, }; const actionContent = latestBlogs(payload); const tempElement = document.createElement('a'); tempElement.setAttribute('href', `data:text/yaml;charset=utf-8,${encodeURIComponent(actionContent)}`); tempElement.setAttribute('download', 'blog-post-workflow.yml'); tempElement.style.display = 'none'; document.body.appendChild(tempElement); tempElement.click(); document.body.removeChild(tempElement); }; const onBadgeUpdate = (option, value) => { const callback = () => { const newVal = option === 'badgeLabel' && value === '' ? 'Profile views' : value; setBadgeOptions({ ...badgeOptions, [option]: newVal }); handleDataChange(option, { target: { value: newVal } }); }; clearTimeout(debounce); setDebounce(setTimeout(callback, 300)); }; const onStatsUpdate = (option, value) => { const newStatsOptions = { ...githubStatsOptions, [option]: value }; setGithubStatsOptions(newStatsOptions); handleDataChange('githubStatsOptions', { target: { value: newStatsOptions }, }); }; const onTopLangUpdate = (option, value) => { const newLangOptions = { ...topLanguagesOptions, [option]: value }; setTopLanguagesOptions(newLangOptions); handleDataChange('topLanguagesOptions', { target: { value: newLangOptions }, }); }; const onStreakStatsUpdate = (option, value) => { const newStreakStatsOptions = { ...streakStatsOptions, [option]: value }; setStreakStatsOptions(newStreakStatsOptions); handleDataChange('streakStatsOptions', { target: { value: newStreakStatsOptions }, }); }; 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">Add-ons</div> <AddonsItem inputId="visitors-count" inputChecked={data.visitorsBadge} onInputChange={() => handleCheckChange('visitorsBadge')} Options={ <CustomizeOptions title="Customize Badge" CustomizationOptions={ <CustomizeBadge githubName={social.github} badgeOptions={badgeOptions} onBadgeUpdate={onBadgeUpdate} /> } /> } > display visitors count badge </AddonsItem> <AddonsItem inputId="github-profile-trophy" inputChecked={data.githubProfileTrophy} onInputChange={() => handleCheckChange('githubProfileTrophy')} > display github trophy </AddonsItem> <AddonsItem inputId="github-stats" inputChecked={data.githubStats} onInputChange={() => handleCheckChange('githubStats')} Options={ <CustomizeOptions title="Customize Github Stats Card" CustomizationOptions={ <CustomizeGithubStatsBase prefix="stats" options={githubStatsOptions} onUpdate={onStatsUpdate} /> } /> } > display github profile stats card </AddonsItem> <AddonsItem inputId="top-languages" inputChecked={data.topLanguages} onInputChange={() => handleCheckChange('topLanguages')} Options={ <CustomizeOptions title="Customize Top Skills Card" CustomizationOptions={ <CustomizeGithubStatsBase prefix="top-lang" options={topLanguagesOptions} onUpdate={onTopLangUpdate} /> } /> } > display top skills </AddonsItem> <AddonsItem inputId="streak-stats" inputChecked={data.streakStats} onInputChange={() => handleCheckChange('streakStats')} Options={ <CustomizeOptions title="Customize Streak Stats Card" CustomizationOptions={ <CustomizeStreakStats prefix="streak-stats" options={streakStatsOptions} onUpdate={onStreakStatsUpdate} /> } /> } > display github streak stats </AddonsItem> <AddonsItem inputId="twitter-badge" inputChecked={data.twitterBadge} onInputChange={() => handleCheckChange('twitterBadge')} > display twitter badge </AddonsItem> <AddonsItem inputId="dev-dynamic-blogs" inputChecked={data.devDynamicBlogs} onInputChange={() => handleCheckChange('devDynamicBlogs')} > display latest dev.to blogs dynamically (GitHub Action) </AddonsItem> <AddonsItem inputId="medium-dynamic-blogs" inputChecked={data.mediumDynamicBlogs} onInputChange={() => handleCheckChange('mediumDynamicBlogs')} > display latest medium blogs dynamically (GitHub Action) </AddonsItem> <AddonsItem inputId="rss-dynamic-blogs" inputChecked={data.rssDynamicBlogs} onInputChange={() => handleCheckChange('rssDynamicBlogs')} > display latest blogs from your personal blog dynamically (GitHub Action) </AddonsItem> {(data.devDynamicBlogs && social.dev) || (data.rssDynamicBlogs && social.rssurl) || (data.mediumDynamicBlogs && social.medium && isMediumUsernameValid(social.medium)) ? ( <div className="workflow"> <div> download <span id="blog-post-worklow-span" onClick={blogPostPorkflow} onKeyDown={(e) => e.keyCode === 13 && blogPostPorkflow()} role="button" tabIndex="0" style={{ cursor: 'pointer', color: '#002ead' }} > {' '} blog-post-workflow.yml </span>{' '} file(learn <a href={withPrefix(links.addons)} target="blank" style={{ color: '#002ead' }}> {' '} how to setup </a> ) </div> </div> ) : ( '' )} </div> ); }; export default Addons; Addons.propTypes = { data: PropTypes.object.isRequired, social: PropTypes.object.isRequired, handleDataChange: PropTypes.func.isRequired, handleCheckChange: PropTypes.func.isRequired, };