|
| 1 | +async function processGHRepoCards() { |
| 2 | + |
| 3 | + const GITHUB_API_SETTINGS_GHREPOCARDS = { |
| 4 | + method: 'GET', |
| 5 | + headers: { |
| 6 | + 'Accept': 'application/vnd.github+json', |
| 7 | + 'X-GitHub-Api-Version': '2022-11-28' |
| 8 | + } |
| 9 | + }; |
| 10 | + |
| 11 | + const GITHUB_API_ENDPOINT_GHREPOCARDS = "https://api.github.com/"; |
| 12 | + |
| 13 | + function saveDataLocal(repoData) { |
| 14 | + const now = new Date(); |
| 15 | + let expirationHours = 1; |
| 16 | + const item = { |
| 17 | + repoData: repoData, |
| 18 | + expiry: now.getTime() + (expirationHours * 60 * 60 * 1000) |
| 19 | + }; |
| 20 | + let key = repoData.name.toLowerCase(); |
| 21 | + localStorage.setItem(key, JSON.stringify(item)); |
| 22 | + } |
| 23 | + |
| 24 | + function getDataLocal() { |
| 25 | + const items = {}; |
| 26 | + let l = localStorage.length; |
| 27 | + for (let i = 0; i < l; i++) { |
| 28 | + const key = localStorage.key(i); |
| 29 | + const value = localStorage.getItem(key); |
| 30 | + try { |
| 31 | + const parsedValue = JSON.parse(value); |
| 32 | + if (parsedValue && typeof parsedValue === 'object') { |
| 33 | + const now = new Date(); |
| 34 | + if (now.getTime() > parsedValue.expiry) { |
| 35 | + localStorage.removeItem(key); |
| 36 | + continue; |
| 37 | + } |
| 38 | + items[key] = parsedValue; |
| 39 | + } |
| 40 | + } catch (error) { |
| 41 | + localStorage.removeItem(key); |
| 42 | + throw new Error(`Error in getDataLocal(): ${error}.`); |
| 43 | + } |
| 44 | + } |
| 45 | + return items; |
| 46 | + } |
| 47 | + |
| 48 | + function putData(repoData, cardDiv, userString) { |
| 49 | + let userData = repoData.owner; |
| 50 | + userData.userString = userString; |
| 51 | + |
| 52 | + function appendTextContent(selector, content) { |
| 53 | + const elements = cardDiv.querySelectorAll(selector); |
| 54 | + elements.forEach(element => { |
| 55 | + element.textContent += content; |
| 56 | + }); |
| 57 | + } |
| 58 | + |
| 59 | + function setAttribute(selector, attr, value) { |
| 60 | + const elements = cardDiv.querySelectorAll(selector); |
| 61 | + elements.forEach(element => { |
| 62 | + element.setAttribute(attr, value); |
| 63 | + }); |
| 64 | + } |
| 65 | + |
| 66 | + appendTextContent(".gh-repo-cards-name", repoData.name); |
| 67 | + setAttribute(".gh-repo-cards-avatar", "src", userData.avatar_url); |
| 68 | + appendTextContent(".gh-repo-cards-username", userData.userString); |
| 69 | + appendTextContent(".gh-repo-cards-description", repoData.description || ""); |
| 70 | + |
| 71 | + const topicsContainers = cardDiv.querySelectorAll(".gh-repo-cards-topics"); |
| 72 | + topicsContainers.forEach(topicsContainer => { |
| 73 | + if (repoData.repoTopics && repoData.repoTopics.length > 0) { |
| 74 | + topicsContainer.innerHTML += repoData.repoTopics.map(topic => `<li>${topic}</li>`).join(''); |
| 75 | + } |
| 76 | + }); |
| 77 | + |
| 78 | + const languagesContainers = cardDiv.querySelectorAll(".gh-repo-cards-languages"); |
| 79 | + languagesContainers.forEach(languagesContainer => { |
| 80 | + if (repoData.repoLanguages && Object.keys(repoData.repoLanguages).length > 0) { |
| 81 | + const sortedLanguages = Object.keys(repoData.repoLanguages) |
| 82 | + .sort((a, b) => repoData.repoLanguages[b] - repoData.repoLanguages[a]); |
| 83 | + languagesContainer.innerHTML += sortedLanguages.map(lang => `<li>${lang}</li>`).join(''); |
| 84 | + } |
| 85 | + }); |
| 86 | + |
| 87 | + appendTextContent(".gh-repo-cards-stars", String(repoData.repoStars)); |
| 88 | + appendTextContent(".gh-repo-cards-watchers", String(repoData.repoWatchers)); |
| 89 | + appendTextContent(".gh-repo-cards-updatedat", repoData.repoUpdatedAt); |
| 90 | + appendTextContent(".gh-repo-cards-forks", String(repoData.repoForks)); |
| 91 | + } |
| 92 | + |
| 93 | + async function getData(query) { |
| 94 | + try { |
| 95 | + const response = await fetch(GITHUB_API_ENDPOINT_GHREPOCARDS + query, GITHUB_API_SETTINGS_GHREPOCARDS); |
| 96 | + if (!response.ok) { |
| 97 | + throw new Error(`GitHub API error! Status: ${response.status}.`); |
| 98 | + } |
| 99 | + return await response.json(); |
| 100 | + } catch (error) { |
| 101 | + throw new Error(`Error in getData(): ${error}.`); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + async function getAllRepos(userString, sortstring) { |
| 106 | + let allRepos = []; |
| 107 | + let page = 1; |
| 108 | + |
| 109 | + while (true) { |
| 110 | + let repos = await getData(`users/${userString}/repos?per_page=100&page=${page}${sortstring}`); |
| 111 | + if (repos.length === 0) break; |
| 112 | + |
| 113 | + const repoPromises = repos.map(async (repo) => { |
| 114 | + const [topics, languages, watchers] = await Promise.all([ |
| 115 | + getData(`repos/${userString}/${repo.name}/topics`), |
| 116 | + getData(`repos/${userString}/${repo.name}/languages`), |
| 117 | + getData(`repos/${userString}/${repo.name}/subscribers`) |
| 118 | + ]); |
| 119 | + |
| 120 | + return { |
| 121 | + ...repo, |
| 122 | + repoStars: repo.stargazers_count, |
| 123 | + repoUpdatedAt: repo.updated_at, |
| 124 | + repoForks: repo.forks_count, |
| 125 | + repoTopics: topics && topics.names ? topics.names : [], |
| 126 | + repoLanguages: languages || {}, |
| 127 | + repoWatchers: watchers.length |
| 128 | + }; |
| 129 | + }); |
| 130 | + |
| 131 | + allRepos.push(...await Promise.all(repoPromises)); |
| 132 | + page++; |
| 133 | + } |
| 134 | + |
| 135 | + return allRepos; |
| 136 | + } |
| 137 | + |
| 138 | + async function getSingleRepo(userString, repoString) { |
| 139 | + let localRepos = getDataLocal(); |
| 140 | + let repoData = localRepos[repoString]; |
| 141 | + |
| 142 | + if (repoData == undefined) { |
| 143 | + const [repo, topics, languages, watchers] = await Promise.all([ |
| 144 | + getData(`repos/${userString}/${repoString}`), |
| 145 | + getData(`repos/${userString}/${repoString}/topics`), |
| 146 | + getData(`repos/${userString}/${repoString}/languages`), |
| 147 | + getData(`repos/${userString}/${repoString}/subscribers`) |
| 148 | + ]); |
| 149 | + |
| 150 | + repoData = { |
| 151 | + ...repo, |
| 152 | + repoStars: repo.stargazers_count, |
| 153 | + repoUpdatedAt: repo.updated_at, |
| 154 | + repoForks: repo.forks_count, |
| 155 | + repoTopics: topics && topics.names ? topics.names : [], |
| 156 | + repoLanguages: languages || {}, |
| 157 | + repoWatchers: watchers.length |
| 158 | + }; |
| 159 | + |
| 160 | + saveDataLocal(repoData); |
| 161 | + } else { |
| 162 | + repoData = repoData.repoData; |
| 163 | + } |
| 164 | + |
| 165 | + return repoData; |
| 166 | + } |
| 167 | + |
| 168 | + let cardsElements = [...document.getElementsByTagName("gh-repo-cards")]; |
| 169 | + |
| 170 | + if (cardsElements.length == 0) { |
| 171 | + console.warn(`GitHub-Repo-WebCards: No <gh-repo-cards> tag found.`); |
| 172 | + return; |
| 173 | + } |
| 174 | + |
| 175 | + // Giving priority to %all. |
| 176 | + cardsElements.sort((a, b) => { |
| 177 | + const repoA = a.getAttribute('data-repo'); |
| 178 | + const repoB = b.getAttribute('data-repo'); |
| 179 | + if (repoA === '%all') return -1; |
| 180 | + if (repoB === '%all') return 1; |
| 181 | + return repoA.localeCompare(repoB); |
| 182 | + }); |
| 183 | + |
| 184 | + for (let cardTag of cardsElements) { |
| 185 | + const userString = cardTag.getAttribute("data-user")?.toLowerCase(); |
| 186 | + const repoString = cardTag.getAttribute("data-repo")?.toLowerCase(); |
| 187 | + |
| 188 | + if (!userString) { |
| 189 | + throw new Error(`GitHub-Repo-WebCards: Invalid 'data-user' attribute in <gh-repo-cards> tag.`); |
| 190 | + } |
| 191 | + if (!repoString) { |
| 192 | + throw new Error(`GitHub-Repo-WebCards: Invalid 'data-repo' attribute in <gh-repo-cards> tag.`); |
| 193 | + } |
| 194 | + |
| 195 | + const cardDiv = cardTag.querySelector(".gh-repo-cards-div"); |
| 196 | + if (!cardDiv) { |
| 197 | + throw new Error(`GitHub-Repo-WebCards: Missing div with class 'gh-repo-cards-div' in <gh-repo-cards> tag, at least 1 must be present.`); |
| 198 | + } |
| 199 | + |
| 200 | + if (repoString === "%all") { |
| 201 | + // All repos. |
| 202 | + const reposort = cardTag.getAttribute("data-sort")?.toLowerCase(); |
| 203 | + const directionsort = cardTag.getAttribute("data-direction")?.toLocaleLowerCase(); |
| 204 | + let sortstring = ""; |
| 205 | + |
| 206 | + if (reposort) { |
| 207 | + const admittedsort = ['created', 'updated', 'pushed', 'full_name']; |
| 208 | + const admitteddirection = ['asc', 'desc']; |
| 209 | + |
| 210 | + if (!admittedsort.includes(reposort)) { |
| 211 | + throw new Error(`GitHub-Repo-WebCards: Invalid 'data-sort' attribute. Must be one of: ${admittedsort.join(', ')}.`); |
| 212 | + } |
| 213 | + if (directionsort && !admitteddirection.includes(directionsort)) { |
| 214 | + throw new Error(`GitHub-Repo-WebCards: Invalid 'data-direction' attribute. Must be 'asc' or 'desc'.`); |
| 215 | + } |
| 216 | + |
| 217 | + sortstring = `&sort=${reposort}&direction=${directionsort}`; |
| 218 | + } |
| 219 | + |
| 220 | + const localRepos = getDataLocal(); |
| 221 | + const redownload = !Object.values(localRepos).some(repo => repo.repoData.owner.login.toLowerCase() === userString); |
| 222 | + |
| 223 | + let repolist; |
| 224 | + if (redownload) { |
| 225 | + repolist = await getAllRepos(userString, sortstring); |
| 226 | + repolist.forEach(saveDataLocal); |
| 227 | + } else { |
| 228 | + repolist = Object.values(localRepos).map(e => e.repoData).filter(e => e.owner.login.toLowerCase() === userString); |
| 229 | + } |
| 230 | + |
| 231 | + const originalContent = cardDiv.innerHTML; |
| 232 | + |
| 233 | + repolist.forEach((repoFromList, index) => { |
| 234 | + const newCardDiv = cardDiv.cloneNode(true); |
| 235 | + newCardDiv.innerHTML = originalContent; |
| 236 | + putData(repoFromList, newCardDiv, userString); |
| 237 | + |
| 238 | + if (index === 0) { |
| 239 | + cardDiv.innerHTML = newCardDiv.innerHTML; |
| 240 | + } else { |
| 241 | + cardTag.appendChild(newCardDiv); |
| 242 | + } |
| 243 | + }); |
| 244 | + |
| 245 | + } else { |
| 246 | + // Single repo. |
| 247 | + const repoData = await getSingleRepo(userString, repoString); |
| 248 | + putData(repoData, cardDiv, userString); |
| 249 | + } |
| 250 | + |
| 251 | + cardTag.style.display = "block"; |
| 252 | + } |
| 253 | + |
| 254 | + processGHRepoCards.saveDataLocal = saveDataLocal; |
| 255 | + processGHRepoCards.getDataLocal = getDataLocal; |
| 256 | + processGHRepoCards.putData = putData; |
| 257 | + processGHRepoCards.getData = getData; |
| 258 | + processGHRepoCards.getAllRepos = getAllRepos; |
| 259 | + processGHRepoCards.getSingleRepo = getSingleRepo; |
| 260 | + |
| 261 | +} |
| 262 | + |
| 263 | +processGHRepoCards(); |
| 264 | + |
0 commit comments