|
| 1 | +/** |
| 2 | + * RepoWidget - A professional GitHub repository display widget |
| 3 | + * @version 1.1.0 |
| 4 | + * @author Peter Benoit |
| 5 | + * @license MIT |
| 6 | + */ |
| 7 | +// ESM version of RepoWidget |
| 8 | +export function createRepoWidget { |
| 9 | + function createRepoWidget({ |
| 10 | + username, // GitHub username |
| 11 | + containerId, // ID of the container element |
| 12 | + columns = { mobile: 1, tablet: 2, desktop: 3 }, |
| 13 | + cardStyles = {}, // Optional custom styles for the card background and container |
| 14 | + textStyles = {}, // Optional custom styles for text and icon colors |
| 15 | + scaleOnHover = 1.05, // Default scale factor on hover; set to 0 or false to disable |
| 16 | + maxRepos = columns.desktop === 1 ? 10 : columns.desktop * 2, // Default maxRepos is double the desktop column count |
| 17 | + sortBy = 'stars', // Sorting parameter; options: "stars", "forks", "size", "name", "updated" |
| 18 | + exclude = [], // Array of repository names to exclude |
| 19 | + }) { |
| 20 | + const repoContainer = document.getElementById(containerId); |
| 21 | + |
| 22 | + const languageColors = { |
| 23 | + JavaScript: '#f1e05a', |
| 24 | + Python: '#3572A5', |
| 25 | + TypeScript: '#2b7489', |
| 26 | + Vue: '#41b883', |
| 27 | + React: '#61DAFB', |
| 28 | + Angular: '#E53238', |
| 29 | + Node: '#339933', |
| 30 | + Express: '#000000', |
| 31 | + Django: '#092E20', |
| 32 | + CSS: '#563d7c', |
| 33 | + HTML: '#e34c26', |
| 34 | + Java: '#b07219', |
| 35 | + C: '#555555', |
| 36 | + 'C#': '#178600', |
| 37 | + 'C++': '#f34b7d', |
| 38 | + Go: '#00add8', |
| 39 | + Ruby: '#701516', |
| 40 | + PHP: '#4F5D95', |
| 41 | + Swift: '#ffac45', |
| 42 | + Kotlin: '#F18E33', |
| 43 | + Rust: '#dea584', |
| 44 | + SQL: '#e38c00', |
| 45 | + MySQL: '#4479A1', |
| 46 | + PostgreSQL: '#336791', |
| 47 | + MongoDB: '#47A248', |
| 48 | + Docker: '#2496ED', |
| 49 | + GitHub: '#181717', |
| 50 | + Azure: '#0078D4', |
| 51 | + AWS: '#FF9900', |
| 52 | + }; |
| 53 | + |
| 54 | + repoContainer.style.display = 'grid'; |
| 55 | + repoContainer.style.gap = '16px'; |
| 56 | + |
| 57 | + const styles = ` |
| 58 | + #${containerId} { |
| 59 | + grid-template-columns: repeat(${columns.mobile}, 1fr); |
| 60 | + } |
| 61 | + @media (min-width: 640px) { |
| 62 | + #${containerId} { |
| 63 | + grid-template-columns: repeat(${columns.tablet}, 1fr); |
| 64 | + } |
| 65 | + } |
| 66 | + @media (min-width: 1024px) { |
| 67 | + #${containerId} { |
| 68 | + grid-template-columns: repeat(${columns.desktop}, 1fr); |
| 69 | + } |
| 70 | + } |
| 71 | + `; |
| 72 | + |
| 73 | + const styleSheet = document.createElement('style'); |
| 74 | + styleSheet.innerText = styles; |
| 75 | + document.head.appendChild(styleSheet); |
| 76 | + |
| 77 | + // Cache response for 1 day |
| 78 | + const CACHE_EXPIRATION = 24 * 60 * 60 * 1000; |
| 79 | + |
| 80 | + async function fetchRepos() { |
| 81 | + const cacheKey = `repos_${username}`; |
| 82 | + const cachedData = localStorage.getItem(cacheKey); |
| 83 | + const cachedETag = localStorage.getItem(`${cacheKey}_etag`); |
| 84 | + const cacheTimestamp = localStorage.getItem(`${cacheKey}_timestamp`); |
| 85 | + const now = Date.now(); |
| 86 | + const headers = {}; |
| 87 | + |
| 88 | + if (cachedETag) { |
| 89 | + headers['If-None-Match'] = cachedETag; |
| 90 | + } |
| 91 | + |
| 92 | + const response = await fetch(`https://api.github.com/users/${username}/repos`, { |
| 93 | + headers, |
| 94 | + }); |
| 95 | + |
| 96 | + if (response.status === 304 && cachedData) { |
| 97 | + return JSON.parse(cachedData); |
| 98 | + } |
| 99 | + |
| 100 | + if (!response.ok) { |
| 101 | + console.error('GitHub API error:', response.statusText); |
| 102 | + return []; |
| 103 | + } |
| 104 | + |
| 105 | + const repos = await response.json(); |
| 106 | + const eTag = response.headers.get('ETag'); |
| 107 | + |
| 108 | + localStorage.setItem(cacheKey, JSON.stringify(repos)); |
| 109 | + localStorage.setItem(`${cacheKey}_timestamp`, now); |
| 110 | + if (eTag) { |
| 111 | + localStorage.setItem(`${cacheKey}_etag`, eTag); |
| 112 | + } |
| 113 | + |
| 114 | + return repos; |
| 115 | + } |
| 116 | + |
| 117 | + // Sort repositories based on the provided sortBy parameter |
| 118 | + function sortRepositories(repos) { |
| 119 | + switch (sortBy) { |
| 120 | + case 'stars': |
| 121 | + return repos.sort((a, b) => b.stargazers_count - a.stargazers_count); |
| 122 | + case 'forks': |
| 123 | + return repos.sort((a, b) => b.forks_count - a.forks_count); |
| 124 | + case 'size': |
| 125 | + return repos.sort((a, b) => b.size - a.size); |
| 126 | + case 'name': |
| 127 | + return repos.sort((a, b) => a.name.localeCompare(b.name)); |
| 128 | + case 'updated': |
| 129 | + return repos.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); |
| 130 | + default: |
| 131 | + return repos; |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + async function initializeWidget() { |
| 136 | + let repos = await fetchRepos(); |
| 137 | + |
| 138 | + if (exclude && exclude.length > 0) { |
| 139 | + repos = repos.filter((repo) => !exclude.includes(repo.name)); |
| 140 | + } |
| 141 | + |
| 142 | + repos = sortRepositories(repos).slice(0, maxRepos); |
| 143 | + |
| 144 | + const fragment = document.createDocumentFragment(); |
| 145 | + |
| 146 | + repos.forEach((repo) => { |
| 147 | + const card = document.createElement('article'); |
| 148 | + card.setAttribute('role', 'region'); |
| 149 | + card.setAttribute('aria-labelledby', `repo-title-${repo.name}`); |
| 150 | + card.style.cssText = ` |
| 151 | + background: #fff; |
| 152 | + box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
| 153 | + border-radius: 8px; |
| 154 | + overflow: hidden; |
| 155 | + transition: transform 0.3s; |
| 156 | + `; |
| 157 | + |
| 158 | + Object.assign(card.style, cardStyles); |
| 159 | + |
| 160 | + if (scaleOnHover) { |
| 161 | + card.onmouseover = () => (card.style.transform = `scale(${scaleOnHover})`); |
| 162 | + card.onmouseleave = () => (card.style.transform = 'scale(1)'); |
| 163 | + } |
| 164 | + |
| 165 | + const languageColor = languageColors[repo.language] || '#cccccc'; |
| 166 | + |
| 167 | + card.innerHTML = ` |
| 168 | + <a href="${repo.html_url |
| 169 | + }" target="_blank" style="text-decoration: none; color: inherit; display: flex; flex-direction: column; height: 100%; padding: 16px;" aria-label="Repository ${repo.name |
| 170 | + }"> |
| 171 | + <div style="flex: 1;"> |
| 172 | + <h3 id="repo-title-${repo.name |
| 173 | + }" style="font-size: 1.25rem; font-weight: bold; color: ${textStyles.titleColor || '#333333' |
| 174 | + };">${repo.name}</h3> |
| 175 | + <p style="color: ${textStyles.descriptionColor || '#666666'}; margin: 8px 0;">${repo.description || 'No description provided' |
| 176 | + }</p> |
| 177 | + </div> |
| 178 | + <div style="margin-top: auto;"> |
| 179 | + <div style="display: flex; align-items: center; color: ${textStyles.iconColor || '#888888' |
| 180 | + }; font-size: 0.875rem;"> |
| 181 | + <span style="display: flex; align-items: center; margin-right: 16px;"> |
| 182 | + <span style="width: 10px; height: 10px; background-color: ${languageColor}; border-radius: 50%; margin-right: 4px;"></span> |
| 183 | + ${repo.language || 'N/A'} |
| 184 | + </span> |
| 185 | + <span style="display: flex; align-items: center; margin-right: 16px;"> |
| 186 | + <svg width="16" height="16" fill="${textStyles.iconColor || '#888888' |
| 187 | + }" style="margin-right: 4px;"><path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path></svg> |
| 188 | + ${repo.forks_count} |
| 189 | + </span> |
| 190 | + <span style="display: flex; align-items: center;"> |
| 191 | + <svg width="16" height="16" fill="${textStyles.iconColor || '#888888' |
| 192 | + }" style="margin-right: 4px;"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"></path></svg> |
| 193 | + ${repo.stargazers_count} |
| 194 | + </span> |
| 195 | + </div> |
| 196 | + <div style="color: ${textStyles.sizeColor || '#aaaaaa' |
| 197 | + }; font-size: 0.75rem; margin-top: 8px;">Size: ${repo.size} KB</div> |
| 198 | + </div> |
| 199 | + </a> |
| 200 | + `; |
| 201 | + |
| 202 | + fragment.appendChild(card); |
| 203 | + }); |
| 204 | + |
| 205 | + repoContainer.innerHTML = ''; |
| 206 | + repoContainer.appendChild(fragment); |
| 207 | + } |
| 208 | + |
| 209 | + initializeWidget(); |
| 210 | + } |
| 211 | + |
| 212 | + |
| 213 | + |
0 commit comments