├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── commit-preview.yml │ ├── deploy-pages.yml │ ├── pr-preview.yml │ └── release.yml ├── .gitignore ├── .storybook ├── main.ts └── preview.ts ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── noto-sans-v27-latin-regular.ttf ├── scripts └── generate.ts ├── src ├── OGCard.stories.tsx ├── OGCard.tsx ├── decorations.tsx ├── index.css ├── index.ts ├── logo.tsx ├── main.tsx ├── rating.ts └── theme.ts ├── tsconfig.json ├── vite.config.ts └── vite.playground.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "gitroll-dev/gitroll-profile-card" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/commit-preview.yml: -------------------------------------------------------------------------------- 1 | name: Commit Preview 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["PR Preview"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | commit: 11 | runs-on: ubuntu-latest 12 | if: github.event.workflow_run.conclusion == 'success' 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | steps: 18 | - name: Download preview artifact 19 | uses: dawidd6/action-download-artifact@v7 20 | with: 21 | run_id: ${{ github.event.workflow_run.id }} 22 | name: preview-image 23 | path: . 24 | 25 | - name: Download PR info artifact 26 | uses: dawidd6/action-download-artifact@v7 27 | with: 28 | run_id: ${{ github.event.workflow_run.id }} 29 | name: pr-info 30 | path: . 31 | 32 | - name: Read PR info 33 | id: pr-info 34 | run: | 35 | echo "pr_number=$(cat pr_number.txt)" >> $GITHUB_OUTPUT 36 | echo "commit_sha=$(cat commit_sha.txt)" >> $GITHUB_OUTPUT 37 | echo "new_themes=$(cat new_themes.txt)" >> $GITHUB_OUTPUT 38 | echo "theme=$(cat theme.txt)" >> $GITHUB_OUTPUT 39 | 40 | - name: Setup preview branch 41 | run: | 42 | # Configure git 43 | git config --global user.name 'github-actions[bot]' 44 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 45 | 46 | # Create a temporary directory for git operations 47 | mkdir -p temp_git 48 | cd temp_git 49 | 50 | REPO_URL="https://github.com/${{ github.repository }}.git" 51 | 52 | # Clone only the preview branch, or create new if doesn't exist 53 | if git ls-remote --heads $REPO_URL previews | grep -q 'refs/heads/previews'; then 54 | git clone --branch previews --single-branch $REPO_URL . 55 | else 56 | git clone $REPO_URL . 57 | git checkout --orphan previews 58 | git rm -rf . 59 | git clean -fxd 60 | fi 61 | 62 | # Setup the preview branch 63 | mkdir -p previews 64 | 65 | # Copy the new preview with PR number and commit SHA from parent directory 66 | cp ../preview.png "previews/pr-${{ steps.pr-info.outputs.pr_number }}-${{ steps.pr-info.outputs.commit_sha }}.png" 67 | 68 | # Commit and push 69 | git add previews/ 70 | git commit -m "Update preview for PR #${{ steps.pr-info.outputs.pr_number }} commit ${{ steps.pr-info.outputs.commit_sha }}" || echo "No changes to commit" 71 | git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git previews 72 | 73 | - name: Find Comment 74 | uses: peter-evans/find-comment@v3 75 | id: find-comment 76 | with: 77 | issue-number: ${{ steps.pr-info.outputs.pr_number }} 78 | comment-author: "github-actions[bot]" 79 | body-includes: "### GitRoll Preview Cards" 80 | 81 | - name: Create comment body 82 | id: create-comment 83 | run: | 84 | IMAGE_URL="https://raw.githubusercontent.com/${{ github.repository }}/previews/previews/pr-${{ steps.pr-info.outputs.pr_number }}-${{ steps.pr-info.outputs.commit_sha }}.png" 85 | 86 | echo "body<> $GITHUB_OUTPUT 87 | echo "### GitRoll Preview Cards" >> $GITHUB_OUTPUT 88 | echo "" >> $GITHUB_OUTPUT 89 | if [[ -n "${{ steps.pr-info.outputs.new_themes }}" ]]; then 90 | echo "New theme(s) detected: \`${{ steps.pr-info.outputs.new_themes }}\`" >> $GITHUB_OUTPUT 91 | else 92 | echo "No new theme detected, using: \`${{ steps.pr-info.outputs.theme }}\`" >> $GITHUB_OUTPUT 93 | fi 94 | echo "" >> $GITHUB_OUTPUT 95 | echo "![Preview Cards]($IMAGE_URL)" >> $GITHUB_OUTPUT 96 | echo "" >> $GITHUB_OUTPUT 97 | echo "These are preview cards showing possible ratings. Get your real score at [GitRoll.io](https://gitroll.io)" >> $GITHUB_OUTPUT 98 | echo "EOF" >> $GITHUB_OUTPUT 99 | 100 | - name: Create or update comment 101 | uses: peter-evans/create-or-update-comment@v4 102 | with: 103 | comment-id: ${{ steps.find-comment.outputs.comment-id }} 104 | issue-number: ${{ steps.pr-info.outputs.pr_number }} 105 | body: ${{ steps.create-comment.outputs.body }} 106 | edit-mode: replace 107 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | deploy: 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Setup pnpm 33 | uses: pnpm/action-setup@v4 34 | with: 35 | run_install: true 36 | 37 | - name: Install dependencies 38 | run: pnpm install 39 | 40 | - name: Build playground 41 | run: pnpm build:playground 42 | 43 | - name: Setup Pages 44 | uses: actions/configure-pages@v5 45 | 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v3 48 | with: 49 | path: "playground" 50 | 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | -------------------------------------------------------------------------------- /.github/workflows/pr-preview.yml: -------------------------------------------------------------------------------- 1 | name: PR Preview 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | preview: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pull-requests: write 12 | 13 | steps: 14 | - name: Checkout PR Branch 15 | uses: actions/checkout@v4 16 | 17 | - name: Checkout main branch 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | path: main-branch 22 | 23 | - uses: pnpm/action-setup@v4 24 | with: 25 | run_install: true 26 | 27 | - name: Find new theme 28 | id: get-theme 29 | run: | 30 | # Extract themes from both branches 31 | PR_THEMES=$(pnpm tsx -e "import { preset } from './src/theme'; console.log(Object.keys(preset).join(','))") 32 | MAIN_THEMES=$(pnpm tsx -e "import { preset } from './main-branch/src/theme'; console.log(Object.keys(preset).join(','))") 33 | 34 | # Convert to arrays 35 | IFS=',' read -ra PR_ARRAY <<< "$PR_THEMES" 36 | IFS=',' read -ra MAIN_ARRAY <<< "$MAIN_THEMES" 37 | 38 | # Find new themes 39 | NEW_THEMES=() 40 | for theme in "${PR_ARRAY[@]}"; do 41 | if [[ ! " ${MAIN_ARRAY[@]} " =~ " ${theme} " ]]; then 42 | NEW_THEMES+=("$theme") 43 | fi 44 | done 45 | 46 | if [ ${#NEW_THEMES[@]} -eq 0 ]; then 47 | THEME="light" 48 | echo "No new theme found, using default: $THEME" 49 | else 50 | THEME="${NEW_THEMES[0]}" 51 | echo "New theme found: $THEME" 52 | fi 53 | 54 | echo "theme=$THEME" >> $GITHUB_OUTPUT 55 | 56 | # Store all new themes for the comment 57 | if [ ${#NEW_THEMES[@]} -gt 0 ]; then 58 | echo "new_themes=${NEW_THEMES[*]}" >> $GITHUB_OUTPUT 59 | fi 60 | 61 | - name: Generate preview grid 62 | run: | 63 | pnpm generate --grid --theme ${{ steps.get-theme.outputs.theme }} -o preview.png || { 64 | echo "::error::Failed to generate preview cards" 65 | exit 1 66 | } 67 | 68 | - name: Upload preview artifact 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: preview-image 72 | path: preview.png 73 | retention-days: 1 74 | 75 | - name: Save PR info 76 | run: | 77 | echo "${{ github.event.pull_request.number }}" > pr_number.txt 78 | echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt 79 | echo "${{ steps.get-theme.outputs.new_themes }}" > new_themes.txt 80 | echo "${{ steps.get-theme.outputs.theme }}" > theme.txt 81 | 82 | - name: Upload PR info artifact 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: pr-info 86 | path: | 87 | pr_number.txt 88 | commit_sha.txt 89 | new_themes.txt 90 | theme.txt 91 | retention-days: 1 92 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Package 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 60 13 | environment: 14 | name: publish 15 | permissions: 16 | packages: write 17 | contents: write 18 | issues: write 19 | pull-requests: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v4 23 | with: 24 | run_install: true 25 | - run: pnpm build 26 | - uses: changesets/action@v1 27 | with: 28 | publish: pnpm changeset publish 29 | version: pnpm changeset version 30 | title: Release Packages 31 | commit: bump versions 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | *storybook.log 27 | 28 | playground/ 29 | /preview.png 30 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite' 2 | 3 | 4 | const config: StorybookConfig = { 5 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 6 | addons: [ 7 | '@storybook/addon-onboarding', 8 | '@storybook/addon-essentials', 9 | '@chromatic-com/storybook', 10 | '@storybook/addon-interactions', 11 | ], 12 | core: { 13 | builder: '@storybook/builder-vite', 14 | }, 15 | framework: { 16 | name: '@storybook/react-vite', 17 | options: {}, 18 | }, 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react' 2 | 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | }, 13 | } 14 | 15 | export default preview 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @gitroll/profile-card 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Use full text of "maintainability" and shorten the bars. 8 | 9 | ## 0.2.0 10 | 11 | ### Minor Changes 12 | 13 | - [#12](https://github.com/gitroll-dev/gitroll-profile-card/pull/12) [`f7c4773`](https://github.com/gitroll-dev/gitroll-profile-card/commit/f7c4773a2630762e8b366646014fbad83c3614df) Thanks [@JacobLinCool](https://github.com/JacobLinCool)! - kawaiiCat theme 14 | 15 | ## 0.1.0 16 | 17 | ### Minor Changes 18 | 19 | - [#3](https://github.com/gitroll-dev/gitroll-profile-card/pull/3) [`b56b76e`](https://github.com/gitroll-dev/gitroll-profile-card/commit/b56b76eb6a7998a5f845723748be9a90a9852d08) Thanks [@JacobLinCool](https://github.com/JacobLinCool)! - Add midnight theme 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 GitRoll 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitRoll Profile Card 2 | 3 | [Online Playground](https://gitroll-dev.github.io/gitroll-profile-card/) 4 | 5 | ## Available Themes 6 | 7 | We now have 7 preset themes to choose from! 🎉 8 | 9 | - **`light`** (default) 10 | - **`dark`** 11 | - **`sepia`** 12 | - **`solarizedLight`** 13 | - **`solarizedDark`** 14 | - **`tokyoNight`** 15 | - **`nord`** 16 | - **`midnight`** 17 | - **`kawaiiCat`** 18 | 19 | To use a preset theme, simply add the `theme` query parameter to the image URL. For example: 20 | 21 | ``` 22 | https://gitroll.io/api/badges/profiles/v1/uZxjMB3mkXpQQPskvTMcp0UeqPJA3?theme=nord 23 | ``` 24 | 25 | ## Contributing 26 | 27 | We welcome contributions to GitRoll Profile Card! 28 | 29 | ### Adding New Themes 30 | 31 | To keep the project simple and ensure the themes are useful to the community, any new theme must gather **at least 3 emoji reactions** from the community before the pull request (PR) is merged. This process ensures that the theme resonates with the users and maintains the quality of the themes offered. 32 | 33 | If you'd like to propose a new theme: 34 | 35 | 1. Fork the repository. 36 | 2. Develop your theme in a new branch. 37 | 3. Submit a PR for community review. 38 | 4. Gather at least 3 emoji reactions from the community to proceed with merging. 39 | 40 | ## Developing and Testing Themes 41 | 42 | To help you develop new themes and preview your work, please use the playground. 43 | 44 | ### Online Playground 45 | 46 | You can easily preview and test your themes using our [Online Playground](https://gitroll-dev.github.io/gitroll-profile-card/). 47 | 48 | The playground allows you to: 49 | 50 | - Preview your card in real-time with **Hot Module Replacement (HMR)**. 51 | - Try out different preset themes. 52 | - Test with custom properties, such as username, ranks, and scores. 53 | 54 | ### Running the Playground Locally 55 | 56 | To run the playground locally: 57 | 58 | 1. Clone the Repository 59 | 60 | ```sh 61 | git clone https://github.com/gitroll-dev/gitroll-profile-card.git 62 | ``` 63 | 64 | 2. Install Dependencies 65 | 66 | ```sh 67 | pnpm install 68 | ``` 69 | 70 | 3. Start the Development Server 71 | 72 | ```sh 73 | pnpm dev 74 | ``` 75 | 76 | 4. Visit the Playground 77 | 78 | Open your browser and visit . 79 | 80 | This is a great way to experiment with different configurations and see how your card will look before submitting a PR. 81 | 82 | ## Feedback and Support 83 | 84 | If you encounter any issues or have suggestions, please open an issue on our [GitHub Issues page](https://github.com/gitroll-dev/gitroll-profile-card/issues). Your feedback is valuable to us and helps make GitRoll Profile Card better for everyone. 85 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import importPlugin from 'eslint-plugin-import' 2 | import js from '@eslint/js' 3 | import stylistic from '@stylistic/eslint-plugin' 4 | import tseslint from 'typescript-eslint' 5 | import { includeIgnoreFile } from '@eslint/compat' 6 | import path from 'node:path' 7 | import { fileURLToPath } from 'node:url' 8 | 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = path.dirname(__filename) 12 | const gitignorePath = path.resolve(__dirname, '.gitignore') 13 | 14 | 15 | export default [ 16 | includeIgnoreFile(gitignorePath), 17 | js.configs.recommended, 18 | importPlugin.flatConfigs.recommended, 19 | ...tseslint.configs.recommended, 20 | { 21 | files: ['**/*.{js,mjs,cjs,ts,tsx}'], 22 | languageOptions: { 23 | ecmaVersion: 'latest', 24 | sourceType: 'module', 25 | }, 26 | plugins: { 27 | '@stylistic': stylistic 28 | }, 29 | rules: { 30 | '@stylistic/indent': ['warn', 2, { SwitchCase: 1 }], 31 | '@stylistic/quotes': ['error', 'single'], 32 | '@stylistic/semi': ['error', 'never'], 33 | 'import/newline-after-import': ['warn', { count: 2 }], 34 | 'react/jsx-uses-react': 'off', 35 | 'react/react-in-jsx-scope': 'off' 36 | }, 37 | settings: { 38 | 'import/resolver': { 39 | typescript: true, 40 | node: true, 41 | }, 42 | } 43 | }, 44 | ] -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GitRoll Dev Card Playground 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitroll/profile-card", 3 | "version": "0.2.1", 4 | "type": "module", 5 | "exports": { 6 | ".": { 7 | "types": "./dist/index.d.ts", 8 | "import": "./dist/index.es.js", 9 | "require": "./dist/index.cjs.js" 10 | } 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "dev": "vite", 17 | "build": "tsc && vite build", 18 | "build:playground": "vite build -c vite.playground.config.ts", 19 | "lint": "eslint .", 20 | "preview": "vite preview", 21 | "storybook": "storybook dev -p 6006", 22 | "build-storybook": "storybook build", 23 | "changeset": "changeset", 24 | "generate": "tsx scripts/generate.ts" 25 | }, 26 | "peerDependencies": { 27 | "react": "^18.3.1", 28 | "react-dom": "^18.3.1" 29 | }, 30 | "devDependencies": { 31 | "@changesets/changelog-github": "^0.5.0", 32 | "@changesets/cli": "^2.27.10", 33 | "@chromatic-com/storybook": "^3.2.2", 34 | "@eslint/compat": "^1.2.2", 35 | "@eslint/js": "^9.14.0", 36 | "@storybook/addon-essentials": "^8.4.2", 37 | "@storybook/addon-interactions": "^8.4.2", 38 | "@storybook/addon-onboarding": "^8.4.2", 39 | "@storybook/blocks": "^8.4.2", 40 | "@storybook/react": "^8.4.2", 41 | "@storybook/react-vite": "^8.4.2", 42 | "@storybook/test": "^8.4.2", 43 | "@stylistic/eslint-plugin": "^2.10.1", 44 | "@types/node": "^22.8.6", 45 | "@types/react": "^18.3.12", 46 | "@types/react-dom": "^18.3.1", 47 | "@typescript-eslint/eslint-plugin": "^8.14.0", 48 | "@typescript-eslint/parser": "^8.14.0", 49 | "@vitejs/plugin-react": "^4.3.3", 50 | "eslint": "^9.14.0", 51 | "eslint-config-prettier": "^9.1.0", 52 | "eslint-import-resolver-typescript": "^3.6.3", 53 | "eslint-plugin-import": "^2.31.0", 54 | "eslint-plugin-prettier": "^5.2.1", 55 | "eslint-plugin-react": "^7.37.2", 56 | "eslint-plugin-react-hooks": "^5.0.0", 57 | "eslint-plugin-react-refresh": "^0.4.14", 58 | "eslint-plugin-storybook": "^0.11.0", 59 | "globals": "^15.11.0", 60 | "satori": "^0.11.3", 61 | "sharp": "^0.33.5", 62 | "storybook": "^8.4.2", 63 | "tsx": "^4.19.2", 64 | "typescript": "~5.6.3", 65 | "typescript-eslint": "^8.12.2", 66 | "vite": "^5.4.10", 67 | "vite-plugin-dts": "^4.3.0" 68 | }, 69 | "packageManager": "pnpm@9.13.2" 70 | } 71 | -------------------------------------------------------------------------------- /public/noto-sans-v27-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitroll-dev/gitroll-profile-card/88228101f9145bcd54a78ff67e6e569c6586f4d4/public/noto-sans-v27-latin-regular.ttf -------------------------------------------------------------------------------- /scripts/generate.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { fileURLToPath } from 'node:url' 3 | import { join } from 'node:path' 4 | import satori from 'satori' 5 | import sharp from 'sharp' 6 | import { OGCard, OGCardProps } from '../src/OGCard' 7 | import { Rating } from '../src/rating' 8 | import { preset } from '../src/theme' 9 | 10 | 11 | interface CliOptions { 12 | theme?: keyof typeof preset; 13 | output?: string; 14 | user?: string; 15 | avatar?: string; 16 | devType?: string; 17 | contributor?: boolean; 18 | format?: 'svg' | 'png'; 19 | grid?: boolean; 20 | } 21 | 22 | function parseArgs(): CliOptions { 23 | const args = process.argv.slice(2) 24 | const options: CliOptions = {} 25 | 26 | for (let i = 0; i < args.length; i++) { 27 | const arg = args[i] 28 | switch (arg) { 29 | case '--theme': 30 | case '-t': { 31 | options.theme = args[++i] as keyof typeof preset 32 | break 33 | } 34 | case '--output': 35 | case '-o': { 36 | options.output = args[++i] 37 | break 38 | } 39 | case '--user': 40 | case '-u': { 41 | options.user = args[++i] 42 | break 43 | } 44 | case '--avatar': 45 | case '-a': { 46 | options.avatar = args[++i] 47 | break 48 | } 49 | case '--dev-type': 50 | case '-d': { 51 | options.devType = args[++i] 52 | break 53 | } 54 | case '--contributor': 55 | case '-c': { 56 | options.contributor = true 57 | break 58 | } 59 | case '--format': 60 | case '-f': { 61 | const format = args[++i].toLowerCase() 62 | if (format !== 'svg' && format !== 'png') { 63 | console.error('Error: Format must be either "svg" or "png"') 64 | process.exit(1) 65 | } 66 | options.format = format as 'svg' | 'png' 67 | break 68 | } 69 | case '--grid': 70 | case '-g': { 71 | options.grid = true 72 | options.format = 'png' // Force PNG for grid mode 73 | break 74 | } 75 | case '--help': 76 | case '-h': { 77 | printHelp() 78 | process.exit(0) 79 | } 80 | } 81 | } 82 | 83 | // Detect format from output filename if not explicitly specified 84 | if (!options.format && options.output) { 85 | const ext = options.output.toLowerCase().split('.').pop() 86 | if (ext === 'png') options.format = 'png' 87 | else if (ext === 'svg') options.format = 'svg' 88 | } 89 | 90 | options.format = options.format || 'svg' 91 | 92 | return options 93 | } 94 | 95 | function printHelp() { 96 | console.log(` 97 | Usage: generate [options] 98 | 99 | Options: 100 | -t, --theme Theme to use (light/dark) 101 | -o, --output Output file path (defaults to stdout) 102 | -u, --user GitHub username 103 | -a, --avatar Avatar URL 104 | -d, --dev-type Developer type 105 | -c, --contributor Mark as contributor 106 | -f, --format Output format (svg/png) 107 | -g, --grid Generate a grid of all ratings 108 | -h, --help Show this help message 109 | `) 110 | } 111 | 112 | async function generateCard(props: OGCardProps) { 113 | const fontData = fs.readFileSync(fileURLToPath(join(import.meta.url, '../../public/noto-sans-v27-latin-regular.ttf'))) 114 | 115 | return await satori(OGCard(props), { 116 | width: 1200, 117 | height: 675, 118 | fonts: [ 119 | { 120 | name: 'sans serif', 121 | data: fontData, 122 | weight: 700, 123 | style: 'normal', 124 | }, 125 | ], 126 | }) 127 | } 128 | 129 | async function generateNoticeCard() { 130 | const svg = ` 131 | 132 | 133 | 134 | Preview Only 135 | 136 | 137 | These are demo cards showing possible ratings. 138 | 139 | 140 | Get your real score at GitRoll.io 141 | 142 | 143 | ` 144 | return svg 145 | } 146 | 147 | async function generateGrid(baseProps: OGCardProps) { 148 | const ratings = [Rating.S, Rating.A, Rating.B, Rating.C, Rating.D] 149 | const scores = { 150 | [Rating.S]: '9.00', 151 | [Rating.A]: '7.50', 152 | [Rating.B]: '6.00', 153 | [Rating.C]: '4.50', 154 | [Rating.D]: '3.00', 155 | } 156 | const cdfs = { 157 | [Rating.S]: '99', 158 | [Rating.A]: '85', 159 | [Rating.B]: '65', 160 | [Rating.C]: '35', 161 | [Rating.D]: '15', 162 | } 163 | 164 | // Generate all rating cards 165 | const ratingCards = await Promise.all( 166 | ratings.map(async (rating) => { 167 | const props = { 168 | ...baseProps, 169 | overallRating: rating, 170 | overallScore: scores[rating], 171 | overallScoreCDF: cdfs[rating], 172 | } 173 | const svg = await generateCard(props) 174 | return sharp(Buffer.from(svg)).toFormat('png').toBuffer() 175 | }) 176 | ) 177 | 178 | // Generate simple notice card 179 | const noticeSvg = await generateNoticeCard() 180 | const noticeCard = await sharp(Buffer.from(noticeSvg)).toFormat('png').toBuffer() 181 | 182 | // Combine all cards including the notice 183 | const cards = [noticeCard, ...ratingCards] 184 | 185 | const GAP = 20 186 | const COLUMNS = 2 187 | const CARD_WIDTH = 1200 188 | const CARD_HEIGHT = 675 189 | const ROWS = Math.ceil(cards.length / COLUMNS) 190 | 191 | const totalWidth = CARD_WIDTH * COLUMNS + GAP * (COLUMNS - 1) 192 | const totalHeight = CARD_HEIGHT * ROWS + GAP * (ROWS - 1) 193 | 194 | return await sharp({ 195 | create: { 196 | width: totalWidth, 197 | height: totalHeight, 198 | channels: 4, 199 | background: { r: 255, g: 255, b: 255, alpha: 0 }, 200 | }, 201 | }) 202 | .composite( 203 | cards.map((buffer, index) => { 204 | const row = Math.floor(index / COLUMNS) 205 | const col = index % COLUMNS 206 | return { 207 | input: buffer, 208 | top: row * (CARD_HEIGHT + GAP), 209 | left: col * (CARD_WIDTH + GAP), 210 | } 211 | }) 212 | ) 213 | .png() 214 | .toBuffer() 215 | } 216 | 217 | async function main() { 218 | const options = parseArgs() 219 | const user = options.user || 'monatheoctocat' 220 | 221 | const baseProps = { 222 | user, 223 | avatar: options.avatar || `https://github.com/${user}.png`, 224 | devType: options.devType || 'Exemplary Demo Developer', 225 | reliabilityScore: 1.0, 226 | securityScore: 3.0, 227 | maintainabilityScore: 5.0, 228 | contributor: options.contributor ?? true, 229 | regionalRank: [1, 'TW'] as [number, string], 230 | campusRank: [10, 'ntnu'] as [number, string], 231 | theme: preset[options.theme as keyof typeof preset] || preset.light, 232 | overallScore: '9.05', 233 | overallScoreCDF: '99', 234 | overallRating: Rating.S, 235 | } 236 | 237 | if (options.grid) { 238 | const gridBuffer = await generateGrid(baseProps) 239 | if (options.output) { 240 | fs.writeFileSync(options.output, gridBuffer) 241 | } else { 242 | process.stdout.write(gridBuffer) 243 | } 244 | return 245 | } 246 | 247 | const svg = await generateCard(baseProps) 248 | 249 | if (options.format === 'png') { 250 | const pngBuffer = await sharp(Buffer.from(svg)).toFormat('png').toBuffer() 251 | 252 | if (options.output) { 253 | fs.writeFileSync(options.output, pngBuffer) 254 | } else { 255 | process.stdout.write(pngBuffer) 256 | } 257 | } else { 258 | if (options.output) { 259 | fs.writeFileSync(options.output, svg) 260 | } else { 261 | console.log(svg) 262 | } 263 | } 264 | } 265 | 266 | main().catch((err) => { 267 | console.error('Error:', err) 268 | process.exit(1) 269 | }) 270 | -------------------------------------------------------------------------------- /src/OGCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | import { OGCard } from './OGCard' 3 | import { Rating } from './rating' 4 | import { preset } from './theme' 5 | 6 | 7 | const meta: Meta = { 8 | component: OGCard, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | argTypes: { 13 | overallRating: { 14 | control: { type: 'select' }, 15 | options: Object.values(Rating), 16 | }, 17 | }, 18 | } 19 | 20 | export default meta 21 | type Story = StoryObj 22 | 23 | const baseProps = { 24 | user: 'JacobLinCool', 25 | avatar: 'https://github.com/jacoblincool.png', 26 | devType: 'Exemplary AI/ML Developer', 27 | overallScore: '9.05', 28 | overallScoreCDF: '99', 29 | overallRating: Rating.S, 30 | reliabilityScore: 4.37, 31 | securityScore: 5.0, 32 | maintainabilityScore: 4.86, 33 | contributor: true, 34 | regionalRank: [1, 'TW'] as [number, string], 35 | campusRank: [1, 'ntnu'] as [number, string], 36 | } 37 | 38 | export const Light: Story = { 39 | args: { 40 | ...baseProps, 41 | theme: preset.light, 42 | }, 43 | } 44 | 45 | export const Dark: Story = { 46 | args: { 47 | ...baseProps, 48 | theme: preset.dark, 49 | }, 50 | } 51 | 52 | export const Sepia: Story = { 53 | args: { 54 | ...baseProps, 55 | theme: preset.sepia, 56 | }, 57 | } 58 | 59 | export const SolarizedLight: Story = { 60 | args: { 61 | ...baseProps, 62 | theme: preset.solarizedLight, 63 | }, 64 | } 65 | 66 | export const SolarizedDark: Story = { 67 | args: { 68 | ...baseProps, 69 | theme: preset.solarizedDark, 70 | }, 71 | } 72 | 73 | export const TokyoNight: Story = { 74 | args: { 75 | ...baseProps, 76 | theme: preset.tokyoNight, 77 | }, 78 | } 79 | 80 | export const Nord: Story = { 81 | args: { 82 | ...baseProps, 83 | theme: preset.nord, 84 | }, 85 | } 86 | 87 | export const Midnight: Story = { 88 | args:{ 89 | ...baseProps, 90 | theme: preset.midnight 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/OGCard.tsx: -------------------------------------------------------------------------------- 1 | import { Rating } from './rating' 2 | import { preset, type Theme } from './theme' 3 | import { GitRollLogo } from './logo' 4 | import { KawaiiCatDecoration } from './decorations' 5 | 6 | 7 | export interface OGCardProps { 8 | user: string 9 | avatar: string | null 10 | devType: string | null 11 | overallScore: string 12 | overallScoreCDF: string 13 | overallRating: Rating 14 | reliabilityScore: number 15 | securityScore: number 16 | maintainabilityScore: number 17 | contributor: boolean 18 | regionalRank?: [ string | number, string ] | null 19 | campusRank?: [ string | number, string ] | null 20 | theme?: Theme 21 | } 22 | 23 | export function OGCard({ 24 | user, avatar, devType, 25 | overallScore, overallScoreCDF, overallRating, 26 | reliabilityScore, securityScore, maintainabilityScore, 27 | contributor, 28 | regionalRank, campusRank, 29 | theme = preset.light 30 | }: OGCardProps) { 31 | const bg = theme.badgeColors[overallRating] ?? theme.badgeColors[Rating.E] 32 | return ( 33 |
46 | {theme === preset.kawaiiCat && ( 47 | 48 | )} 49 | 50 |
59 | {avatar ? ( 60 | 68 | ) : ( 69 |
78 | )} 79 |
85 |
93 | {user} 94 |
95 |

102 | {devType} 103 |

104 |
105 |
106 |
114 |
123 |
129 | Overall Rating 130 |
131 |
139 |
151 |
159 | {overallRating} 160 |
161 |
162 |
168 | {overallScore} 169 |
170 |
171 |
178 | Above 179 | 183 | {overallScoreCDF}% 184 | 185 | of people 186 |
187 |
188 |
196 |
202 | Code Quality 203 |
204 |
213 |
219 | Reliability 220 |
221 |
232 |
245 |
246 |
247 |
256 |
262 | Security 263 |
264 |
275 |
288 |
289 |
290 |
299 |
305 | Maintainability 306 |
307 |
318 |
331 |
332 |
333 |
334 |
335 |
344 | {contributor && ( 345 |
356 | Open-source contributor 357 |
358 | )} 359 | {regionalRank && ( 360 |
373 | Top {regionalRank[0]}% in {regionalRank[1]} 374 |
375 | )} 376 | {campusRank && ( 377 |
390 | Top {campusRank[0]}% in {campusRank[1]} 391 |
392 | )} 393 |
394 |
395 | ) 396 | } 397 | -------------------------------------------------------------------------------- /src/decorations.tsx: -------------------------------------------------------------------------------- 1 | interface KawaiiCatDecorationProps { 2 | color: string 3 | } 4 | 5 | export function KawaiiCatDecoration({ color }: KawaiiCatDecorationProps) { 6 | return ( 7 | 19 | 20 | {/* Paw prints trail across bottom & top */} 21 | {[200, 400, 600, 800, 1000].map((x, i) => ( 22 | 23 | 24 | 25 | 26 | 27 | ))} 28 | {[200, 400, 600, 800, 1000].map((x, i) => ( 29 | 30 | 31 | 32 | 33 | 34 | ))} 35 | 36 | {/* Stars scattered around */} 37 | {[ 38 | [150, 200], 39 | [950, 150], 40 | [1050, 300], 41 | [850, 450], 42 | ].map(([x, y], i) => ( 43 | 49 | ))} 50 | 51 | {/* Decorative borders */} 52 | 58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | input, select { 8 | font-family: inherit; 9 | } 10 | 11 | label { 12 | font-weight: 500; 13 | color: #4a4a4a; 14 | } 15 | 16 | input:focus, select:focus { 17 | border-color: #0066ff !important; 18 | outline: none; 19 | box-shadow: 0 0 0 3px rgba(0,102,255,0.1); 20 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './OGCard' 2 | export * from './theme' 3 | -------------------------------------------------------------------------------- /src/logo.tsx: -------------------------------------------------------------------------------- 1 | export const GitRollLogo = ({ fill = '#030303', width = 246, height = 60 }: { fill?: string; width?: number; height?: number }) => ( 2 | 7 | 9 | 12 | 15 | 19 | 25 | 30 | 32 | 35 | 36 | ) 37 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import React, { useEffect, useState } from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | import satori from 'satori' 5 | import { OGCard } from './OGCard' 6 | import { Rating } from './rating' 7 | import { preset, type Theme } from './theme' 8 | 9 | 10 | const InputGroup = ({ children }: { children: React.ReactNode }) =>
{children}
11 | 12 | const inputStyle = { 13 | padding: '0.5rem', 14 | borderRadius: '6px', 15 | border: '1px solid #ddd', 16 | fontSize: '1rem', 17 | outline: 'none', 18 | transition: 'border-color 0.2s', 19 | ':focus': { 20 | borderColor: '#0066ff', 21 | }, 22 | } 23 | 24 | const selectStyle = { 25 | ...inputStyle, 26 | backgroundColor: 'white', 27 | cursor: 'pointer', 28 | } 29 | 30 | async function loadFont() { 31 | const fontResponse = await fetch(new URL('../noto-sans-v27-latin-regular.ttf', import.meta.url).href) 32 | return await fontResponse.arrayBuffer() 33 | } 34 | 35 | const ColorInput = ({ value, onChange }: { label: string; value: string; onChange: (value: string) => void }) => ( 36 |
37 | onChange(e.target.value)} 41 | style={{ 42 | width: '40px', 43 | height: '40px', 44 | padding: '0', 45 | border: '1px solid #ddd', 46 | borderRadius: '4px', 47 | cursor: 'pointer', 48 | }} 49 | /> 50 | onChange(e.target.value)} style={{ ...inputStyle, flex: 1 }} /> 51 |
52 | ) 53 | 54 | function App() { 55 | const [, setSvg] = useState('') 56 | const [svgDataUrl, setSvgDataUrl] = useState('') 57 | const [isCustomTheme, setIsCustomTheme] = useState(false) 58 | const [customTheme, setCustomTheme] = useState({ 59 | backgroundColor: '#ffffff', 60 | textColor: '#000000', 61 | textColorSecondary: 'rgba(0, 0, 0, 0.6)', 62 | badgeColors: { 63 | [Rating.S]: '#c4b5fd', 64 | [Rating.A]: '#bbf7d0', 65 | [Rating.B]: '#d9f99d', 66 | [Rating.C]: '#fef08a', 67 | [Rating.D]: '#fed7aa', 68 | [Rating.E]: '#fecaca', 69 | }, 70 | badgeTextColors: { 71 | [Rating.S]: '#000000', 72 | [Rating.A]: '#000000', 73 | [Rating.B]: '#000000', 74 | [Rating.C]: '#000000', 75 | [Rating.D]: '#000000', 76 | [Rating.E]: '#000000', 77 | }, 78 | barBackground: '#F4F4F5', 79 | barForeground: '#18181B', 80 | borderColor: '#E4E4E7', 81 | avatarPlaceholderColor: '#9ca3af', 82 | logoColor: '#030303', 83 | }) 84 | 85 | const [props, setProps] = useState({ 86 | user: 'GitHub Username', 87 | avatar: 'https://avatars.githubusercontent.com/u/9919?s=200&v=4', 88 | devType: 'Exemplary AI/ML Developer', 89 | overallScore: '9.05', 90 | overallScoreCDF: '99', 91 | overallRating: Rating.S, 92 | reliabilityScore: 4.37, 93 | securityScore: 5.0, 94 | maintainabilityScore: 4.86, 95 | contributor: true, 96 | regionalRank: [1, 'TW'] as [number, string], 97 | campusRank: [1, 'ntnu'] as [number, string], 98 | theme: preset.light, 99 | }) 100 | 101 | useEffect(() => { 102 | async function generateSVG() { 103 | const fontData = await loadFont() 104 | 105 | const svg = await satori(OGCard(props), { 106 | width: 1200, 107 | height: 675, 108 | fonts: [ 109 | { 110 | name: 'sans serif', 111 | data: fontData, 112 | weight: 700, 113 | style: 'normal', 114 | }, 115 | ], 116 | }) 117 | 118 | const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` 119 | setSvgDataUrl(dataUrl) 120 | setSvg(svg) 121 | } 122 | 123 | generateSVG() 124 | }, [props]) 125 | 126 | const handleInputChange = (field: string, value: unknown) => { 127 | setProps((prev) => ({ 128 | ...prev, 129 | [field]: value, 130 | })) 131 | } 132 | 133 | return ( 134 |
144 |

151 | GitRoll Dev Card Playground 152 |

153 | 154 |
161 |
169 |

176 | Customize Your Card 177 |

178 |
185 | 186 | 187 | handleInputChange('user', e.target.value)} style={inputStyle} /> 188 | 189 | 190 | 191 | 192 | handleInputChange('devType', e.target.value)} style={inputStyle} /> 193 | 194 | 195 | 196 | 197 | { 204 | const value = Math.min(10, Math.max(0, Number(e.target.value))) 205 | handleInputChange('overallScore', value.toString()) 206 | }} 207 | style={inputStyle} 208 | /> 209 | 210 | 211 | 212 | 213 | { 219 | const value = Math.min(100, Math.max(0, Number(e.target.value))) 220 | handleInputChange('overallScoreCDF', value.toString()) 221 | }} 222 | style={inputStyle} 223 | /> 224 | 225 | 226 | 227 | 228 | 235 | 236 | 237 | 238 | 239 | { 246 | const value = Math.min(5, Math.max(0, Number(e.target.value))) 247 | handleInputChange('reliabilityScore', value) 248 | }} 249 | style={inputStyle} 250 | /> 251 | 252 | 253 | 254 | 255 | { 262 | const value = Math.min(5, Math.max(0, Number(e.target.value))) 263 | handleInputChange('securityScore', value) 264 | }} 265 | style={inputStyle} 266 | /> 267 | 268 | 269 | 270 | 271 | { 278 | const value = Math.min(5, Math.max(0, Number(e.target.value))) 279 | handleInputChange('maintainabilityScore', value) 280 | }} 281 | style={inputStyle} 282 | /> 283 | 284 | 285 | 286 | 287 |
288 | { 294 | const value = Math.max(1, parseInt(e.target.value)) 295 | handleInputChange('regionalRank', [value, props.regionalRank[1]]) 296 | }} 297 | /> 298 | handleInputChange('regionalRank', [props.regionalRank[0], e.target.value.toUpperCase()])} 304 | /> 305 |
306 |
307 | 308 | 309 | 310 |
311 | { 317 | const value = Math.max(1, parseInt(e.target.value)) 318 | handleInputChange('campusRank', [value, props.campusRank[1]]) 319 | }} 320 | /> 321 | handleInputChange('campusRank', [props.campusRank[0], e.target.value.toLowerCase()])} 327 | /> 328 |
329 |
330 | 331 | 332 | 333 | 354 | 355 | 356 | 357 | 358 | handleInputChange('contributor', e.target.checked)} 362 | style={{ 363 | width: '20px', 364 | height: '20px', 365 | cursor: 'pointer', 366 | }} 367 | /> 368 | 369 | 370 | {isCustomTheme && ( 371 | <> 372 | 373 | 374 | { 378 | const updatedTheme = { ...customTheme, backgroundColor: value } 379 | setCustomTheme(updatedTheme) 380 | handleInputChange('theme', updatedTheme) 381 | }} 382 | /> 383 | 384 | 385 | 386 | 387 | { 391 | const updatedTheme = { ...customTheme, textColor: value } 392 | setCustomTheme(updatedTheme) 393 | handleInputChange('theme', updatedTheme) 394 | }} 395 | /> 396 | 397 | 398 | 399 | 400 | { 404 | const updatedTheme = { ...customTheme, textColorSecondary: value } 405 | setCustomTheme(updatedTheme) 406 | handleInputChange('theme', updatedTheme) 407 | }} 408 | /> 409 | 410 | 411 | 412 | 413 | { 417 | const updatedTheme = { ...customTheme, barBackground: value } 418 | setCustomTheme(updatedTheme) 419 | handleInputChange('theme', updatedTheme) 420 | }} 421 | /> 422 | 423 | 424 | 425 | 426 | { 430 | const updatedTheme = { ...customTheme, barForeground: value } 431 | setCustomTheme(updatedTheme) 432 | handleInputChange('theme', updatedTheme) 433 | }} 434 | /> 435 | 436 | 437 | 438 | 439 | { 443 | const updatedTheme = { ...customTheme, borderColor: value } 444 | setCustomTheme(updatedTheme) 445 | handleInputChange('theme', updatedTheme) 446 | }} 447 | /> 448 | 449 | 450 | 451 | 452 | { 456 | const updatedTheme = { ...customTheme, avatarPlaceholderColor: value } 457 | setCustomTheme(updatedTheme) 458 | handleInputChange('theme', updatedTheme) 459 | }} 460 | /> 461 | 462 | 463 | 464 | 465 | { 469 | const updatedTheme = { ...customTheme, logoColor: value } 470 | setCustomTheme(updatedTheme) 471 | handleInputChange('theme', updatedTheme) 472 | }} 473 | /> 474 | 475 | 476 | {Object.values(Rating).map((rating) => ( 477 | 478 | 479 | 480 | { 484 | const updatedTheme = { 485 | ...customTheme, 486 | badgeColors: { 487 | ...customTheme.badgeColors, 488 | [rating]: value, 489 | }, 490 | } 491 | setCustomTheme(updatedTheme) 492 | handleInputChange('theme', updatedTheme) 493 | }} 494 | /> 495 | 496 | 497 | 498 | 499 | { 503 | const updatedTheme = { 504 | ...customTheme, 505 | badgeTextColors: { 506 | ...customTheme.badgeTextColors, 507 | [rating]: value, 508 | }, 509 | } 510 | setCustomTheme(updatedTheme) 511 | handleInputChange('theme', updatedTheme) 512 | }} 513 | /> 514 | 515 | 516 | ))} 517 | 518 | )} 519 |
520 |
521 | 522 |
531 |

538 | Preview 539 |

540 |
552 | {svgDataUrl ? ( 553 | Developer Card Preview 563 | ) : ( 564 |
573 | Loading... 574 |
575 | )} 576 |
577 |
578 |
579 |
580 | ) 581 | } 582 | 583 | ReactDOM.createRoot(document.querySelector('#root')!).render( 584 | 585 | 586 | 587 | ) 588 | -------------------------------------------------------------------------------- /src/rating.ts: -------------------------------------------------------------------------------- 1 | export enum Rating { 2 | S = 'S', 3 | A = 'A', 4 | B = 'B', 5 | C = 'C', 6 | D = 'D', 7 | E = 'E', 8 | } 9 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { Rating } from './rating' 2 | 3 | 4 | export interface Theme { 5 | backgroundColor: string 6 | textColor: string 7 | textColorSecondary: string 8 | badgeColors: Record 9 | badgeTextColors: Record 10 | barBackground: string 11 | barForeground: string 12 | borderColor: string 13 | avatarPlaceholderColor: string 14 | logoColor: string 15 | } 16 | 17 | const light = { 18 | backgroundColor: '#fff', 19 | textColor: '#000', 20 | textColorSecondary: 'rgba(0, 0, 0, 0.6)', 21 | badgeColors: { 22 | [Rating.S]: '#c4b5fd', 23 | [Rating.A]: '#bbf7d0', 24 | [Rating.B]: '#d9f99d', 25 | [Rating.C]: '#fef08a', 26 | [Rating.D]: '#fed7aa', 27 | [Rating.E]: '#fecaca' 28 | }, 29 | badgeTextColors: { 30 | [Rating.S]: '#000', 31 | [Rating.A]: '#000', 32 | [Rating.B]: '#000', 33 | [Rating.C]: '#000', 34 | [Rating.D]: '#000', 35 | [Rating.E]: '#000' 36 | }, 37 | barBackground: '#F4F4F5', 38 | barForeground: '#18181B', 39 | borderColor: '#E4E4E7', 40 | avatarPlaceholderColor: '#9ca3af', 41 | logoColor: '#030303', 42 | } 43 | 44 | const dark = { 45 | backgroundColor: '#18181B', 46 | textColor: '#fff', 47 | textColorSecondary: 'rgba(255, 255, 255, 0.6)', 48 | badgeColors: { 49 | [Rating.S]: '#7c3aed', 50 | [Rating.A]: '#16a34a', 51 | [Rating.B]: '#65a30d', 52 | [Rating.C]: '#ca8a04', 53 | [Rating.D]: '#ea580c', 54 | [Rating.E]: '#dc2626' 55 | }, 56 | badgeTextColors: { 57 | [Rating.S]: '#fff', 58 | [Rating.A]: '#fff', 59 | [Rating.B]: '#fff', 60 | [Rating.C]: '#fff', 61 | [Rating.D]: '#fff', 62 | [Rating.E]: '#fff' 63 | }, 64 | barBackground: '#27272A', 65 | barForeground: '#fff', 66 | borderColor: '#27272A', 67 | avatarPlaceholderColor: '#52525B', 68 | logoColor: '#fff', 69 | } 70 | 71 | const sepia = { 72 | backgroundColor: '#f4ecd8', 73 | textColor: '#5b4636', 74 | textColorSecondary: 'rgba(91, 70, 54, 0.6)', 75 | badgeColors: { 76 | [Rating.S]: '#d2b48c', 77 | [Rating.A]: '#f0e68c', 78 | [Rating.B]: '#eedd82', 79 | [Rating.C]: '#ffd700', 80 | [Rating.D]: '#daa520', 81 | [Rating.E]: '#cd853f' 82 | }, 83 | badgeTextColors: { 84 | [Rating.S]: '#5b4636', 85 | [Rating.A]: '#5b4636', 86 | [Rating.B]: '#5b4636', 87 | [Rating.C]: '#5b4636', 88 | [Rating.D]: '#5b4636', 89 | [Rating.E]: '#5b4636' 90 | }, 91 | barBackground: '#e8dcc2', 92 | barForeground: '#5b4636', 93 | borderColor: '#c2b280', 94 | avatarPlaceholderColor: '#b4a078', 95 | logoColor: '#5b4636', 96 | } 97 | 98 | const solarizedLight = { 99 | backgroundColor: '#fdf6e3', 100 | textColor: '#657b83', 101 | textColorSecondary: 'rgba(101, 123, 131, 0.6)', 102 | badgeColors: { 103 | [Rating.S]: '#b58900', 104 | [Rating.A]: '#859900', 105 | [Rating.B]: '#2aa198', 106 | [Rating.C]: '#268bd2', 107 | [Rating.D]: '#d33682', 108 | [Rating.E]: '#dc322f' 109 | }, 110 | badgeTextColors: { 111 | [Rating.S]: '#002b36', 112 | [Rating.A]: '#002b36', 113 | [Rating.B]: '#002b36', 114 | [Rating.C]: '#fdf6e3', 115 | [Rating.D]: '#fdf6e3', 116 | [Rating.E]: '#fdf6e3' 117 | }, 118 | barBackground: '#eee8d5', 119 | barForeground: '#073642', 120 | borderColor: '#93a1a1', 121 | avatarPlaceholderColor: '#93a1a1', 122 | logoColor: '#657b83', 123 | } 124 | 125 | const solarizedDark = { 126 | backgroundColor: '#002b36', 127 | textColor: '#839496', 128 | textColorSecondary: 'rgba(131, 148, 150, 0.6)', 129 | badgeColors: { 130 | [Rating.S]: '#b58900', 131 | [Rating.A]: '#859900', 132 | [Rating.B]: '#2aa198', 133 | [Rating.C]: '#268bd2', 134 | [Rating.D]: '#d33682', 135 | [Rating.E]: '#dc322f' 136 | }, 137 | badgeTextColors: { 138 | [Rating.S]: '#002b36', 139 | [Rating.A]: '#002b36', 140 | [Rating.B]: '#002b36', 141 | [Rating.C]: '#002b36', 142 | [Rating.D]: '#002b36', 143 | [Rating.E]: '#002b36' 144 | }, 145 | barBackground: '#073642', 146 | barForeground: '#fdf6e3', 147 | borderColor: '#586e75', 148 | avatarPlaceholderColor: '#586e75', 149 | logoColor: '#839496', 150 | } 151 | 152 | const tokyoNight = { 153 | backgroundColor: '#1a1b26', 154 | textColor: '#c0caf5', 155 | textColorSecondary: 'rgba(192, 202, 245, 0.6)', 156 | badgeColors: { 157 | [Rating.S]: '#7aa2f7', 158 | [Rating.A]: '#9ece6a', 159 | [Rating.B]: '#e0af68', 160 | [Rating.C]: '#f7768e', 161 | [Rating.D]: '#ff9e64', 162 | [Rating.E]: '#bb9af7' 163 | }, 164 | badgeTextColors: { 165 | [Rating.S]: '#1a1b26', 166 | [Rating.A]: '#1a1b26', 167 | [Rating.B]: '#1a1b26', 168 | [Rating.C]: '#1a1b26', 169 | [Rating.D]: '#1a1b26', 170 | [Rating.E]: '#1a1b26' 171 | }, 172 | barBackground: '#1f2335', 173 | barForeground: '#c0caf5', 174 | borderColor: '#3b4261', 175 | avatarPlaceholderColor: '#565f89', 176 | logoColor: '#c0caf5', 177 | } 178 | 179 | const nord = { 180 | backgroundColor: '#2e3440', 181 | textColor: '#d8dee9', 182 | textColorSecondary: 'rgba(216, 222, 233, 0.6)', 183 | badgeColors: { 184 | [Rating.S]: '#88c0d0', 185 | [Rating.A]: '#81a1c1', 186 | [Rating.B]: '#5e81ac', 187 | [Rating.C]: '#a3be8c', 188 | [Rating.D]: '#ebcb8b', 189 | [Rating.E]: '#bf616a' 190 | }, 191 | badgeTextColors: { 192 | [Rating.S]: '#2e3440', 193 | [Rating.A]: '#2e3440', 194 | [Rating.B]: '#2e3440', 195 | [Rating.C]: '#2e3440', 196 | [Rating.D]: '#2e3440', 197 | [Rating.E]: '#2e3440' 198 | }, 199 | barBackground: '#3b4252', 200 | barForeground: '#d8dee9', 201 | borderColor: '#4c566a', 202 | avatarPlaceholderColor: '#434c5e', 203 | logoColor: '#d8dee9', 204 | } 205 | 206 | const midnight = { 207 | backgroundColor: '#1c1e2d', 208 | textColor: '#d3d7e1', 209 | textColorSecondary: 'rgba(211, 215, 225, 0.7)', 210 | badgeColors: { 211 | [Rating.S]: '#3A506B', 212 | [Rating.A]: '#4C6A92', 213 | [Rating.B]: '#5C7A9D', 214 | [Rating.C]: '#3D4C6D', 215 | [Rating.D]: '#2B3A4A', 216 | [Rating.E]: '#1D2A38' 217 | }, 218 | badgeTextColors: { 219 | [Rating.S]: '#ffffff', 220 | [Rating.A]: '#ffffff', 221 | [Rating.B]: '#ffffff', 222 | [Rating.C]: '#ffffff', 223 | [Rating.D]: '#ffffff', 224 | [Rating.E]: '#ffffff' 225 | }, 226 | barBackground: '#2c3e50', 227 | barForeground: '#ecf0f1', 228 | borderColor: '#34495e', 229 | avatarPlaceholderColor: '#7f8c8d', 230 | logoColor: '#ecf0f1' 231 | } 232 | 233 | const kawaiiCat = { 234 | backgroundColor: '#F9FFFE', 235 | textColor: '#7A5C58', 236 | textColorSecondary: 'rgba(122, 92, 88, 0.65)', 237 | badgeColors: { 238 | [Rating.S]: '#FFCAD4', 239 | [Rating.A]: '#FFD7DE', 240 | [Rating.B]: '#66B2B2', 241 | [Rating.C]: '#80BFBF', 242 | [Rating.D]: '#99CCCC', 243 | [Rating.E]: '#B3D9D9', 244 | }, 245 | badgeTextColors: { 246 | [Rating.S]: '#7A5C58', 247 | [Rating.A]: '#7A5C58', 248 | [Rating.B]: '#FFFFFF', 249 | [Rating.C]: '#FFFFFF', 250 | [Rating.D]: '#7A5C58', 251 | [Rating.E]: '#7A5C58', 252 | }, 253 | barBackground: '#FFCAD4', 254 | barForeground: '#66B2B2', 255 | borderColor: '#FFCAD4', 256 | avatarPlaceholderColor: '#B3D9D9', 257 | logoColor: '#FFCAD4', 258 | } 259 | 260 | export const preset: Record = { 261 | light, 262 | dark, 263 | sepia, 264 | solarizedLight, 265 | solarizedDark, 266 | tokyoNight, 267 | nord, 268 | midnight, 269 | kawaiiCat 270 | } 271 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", // Specifies the JavaScript version to target when transpiling code. 4 | "useDefineForClassFields": true, // Enables the use of 'define' for class fields. 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], // Specifies the libraries available for the code. 10 | "module": "ESNext", // Defines the module system to use for code generation. 11 | "skipLibCheck": true, // Skips type checking of declaration files. 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", // Specifies how modules are resolved when bundling. 14 | "allowImportingTsExtensions": true, // Allows importing TypeScript files with extensions. 15 | "resolveJsonModule": true, // Enables importing JSON modules. 16 | "isolatedModules": true, // Ensures each file is treated as a separate module. 17 | "noEmit": true, // Prevents TypeScript from emitting output files. 18 | "jsx": "react-jsx", // Configures JSX support for React. 19 | /* Linting */ 20 | "strict": true, // Enables strict type checking. 21 | "noUnusedLocals": true, // Flags unused local variables. 22 | "noUnusedParameters": true, // Flags unused function parameters. 23 | "noFallthroughCasesInSwitch": true, // Requires handling all cases in a switch statement. 24 | "declaration": true, // Generates declaration files for TypeScript. 25 | }, 26 | "include": [ 27 | "src" 28 | ], // Specifies the directory to include when searching for TypeScript files. 29 | "exclude": [ 30 | "src/**/__docs__", 31 | "src/**/__test__" 32 | ] 33 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import dts from 'vite-plugin-dts' 3 | import { peerDependencies } from './package.json' 4 | 5 | 6 | export default defineConfig({ 7 | build: { 8 | lib: { 9 | entry: './src/index.ts', // Specifies the entry point for building the library. 10 | name: 'gitroll-profile-card', // Sets the name of the generated library. 11 | fileName: (format) => `index.${format}.js`, // Generates the output file name based on the format. 12 | formats: ['cjs', 'es'], // Specifies the output formats (CommonJS and ES modules). 13 | }, 14 | rollupOptions: { 15 | external: [...Object.keys(peerDependencies)], // Defines external dependencies for Rollup bundling. 16 | }, 17 | sourcemap: true, // Generates source maps for debugging. 18 | emptyOutDir: true, // Clears the output directory before building. 19 | }, 20 | plugins: [dts()], // Uses the 'vite-plugin-dts' plugin for generating TypeScript declaration files (d.ts). 21 | }) 22 | -------------------------------------------------------------------------------- /vite.playground.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | 4 | export default defineConfig({ 5 | build: { 6 | sourcemap: true, // Generates source maps for debugging. 7 | emptyOutDir: true, // Clears the output directory before building. 8 | outDir: 'playground', // Specifies the output directory for the build. 9 | }, 10 | base: 'gitroll-profile-card/', 11 | }) 12 | --------------------------------------------------------------------------------