Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837<!--ATProto PDS Landing Page for at.aesthetic.computerMission statement + Top Users + All Media feed2026.03.23--><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>at · Aesthetic Computer</title> <meta name="description" content="A self-hosted ATProto Personal Data Server for the Aesthetic Computer community — paintings, moods, code, tapes, papers, and more."> <link rel="icon" type="image/png" href="https://pals-aesthetic-computer.sfo3.cdn.digitaloceanspaces.com/painting-2023.7.29.20.39.png">
<style> * { box-sizing: border-box; } ::-webkit-scrollbar { display: none; }
body { margin: 0; font-size: 14px; font-family: monospace; -webkit-text-size-adjust: none; background: #f5f5f5; color: #000; line-height: 1.4; }
.container { max-width: 900px; margin: 0 auto; padding: 1em 0.5em; }
a { color: rgb(205, 92, 155); text-decoration: none; } a:hover { text-decoration: underline; }
header { text-align: center; padding: 1em 0; border-bottom: 2px solid rgb(205, 92, 155); margin-bottom: 1em; }
#pals-beacon { display: inline-flex; align-items: center; justify-content: center; margin-bottom: 0.2em; text-decoration: none; }
#pals-beacon .pals-logo-container { position: relative; display: inline-block; }
#pals-beacon .pals-logo, #pals-beacon .pals-logo-pink { width: 80px; height: auto; }
#pals-beacon .pals-logo { filter: grayscale(100%) opacity(0.3); transition: filter 0.3s, opacity 0.3s; }
#pals-beacon .pals-logo-pink { position: absolute; top: 0; left: 0; opacity: 0.7; filter: hue-rotate(-30deg) saturate(1.5) brightness(1.2) drop-shadow(0 0 8px rgba(255, 100, 200, 0.8)); animation: pals-idle 2s ease-in-out infinite; }
@keyframes pals-idle { 0%, 100% { transform: scale(1); opacity: 0.7; } 50% { transform: scale(1.01); opacity: 1; } }
h1 { font-size: 1.5em; font-weight: normal; margin: 0 0 0.3em 0; color: rgb(205, 92, 155); }
.subtitle { font-size: 0.85em; opacity: 0.7; margin: 0.3em 0; }
.mission { margin: 1em auto; max-width: 640px; padding: 1em; background: rgba(205, 92, 155, 0.06); border-radius: 6px; font-size: 0.85em; line-height: 1.6; text-align: left; }
.mission p { margin: 0 0 0.6em 0; } .mission p:last-child { margin: 0; }
.support-links { margin: 0.8em auto 0; max-width: 640px; padding: 0.8em 1em; border: 1px solid rgba(205, 92, 155, 0.35); background: rgba(205, 92, 155, 0.06); border-radius: 6px; font-size: 0.8em; line-height: 1.6; text-align: left; }
.support-links p { margin: 0; }
.lexicons { display: flex; gap: 0.4em; justify-content: center; flex-wrap: wrap; margin: 0.8em 0; }
.lex-tag { font-size: 0.7em; padding: 0.3em 0.6em; background: rgba(205, 92, 155, 0.1); border-radius: 3px; white-space: nowrap; }
.stats { display: flex; gap: 1em; justify-content: center; flex-wrap: wrap; margin: 0.8em 0; font-size: 0.75em; }
.stat { padding: 0.3em 0.6em; background: rgba(205, 92, 155, 0.1); border-radius: 3px; }
.stat strong { color: rgb(205, 92, 155); }
.tabs { display: flex; gap: 0; border-bottom: 1px solid rgba(205, 92, 155, 0.3); margin: 1em 0 0.5em 0; }
.tab { padding: 0.6em 1.2em; background: none; border: none; font-family: monospace; font-size: 0.9em; cursor: pointer; border-bottom: 2px solid transparent; color: #666; transition: all 0.2s; }
.tab:hover { color: rgb(205, 92, 155); } .tab.active { color: rgb(205, 92, 155); border-bottom-color: rgb(205, 92, 155); font-weight: bold; }
.tab-panel { display: none; } .tab-panel.active { display: block; }
.search-box { margin: 0.5em 0 1em 0; }
.search-box input { font-family: monospace; font-size: 0.9em; padding: 0.6em 0.8em; width: 100%; border: 1px solid rgba(205, 92, 155, 0.3); border-radius: 3px; outline: none; background: white; }
.search-box input:focus { border-color: rgb(205, 92, 155); }
.user-filters { display: flex; gap: 0.45em; flex-wrap: wrap; margin: 0.2em 0 1em 0; }
.user-filter { border: 1px solid rgba(205, 92, 155, 0.25); background: rgba(205, 92, 155, 0.08); color: rgba(0, 0, 0, 0.7); font-family: monospace; font-size: 0.75em; padding: 0.35em 0.55em; border-radius: 999px; cursor: pointer; transition: all 0.15s; }
.user-filter.active { background: rgba(205, 92, 155, 0.2); border-color: rgba(205, 92, 155, 0.45); color: rgb(205, 92, 155); font-weight: bold; }
.user-filter:hover { background: rgba(205, 92, 155, 0.14); }
/* User list */ .user-list { display: flex; flex-direction: column; }
.user-row { background: white; padding: 0.75em; border-bottom: 1px solid rgba(205, 92, 155, 0.1); text-decoration: none; color: inherit; transition: all 0.15s; display: flex; justify-content: space-between; align-items: center; gap: 1em; }
.user-row:first-child { border-radius: 4px 4px 0 0; } .user-row:last-child { border-radius: 0 0 4px 4px; border-bottom: none; } .user-row:hover { background: rgba(205, 92, 155, 0.05); padding-left: 1em; }
.user-left { display: flex; align-items: center; gap: 0.75em; flex: 1; min-width: 0; } .user-rank { font-size: 0.75em; color: rgba(205, 92, 155, 0.5); font-weight: bold; min-width: 2.5em; text-align: right; } .user-handle { font-weight: bold; color: rgb(205, 92, 155); word-break: break-all; } .user-mood { font-size: 0.75em; color: rgba(0,0,0,0.6); margin-left: 0.5em; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .user-right { display: flex; gap: 0.5em; align-items: center; flex-wrap: wrap; justify-content: flex-end; } .user-badge { font-size: 0.7em; padding: 0.3em 0.5em; background: rgba(205, 92, 155, 0.1); border-radius: 3px; white-space: nowrap; } .user-total { font-size: 0.75em; color: rgba(0,0,0,0.5); font-weight: bold; min-width: 3em; text-align: right; }
.user-kidlisp-preview { width: 36px; height: 36px; border-radius: 6px; overflow: hidden; flex-shrink: 0; border: 1px solid rgba(205, 92, 155, 0.2); } .user-kidlisp-preview img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
/* Media feed */ .feed { display: flex; flex-direction: column; gap: 0.4em; }
.feed-item { background: white; padding: 0.75em; border-radius: 4px; display: flex; align-items: center; gap: 0.75em; }
.feed-type { font-size: 1.2em; width: 2em; text-align: center; flex-shrink: 0; }
.feed-body { flex: 1; min-width: 0; }
.feed-title { font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feed-meta { font-size: 0.75em; color: rgba(0,0,0,0.5); margin-top: 0.2em; }
.feed-meta a { color: rgb(205, 92, 155); }
.feed-thumb { width: 48px; height: 48px; border-radius: 4px; overflow: hidden; flex-shrink: 0; border: 1px solid rgba(205, 92, 155, 0.15); } .feed-thumb img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
.loading { text-align: center; padding: 3em 2em; opacity: 0.6; } .error { text-align: center; padding: 3em 2em; color: #d32f2f; } .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(205, 92, 155, 0.3); border-radius: 50%; border-top-color: rgb(205, 92, 155); animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } }
.feed-filters { display: flex; gap: 0.5em; flex-wrap: wrap; margin: 0.5em 0 1em 0; }
.feed-filter { display: flex; align-items: center; gap: 0.3em; font-size: 0.8em; cursor: pointer; padding: 0.3em 0.6em; background: rgba(205, 92, 155, 0.1); border-radius: 3px; user-select: none; transition: opacity 0.15s; }
.feed-filter.off { opacity: 0.35; }
.feed-filter input { display: none; }
.load-more { display: block; margin: 1em auto; padding: 0.6em 1.5em; font-family: monospace; font-size: 0.85em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgba(205, 92, 155, 0.3); border-radius: 4px; cursor: pointer; color: rgb(205, 92, 155); } .load-more:hover { background: rgba(205, 92, 155, 0.2); }
footer { text-align: center; padding: 2em 1em 1em; opacity: 0.5; font-size: 0.75em; }
@media (max-width: 560px) { .user-mood { display: none; } .user-kidlisp-preview { width: 28px; height: 28px; } .user-badge { font-size: 0.6em; } .mission { font-size: 0.8em; padding: 0.8em; } }
@media (max-width: 400px) { .user-right { display: none; } }
@media (prefers-color-scheme: dark) { body { background: rgb(64, 56, 74); color: rgba(255, 255, 255, 0.85); } .user-row, .feed-item { background: rgba(255, 255, 255, 0.05); } .user-row:hover, .feed-item:hover { background: rgba(205, 92, 155, 0.1); } .search-box input { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.85); } .user-total, .feed-meta { color: rgba(255, 255, 255, 0.5); } .user-mood { color: rgba(255, 255, 255, 0.6); } .mission { background: rgba(205, 92, 155, 0.1); } } </style></head>
<body> <div class="container"> <header> <a id="pals-beacon" href="https://at.aesthetic.computer" aria-label="at.aesthetic.computer"> <div class="pals-logo-container"> <img src="https://aesthetic.computer/purple-pals.svg" alt="" class="pals-logo"> <img src="https://aesthetic.computer/purple-pals.svg" alt="" class="pals-logo-pink"> </div> </a> <h1>at.aesthetic.computer</h1> <div class="subtitle">Self-Hosted ATProto Personal Data Server</div>
<div class="mission"> <p>This is the federated data layer for <a href="https://aesthetic.computer">Aesthetic Computer</a> — a mobile-first runtime and social network for creative computing.</p> <p>Every painting, mood, tape, piece of code, and paper created on AC is stored here as an open ATProto record, addressable by anyone on the network. Your data lives on infrastructure we operate, not on a platform you don't control.</p> </div>
<div class="support-links"> <p>Learn about operating costs at <a href="https://bills.aesthetic.computer" target="_blank">bills.aesthetic.computer</a>. Help fund infrastructure at <a href="https://give.aesthetic.computer" target="_blank">give.aesthetic.computer</a> to keep the network free and open source.</p> </div>
<div class="lexicons"> <span class="lex-tag">painting</span> <span class="lex-tag">mood</span> <span class="lex-tag">kidlisp</span> <span class="lex-tag">piece</span> <span class="lex-tag">tape</span> <span class="lex-tag">news</span> <span class="lex-tag">paper</span> </div>
<div class="stats"> <div class="stat"><strong id="total-users">...</strong> users</div> <div class="stat"><strong id="total-records">...</strong> records</div> <div class="stat"><strong id="active-users">...</strong> active</div> </div> </header>
<div class="tabs"> <button class="tab" data-tab="users">Top Handles</button> <button class="tab active" data-tab="feed">All Media</button> </div>
<!-- Top Users --> <div class="tab-panel" id="panel-users"> <div class="search-box"> <input type="text" id="search" placeholder="Search by handle..." autocomplete="off"> </div> <div class="user-filters" id="user-filters"></div> <div id="users-container"> <div class="loading"><div class="spinner"></div><p>Loading users...</p></div> </div> </div>
<!-- All Media Feed --> <div class="tab-panel active" id="panel-feed"> <div class="feed-filters" id="feed-filters"></div> <div id="feed-container"> <div class="loading"><div class="spinner"></div><p>Loading media...</p></div> </div> </div>
<footer> <p> Powered by <a href="https://atproto.com" target="_blank">AT Protocol</a> · <a href="https://aesthetic.computer" target="_blank">Aesthetic Computer</a> · <a href="https://art.at.aesthetic.computer" target="_blank">Guest Art</a> · <a href="https://papers.aesthetic.computer" target="_blank">Papers</a> </p> <p>mail@aesthetic.computer</p> </footer> </div>
<script src="/media-modal.js"></script> <script src="/media-records.js"></script> <script> const API_URL = 'https://aesthetic.computer'; const PDS_URL = 'https://at.aesthetic.computer'; const COLLECTIONS = [ { id: 'computer.aesthetic.painting', icon: '🎨', label: 'painting' }, { id: 'computer.aesthetic.mood', icon: '💬', label: 'mood' }, { id: 'computer.aesthetic.kidlisp', icon: '📝', label: 'kidlisp' }, { id: 'computer.aesthetic.piece', icon: '🧩', label: 'piece' }, { id: 'computer.aesthetic.tape', icon: '📼', label: 'tape' }, { id: 'computer.aesthetic.news', icon: '📰', label: 'news' }, { id: 'computer.aesthetic.paper', icon: '📄', label: 'paper' }, ];
const COLLECTION_BY_LABEL = Object.fromEntries(COLLECTIONS.map(c => [c.label, c.id]));
let allUsers = []; let feedLoaded = false; let activeUserCollection = 'all'; let userSearchQuery = ''; let didToHandle = new Map();
function normalizeHandle(value) { return String(value || '') .trim() .replace(/^@/, '') .replace(/\.at\.aesthetic\.computer$/i, '') .toLowerCase(); }
function formatDid(did) { if (!did) return 'unknown'; return did.length > 24 ? `${did.slice(0, 20)}...` : did; }
function userMatchesQuery(user, query) { if (!query) return true; const fields = [ user.handle, user.code, normalizeHandle(user.handle), normalizeHandle(user.code), ]; return fields.some(v => String(v || '').toLowerCase().includes(query)); }
function getFilteredUsers() { let users = allUsers; if (activeUserCollection !== 'all') { const colId = COLLECTION_BY_LABEL[activeUserCollection]; users = users.filter(user => (user.collections || []).includes(colId)); } const q = normalizeHandle(userSearchQuery); if (q) users = users.filter(user => userMatchesQuery(user, q)); return users; }
function renderFilteredUsers() { renderUsers(getFilteredUsers()); }
function buildUserFilters() { const el = document.getElementById('user-filters'); if (!el) return;
const filters = [{ label: 'all', icon: '⭐' }, ...COLLECTIONS]; el.innerHTML = filters .map(f => `<button class="user-filter ${f.label === activeUserCollection ? 'active' : ''}" data-label="${f.label}">${f.icon} ${f.label}</button>`) .join('');
el.querySelectorAll('.user-filter').forEach(btn => { btn.addEventListener('click', () => { activeUserCollection = btn.dataset.label; buildUserFilters(); renderFilteredUsers(); }); }); }
function repoLabelForItem(item) { if (item._handle) return item._handle; const repo = item.uri ? item.uri.split('/')[2] : item._did; if (!repo) return 'unknown'; if (repo.startsWith('did:')) return formatDid(repo); return `@${normalizeHandle(repo)}`; }
// --- Tabs --- document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); tab.classList.add('active'); document.getElementById('panel-' + tab.dataset.tab).classList.add('active'); if (tab.dataset.tab === 'feed' && !feedLoaded) loadFeed(); }); });
// --- Users Tab --- async function fetchUsers() { try { const response = await fetch(`${API_URL}/.netlify/functions/atproto-user-stats?limit=100`); const data = await response.json(); if (!data.users) throw new Error('Invalid response'); allUsers = data.users;
if (data.stats) { document.getElementById('total-users').textContent = data.stats.totalUsers.toLocaleString(); document.getElementById('total-records').textContent = data.stats.totalRecords.toLocaleString(); document.getElementById('active-users').textContent = data.stats.activeUsers.toLocaleString(); } buildUserFilters(); renderFilteredUsers(); } catch (error) { document.getElementById('users-container').innerHTML = '<div class="error">Failed to load users.</div>'; } }
function renderUsers(users) { const container = document.getElementById('users-container'); if (!users.length) { container.innerHTML = '<div class="loading">No users found</div>'; return; }
const list = document.createElement('div'); list.className = 'user-list';
users.forEach((user, index) => { const row = document.createElement('a'); row.className = 'user-row'; const identifier = user.handle || user.code; const shortHandle = normalizeHandle(identifier); row.href = `https://${shortHandle}.at.aesthetic.computer`; row.target = '_blank';
const badges = []; const badgeMap = { 'computer.aesthetic.painting': '🎨', 'computer.aesthetic.mood': '💬', 'computer.aesthetic.piece': '🧩', 'computer.aesthetic.kidlisp': '📝', 'computer.aesthetic.tape': '📼', }; for (const [col, emoji] of Object.entries(badgeMap)) { if ((user.collections || []).includes(col)) { badges.push(`<span class="user-badge">${emoji} ${user.recordCounts[col] || 0}</span>`); } }
const displayHandle = user.isUserCode ? shortHandle : `@${shortHandle}`; const mood = user.latestMood ? `"${user.latestMood.replace(/\s+/g, ' ').trim()}"` : ''; const klCode = user.latestKidlispCode || ''; const klPreview = klCode ? `<div class="user-kidlisp-preview"><img src="https://oven.aesthetic.computer/grab/webp/100/100/$${klCode}?duration=2000&fps=8&quality=70&density=1&nowait=true" alt="" loading="lazy"></div>` : '';
row.innerHTML = ` <div class="user-left"> <div class="user-rank">#${index + 1}</div> ${klPreview} <div class="user-handle">${displayHandle}</div> ${mood ? `<div class="user-mood">${mood}</div>` : ''} </div> <div class="user-right"> ${badges.join('')} <div class="user-total">${user.totalRecords}</div> </div>`; list.appendChild(row); });
container.innerHTML = ''; container.appendChild(list); }
document.getElementById('search').addEventListener('input', (e) => { userSearchQuery = e.target.value || ''; renderFilteredUsers(); });
// --- All Media Feed --- let allFeedItems = []; let activeFilters = new Set(COLLECTIONS.map(c => c.label));
function timeAgo(dateStr) { const now = Date.now(); const then = new Date(dateStr).getTime(); const sec = Math.floor((now - then) / 1000); if (sec < 60) return 'just now'; const min = Math.floor(sec / 60); if (min < 60) return `${min}m ago`; const hr = Math.floor(min / 60); if (hr < 24) return `${hr}h ago`; const days = Math.floor(hr / 24); if (days < 30) return `${days}d ago`; if (days < 365) { const months = Math.floor(days / 30); return `${months}mo ago`; } const years = Math.floor(days / 365); return `${years}y ago`; }
const mediaRecords = window.ACMediaRecords || {}; const normalizeExternalLink = mediaRecords.normalizeLink || ((link) => String(link || '').trim()); const escapeHtml = mediaRecords.escapeHtml || ((value) => String(value ?? '')); const buildFeedModalPayload = mediaRecords.buildFeedModalPayload || ((item, col, repoLabel, ago, title, primaryLink) => ({ title: `${col.icon} ${title || col.label}`, subtitle: `${col.label} · ${ago || 'recent'} · ${repoLabel || 'unknown'}`, bodyHtml: `<pre>${escapeHtml(JSON.stringify(item, null, 2))}</pre>`, iframeUrl: normalizeExternalLink(primaryLink), actions: [], }));
function buildFilters() { const el = document.getElementById('feed-filters'); el.innerHTML = COLLECTIONS.map(col => `<label class="feed-filter" data-col="${col.label}"> <input type="checkbox" checked> ${col.icon} ${col.label} </label>` ).join(''); el.querySelectorAll('.feed-filter').forEach(label => { label.addEventListener('click', (e) => { e.preventDefault(); const col = label.dataset.col; if (activeFilters.has(col)) { activeFilters.delete(col); label.classList.add('off'); } else { activeFilters.add(col); label.classList.remove('off'); } renderFeed(allFeedItems, document.getElementById('feed-container')); }); }); }
async function loadFeed() { feedLoaded = true; const container = document.getElementById('feed-container'); buildFilters(); didToHandle = new Map();
try { // Use top users' DIDs from already-loaded data (avoids extra listRepos call) // Fall back to listRepos if users haven't loaded yet let dids = []; if (allUsers.length > 0) { // Get unique user IDs, resolve to DIDs via the users data const seen = new Set(); for (const u of allUsers.slice(0, 15)) { const handle = normalizeHandle(u.handle || u.code || ''); if (handle && !seen.has(handle)) { seen.add(handle); } } // Resolve handles to DIDs const resolves = await Promise.allSettled( [...seen].map(h => fetch(`${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(h + '.at.aesthetic.computer')}`) .then(r => r.ok ? r.json() : null) .then(d => { if (d?.did) didToHandle.set(d.did, `@${h}`); return d?.did; }) ) ); dids = resolves.map(r => r.value).filter(Boolean); }
if (dids.length === 0) { const reposRes = await fetch(`${PDS_URL}/xrpc/com.atproto.sync.listRepos?limit=15`); const reposData = await reposRes.json(); const repos = reposData.repos || []; dids = repos.map(r => r.did); for (const repo of repos) { const handle = normalizeHandle(repo.handle || ''); if (repo.did && handle) didToHandle.set(repo.did, `@${handle}`); } }
// Fire ALL requests in parallel: dids × collections const fetches = []; for (const did of dids.slice(0, 12)) { for (const col of COLLECTIONS) { fetches.push( fetch(`${PDS_URL}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(col.id)}&limit=5&reverse=true`) .then(r => r.ok ? r.json() : { records: [] }) .then(data => (data.records || []).map(rec => ({ ...rec.value, uri: rec.uri, _col: col, _did: did, _handle: didToHandle.get(did) || '' }))) .catch(() => []) ); } }
const results = await Promise.all(fetches); allFeedItems = results.flat();
// Sort reverse chrono allFeedItems.sort((a, b) => new Date(b.when || b.createdAt || 0) - new Date(a.when || a.createdAt || 0));
renderFeed(allFeedItems, container); } catch (error) { container.innerHTML = '<div class="error">Failed to load media feed.</div>'; } }
function renderFeed(items, container) { const filtered = items.filter(item => activeFilters.has(item._col.label)); if (!filtered.length) { container.innerHTML = '<div class="loading">No media found</div>'; return; }
const feed = document.createElement('div'); feed.className = 'feed';
for (const item of filtered.slice(0, 150)) { const el = document.createElement('div'); el.className = 'feed-item';
const col = item._col; const when = item.when || item.createdAt; const ago = when ? timeAgo(when) : ''; const repoLabel = repoLabelForItem(item);
let title = ''; let thumb = ''; let link = '';
switch (col.label) { case 'painting': title = item.slug || item.code || 'Untitled'; link = item.imageUrl || `https://aesthetic.computer/#${item.code}`; if (item.code) thumb = `<div class="feed-thumb"><img src="https://aesthetic.computer/media/paintings/${item.code}" loading="lazy" alt=""></div>`; break; case 'mood': title = item.mood || ''; break; case 'kidlisp': title = item.source ? item.source.slice(0, 80) : (item.code || ''); if (item.code) { link = item.acUrl || `https://aesthetic.computer/$${item.code}`; thumb = `<div class="feed-thumb"><img src="https://oven.aesthetic.computer/grab/webp/100/100/$${item.code}?duration=1000&fps=4&quality=60&density=1&nowait=true" loading="lazy" alt=""></div>`; } break; case 'piece': title = item.slug || 'Untitled piece'; break; case 'tape': title = item.code || item.slug || 'Tape'; link = item.acUrl || `https://aesthetic.computer/!${item.code}`; break; case 'news': title = item.headline || ''; link = normalizeExternalLink(item.link || item.url || ''); break; case 'paper': title = item.title || ''; link = normalizeExternalLink(item.pdfUrl || ''); break; }
const titleHtml = escapeHtml(title || '(untitled)'); const modalPayload = buildFeedModalPayload(item, col, repoLabel, ago, title, link);
el.innerHTML = ` <div class="feed-type">${col.icon}</div> ${thumb} <div class="feed-body"> <div class="feed-title">${titleHtml}</div> <div class="feed-meta">${col.label} · ${ago} · ${repoLabel}</div> </div>`; el.style.cursor = 'pointer'; el.setAttribute('role', 'button'); el.tabIndex = 0; el.addEventListener('click', () => { if (window.ACMediaModal?.open) window.ACMediaModal.open(modalPayload); }); el.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); if (window.ACMediaModal?.open) window.ACMediaModal.open(modalPayload); } }); feed.appendChild(el); }
container.innerHTML = ''; container.appendChild(feed); }
// Init fetchUsers().finally(() => { if (!feedLoaded) loadFeed(); });
</script></body></html>