├── .github ├── copilot-instructions.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── _config.yml ├── build.js ├── demo ├── demo.js ├── index.html ├── screenshot.png ├── style.css └── widget-generator.js ├── dist └── gh-profile-card.min.js ├── eslint.config.js ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── css │ ├── base.scss │ ├── profile-header.scss │ ├── repositories-list.scss │ └── shared.scss ├── gh-cache-storage.spec.ts ├── gh-cache-storage.ts ├── gh-data-loader.spec.ts ├── gh-data-loader.ts ├── gh-dom-operator.spec.ts ├── gh-dom-operator.ts ├── gh-dom.utils.spec.ts ├── gh-dom.utils.ts ├── gh-profile-card.spec.ts ├── gh-profile-card.ts ├── gh-widget-init.spec.ts ├── gh-widget-init.ts ├── interface │ ├── IGitHubApi.ts │ ├── IWidget.ts │ └── storage.ts └── testing │ ├── cache-mock.ts │ ├── fetch-mock.ts │ ├── in-memory-storage.ts │ ├── index.ts │ ├── mock-github-data.ts │ ├── style-mock.js │ ├── test-setup.ts │ └── test-utils.ts └── tsconfig.json /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # GitHub Copilot Instructions 2 | 3 | ## Project Overview 4 | 5 | This is a **GitHub Profile Card Widget** - a vanilla JavaScript/TypeScript library that displays GitHub user profiles and repositories on websites. The widget is built with modern TypeScript, uses no external dependencies, and follows a modular architecture. 6 | 7 | ### Key Technologies 8 | - **TypeScript** (v3.7.5) - Primary language 9 | - **Jest** (v24.9.0) - Testing framework 10 | - **ESBuild** - Build tool (can evolve as needed) 11 | - **SCSS** - Styling 12 | - **Vanilla JavaScript** - No frameworks, pure browser compatibility 13 | 14 | ## Architecture & Code Organization 15 | 16 | ### Core Components 17 | - `gh-profile-card.ts` - Main widget class and entry point 18 | - `gh-data-loader.ts` - GitHub API interaction and data fetching 19 | - `gh-dom-operator.ts` - DOM manipulation and rendering 20 | - `gh-cache-storage.ts` - Caching mechanism for API responses 21 | - `gh-widget-init.ts` - Widget initialization and configuration 22 | 23 | ### Directory Structure 24 | ``` 25 | src/ 26 | ├── interface/ # TypeScript interfaces and types 27 | ├── css/ # SCSS stylesheets 28 | └── testing/ # Test utilities and mocks 29 | ``` 30 | 31 | ### Key Interfaces - You cannot modify these as they come from GitHub API 32 | - `WidgetConfig` - Widget configuration options 33 | - `ApiProfile` - GitHub user profile data structure 34 | - `ApiRepository` - GitHub repository data structure 35 | - `ApiUserData` - Combined user data (profile + repositories) 36 | 37 | ## Core Principles 38 | 39 | ### Type Safety 40 | - Use **TypeScript strict mode** for all code 41 | - Define clear interfaces for data structures 42 | - Leverage type checking to prevent runtime errors 43 | 44 | ### Error Handling 45 | - Use custom `ApiError` interface for API-related errors 46 | - Distinguish between network errors and user-not-found errors 47 | - Provide meaningful error messages to users 48 | - Implement graceful degradation 49 | 50 | ### Performance 51 | - **Prefer** caching API responses to reduce GitHub rate limiting 52 | - **Consider** lazy loading for non-critical features like language statistics 53 | - **Minimize** DOM manipulations when performance is a concern 54 | - **Evaluate** debouncing for user-triggered API calls 55 | 56 | ### Accessibility 57 | - Ensure proper **semantic HTML** structure 58 | - Use appropriate **ARIA labels** where needed 59 | - Support **keyboard navigation** patterns 60 | - Maintain **color contrast** standards 61 | 62 | ## Development Guidelines 63 | 64 | ### Testing Strategy 65 | - **Prefer** using `src/testing/` utilities for consistent test setup 66 | - Extract results to separate variables for better debugging 67 | - Mock external dependencies (API, DOM, storage) unless integration testing 68 | - Test both success and error scenarios 69 | - Include edge cases and invalid inputs 70 | - Use Given-When-Then style for test cases 71 | - Avoid overcomplexity in tests; keep them focused on single behaviors 72 | 73 | ### Key Testing Utilities 74 | - `mock-github-data.ts` - Standardized GitHub API mock data 75 | - `fetch-mock.ts` - HTTP request mocking utilities 76 | - `cache-mock.ts` - Cache storage mocking 77 | - `test-utils.ts` - Common test helper functions 78 | 79 | ### API Integration 80 | - **Use** GitHub REST API v3 for consistency with existing implementation 81 | - **Implement** caching to reduce API calls and respect rate limits 82 | - **Handle** rate limiting gracefully with proper error messages 83 | - **Support** error states (network, 404, invalid JSON) 84 | - **Consider** retry logic for transient failures 85 | 86 | ### Widget Configuration 87 | Support both **programmatic** and **HTML data-attribute** configuration: 88 | ```html 89 |
` tag: 36 | 37 | ``` 38 | 39 | ``` 40 | 41 | Include HTML code anywhere you would like to place widget: 42 | 43 | ``` 44 |
47 | ``` 48 | 49 | Great! Widget will autoload. We're done here. 50 | 51 | ## Download 52 | 53 | With [_npm_](https://www.npmjs.com/package/github-profile-card) 54 | 55 | ``` 56 | npm install github-profile-card --save 57 | ``` 58 | 59 | ## Advanced configuration 60 | 61 | Configure widget in HTML: 62 | 63 | ``` 64 |
70 | ``` 71 | 72 | For special usages, it is possible to configure widget(s) in JavaScript. 73 | You have to use different template than `#github-card`. 74 | 75 | ``` 76 | var widget = new GitHubCard({ 77 | username: 'YOUR_GITHUB_USERNAME' 78 | template: '#github-card-demo', 79 | sortBy: 'stars', 80 | reposHeaderText: 'Most starred', 81 | maxRepos: 5, 82 | hideTopLanguages: false, 83 | }); 84 | 85 | widget.init(); 86 | ``` 87 | 88 | ## Configuration options 89 | 90 | | HTML option (`data-` prefix) | JavaScript option | Default | Details | 91 | | ---------------------------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | 92 | | `username` | `username` | None | GitHub profile username | 93 | | `—` | `template` | `#github-card` | DOM selector of your widget in HTML | 94 | | `sort-by` | `sortBy` | `stars` | Repositories sorting method (`stars` or `updateTime`) | 95 | | `max-repos` | `maxRepos` | `5` | Amount of listed repositories. `0` disables section | 96 | | `header-text` | `headerText` | `Most starred repositories` | Text label above repositories list | 97 | | `hide-top-languages` | `hideTopLanguages` | `false` | Avoids heavy network traffic for calculating `Top Languages` section. Recommended for profiles with huge amount of repositories. | 98 | 99 | ## FAQ 100 | 101 | - **My language statistic is affected by libraries and dependencies** 102 | 103 | Consider ignoring them with .gitattributes: [My repository is detected as the wrong language](https://github.com/github/linguist#overrides) 104 | 105 | - **How language statistic is build?** 106 | 107 | It is sum of all characters written in language you use. 108 | One big repository in `C#` will be ranked higher than many small `JavaScript` repositories. 109 | 110 | It is based on 10 last updated repositories, to represent your current interests. 111 | 112 | - **How to show two or more profiles on page?** 113 | 114 | You have to create two widgets with different ID, then initialize each manually in JS. 115 | 116 | e.g. 117 | 118 | ``` 119 |
120 |
121 | 122 | 126 | ``` 127 | 128 | ## Feedback 129 | 130 | I love feedback, send me one! 131 | 132 | - show me website on which you're using this widget: [leave comment](https://github.com/piotrl/github-profile-card/issues/15) 133 | - ping me on twitter: [@constjs](https://twitter.com/constjs) 134 | - create [new issue](https://github.com/piotrl/github-profile-card/issues/new) 135 | 136 | Remember no other libraries required. It's like gluten free 😉 137 | 138 |  139 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-dinky 2 | gems: 3 | - jemoji 4 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import { sassPlugin } from 'esbuild-sass-plugin'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import PACKAGE from './package.json' with { type: 'json' }; 6 | 7 | const ENV = process.env.WEBPACK_ENV; 8 | const libraryName = 'gh-profile-card'; 9 | const banner = `/** 10 | * ${PACKAGE.name} - ${PACKAGE.version} | ${PACKAGE.license} 11 | * (c) 2014 - ${new Date().getFullYear()} ${PACKAGE.author} | ${PACKAGE.homepage} 12 | */ 13 | `; 14 | 15 | const outfilePath = path.resolve('dist', `${libraryName}.min.js`); 16 | 17 | esbuild 18 | .build({ 19 | entryPoints: ['./src/gh-widget-init.ts'], 20 | outfile: outfilePath, 21 | bundle: true, 22 | minify: false, 23 | sourcemap: ENV === 'dev', // Inline source map in development 24 | format: 'iife', 25 | target: ['es2022'], 26 | plugins: [ 27 | sassPlugin({ 28 | type: 'style', 29 | }), // Load SCSS using esbuild-sass-plugin 30 | { 31 | name: 'banner-plugin', 32 | setup(build) { 33 | build.onEnd((result) => { 34 | const content = fs.readFileSync(outfilePath, 'utf8'); 35 | fs.writeFileSync(outfilePath, `${banner}\n${content}`); 36 | }); 37 | }, 38 | }, 39 | ], 40 | }) 41 | .catch(() => process.exit(1)); 42 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | (function (GitHubCard, widgetGenerator) { 2 | 'use strict'; 3 | 4 | // Generating new widget from user input 5 | document.addEventListener('DOMContentLoaded', () => { 6 | const options = { 7 | template: '#github-card-demo', 8 | sortBy: 'stars', // possible: 'stars', 'updateTime' 9 | headerText: 'Most starred repositories', 10 | maxRepos: 5, 11 | }; 12 | overrideOptionsByUrlParams(options); 13 | 14 | let widget = new GitHubCard(options); 15 | widget.init(); 16 | refreshConfigTextarea(options); 17 | 18 | initSortingControl(options, refreshWidget); 19 | initRepositoriesControl(options, refreshWidget); 20 | initUserControl(options, initWidget); 21 | 22 | function initWidget(options) { 23 | widget = new GitHubCard(options); 24 | widget.init(); 25 | refreshConfigTextarea(options); 26 | } 27 | 28 | function refreshWidget(updatedOptions) { 29 | widget.refresh(updatedOptions); 30 | refreshConfigTextarea(updatedOptions); 31 | } 32 | }); 33 | 34 | function refreshConfigTextarea(updatedOptions) { 35 | const textarea = document.getElementById('install-code'); 36 | textarea.value = widgetGenerator.regenerate(updatedOptions); 37 | } 38 | 39 | // Sort repository acording to 40 | // radio inputs on website 41 | function initSortingControl(options, refreshWidget) { 42 | var $sortingRadios = document.querySelectorAll( 43 | '.choose-repo-sorting label', 44 | ); 45 | 46 | // sort by update time 47 | $sortingRadios[0].addEventListener('click', (event) => { 48 | event.target.classList.add('active'); 49 | $sortingRadios[1].classList.remove('active'); 50 | 51 | options.sortBy = 'updateTime'; 52 | options.headerText = event.target.textContent + ' repositories'; 53 | 54 | refreshWidget(options); 55 | }); 56 | 57 | // sort by starrgazers 58 | $sortingRadios[1].addEventListener('click', (event) => { 59 | event.target.classList.add('active'); 60 | $sortingRadios[0].classList.remove('active'); 61 | 62 | options.sortBy = 'stars'; 63 | options.headerText = event.target.textContent + ' repositories'; 64 | 65 | refreshWidget(options); 66 | }); 67 | } 68 | 69 | // Manipulating the number of repositories 70 | function initRepositoriesControl(options, refreshWidget) { 71 | const $inputNumber = document.getElementById('gh-reposNum'); 72 | 73 | $inputNumber.onchange = () => { 74 | options.maxRepos = $inputNumber.value; 75 | refreshWidget(options); 76 | }; 77 | } 78 | 79 | // Creating brand new widget instance 80 | // for user that we type in input 81 | function initUserControl(options, cb) { 82 | const $input = document.getElementById('gh-uname'); 83 | const $submit = document.getElementById('gh-uname-submit'); 84 | 85 | $submit.addEventListener('click', (event) => { 86 | options.username = $input.value; 87 | cb(options); 88 | 89 | event.preventDefault(); 90 | }); 91 | } 92 | 93 | function overrideOptionsByUrlParams(options) { 94 | const queryParameters = new URL(document.location).searchParams; 95 | for (const [key, value] of queryParameters) { 96 | options[key] = value; 97 | } 98 | } 99 | })(window.GitHubCard, window.widgetGenerator); 100 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 |
132 |