From 200218ba86a66cc45d19ecd83b3f5b5816b8d098 Mon Sep 17 00:00:00 2001
From: Jarek Potiuk
Date: Fri, 30 Oct 2026 21:37:12 +0100
Subject: [PATCH 1/3] Chooser: update matching projects in place as answers
change
Show the best matches under every question instead of only at the end,
and re-rank them live as options are toggled, before pressing Next.
Project cards are built once and reused, so moving between questions
only swaps the question block rather than re-rendering the whole stage.
"Show matches now" also keeps any options ticked on the current question.
Generated-by: Claude Opus 5
---
site/chooser.html | 98 +++++++++++++++++++++++++++++++++++------------
1 file changed, 73 insertions(+), 25 deletions(-)
diff --git a/site/chooser.html b/site/chooser.html
index cff357b7..e3ac3d4d 100644
--- a/site/chooser.html
+++ b/site/chooser.html
@@ -31,6 +31,9 @@
.result h3 { margin: 0 0 4px; }
.result .why { font-size: 85%; color: #666; margin-top: 6px; }
.result .links a { margin-right: 12px; font-size: 90%; }
+ .result.new { animation: fadein .4s; }
+ @keyframes fadein { from { opacity: 0; transform: translateY(-4px); } }
+ #matches-h { margin-top: 28px; }
.score { float: right; font-size: 85%; color: #c6262e; }
.mock { background: #fff6d5; border: 1px solid #e6c84f; padding: 8px 12px; border-radius: 4px; font-size: 90%; }
@@ -50,6 +53,12 @@
Help me choose a project
support, design, translation and outreach just as much. Skip anything you don't care about.
Loading project data…
+
+
+
+
No matches - try going back and loosening an answer.
+
+
@@ -86,13 +95,12 @@
Help me choose a project
(answers[qid] || []).some(v => vals.includes(v)));
}
-function score(p) {
- let s = 0, why = [];
- for (const [qid, vals] of Object.entries(answers)) {
- if (!vals.length || !isFilter(qid)) continue;
- if (matches(p, qid, vals)) { s++; why.push(vals.filter(v => p[qid].includes(v)).map(labelFor.bind(null, qid)).join(', ')); }
+function score(p, ans) {
+ let s = 0;
+ for (const [qid, vals] of Object.entries(ans)) {
+ if (vals.length && isFilter(qid) && matches(p, qid, vals)) s++;
}
- return {s, why};
+ return s;
}
function labelFor(qid, v) {
@@ -102,9 +110,9 @@
Help me choose a project
}
// projects matching every non-skipped answer
-function candidates() {
+function candidates(ans = answers) {
return DATA.projects.filter(p =>
- Object.entries(answers).every(([qid, vals]) => !vals.length || matches(p, qid, vals)));
+ Object.entries(ans).every(([qid, vals]) => !vals.length || matches(p, qid, vals)));
}
// entropy of a question's split over the current candidates
@@ -133,6 +141,7 @@
- ${asked ? `matches ${s} of ${asked}` : ''}
+// One card per project, built on first use and reused afterwards, so the
+// list can be re-ranked by moving nodes instead of rebuilding the page.
+const cards = new Map();
+function card(p) {
+ if (cards.has(p.id)) return cards.get(p.id);
+ const el = document.createElement('div');
+ el.className = 'result';
+ el.dataset.id = p.id;
+ el.innerHTML = `
No matches - try going back and loosening an answer.
@@ -80,7 +82,7 @@
* projects with incomplete metadata aren't thrown out entirely.
* - We stop when few enough projects remain, or questions run out.
*/
-const STOP_AT = 8, SHOW = 12;
+const STOP_AT = 8, SHOW = 10;
let DATA, answers = {}, history = [];
const $s = () => document.getElementById('stage');
@@ -167,7 +169,7 @@
${esc(q.text)}
const preview = () => {
document.getElementById('remaining').textContent =
`${candidates(pending()).length} communities match so far`;
- showMatches(pending(), 'Best matches so far');
+ showMatches(pending(), 'matches so far');
};
$s().querySelectorAll('.opt').forEach(el => el.onclick = () => {
const v = el.dataset.v;
@@ -219,12 +221,12 @@
${esc(p.name)}
// Update the match list in place: drop cards that fell out of the top
// SHOW, move the rest into rank order, and fade in newcomers.
-function showMatches(ans, heading) {
+function showMatches(ans, what) {
const asked = Object.entries(ans).filter(([k, v]) => v.length && isFilter(k)).length;
- const ranked = DATA.projects.map(p => ({p, s: score(p, ans)}))
+ const all = DATA.projects.map(p => ({p, s: score(p, ans)}))
.filter(r => r.s > 0 || !asked)
- .sort((a, b) => b.s - a.s || (b.p.committers || 0) - (a.p.committers || 0))
- .slice(0, SHOW);
+ .sort((a, b) => b.s - a.s || (b.p.committers || 0) - (a.p.committers || 0));
+ const ranked = all.slice(0, SHOW);
const list = document.getElementById('list');
const keep = new Set(ranked.map(r => r.p.id));
[...list.children].forEach(el => { if (!keep.has(el.dataset.id)) el.remove(); });
@@ -234,7 +236,16 @@
${esc(p.name)}
if (!el.isConnected) { el.classList.remove('new'); void el.offsetWidth; el.classList.add('new'); }
if (list.children[i] !== el) list.insertBefore(el, list.children[i] || null);
});
- document.getElementById('matches-h').textContent = heading;
+ document.getElementById('matches-h').textContent = all.length > SHOW
+ ? `Top ${SHOW} ${what}` : what[0].toUpperCase() + what.slice(1);
+ // Always say that this is only the top of the list, not every match.
+ document.getElementById('shown').textContent = all.length <= SHOW
+ ? `Showing all ${all.length} matching communities.`
+ : `Showing only the top ${SHOW} of ${all.length} ${asked
+ ? 'communities that match at least one of your answers, ranked by how many they match, then by community size'
+ : 'communities, largest first'}.${
+ document.getElementById('next') ? ' Answer more questions to narrow it down.' : ''}`;
+ document.getElementById('shown').hidden = !all.length;
document.getElementById('empty').hidden = ranked.length > 0;
document.getElementById('matches').hidden = false;
}
@@ -243,7 +254,7 @@