···
transition: all 0.2s ease;
.app-view:hover .app-circle {{
···
660
+
// Try to fetch app avatar from their bsky profile
661
+
async function fetchAppAvatar(namespace) {{
663
+
// Reverse namespace to get domain (e.g., io.zzstoatzz -> zzstoatzz.io)
664
+
const reversed = namespace.split('.').reverse().join('.');
665
+
// Try reversed domain, then reversed.bsky.social
666
+
const handles = [reversed, `${{reversed}}.bsky.social`];
668
+
for (const handle of handles) {{
670
+
const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${{handle}}`);
671
+
if (!didRes.ok) continue;
673
+
const {{ did }} = await didRes.json();
674
+
const profileRes = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${{did}}`);
675
+
if (!profileRes.ok) continue;
677
+
const profile = await profileRes.json();
678
+
if (profile.avatar) {{
679
+
return profile.avatar;
686
+
console.log('Could not fetch avatar for', namespace);
document.getElementById('logoutBtn').addEventListener('click', (e) => {{
···
const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase();
770
-
<div class="app-circle">${{firstLetter}}</div>
808
+
<div class="app-circle" data-namespace="${{namespace}}">${{firstLetter}}</div>
<div class="app-name">${{namespace}}</div>
812
+
// Try to fetch and display avatar
813
+
fetchAppAvatar(namespace).then(avatarUrl => {{
815
+
const circle = div.querySelector('.app-circle');
816
+
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) {{
785
-
collections.sort().forEach(lexicon => {{
786
-
const shortName = lexicon.split('.').slice(2).join('.') || lexicon;
788
-
<div class="tree-item" data-lexicon="${{lexicon}}">
789
-
<div class="tree-item-header">
790
-
<span>${{shortName}}</span>
791
-
<span class="tree-item-count">loading...</span>
831
+
// Group collections by sub-namespace (third segment)
832
+
const grouped = {{}};
833
+
collections.forEach(lexicon => {{
834
+
const parts = lexicon.split('.');
835
+
const subNamespace = parts.slice(2).join('.');
836
+
const firstPart = parts[2] || lexicon;
838
+
if (!grouped[firstPart]) grouped[firstPart] = [];
839
+
grouped[firstPart].push({{ lexicon, subNamespace }});
842
+
// Sort and display grouped items
843
+
Object.keys(grouped).sort().forEach(group => {{
844
+
const items = grouped[group];
846
+
if (items.length === 1 && items[0].subNamespace === group) {{
847
+
// Single item with no further nesting
849
+
<div class="tree-item" data-lexicon="${{items[0].lexicon}}">
850
+
<div class="tree-item-header">
851
+
<span>${{group}}</span>
852
+
<span class="tree-item-count">loading...</span>
858
+
html += `<div style="margin-bottom: 0.75rem;">`;
859
+
html += `<div style="font-size: 0.7rem; color: var(--text-light); margin-bottom: 0.4rem; font-weight: 500;">${{group}}</div>`;
862
+
items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => {{
863
+
const displayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace;
865
+
<div class="tree-item" data-lexicon="${{item.lexicon}}" style="margin-left: 0.75rem;">
866
+
<div class="tree-item-header">
867
+
<span>${{displayName}}</span>
868
+
<span class="tree-item-count">loading...</span>
html += `<div class="tree-item">no collections found</div>`;
···
871
-
if (data.cursor) {{
951
+
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);
934
-
if (moreData.cursor) {{
1014
+
if (moreData.cursor && moreData.records.length === 5) {{
recordListDiv.insertAdjacentHTML('beforeend',
`<button class="load-more" data-cursor="${{moreData.cursor}}" data-lexicon="${{lexicon}}">load more</button>`