UNPKG

@fajarkim/github-readme-profile

Version:

🙀 Generate your Stats GitHub Profile in SVG

219 lines (196 loc) • 9.83 kB
import type { GetData } from "./getData"; import type { UiConfig } from "../api/index"; import { locales, Locales } from "./translations"; import icons from "./icons"; import { parseBoolean } from "./common/utils"; /** * Generates the SVG markup for the GitHub stats card. * * @param {GetData} data - GitHub user data stats. * @param {UiConfig} uiConfig - Configuration for the UI card options. * @returns {string} - SVG markup for the GitHub stats card. */ function card(data: GetData, uiConfig: UiConfig): string { const fallbackLocale = "en"; const defaultLocale: Locales[keyof Locales] = locales[fallbackLocale]; const selectLocale: Locales[keyof Locales] = locales[uiConfig.Locale] || defaultLocale; const isRtlDirection = parseBoolean(selectLocale.rtlDirection); const isDisabledAnimations = parseBoolean(uiConfig.disabledAnimations || uiConfig.Format === "png"); const direction = isRtlDirection ? "rtl" : "ltr"; const angle = { titleXAngle: isDisabledAnimations ? (isRtlDirection ? 520 : 15) : (isRtlDirection ? 510 : 5), titleYAngle: isDisabledAnimations ? 0 : -10, textXAngle: isRtlDirection ? 215 : 20, dataXAngle: isRtlDirection ? 15 : 220, iconXAngle: isRtlDirection ? 225 : -5, imageXAngle: isDisabledAnimations ? 120 : 125, imageYAngle: isDisabledAnimations ? 70 : 65, userXAngle: isDisabledAnimations ? 119.9 : 109.9, userYAngle: isDisabledAnimations ? 140 : 130, follXAngle: isDisabledAnimations ? 120 : 110, follYAngle: isDisabledAnimations ? 161 : 151, }; const hideStroke = parseBoolean(uiConfig.hideStroke) ? `` : `stroke="#${uiConfig.strokeColor}" stroke-width="5"`; const hideBorder = parseBoolean(uiConfig.hideBorder) ? `` : `stroke="#${uiConfig.borderColor}" stroke-opacity="1" stroke-width="${uiConfig.borderWidth}"`; const animations = parseBoolean(uiConfig.disabledAnimations || uiConfig.Format === "png") ? `` : ` /* Animations */ @keyframes scaleInAnimation { from { transform: translate(-5px, 5px) scale(0); } to { transform: translate(-5px, 5px) scale(1); } } @keyframes fadeInAnimation { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeLeftInAnimation { from { opacity: 0; transform: translate(-90px, 10px); } to { opacity: 1; transform: translate(10px, 10px); } } .div-animation { animation: fadeLeftInAnimation 0.7s ease-in-out forwards; } .image-profile-animation { animation: scaleInAnimation 1.2s ease-in-out forwards; transform-origin: ${angle.imageXAngle}px ${angle.imageYAngle}px; } .single-item-animation { opacity: 0; animation: fadeInAnimation 0.3s ease-in-out forwards; }`; const hiddenItems = uiConfig.hiddenItems || ""; const hiddenItemsArray = hiddenItems.split(","); const showItems = uiConfig.showItems || ""; const showItemsArray = showItems.split(","); const cardItems = [ { text: selectLocale.totalReposText || defaultLocale.totalReposText, value: data.public_repos, icon: icons.repository, hidden: hiddenItemsArray.includes("repos") }, { text: selectLocale.starsCountText || defaultLocale.starsCountText, value: data.total_stars, icon: icons.star, hidden: hiddenItemsArray.includes("stars") }, { text: selectLocale.forksCountText || defaultLocale.forksCountText, value: data.total_forks, icon: icons.fork, hidden: hiddenItemsArray.includes("forks") }, { text: selectLocale.commitsCountText || defaultLocale.commitsCountText, value: data.total_commits, icon: icons.commit, hidden: hiddenItemsArray.includes("commits") }, { text: selectLocale.totalPRText || defaultLocale.totalPRText, value: data.total_prs, icon: icons.pull_request, hidden: hiddenItemsArray.includes("prs") }, { text: selectLocale.totalPRMergedText || defaultLocale.totalPRMergedText, value: data.total_prs_merged, icon: icons.pull_request_merged, hidden: hiddenItemsArray.includes("prs_merged") }, { text: selectLocale.totalPRReviewedText || defaultLocale.totalPRReviewedText, value: data.total_review, icon: icons.review, hidden: !showItemsArray.includes("reviews") }, { text: selectLocale.totalIssuesText || defaultLocale.totalIssuesText, value: data.total_issues, icon: icons.issue, hidden: hiddenItemsArray.includes("issues") }, { text: selectLocale.totalIssuesClosedText || defaultLocale.totalIssuesClosedText, value: data.total_closed_issues, icon: icons.issue_closed, hidden: !showItemsArray.includes("issues_closed") }, { text: selectLocale.totalDiscussionStartedText || defaultLocale.totalDiscussionStartedText, value: data.total_discussion_started, icon: icons.discussion_started, hidden: !showItemsArray.includes("discussions_started") }, { text: selectLocale.totalDiscussionAnsweredText || defaultLocale.totalDiscussionAnsweredText, value: data.total_discussion_answered, icon: icons.discussion_answered, hidden: !showItemsArray.includes("discussions_answered") }, { text: selectLocale.contributedToText || defaultLocale.contributedToText, value: data.total_contributed_to, icon: icons.contributed_to, hidden: hiddenItemsArray.includes("contributed") }, ]; const cardItemsToShow = cardItems.filter(item => !item.hidden); const cardItemsSVG = cardItemsToShow.map((item, index) => ` <g transform="translate(230, ${15 + index * 25})"> <g class="single-item-animation" style="animation-delay: ${210 + index * 100}ms" transform="translate(25, 0)"> <svg x="${angle.iconXAngle}" y="0" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16"> ${item.icon} </svg> <text class="text" x="${angle.textXAngle}" y="12.5">${item.text}:</text> <text class="text text-bold" x="${angle.dataXAngle}" y="12.5">${item.value}</text> </g> </g>`).join("\n"); function generateGradient(colors: string[]): string { const gradientId = "gradient"; const gradientAngle = colors[0]; const getColors = colors.slice(1); const gradientStops = getColors.map((color, index) => { const offset = (index * 100) / (getColors.length - 1); return `<stop offset="${offset}%" stop-color="#${color}"/>`; }).join(""); return ` <defs> <linearGradient id="${gradientId}" gradientTransform="rotate(${gradientAngle})" gradientUnits="userSpaceOnUse"> ${gradientStops} </linearGradient> </defs> <rect x="0.5" y="0.5" rx="${uiConfig.borderRadius}" height="99.4%" width="99.8%" fill="url(#${gradientId})" ${hideBorder}/>`; } let backgroundSVG; if (uiConfig.bgColor) { if (Array.isArray(uiConfig.bgColor)) { backgroundSVG = generateGradient(uiConfig.bgColor); } else if (typeof uiConfig.bgColor === 'string') { const gradientHexArray = uiConfig.bgColor.split(','); if (gradientHexArray.length >= 2) { const gradientColors = gradientHexArray.map(color => color.trim()); backgroundSVG = generateGradient(gradientColors); } else { backgroundSVG = ` <rect x="0.5" y="0.5" rx="${uiConfig.borderRadius}" height="99.4%" width="99.8%" fill="#${uiConfig.bgColor}" ${hideBorder}/>`; } } } return ` <svg width="535" height="${Math.max(220, 45 + cardItemsToShow.length * 25)}" direction="${direction}" viewBox="0 0 535 ${Math.max(220, 45 + cardItemsToShow.length * 25)}" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <style> ${animations} .text { font-family: "Segoe UI", Ubuntu, sans-serif; fill: #${uiConfig.textColor}; font-size: 14px; } .text-bold { font-weight: 700; } .text-middle { alignment-baseline: middle; text-anchor: middle; } .text-followers { font-family: "Segoe UI", Ubuntu, sans-serif; fill: #${uiConfig.textColor}; font-size: 13px; } .text-username { font-family: "Segoe UI", Ubuntu, sans-serif; fill: #${uiConfig.usernameColor}; font-weight: 750; font-size: 14.6px; alignment-baseline: middle; text-anchor: middle; } .text-title { font-family: "Segoe UI", Ubuntu, sans-serif; fill: #${uiConfig.titleColor}; font-size: 17px; font-weight: 600; } .icon { fill: #${uiConfig.iconColor}; display: block; } </style> <title id="titleId">${selectLocale.titleCard.split("{name}").join(data.name) || defaultLocale.titleCard.split("{name}").join(data.name)}</title> ${backgroundSVG} <g transform="translate(0, 25)"> <g class="div-animation"> <text x="${angle.titleXAngle}" y="${angle.titleYAngle}" class="text-title">${selectLocale.titleCard.split("{name}").join(data.name) || defaultLocale.titleCard.split("{name}").join(data.name)}</text> </g> <g class="image-profile-animation"> <defs> <pattern id="image" x="0%" y="0%" height="100%" width="100%" viewBox="0 0 512 512"> <image x="0%" y="0%" width="512" height="512" href="data:image/jpeg;base64,${data.picture}"></image> </pattern> </defs> <circle cx="${angle.imageXAngle}" cy="${angle.imageYAngle}" r="50" fill="url(#image)" ${hideStroke}/> </g> <text x="${angle.userXAngle}" y="${angle.userYAngle}" direction="ltr" class="text-username div-animation">@${data.username}</text> <g class="div-animation text-middle"> <text x="${angle.follXAngle}" y="${angle.follYAngle}" class="text-followers"><tspan class="text-bold">${data.followers}</tspan> ${selectLocale.followersText || defaultLocale.followersText} · <tspan class="text-bold">${data.following}</tspan> ${selectLocale.followingText || defaultLocale.followingText}</text> </g> ${cardItemsSVG} </g> </svg>`; } export default card;