|
1 | 1 | <script> |
2 | | - // ---- CONFIG ---- |
| 2 | + // ===== Configuration ===== |
3 | 3 | const ORG = 'Democracy-Lab'; |
4 | | - const ORG_PAGES_BASE = 'https://democracy-lab.github.io'; // base org pages host |
| 4 | + const ORG_PAGES_BASE = 'https://democracy-lab.github.io'; |
| 5 | + const SHOW_ALL = true; // true = show all public repos immediately; false = only those with Pages |
| 6 | + const AUTO_REFRESH_MS = 120000; // 2 minutes; set to 0 to disable auto-refresh |
5 | 7 |
|
6 | | - // Fetch all org repos (handles pagination if >100) |
7 | | - async function fetchAllRepos(url = `https://api.github.com/orgs/${ORG}/repos?per_page=100&type=public`) { |
| 8 | + // Build the expected Pages URL for a repo (works for org project pages) |
| 9 | + function pagesUrl(repoName) { |
| 10 | + if (repoName.toLowerCase() === `${ORG.toLowerCase()}.github.io`) return `${ORG_PAGES_BASE}/`; |
| 11 | + return `${ORG_PAGES_BASE}/${encodeURIComponent(repoName)}/`; |
| 12 | + } |
| 13 | + |
| 14 | + // Fetch all public repos with pagination; bust caches aggressively |
| 15 | + async function fetchAllRepos(url = `https://api.github.com/orgs/${ORG}/repos?per_page=100&type=public&ts=${Date.now()}`) { |
8 | 16 | const all = []; |
9 | 17 | while (url) { |
10 | | - const res = await fetch(url); |
| 18 | + const res = await fetch(url, { |
| 19 | + cache: 'no-store', |
| 20 | + headers: { 'Accept': 'application/vnd.github+json' } |
| 21 | + }); |
11 | 22 | if (!res.ok) throw new Error(`GitHub API: ${res.status} ${res.statusText}`); |
12 | 23 | const page = await res.json(); |
13 | 24 | all.push(...page); |
14 | | - // parse Link header for pagination |
| 25 | + |
15 | 26 | const link = res.headers.get('Link'); |
16 | 27 | const next = link && link.split(',').find(s => s.includes('rel="next"')); |
17 | | - url = next ? next.split(';')[0].trim().slice(1, -1) : null; |
| 28 | + url = next ? next.split(';')[0].trim().slice(1, -1) + `&ts=${Date.now()}` : null; |
18 | 29 | } |
19 | 30 | return all; |
20 | 31 | } |
21 | 32 |
|
22 | | - // Construct org Pages URL for a repo |
23 | | - function pagesUrl(repo) { |
24 | | - if (repo.name.toLowerCase() === `${ORG.toLowerCase()}.github.io`) { |
25 | | - return `${ORG_PAGES_BASE}/`; |
26 | | - } |
27 | | - return `${ORG_PAGES_BASE}/${encodeURIComponent(repo.name)}/`; |
28 | | - } |
29 | | - |
30 | | - function fmtDate(iso) { |
31 | | - try { return new Date(iso).toLocaleString(); } catch { return iso; } |
32 | | - } |
| 33 | + function fmtDate(iso) { try { return new Date(iso).toLocaleString(); } catch { return iso; } } |
33 | 34 |
|
34 | 35 | function render(list) { |
35 | 36 | const grid = document.getElementById('grid'); |
|
39 | 40 | empty.style.display = 'none'; |
40 | 41 |
|
41 | 42 | for (const r of list) { |
42 | | - const site = pagesUrl(r); |
| 43 | + const live = !!r.has_pages || r.name.toLowerCase() === `${ORG.toLowerCase()}.github.io`; |
| 44 | + const site = pagesUrl(r.name); |
43 | 45 |
|
44 | 46 | const card = document.createElement('div'); |
45 | 47 | card.className = 'card'; |
| 48 | + if (!live) card.style.opacity = 0.6; |
| 49 | + |
46 | 50 | card.innerHTML = ` |
47 | 51 | <h3> |
48 | | - <a href="${site}" target="_blank" rel="noopener" style="color:#67b7ff;text-decoration:none;"> |
| 52 | + <a href="${live ? site : r.html_url}" target="_blank" rel="noopener" |
| 53 | + style="color:#67b7ff;text-decoration:none;"> |
49 | 54 | ${r.name} |
50 | 55 | </a> |
51 | 56 | </h3> |
52 | 57 | <div class="meta">${r.description ? r.description : ''}</div> |
53 | 58 | <div class="meta">Updated: ${fmtDate(r.updated_at)}</div> |
54 | 59 | <div class="links"> |
55 | | - <a href="${site}" target="_blank" rel="noopener">View site</a> |
| 60 | + ${live |
| 61 | + ? `<a href="${site}" target="_blank" rel="noopener">View site</a>` |
| 62 | + : `<span class="meta">Not deployed yet</span>`} |
56 | 63 | <a href="${r.html_url}" target="_blank" rel="noopener">Repo</a> |
57 | 64 | </div> |
58 | 65 | `; |
59 | 66 |
|
60 | | - // Make the whole card open the site when clicked (but not when clicking an inner link) |
| 67 | + // Make the whole card open (site if live, else repo) unless a link was clicked |
61 | 68 | card.style.cursor = 'pointer'; |
62 | 69 | card.addEventListener('click', (e) => { |
63 | 70 | if (!(e.target instanceof HTMLAnchorElement)) { |
64 | | - window.open(site, '_blank', 'noopener'); |
| 71 | + window.open(live ? site : r.html_url, '_blank', 'noopener'); |
65 | 72 | } |
66 | 73 | }); |
67 | 74 |
|
|
73 | 80 | const q = document.getElementById('q').value.toLowerCase().trim(); |
74 | 81 | const sort = document.getElementById('sort').value; |
75 | 82 |
|
76 | | - let list = repos; |
| 83 | + let list = repos.slice(); |
| 84 | + if (!SHOW_ALL) { |
| 85 | + list = list.filter(r => r.has_pages || r.name.toLowerCase() === `${ORG.toLowerCase()}.github.io`); |
| 86 | + } |
77 | 87 | if (q) { |
78 | | - list = repos.filter(r => |
| 88 | + list = list.filter(r => |
79 | 89 | (r.name && r.name.toLowerCase().includes(q)) || |
80 | 90 | (r.description && r.description.toLowerCase().includes(q)) |
81 | 91 | ); |
82 | 92 | } |
| 93 | + if (sort === 'name') list.sort((a,b) => a.name.localeCompare(b.name)); |
| 94 | + else list.sort((a,b) => new Date(b.updated_at) - new Date(a.updated_at)); |
83 | 95 |
|
84 | | - if (sort === 'name') { |
85 | | - list.sort((a,b) => a.name.localeCompare(b.name)); |
86 | | - } else { |
87 | | - list.sort((a,b) => new Date(b.updated_at) - new Date(a.updated_at)); |
88 | | - } |
89 | 96 | render(list); |
90 | 97 | } |
91 | 98 |
|
92 | | - (async function init() { |
| 99 | + async function loadAndRender() { |
| 100 | + const btn = document.getElementById('refreshBtn'); |
| 101 | + if (btn) btn.disabled = true; |
| 102 | + |
93 | 103 | try { |
94 | 104 | const repos = await fetchAllRepos(); |
95 | | - // Only show repos with Pages enabled OR the org root repo |
96 | | - const withPages = repos.filter(r => |
97 | | - r.has_pages || r.name.toLowerCase() === `${ORG.toLowerCase()}.github.io` |
98 | | - ); |
99 | | - window.__REPOS__ = withPages; |
100 | | - filterSort(withPages); |
101 | | - |
102 | | - document.getElementById('q').addEventListener('input', () => filterSort(window.__REPOS__)); |
103 | | - document.getElementById('sort').addEventListener('change', () => filterSort(window.__REPOS__)); |
| 105 | + window.__REPOS__ = repos; |
| 106 | + filterSort(repos); |
104 | 107 | } catch (err) { |
105 | 108 | document.getElementById('grid').innerHTML = |
106 | 109 | `<div class="empty">Error loading list: ${err.message}</div>`; |
| 110 | + } finally { |
| 111 | + if (btn) btn.disabled = false; |
107 | 112 | } |
| 113 | + } |
| 114 | + |
| 115 | + function addRefreshButtonOnce() { |
| 116 | + if (document.getElementById('refreshBtn')) return; |
| 117 | + const toolbar = document.querySelector('.toolbar'); |
| 118 | + const btn = document.createElement('button'); |
| 119 | + btn.id = 'refreshBtn'; |
| 120 | + btn.textContent = 'Refresh'; |
| 121 | + btn.style.cssText = |
| 122 | + 'padding:10px 12px;border-radius:10px;border:1px solid #30363d;background:#0d1117;color:#e6edf3;cursor:pointer;'; |
| 123 | + btn.addEventListener('click', loadAndRender); |
| 124 | + toolbar.appendChild(btn); |
| 125 | + } |
| 126 | + |
| 127 | + (function start() { |
| 128 | + addRefreshButtonOnce(); |
| 129 | + loadAndRender(); |
| 130 | + document.getElementById('q').addEventListener('input', () => filterSort(window.__REPOS__ || [])); |
| 131 | + document.getElementById('sort').addEventListener('change', () => filterSort(window.__REPOS__ || [])); |
| 132 | + if (AUTO_REFRESH_MS > 0) setInterval(loadAndRender, AUTO_REFRESH_MS); |
108 | 133 | })(); |
109 | 134 | </script> |
110 | 135 |
|
| 136 | + |
0 commit comments