|
85 | 85 | } |
86 | 86 |
|
87 | 87 | var user = await uRes.json(); |
| 88 | + if (!rRes.ok) throw new Error(`repos_http_${rRes.status}`); |
88 | 89 | var list = await rRes.json(); |
89 | 90 | if (!Array.isArray(list)) throw new Error('repos_failed'); |
90 | 91 |
|
91 | 92 | var nonFork = list.filter(function (r) { return !r.fork; }); |
92 | 93 | var stats = { |
93 | | - followers: user.followers ?? 0, |
94 | | - following: user.following ?? 0, |
| 94 | + followers: Number(user.followers) || 0, |
| 95 | + following: Number(user.following) || 0, |
95 | 96 | repos: nonFork.length, |
96 | 97 | stars: list.reduce(function (s, r) { return s + (r.stargazers_count ?? 0); }, 0), |
97 | | - gists: user.public_gists ?? 0, |
98 | | - joinedAt: user.created_at, |
99 | | - hireable: user.hireable ?? false, |
100 | | - bio: user.bio ?? '', |
| 98 | + gists: Number(user.public_gists) || 0, |
| 99 | + joinedAt: user.created_at ?? null, |
| 100 | + hireable: Boolean(user.hireable), |
| 101 | + bio: String(user.bio ?? ''), |
101 | 102 | }; |
102 | 103 |
|
103 | 104 | var langResults = await Promise.allSettled( |
|
139 | 140 |
|
140 | 141 | /* ── Fetch: contributions ──────────────────────────────────────── */ |
141 | 142 | function computeStreak(flat) { |
| 143 | + if (!Array.isArray(flat) || !flat.length) return { current: 0, longest: 0 }; |
142 | 144 | var now = new Date(); |
143 | 145 | var today = [ |
144 | 146 | now.getFullYear(), |
|
174 | 176 | if (!res.ok) throw new Error('contrib_error'); |
175 | 177 |
|
176 | 178 | var json = await res.json(); |
177 | | - var flat = json.contributions ?? []; |
| 179 | + var flat = Array.isArray(json.contributions) |
| 180 | + ? json.contributions.filter(function (e) { |
| 181 | + return e && typeof e.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(e.date); |
| 182 | + }) |
| 183 | + : []; |
178 | 184 |
|
179 | 185 | var weeksMap = {}; |
180 | 186 | flat.forEach(function (e) { |
|
239 | 245 | var theme = (stored === 'dark' || stored === 'light') ? stored |
240 | 246 | : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); |
241 | 247 | applyTheme(theme); |
242 | | - document.getElementById('theme-toggle')?.addEventListener('click', function () { |
| 248 | + document.getElementById('theme-toggle')?.addEventListener('click', debounce(function () { |
243 | 249 | applyTheme(getTheme() === 'dark' ? 'light' : 'dark'); |
244 | | - }); |
| 250 | + }, 200)); |
245 | 251 | } |
246 | 252 |
|
247 | 253 | /* ── Contact form ──────────────────────────────────────────────── */ |
|
286 | 292 | e.preventDefault(); |
287 | 293 | clearTimeout(clearTimer); |
288 | 294 | if (feedback) feedback.hidden = true; |
| 295 | + |
| 296 | + if (!form.checkValidity()) { |
| 297 | + form.querySelectorAll('[required]').forEach(function (field) { |
| 298 | + field.classList.toggle('is-invalid', !field.validity.valid); |
| 299 | + }); |
| 300 | + return; |
| 301 | + } |
| 302 | + |
289 | 303 | setSending(true); |
290 | 304 | try { |
291 | 305 | var res = await fetch(FORMSPREE_ENDPOINT, { |
|
555 | 569 | icon.className = 'fa-solid fa-check'; |
556 | 570 | setTimeout(function () { icon.className = 'fa-regular fa-copy'; }, 1500); |
557 | 571 | } |
558 | | - }).catch(function () {}); |
| 572 | + }).catch(function () { |
| 573 | + var icon = copyBtn.querySelector('i'); |
| 574 | + if (icon) { |
| 575 | + icon.className = 'fa-solid fa-xmark'; |
| 576 | + setTimeout(function () { icon.className = 'fa-regular fa-copy'; }, 1500); |
| 577 | + } |
| 578 | + }); |
559 | 579 | return; |
560 | 580 | } |
561 | 581 | if (e.target.closest('.card-flip-btn')) { e.target.closest('[data-npm]')?.classList.add('flipped'); return; } |
|
812 | 832 | } |
813 | 833 |
|
814 | 834 | function renderExpDetail(idx) { |
| 835 | + if (idx < 0 || idx >= EXP_DATA.length) return; |
815 | 836 | var d = EXP_DATA[idx]; |
816 | 837 | var detail = document.getElementById('exp-detail'); |
817 | 838 | if (!detail) return; |
|
866 | 887 |
|
867 | 888 | tabs.forEach(function (tab, i) { |
868 | 889 | tab.id = `exp-tab-${tab.dataset.idx}`; |
869 | | - var dateEl = tab.querySelector('.exp-tab__date'); |
870 | | - if (dateEl) dateEl.textContent = calcDuration(EXP_DATA[Number(tab.dataset.idx)].period); |
| 890 | + var dateEl = tab.querySelector('.exp-tab__date'); |
| 891 | + var expIdx = Number(tab.dataset.idx); |
| 892 | + if (dateEl && expIdx >= 0 && expIdx < EXP_DATA.length) { |
| 893 | + dateEl.textContent = calcDuration(EXP_DATA[expIdx].period); |
| 894 | + } |
871 | 895 | tab.addEventListener('click', function () { activate(i); }); |
872 | 896 | tab.addEventListener('keydown', function (e) { |
873 | 897 | var next = null; |
|
1164 | 1188 | var k = `${dt.getFullYear()}-${dt.getMonth()}`; |
1165 | 1189 | if (!seen.has(k) && dt.getDate() <= 7) { |
1166 | 1190 | seen.add(k); |
1167 | | - monthsEl.insertAdjacentHTML('beforeend', |
1168 | | - `<span class="contrib-chart__month" style="left:${wi * CELL_STEP}px">${dt.toLocaleString('default', { month: 'short' })}</span>` |
1169 | | - ); |
| 1191 | + var mSpan = document.createElement('span'); |
| 1192 | + mSpan.className = 'contrib-chart__month'; |
| 1193 | + mSpan.style.left = (wi * CELL_STEP) + 'px'; |
| 1194 | + mSpan.textContent = dt.toLocaleString('default', { month: 'short' }); |
| 1195 | + monthsEl.appendChild(mSpan); |
1170 | 1196 | } |
1171 | 1197 | }); |
1172 | 1198 | }); |
1173 | 1199 | } |
1174 | 1200 |
|
1175 | 1201 | var daysEl = content.querySelector('.contrib-chart__day-labels'); |
1176 | 1202 | if (daysEl) { |
1177 | | - daysEl.innerHTML = DAY_LABELS.map(function (label, i) { |
1178 | | - return `<div class="contrib-chart__day" style="height:${CELL_SIZE}px;margin-bottom:${i === 6 ? 0 : CELL_GAP}px">${label}</div>`; |
1179 | | - }).join(''); |
| 1203 | + var dayFrag = document.createDocumentFragment(); |
| 1204 | + DAY_LABELS.forEach(function (label, i) { |
| 1205 | + var div = document.createElement('div'); |
| 1206 | + div.className = 'contrib-chart__day'; |
| 1207 | + div.style.height = CELL_SIZE + 'px'; |
| 1208 | + div.style.marginBottom = (i === 6 ? 0 : CELL_GAP) + 'px'; |
| 1209 | + div.textContent = label; |
| 1210 | + dayFrag.appendChild(div); |
| 1211 | + }); |
| 1212 | + daysEl.replaceChildren(dayFrag); |
1180 | 1213 | } |
1181 | 1214 |
|
1182 | 1215 | var innerEl = content.querySelector('.contrib-chart__scroll > div'); |
|
1186 | 1219 | if (grid) { |
1187 | 1220 | var total = weeks.flat().reduce(function (s, d) { return s + (d?.count ?? 0); }, 0); |
1188 | 1221 | grid.setAttribute('aria-label', `Contributions ${year} — ${total} total`); |
1189 | | - grid.innerHTML = weeks.map(function (w) { |
1190 | | - return w.map(function (d) { |
1191 | | - if (!d) return '<div class="cell"></div>'; |
1192 | | - var date = String(d.date).replace(/[^0-9-]/g, ''); |
1193 | | - var count = Number(d.count); |
1194 | | - return `<div class="cell contrib-cell ${d.disabled ? 'is-disabled' : 'is-active'}" style="background-color:${contribColor(count, theme)}" data-date="${date}" data-count="${count}"></div>`; |
1195 | | - }).join(''); |
1196 | | - }).join(''); |
| 1222 | + var gridFrag = document.createDocumentFragment(); |
| 1223 | + weeks.forEach(function (w) { |
| 1224 | + w.forEach(function (d) { |
| 1225 | + var cell = document.createElement('div'); |
| 1226 | + if (!d) { |
| 1227 | + cell.className = 'cell'; |
| 1228 | + } else { |
| 1229 | + var count = Number(d.count); |
| 1230 | + cell.className = 'cell contrib-cell ' + (d.disabled ? 'is-disabled' : 'is-active'); |
| 1231 | + cell.style.backgroundColor = contribColor(count, theme); |
| 1232 | + cell.dataset.date = String(d.date).replace(/[^0-9-]/g, ''); |
| 1233 | + cell.dataset.count = String(count); |
| 1234 | + } |
| 1235 | + gridFrag.appendChild(cell); |
| 1236 | + }); |
| 1237 | + }); |
| 1238 | + grid.replaceChildren(gridFrag); |
1197 | 1239 | } |
1198 | 1240 |
|
1199 | 1241 | var legendEl = content.querySelector('.contrib-legend'); |
1200 | 1242 | if (legendEl) { |
1201 | | - var less = legendEl.querySelector('.legend-less')?.outerHTML ?? ''; |
1202 | | - var more = legendEl.querySelector('.legend-more')?.outerHTML ?? ''; |
1203 | | - legendEl.innerHTML = less |
1204 | | - + LEGEND_COUNTS.map(function (v) { return `<div class="cell" style="background-color:${contribColor(v, theme)}"></div>`; }).join('') |
1205 | | - + more; |
| 1243 | + var lessNode = legendEl.querySelector('.legend-less'); |
| 1244 | + var moreNode = legendEl.querySelector('.legend-more'); |
| 1245 | + var lessClone = lessNode ? lessNode.cloneNode(true) : null; |
| 1246 | + var moreClone = moreNode ? moreNode.cloneNode(true) : null; |
| 1247 | + var legFrag = document.createDocumentFragment(); |
| 1248 | + if (lessClone) legFrag.appendChild(lessClone); |
| 1249 | + LEGEND_COUNTS.forEach(function (v) { |
| 1250 | + var cell = document.createElement('div'); |
| 1251 | + cell.className = 'cell'; |
| 1252 | + cell.style.backgroundColor = contribColor(v, theme); |
| 1253 | + legFrag.appendChild(cell); |
| 1254 | + }); |
| 1255 | + if (moreClone) legFrag.appendChild(moreClone); |
| 1256 | + legendEl.replaceChildren(legFrag); |
1206 | 1257 | } |
1207 | 1258 | } |
1208 | 1259 |
|
|
0 commit comments