add app logos and minor UI fixes #4

merged
opened by zzstoatzz.io targeting main from add-app-logos

closes #1 closes #2 closes #3

Changed files
+92 -12
src
+92 -12
src/templates.rs
···
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 {{
···
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();
···
const firstLetter = namespace.split('.')[1]?.[0]?.toUpperCase() || namespace[0].toUpperCase();
div.innerHTML = `
-
<div class="app-circle">${{firstLetter}}</div>
+
<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 => {{
+
if (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) {{
-
collections.sort().forEach(lexicon => {{
-
const shortName = lexicon.split('.').slice(2).join('.') || lexicon;
-
html += `
-
<div class="tree-item" data-lexicon="${{lexicon}}">
-
<div class="tree-item-header">
-
<span>${{shortName}}</span>
-
<span class="tree-item-count">loading...</span>
+
// 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 += `
+
<div class="tree-item" data-lexicon="${{items[0].lexicon}}">
+
<div class="tree-item-header">
+
<span>${{group}}</span>
+
<span class="tree-item-count">loading...</span>
+
</div>
</div>
-
</div>
-
`;
+
`;
+
}} else {{
+
// Group header
+
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 in group
+
items.sort((a, b) => a.subNamespace.localeCompare(b.subNamespace)).forEach(item => {{
+
const displayName = item.subNamespace.split('.').slice(1).join('.') || item.subNamespace;
+
html += `
+
<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>
+
</div>
+
</div>
+
`;
+
}});
+
html += `</div>`;
+
}}
}});
}} else {{
html += `<div class="tree-item">no collections found</div>`;
···
`;
}});
-
if (data.cursor) {{
+
if (data.cursor && data.records.length === 5) {{
recordsHtml += `<button class="load-more" data-cursor="${{data.cursor}}" data-lexicon="${{lexicon}}">load more</button>`;
}}
···
loadMoreBtn.remove();
recordListDiv.insertAdjacentHTML('beforeend', moreHtml);
-
if (moreData.cursor) {{
+
if (moreData.cursor && moreData.records.length === 5) {{
recordListDiv.insertAdjacentHTML('beforeend',
`<button class="load-more" data-cursor="${{moreData.cursor}}" data-lexicon="${{lexicon}}">load more</button>`
);