diff options
Diffstat (limited to 'static')
-rw-r--r-- | static/index.html | 20 | ||||
-rw-r--r-- | static/script.js | 110 | ||||
-rw-r--r-- | static/style.css | 39 |
3 files changed, 169 insertions, 0 deletions
diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..8d7bd7a --- /dev/null +++ b/static/index.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <title>OSM proposals</title> + <link rel=stylesheet href=style.css /> + <h1>OSM proposals</h1> + <p>This page provides an overview of <a href="https://wiki.openstreetmap.org/wiki/Proposal">proposals</a> for OpenStreetMap. The page is updated every hour.</p> + <noscript>This page requires JavaScript.</noscript> + <p id=status></p> + <table border=1> + <thead> + <th>Status</th> + <th>Date</th> + <th>Name<br><input id=name autocomplete=off autofocus placeholder="Type to filter"></th> + <th>Authors<br><input id=authors autocomplete=off placeholder="Type to filter"></th> + </thead> + <tbody id=tbody> + </tbody> + </table> + <script src=script.js></script> +</html> diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..35aa43e --- /dev/null +++ b/static/script.js @@ -0,0 +1,110 @@ +const tbody = document.getElementById('tbody'); +const statusSpan = document.getElementById('status'); + +function newEl(tagname, content) { + const cell = document.createElement(tagname); + if (content instanceof Node) + cell.appendChild(content); + else + cell.textContent = content; + return cell; +} + +const updateUrl = debounce(params => { + if (Object.keys(params).length == 0) + window.history.pushState({}, '', '/'); + else + window.history.pushState({}, '', '?' + new URLSearchParams(params).toString()); +}, 500); + +function normalize(str) { + return str.toLowerCase().replaceAll('_', ' ') +} + +function escapeForRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function display(proposals) { + const params = {}; + if (nameInput.value) { + const regex = new RegExp('\\b' + escapeForRegex(normalize(nameInput.value))); + proposals = proposals.filter(p => regex.test(normalize(p.name || p.page_title))); + params.q = nameInput.value; + } + + if (authorsInput.value) { + // The first letter of MediaWiki usernames is case-insensitive. + const firstChar = authorsInput.value.charAt(0); + const rest = authorsInput.value.slice(1); + const lowercase = firstChar.toLowerCase() + rest; + const uppercase = firstChar.toUpperCase() + rest; + proposals = proposals.filter(p => p.authors && (p.authors.includes(lowercase) || p.authors.includes(uppercase))); + params.author = authorsInput.value; + } + + const nf = Intl.NumberFormat(); + statusSpan.textContent = `Found ${nf.format(proposals.length)} proposals.`; + + updateUrl(params); + + tbody.innerHTML = ''; + proposals.forEach(proposal => { + const row = document.createElement('tr'); + + const statusCell = newEl('td', proposal.status); + statusCell.className = 'status-' + proposal.status; + row.appendChild(statusCell); + + let date = '???'; + if (proposal.status == 'voting' || proposal.status == 'approved' || proposal.status == 'rejected') { + date = proposal.vote_start; + } else if (proposal.status == 'proposed') { + date = proposal.rfc_start; + } else { + date = proposal.draft_start; + } + row.appendChild(newEl('td', date)); + + const link = newEl('a', proposal.name || proposal.page_title); + link.href = 'https://wiki.openstreetmap.org/wiki/' + proposal.page_title.replaceAll(' ', '_'); + + const nameCell = newEl('td', link); + if (proposal.lang) + nameCell.appendChild(document.createTextNode(` (${proposal.lang})`)); + row.appendChild(nameCell); + row.appendChild(newEl('td', proposal.authors)); + + tbody.appendChild(row); + }); +} + +const nameInput = document.getElementById('name'); +const authorsInput = document.getElementById('authors'); + +function debounce(callback, wait) { + let timeoutId = null; + return (...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback.apply(null, args); + }, wait); + }; +} + +(async function() { + const proposals = await (await fetch('proposals.json')).json(); + + const params = new URLSearchParams(location.search); + nameInput.value = params.get('q'); + authorsInput.value = params.get('author'); + + display(proposals); + + nameInput.addEventListener('input', debounce(e => { + display(proposals); + }, 100)); + authorsInput.addEventListener('input', debounce(e => { + display(proposals); + }, 100)); +})(); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..66fe28f --- /dev/null +++ b/static/style.css @@ -0,0 +1,39 @@ +body { + font-family: sans-serif; +} +a { + text-decoration: none; +} +th { + position: sticky; + top: 0; + background: gainsboro; +} +td, th { + padding: 0.2em; +} +td:first-of-type { text-align: center; } +.status-voting { background: #baf; } +.status-proposed { background: #eef; } +.status-approved { background: #dfd; } +.status-rejected { background: #fbb; } + +/* Cleaner table borders compatible with the sticky table header */ +table { + border-spacing: 0; + border: none; + --border: 1px solid #999; +} +table th { + border: var(--border); + border-left: none; +} +table td { + border: var(--border); + border-left: none; + border-top: none; +} +table tr > th:first-child, +table tr > td:first-child { + border-left: var(--border); +} |