+130
src/templates.rs
+130
src/templates.rs
···
354
354
function toggleInfo() {
355
355
document.getElementById('infoContent').classList.toggle('expanded');
356
356
}
357
+
358
+
// Atmosphere rendering
359
+
async function fetchAtmosphere() {
360
+
const CACHE_KEY = 'atme_atmosphere';
361
+
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
362
+
363
+
const cached = localStorage.getItem(CACHE_KEY);
364
+
if (cached) {
365
+
const { data, timestamp } = JSON.parse(cached);
366
+
if (Date.now() - timestamp < CACHE_DURATION) {
367
+
return data;
368
+
}
369
+
}
370
+
371
+
try {
372
+
const response = await fetch('https://ufos-api.microcosm.blue/collections?order=dids-estimate&limit=50');
373
+
const json = await response.json();
374
+
375
+
// Group by namespace (first two segments)
376
+
const namespaces = {};
377
+
json.collections.forEach(col => {
378
+
const parts = col.nsid.split('.');
379
+
if (parts.length >= 2) {
380
+
const ns = `${parts[0]}.${parts[1]}`;
381
+
if (!namespaces[ns]) {
382
+
namespaces[ns] = {
383
+
namespace: ns,
384
+
dids_total: 0,
385
+
records_total: 0,
386
+
collections: []
387
+
};
388
+
}
389
+
namespaces[ns].dids_total += col.dids_estimate;
390
+
namespaces[ns].records_total += col.creates;
391
+
namespaces[ns].collections.push(col.nsid);
392
+
}
393
+
});
394
+
395
+
const data = Object.values(namespaces).sort((a, b) => b.dids_total - a.dids_total).slice(0, 30);
396
+
397
+
localStorage.setItem(CACHE_KEY, JSON.stringify({
398
+
data,
399
+
timestamp: Date.now()
400
+
}));
401
+
402
+
return data;
403
+
} catch (e) {
404
+
console.error('Failed to fetch atmosphere data:', e);
405
+
return [];
406
+
}
407
+
}
408
+
409
+
async function fetchAppAvatar(namespace) {
410
+
const reversed = namespace.split('.').reverse().join('.');
411
+
const handles = [reversed, `${reversed}.bsky.social`];
412
+
413
+
for (const handle of handles) {
414
+
try {
415
+
const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`);
416
+
if (!didRes.ok) continue;
417
+
418
+
const { did } = await didRes.json();
419
+
const profileRes = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}`);
420
+
if (!profileRes.ok) continue;
421
+
422
+
const profile = await profileRes.json();
423
+
if (profile.avatar) return profile.avatar;
424
+
} catch (e) {
425
+
continue;
426
+
}
427
+
}
428
+
return null;
429
+
}
430
+
431
+
async function renderAtmosphere() {
432
+
const data = await fetchAtmosphere();
433
+
if (!data.length) return;
434
+
435
+
const atmosphere = document.getElementById('atmosphere');
436
+
const maxSize = Math.max(...data.map(d => d.dids_total));
437
+
438
+
data.forEach((app, i) => {
439
+
const orb = document.createElement('div');
440
+
orb.className = 'app-orb';
441
+
442
+
// Size based on user count (20-80px)
443
+
const size = 20 + (app.dids_total / maxSize) * 60;
444
+
445
+
// Position in 3D space
446
+
const angle = (i / data.length) * Math.PI * 2;
447
+
const radius = 250 + (i % 3) * 100;
448
+
const y = (i % 5) * 80 - 160;
449
+
const x = Math.cos(angle) * radius;
450
+
const z = Math.sin(angle) * radius;
451
+
452
+
orb.style.width = `${size}px`;
453
+
orb.style.height = `${size}px`;
454
+
orb.style.left = `calc(50% + ${x}px)`;
455
+
orb.style.top = `calc(50% + ${y}px)`;
456
+
orb.style.transform = `translateZ(${z}px) translate(-50%, -50%)`;
457
+
orb.style.background = `radial-gradient(circle, rgba(255,255,255,0.1), rgba(255,255,255,0.02))`;
458
+
orb.style.border = '1px solid rgba(255,255,255,0.1)';
459
+
orb.style.boxShadow = '0 0 20px rgba(255,255,255,0.1)';
460
+
461
+
// Fallback letter
462
+
const letter = app.namespace.split('.')[1]?.[0]?.toUpperCase() || app.namespace[0].toUpperCase();
463
+
orb.innerHTML = `<div class="fallback">${letter}</div>`;
464
+
465
+
// Tooltip
466
+
const tooltip = document.createElement('div');
467
+
tooltip.className = 'app-tooltip';
468
+
const users = app.dids_total >= 1000000
469
+
? `${(app.dids_total / 1000000).toFixed(1)}M users`
470
+
: `${(app.dids_total / 1000).toFixed(0)}K users`;
471
+
tooltip.textContent = `${app.namespace} • ${users}`;
472
+
orb.appendChild(tooltip);
473
+
474
+
atmosphere.appendChild(orb);
475
+
476
+
// Fetch and apply avatar
477
+
fetchAppAvatar(app.namespace).then(avatarUrl => {
478
+
if (avatarUrl) {
479
+
orb.innerHTML = `<img src="${avatarUrl}" alt="${app.namespace}" />`;
480
+
orb.appendChild(tooltip);
481
+
}
482
+
});
483
+
});
484
+
}
485
+
486
+
renderAtmosphere();
357
487
</script>
358
488
</body>
359
489
</html>