Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 115 additions & 17 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@
.pill{padding:0.4rem 0.85rem;border:1px solid var(--border);border-radius:100px;background:transparent;color:var(--text-secondary);cursor:pointer;font-size:0.75rem;font-weight:500;font-family:'Inter',sans-serif;transition:all 0.15s;white-space:nowrap}
.pill:hover{border-color:var(--border-light);color:var(--text)}
.pill.active{background:var(--text);color:var(--bg);border-color:var(--text)}
.scorecard-actions{display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;justify-content:flex-end}
.view-link-button{padding:0.42rem 0.85rem;border:1px solid rgba(247,147,26,0.25);border-radius:var(--radius-xs);background:rgba(247,147,26,0.08);color:var(--accent);cursor:pointer;font-size:0.75rem;font-weight:600;font-family:'Inter',sans-serif;transition:all 0.15s;white-space:nowrap}
.view-link-button:hover{border-color:rgba(247,147,26,0.45);background:rgba(247,147,26,0.14);color:#fbbf24}
.copy-status{min-width:3.5rem;font-size:0.72rem;color:var(--text-muted)}

/* Table */
.table-wrap{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
Expand Down Expand Up @@ -176,6 +180,7 @@
th,td{padding:0.5rem 0.6rem}
.td-summary{max-width:200px;font-size:0.75rem}
.method-grid{grid-template-columns:1fr}
.scorecard-actions{justify-content:flex-start;width:100%}
}

/* Search */
Expand Down Expand Up @@ -318,7 +323,11 @@ <h1>Bitcoin Developer <span>Quantum Urgency</span> Map</h1>
<h2 class="section-title">Scorecard</h2>
<span class="section-count" id="showing-count"></span>
</div>
<div class="scorecard-actions">
<div class="filters" id="filters"></div>
<button type="button" class="view-link-button" id="copy-view" title="Copy the current filters and selected developer as a shareable link">Copy view</button>
<span class="copy-status" id="copy-status" aria-live="polite"></span>
</div>
</div>
<div class="filter-row">
<div class="search-wrap">
Expand Down Expand Up @@ -460,11 +469,43 @@ <h4>Ecosystem</h4>
const total=data.metadata.total_assessed;
const qri=data.metadata.quantum_readiness_index;

function levelForScore(value){
if(value<20)return 'Asleep';
if(value<40)return 'Sleepwalking';
if(value<60)return 'Awakening';
if(value<80)return 'Mobilizing';
return 'Ready';
}

function normalizeReadiness(raw){
if(raw&&typeof raw==='object'&&raw.composite_readiness&&raw.voiced_urgency&&raw.coverage)return raw;
const silent=dist['1_no_known_view']||0;
const voicedCount=Math.max(0,total-silent);
const weighted=(dist['5_urgent']||0)*5+(dist['4_proactive']||0)*4+(dist['3_cautious']||0)*3+(dist['2_dismissive']||0)*2;
const voicedScore=voicedCount?Math.round(weighted/(voicedCount*5)*100):0;
const coverageScore=total?Math.round(voicedCount/total*100):0;
let compositeScore=Math.round(voicedScore*coverageScore/100);
if(typeof raw==='number'&&Number.isFinite(raw))compositeScore=Math.round(raw<=1?raw*100:raw);
const currentLevel=levelForScore(compositeScore);
return {
composite_readiness:{
score:compositeScore,
current_level:currentLevel,
formula:typeof raw==='number'
?'Index loaded from metadata. Voiced urgency and coverage panels are derived from the current score distribution.'
:'Voiced urgency multiplied by coverage. Derived from the current score distribution because no structured index object was present.'
},
voiced_urgency:{score:voicedScore,voices_count:voicedCount},
coverage:{score:coverageScore,silent,total}
};
}

// Readiness Index
const indexEl=document.getElementById('index-card');
const comp=qri.composite_readiness;
const voiced=qri.voiced_urgency;
const coverage=qri.coverage;
const normalizedQri=normalizeReadiness(qri);
const comp=normalizedQri.composite_readiness;
const voiced=normalizedQri.voiced_urgency;
const coverage=normalizedQri.coverage;
const score=comp.score;
const circumference=2*Math.PI*65;
const offset=circumference-(score/100)*circumference;
Expand Down Expand Up @@ -530,10 +571,12 @@ <h4>Ecosystem</h4>
});

// State
let activeScore='all';
let activeAffiliation='all';
let searchQuery='';
let activeDevName=null;
const urlState=new URLSearchParams(window.location.search);
const validScores=new Set(scores.map(s=>String(s.score)));
let activeScore=validScores.has(urlState.get('score'))?urlState.get('score'):'all';
let activeAffiliation=urlState.get('affiliation')||'all';
let searchQuery=urlState.get('q')||'';
let activeDevName=urlState.get('dev')||null;

// Affiliation dropdown — populate from data
const affilSelect=document.getElementById('affil-select');
Expand All @@ -545,6 +588,45 @@ <h4>Ecosystem</h4>
opt.textContent=`${a} (${affilCounts[a]})`;
affilSelect.appendChild(opt);
});
if(activeAffiliation!=='all'&&!affilCounts[activeAffiliation])activeAffiliation='all';

