summaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/index.html20
-rw-r--r--static/script.js110
-rw-r--r--static/style.css39
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);
+}