···
transition: all 0.2s ease;
.app-view:hover .app-circle {{
···
+
// Try to fetch app avatar from their bsky profile
+
async function fetchAppAvatar(namespace) {{
+
// Reverse namespace to get domain (e.g., io.zzstoatzz -> zzstoatzz.io)
+
const reversed = namespace.split('.').reverse().join('.');
+
// Try reversed domain, then reversed.bsky.social
+
const handles = [reversed, `${{reversed}}.bsky.social`];
+
for (const handle of handles) {{
+
const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${{handle}}`);
+
if (!didRes.ok) continue;
+
const {{ did }} = await didRes.json();
+
const profileRes = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${{did}}`);
+
if (!profileRes.ok) continue;
+
const profile = await profileRes.json();
+
console.log('Could not fetch avatar for', namespace);
document.getElementById('logoutBtn').addEventListener('click', (e) => {{
···
const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase();
+
<div class="app-circle" data-namespace="${{namespace}}">${{firstLetter}}</div>
<div class="app-name">${{namespace}}</div>
+
// Try to fetch and display avatar
+
fetchAppAvatar(namespace).then(avatarUrl => {{
+
const circle = div.querySelector('.app-circle');
+
circle.innerHTML = `<img src="${{avatarUrl}}" class="app-logo" alt="${{namespace}}" />`;
div.addEventListener('click', () => {{
const detail = document.getElementById('detail');
const collections = apps[namespace];
···
if (collections && collections.length > 0) {{
+
// Group collections by sub-namespace (third segment)
+
collections.forEach(lexicon => {{
+
const parts = lexicon.split('.');
+
const subNamespace = parts.slice(2).join('.');
+
const firstPart = parts[2] || lexicon;
+
if (!grouped[firstPart]) grouped[firstPart] = [];
+
grouped[firstPart].push({{ lexicon, subNamespace }});
+
// Sort and display grouped items
+
Object.keys(grouped).sort().forEach(group => {{
+
const items = grouped[group];
+
if (items.length === 1 && items[0].subNamespace === group) {{
+
// Single item with no further nesting
+
<div class="tree-item" data-lexicon="${{items[0].lexicon}}">
+
<div class="tree-item-header">
+
<span>${{group}}</span>
+
<span class="tree-item-count">loading...</span>
+
html += `<div style="margin-bottom: 0.75rem;">`;
+
html += `<div style="font-size: 0.7rem; color: var(--text-light); margin-bottom: 0.4rem; font-weight: 500;">${{group}}</div>`;
+
items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => {{
+
const displayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace;
+
<div class="tree-item" data-lexicon="${{item.lexicon}}" style="margin-left: 0.75rem;">
+
<div class="tree-item-header">
+
<span>${{displayName}}</span>
+
<span class="tree-item-count">loading...</span>
html += `<div class="tree-item">no collections found</div>`;
···
+
if (data.cursor && data.records.length === 5) {{
recordsHtml += `<button class="load-more" data-cursor="${{data.cursor}}" data-lexicon="${{lexicon}}">load more</button>`;
···
recordListDiv.insertAdjacentHTML('beforeend', moreHtml);
+
if (moreData.cursor && moreData.records.length === 5) {{
recordListDiv.insertAdjacentHTML('beforeend',
`<button class="load-more" data-cursor="${{moreData.cursor}}" data-lexicon="${{lexicon}}">load more</button>`