closes #1 closes #2 closes #3
+92
-12
src/templates.rs
+92
-12
src/templates.rs
···
356
align-items: center;
357
justify-content: center;
358
transition: all 0.2s ease;
359
}}
360
361
.app-view:hover .app-circle {{
···
650
let globalPds = null;
651
let globalHandle = null;
652
653
// Logout handler
654
document.getElementById('logoutBtn').addEventListener('click', (e) => {{
655
e.preventDefault();
···
767
const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase();
768
769
div.innerHTML = `
770
-
<div class="app-circle">${{firstLetter}}</div>
771
<div class="app-name">${{namespace}}</div>
772
`;
773
774
div.addEventListener('click', () => {{
775
const detail = document.getElementById('detail');
776
const collections = apps[namespace];
···
782
`;
783
784
if (collections && collections.length > 0) {{
785
-
collections.sort().forEach(lexicon => {{
786
-
const shortName = lexicon.split('.').slice(2).join('.') || lexicon;
787
-
html += `
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>
792
</div>
793
-
</div>
794
-
`;
795
}});
796
}} else {{
797
html += `<div class="tree-item">no collections found</div>`;
···
868
`;
869
}});
870
871
-
if (data.cursor) {{
872
recordsHtml += `<button class="load-more" data-cursor="${{data.cursor}}" data-lexicon="${{lexicon}}">load more</button>`;
873
}}
874
···
931
loadMoreBtn.remove();
932
recordListDiv.insertAdjacentHTML('beforeend', moreHtml);
933
934
-
if (moreData.cursor) {{
935
recordListDiv.insertAdjacentHTML('beforeend',
936
`<button class="load-more" data-cursor="${{moreData.cursor}}" data-lexicon="${{lexicon}}">load more</button>`
937
);
···
356
align-items: center;
357
justify-content: center;
358
transition: all 0.2s ease;
359
+
overflow: hidden;
360
+
}}
361
+
362
+
.app-logo {{
363
+
width: 100%;
364
+
height: 100%;
365
+
object-fit: cover;
366
}}
367
368
.app-view:hover .app-circle {{
···
657
let globalPds = null;
658
let globalHandle = null;
659
660
+
// Try to fetch app avatar from their bsky profile
661
+
async function fetchAppAvatar(namespace) {{
662
+
try {{
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`];
667
+
668
+
for (const handle of handles) {{
669
+
try {{
670
+
const didRes = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${{handle}}`);
671
+
if (!didRes.ok) continue;
672
+
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;
676
+
677
+
const profile = await profileRes.json();
678
+
if (profile.avatar) {{
679
+
return profile.avatar;
680
+
}}
681
+
}} catch (e) {{
682
+
continue;
683
+
}}
684
+
}}
685
+
}} catch (e) {{
686
+
console.log('Could not fetch avatar for', namespace);
687
+
}}
688
+
return null;
689
+
}}
690
+
691
// Logout handler
692
document.getElementById('logoutBtn').addEventListener('click', (e) => {{
693
e.preventDefault();
···
805
const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase();
806
807
div.innerHTML = `
808
+
<div class="app-circle" data-namespace="${{namespace}}">${{firstLetter}}</div>
809
<div class="app-name">${{namespace}}</div>
810
`;
811
812
+
// Try to fetch and display avatar
813
+
fetchAppAvatar(namespace).then(avatarUrl => {{
814
+
if (avatarUrl) {{
815
+
const circle = div.querySelector('.app-circle');
816
+
circle.innerHTML = `<img src="${{avatarUrl}}" class="app-logo" alt="${{namespace}}" />`;
817
+
}}
818
+
}});
819
+
820
div.addEventListener('click', () => {{
821
const detail = document.getElementById('detail');
822
const collections = apps[namespace];
···
828
`;
829
830
if (collections && collections.length > 0) {{
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;
837
+
838
+
if (!grouped[firstPart]) grouped[firstPart] = [];
839
+
grouped[firstPart].push({{ lexicon, subNamespace }});
840
+
}});
841
+
842
+
// Sort and display grouped items
843
+
Object.keys(grouped).sort().forEach(group => {{
844
+
const items = grouped[group];
845
+
846
+
if (items.length === 1 && items[0].subNamespace === group) {{
847
+
// Single item with no further nesting
848
+
html += `
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>
853
+
</div>
854
</div>
855
+
`;
856
+
}} else {{
857
+
// Group header
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>`;
860
+
861
+
// Items in group
862
+
items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => {{
863
+
const displayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace;
864
+
html += `
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>
869
+
</div>
870
+
</div>
871
+
`;
872
+
}});
873
+
html += `</div>`;
874
+
}}
875
}});
876
}} else {{
877
html += `<div class="tree-item">no collections found</div>`;
···
948
`;
949
}});
950
951
+
if (data.cursor && data.records.length === 5) {{
952
recordsHtml += `<button class="load-more" data-cursor="${{data.cursor}}" data-lexicon="${{lexicon}}">load more</button>`;
953
}}
954
···
1011
loadMoreBtn.remove();
1012
recordListDiv.insertAdjacentHTML('beforeend', moreHtml);
1013
1014
+
if (moreData.cursor && moreData.records.length === 5) {{
1015
recordListDiv.insertAdjacentHTML('beforeend',
1016
`<button class="load-more" data-cursor="${{moreData.cursor}}" data-lexicon="${{lexicon}}">load more</button>`
1017
);