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
··· 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 );