From c89b3042d4e9f6a80dfe1f37a058757dc92e5768 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 6 Oct 2025 16:16:25 -0500 Subject: [PATCH] feat: add app logos and improve ux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetch app logos from reversed namespace handles (e.g., io.zzstoatzz -> zzstoatzz.io) - group collections hierarchically by sub-namespace in detail panel - fix load more button to only show when full page returned 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/templates.rs | 104 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/src/templates.rs b/src/templates.rs index d31179c..9bc1fe5 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -356,6 +356,13 @@ pub fn app_page(did: &str) -> String { align-items: center; justify-content: center; transition: all 0.2s ease; + overflow: hidden; + }} + + .app-logo {{ + width: 100%; + height: 100%; + object-fit: cover; }} .app-view:hover .app-circle {{ @@ -650,6 +657,37 @@ pub fn app_page(did: &str) -> String { let globalPds = null; let globalHandle = null; + // Try to fetch app avatar from their bsky profile + async function fetchAppAvatar(namespace) {{ + try {{ + // 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) {{ + try {{ + 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(); + if (profile.avatar) {{ + return profile.avatar; + }} + }} catch (e) {{ + continue; + }} + }} + }} catch (e) {{ + console.log('Could not fetch avatar for', namespace); + }} + return null; + }} + // Logout handler document.getElementById('logoutBtn').addEventListener('click', (e) => {{ e.preventDefault(); @@ -767,10 +805,18 @@ pub fn app_page(did: &str) -> String { const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase(); div.innerHTML = ` -
${{firstLetter}}
+
${{firstLetter}}
${{namespace}}
`; + // Try to fetch and display avatar + fetchAppAvatar(namespace).then(avatarUrl => {{ + if (avatarUrl) {{ + const circle = div.querySelector('.app-circle'); + circle.innerHTML = ``; + }} + }}); + div.addEventListener('click', () => {{ const detail = document.getElementById('detail'); const collections = apps[namespace]; @@ -782,16 +828,50 @@ pub fn app_page(did: &str) -> String { `; if (collections && collections.length > 0) {{ - collections.sort().forEach(lexicon => {{ - const shortName = lexicon.split('.').slice(2).join('.') || lexicon; - html += ` -
-
- ${{shortName}} - loading... + // Group collections by sub-namespace (third segment) + const grouped = {{}}; + 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 + html += ` +
+
+ ${{group}} + loading... +
-
- `; + `; + }} else {{ + // Group header + html += `
`; + html += `
${{group}}
`; + + // Items in group + items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => {{ + const displayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace; + html += ` +
+
+ ${{displayName}} + loading... +
+
+ `; + }}); + html += `
`; + }} }}); }} else {{ html += `
no collections found
`; @@ -868,7 +948,7 @@ pub fn app_page(did: &str) -> String { `; }}); - if (data.cursor) {{ + if (data.cursor && data.records.length === 5) {{ recordsHtml += ``; }} @@ -931,7 +1011,7 @@ pub fn app_page(did: &str) -> String { loadMoreBtn.remove(); recordListDiv.insertAdjacentHTML('beforeend', moreHtml); - if (moreData.cursor) {{ + if (moreData.cursor && moreData.records.length === 5) {{ recordListDiv.insertAdjacentHTML('beforeend', `` ); -- 2.43.0