function findDevByName(name){
if(!name)return null;
const needle=String(name).toLowerCase();
return devs.find(d=>d.name&&d.name.toLowerCase()===needle)||null;
}

function updateUrlState(){
const params=new URLSearchParams();
if(activeScore!=='all')params.set('score',activeScore);
if(activeAffiliation!=='all')params.set('affiliation',activeAffiliation);
if(searchQuery.trim())params.set('q',searchQuery.trim());
if(activeDevName)params.set('dev',activeDevName);
const query=params.toString();
const nextUrl=query?`${window.location.pathname}?${query}`:window.location.pathname;
window.history.replaceState(null,'',nextUrl);
}

function copyText(text){
if(navigator.clipboard&&window.isSecureContext)return navigator.clipboard.writeText(text);
const textarea=document.createElement('textarea');
textarea.value=text;
textarea.setAttribute('readonly','');
textarea.style.position='fixed';
textarea.style.left='-9999px';
document.body.appendChild(textarea);
textarea.select();
const ok=document.execCommand('copy');
document.body.removeChild(textarea);
return ok?Promise.resolve():Promise.reject(new Error('copy failed'));
}

function syncControlsFromState(){
document.querySelectorAll('.pill').forEach(btn=>btn.classList.toggle('active',btn.dataset.score===activeScore));
document.querySelectorAll('.stat-cell').forEach(cell=>cell.classList.toggle('active',activeScore!=='all'&&cell.dataset.score===activeScore));
document.getElementById('search').value=searchQuery;
affilSelect.value=activeAffiliation;
}

function escapeHtml(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);}

Expand Down Expand Up @@ -585,14 +667,9 @@ <h4>Ecosystem</h4>
// Filter clicks (pills)
document.querySelectorAll('.pill').forEach(btn=>{
btn.addEventListener('click',()=>{
document.querySelectorAll('.pill').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
activeScore=btn.dataset.score;
// sync stat cells
document.querySelectorAll('.stat-cell').forEach(c=>c.classList.remove('active'));
if(activeScore!=='all'){
document.querySelector(`.stat-cell[data-score="${activeScore}"]`)?.classList.add('active');
}
syncControlsFromState();
updateUrlState();
render();
});
});
Expand All @@ -601,25 +678,41 @@ <h4>Ecosystem</h4>
document.querySelectorAll('.stat-cell').forEach(cell=>{
cell.addEventListener('click',()=>{
const score=cell.dataset.score;
if(activeScore===score){activeScore='all';cell.classList.remove('active');}
else{activeScore=score;document.querySelectorAll('.stat-cell').forEach(c=>c.classList.remove('active'));cell.classList.add('active');}
document.querySelectorAll('.pill').forEach(b=>{b.classList.remove('active');if(b.dataset.score===activeScore)b.classList.add('active');});
activeScore=activeScore===score?'all':score;
syncControlsFromState();
updateUrlState();
render();
});
});

// Search
document.getElementById('search').addEventListener('input',e=>{
searchQuery=e.target.value;
updateUrlState();
render();
});

// Affiliation filter
affilSelect.addEventListener('change',e=>{
activeAffiliation=e.target.value;
updateUrlState();
render();
});

const copyViewBtn=document.getElementById('copy-view');
const copyStatus=document.getElementById('copy-status');
copyViewBtn.addEventListener('click',async()=>{
updateUrlState();
try{
await copyText(window.location.href);
copyStatus.textContent='Copied';
setTimeout(()=>{copyStatus.textContent='';},1800);
}catch{
copyStatus.textContent='Copy failed';
setTimeout(()=>{copyStatus.textContent='';},2200);
}
});

// === DETAIL DRAWER ===
const drawer=document.getElementById('drawer');
const drawerBackdrop=document.getElementById('drawer-backdrop');
Expand All @@ -628,6 +721,7 @@ <h4>Ecosystem</h4>

function openDrawer(dev){
activeDevName=dev.name;
updateUrlState();
const scoreLabelMap={5:'Urgent',4:'Proactive',3:'Cautious',2:'Dismissive',1:'No View'};
document.getElementById('drawer-meta').textContent=dev.notable?'Notable Contributor':`Influence Rank #${dev.rank}`;
document.getElementById('drawer-name').textContent=dev.name;
Expand Down Expand Up @@ -667,6 +761,7 @@ <h4>Ecosystem</h4>

function closeDrawer(){
activeDevName=null;
updateUrlState();
drawer.classList.remove('is-open');
drawerBackdrop.classList.remove('is-open');
drawer.setAttribute('aria-hidden','true');
Expand All @@ -683,7 +778,10 @@ <h4>Ecosystem</h4>
drawer.setAttribute('aria-hidden','true');
drawerBackdrop.setAttribute('aria-hidden','true');

syncControlsFromState();
render();
const initialDev=findDevByName(activeDevName);
if(initialDev)openDrawer(initialDev);

// === BUBBLE CHART ===
const canvas = document.getElementById('bubble-chart');
Expand Down