Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 455 lines 12 kB view raw
1<!-- 2ATProto PDS Landing Page for at.aesthetic.computer 3Uses ONLY AT Protocol APIs - no aesthetic.computer backend dependencies 4Created: 2025.10.20 5--> 6<!DOCTYPE html> 7<html lang="en"> 8<head> 9 <meta charset="UTF-8"> 10 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 11 <title>at · Aesthetic Computer</title> 12 <meta name="description" content="Personal Data Server for the Aesthetic Computer community"> 13 <link rel="icon" type="image/png" 14 href="https://pals-aesthetic-computer.sfo3.cdn.digitaloceanspaces.com/painting-2023.7.29.20.39.png"> 15 16 <style> 17 * { 18 box-sizing: border-box; 19 } 20 21 ::-webkit-scrollbar { 22 display: none; 23 } 24 25 body { 26 margin: 0; 27 font-size: 16px; 28 font-family: monospace; 29 -webkit-text-size-adjust: none; 30 background: #f5f5f5; 31 color: #000; 32 line-height: 1.5; 33 } 34 35 .container { 36 max-width: 1200px; 37 margin: 0 auto; 38 padding: 2em 1em; 39 } 40 41 header { 42 text-align: center; 43 padding: 2em 0; 44 border-bottom: 3px solid rgb(205, 92, 155); 45 margin-bottom: 2em; 46 } 47 48 h1 { 49 font-size: 2.5em; 50 font-weight: normal; 51 margin: 0 0 0.3em 0; 52 color: rgb(205, 92, 155); 53 } 54 55 h2 { 56 font-size: 1.5em; 57 font-weight: normal; 58 margin: 2em 0 1em 0; 59 color: rgb(205, 92, 155); 60 } 61 62 .subtitle { 63 font-size: 0.9em; 64 opacity: 0.7; 65 margin: 0.5em 0; 66 } 67 68 .intro { 69 text-align: center; 70 max-width: 600px; 71 margin: 0 auto 2em; 72 line-height: 1.6; 73 } 74 75 .stats-grid { 76 display: grid; 77 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 78 gap: 1em; 79 margin: 2em 0; 80 } 81 82 .stat-card { 83 background: white; 84 padding: 1.5em; 85 border-radius: 8px; 86 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 87 text-align: center; 88 } 89 90 .stat-value { 91 font-size: 2em; 92 font-weight: bold; 93 color: rgb(205, 92, 155); 94 margin: 0.2em 0; 95 } 96 97 .stat-label { 98 font-size: 0.85em; 99 opacity: 0.7; 100 text-transform: uppercase; 101 letter-spacing: 0.05em; 102 } 103 104 .user-grid { 105 display: grid; 106 grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); 107 gap: 0.75em; 108 margin: 2em 0; 109 } 110 111 .user-card { 112 background: white; 113 padding: 1em; 114 border-radius: 6px; 115 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 116 text-decoration: none; 117 color: inherit; 118 transition: all 0.2s; 119 display: flex; 120 flex-direction: column; 121 align-items: center; 122 text-align: center; 123 } 124 125 .user-card:hover { 126 transform: translateY(-2px); 127 box-shadow: 0 4px 12px rgba(205, 92, 155, 0.2); 128 border: 2px solid rgb(205, 92, 155); 129 padding: calc(1em - 2px); 130 } 131 132 .user-handle { 133 font-weight: bold; 134 color: rgb(205, 92, 155); 135 margin-bottom: 0.5em; 136 word-break: break-word; 137 } 138 139 .user-stats { 140 font-size: 0.75em; 141 opacity: 0.6; 142 margin-top: 0.5em; 143 } 144 145 .user-badge { 146 display: inline-block; 147 padding: 0.2em 0.5em; 148 background: rgba(205, 92, 155, 0.1); 149 border-radius: 3px; 150 font-size: 0.7em; 151 margin: 0.2em; 152 } 153 154 .loading { 155 text-align: center; 156 padding: 3em; 157 opacity: 0.5; 158 } 159 160 .error { 161 text-align: center; 162 padding: 3em; 163 color: #d32f2f; 164 } 165 166 .spinner { 167 display: inline-block; 168 width: 20px; 169 height: 20px; 170 border: 3px solid rgba(205, 92, 155, 0.3); 171 border-radius: 50%; 172 border-top-color: rgb(205, 92, 155); 173 animation: spin 1s ease-in-out infinite; 174 } 175 176 @keyframes spin { 177 to { transform: rotate(360deg); } 178 } 179 180 .search-box { 181 margin: 2em 0; 182 text-align: center; 183 } 184 185 .search-box input { 186 font-family: monospace; 187 font-size: 1em; 188 padding: 0.75em 1em; 189 width: 100%; 190 max-width: 400px; 191 border: 2px solid #e0e0e0; 192 border-radius: 6px; 193 outline: none; 194 transition: border-color 0.2s; 195 } 196 197 .search-box input:focus { 198 border-color: rgb(205, 92, 155); 199 } 200 201 footer { 202 text-align: center; 203 padding: 3em 1em 1em; 204 opacity: 0.5; 205 font-size: 0.85em; 206 } 207 208 footer a { 209 color: rgb(205, 92, 155); 210 text-decoration: none; 211 } 212 213 footer a:hover { 214 text-decoration: underline; 215 } 216 217 @media (prefers-color-scheme: dark) { 218 body { 219 background: rgb(64, 56, 74); 220 color: rgba(255, 255, 255, 0.85); 221 } 222 223 .stat-card, .user-card { 224 background: rgba(255, 255, 255, 0.05); 225 color: rgba(255, 255, 255, 0.85); 226 } 227 228 .search-box input { 229 background: rgba(255, 255, 255, 0.05); 230 border-color: rgba(255, 255, 255, 0.2); 231 color: rgba(255, 255, 255, 0.85); 232 } 233 234 .search-box input:focus { 235 border-color: rgb(205, 92, 155); 236 } 237 } 238 </style> 239</head> 240 241<body> 242 <div class="container"> 243 <header> 244 <h1>at.aesthetic.computer</h1> 245 <div class="subtitle">Personal Data Server · ATProto Network</div> 246 </header> 247 248 <div class="intro"> 249 <p> 250 Welcome to the <strong>Aesthetic Computer</strong> Personal Data Server (PDS). 251 This server hosts ATProto records for the community, including paintings, moods, pieces, and kidlisp code. 252 </p> 253 <p> 254 Browse user pages below to explore their creative work! 🎨 255 </p> 256 </div> 257 258 <div class="stats-grid"> 259 <div class="stat-card"> 260 <div class="stat-value" id="total-users">...</div> 261 <div class="stat-label">Total Users</div> 262 </div> 263 <div class="stat-card"> 264 <div class="stat-value" id="total-records">...</div> 265 <div class="stat-label">Total Records</div> 266 </div> 267 <div class="stat-card"> 268 <div class="stat-value" id="active-today">...</div> 269 <div class="stat-label">Active Today</div> 270 </div> 271 </div> 272 273 <h2>🌟 Top Active Users</h2> 274 <div class="search-box"> 275 <input 276 type="text" 277 id="search" 278 placeholder="Search users by handle..." 279 autocomplete="off" 280 > 281 </div> 282 283 <div id="users-container"> 284 <div class="loading"> 285 <div class="spinner"></div> 286 <p>Loading users from ATProto...</p> 287 </div> 288 </div> 289 290 <footer> 291 <p> 292 Powered by <a href="https://atproto.com" target="_blank">AT Protocol</a> · 293 Part of the <a href="https://aesthetic.computer" target="_blank">Aesthetic Computer</a> network 294 </p> 295 </footer> 296 </div> 297 298 <script> 299 const PDS_URL = 'https://at.aesthetic.computer'; 300 301 let allUsers = []; 302 let displayedUsers = []; 303 let loadingProgress = 0; 304 305 // Fetch list of repositories (users) from PDS 306 async function fetchUsers() { 307 try { 308 const response = await fetch(`${PDS_URL}/xrpc/com.atproto.sync.listRepos?limit=1000`); 309 const data = await response.json(); 310 311 if (!data.repos || !Array.isArray(data.repos)) { 312 throw new Error('Invalid response from PDS'); 313 } 314 315 const totalRepos = Math.min(data.repos.length, 200); // Limit to avoid too many requests 316 317 // Get details for each repo (with progress) 318 for (let i = 0; i < totalRepos; i++) { 319 const repo = data.repos[i]; 320 loadingProgress = Math.round((i / totalRepos) * 100); 321 updateLoadingProgress(); 322 323 try { 324 const detailsResponse = await fetch( 325 `${PDS_URL}/xrpc/com.atproto.repo.describeRepo?repo=${repo.did}` 326 ); 327 const details = await detailsResponse.json(); 328 329 if (details.handle) { 330 allUsers.push({ 331 did: repo.did, 332 handle: details.handle, 333 collections: details.collections || [], 334 recordCount: details.collections?.length || 0, // Use collection count as proxy 335 lastActive: repo.head 336 }); 337 } 338 } catch (e) { 339 // Skip failed repos 340 } 341 342 // Batch render every 20 users 343 if (i % 20 === 0) { 344 displayedUsers = [...allUsers].sort((a, b) => b.recordCount - a.recordCount); 345 updateStats(); 346 renderUsers(displayedUsers); 347 } 348 } 349 350 // Final sort and render 351 allUsers.sort((a, b) => b.recordCount - a.recordCount); 352 displayedUsers = allUsers; 353 354 updateStats(); 355 renderUsers(displayedUsers); 356 } catch (error) { 357 console.error('Error fetching users:', error); 358 document.getElementById('users-container').innerHTML = 359 '<div class="error">Failed to load users from PDS. Try refreshing the page.</div>'; 360 } 361 } 362 363 function updateLoadingProgress() { 364 const container = document.getElementById('users-container'); 365 if (allUsers.length === 0) { 366 container.innerHTML = ` 367 <div class="loading"> 368 <div class="spinner"></div> 369 <p>Loading users from ATProto... ${loadingProgress}%</p> 370 </div> 371 `; 372 } 373 } 374 375 // Update stats display 376 function updateStats() { 377 document.getElementById('total-users').textContent = allUsers.length; 378 379 const totalRecords = allUsers.reduce((sum, u) => sum + u.recordCount, 0); 380 document.getElementById('total-records').textContent = totalRecords.toLocaleString(); 381 382 // For "active today", we'd need timestamps which requires more API calls 383 // For now, show users with any records 384 const activeUsers = allUsers.filter(u => u.recordCount > 0).length; 385 document.getElementById('active-today').textContent = activeUsers; 386 } 387 388 // Render user cards 389 function renderUsers(users) { 390 const container = document.getElementById('users-container'); 391 392 if (users.length === 0) { 393 container.innerHTML = '<div class="loading">No users found</div>'; 394 return; 395 } 396 397 const grid = document.createElement('div'); 398 grid.className = 'user-grid'; 399 400 users.forEach(user => { 401 const card = document.createElement('a'); 402 card.className = 'user-card'; 403 card.href = `https://${user.handle}.${PDS_URL.replace('https://', '')}`; 404 card.target = '_blank'; 405 406 const badges = []; 407 if (user.collections.includes('computer.aesthetic.painting')) { 408 badges.push('<span class="user-badge">🎨 Paintings</span>'); 409 } 410 if (user.collections.includes('computer.aesthetic.mood')) { 411 badges.push('<span class="user-badge">💬 Moods</span>'); 412 } 413 if (user.collections.includes('computer.aesthetic.piece')) { 414 badges.push('<span class="user-badge">🎵 Pieces</span>'); 415 } 416 if (user.collections.includes('computer.aesthetic.kidlisp')) { 417 badges.push('<span class="user-badge">📝 Code</span>'); 418 } 419 if (user.collections.includes('computer.aesthetic.tape')) { 420 badges.push('<span class="user-badge">📼 Tapes</span>'); 421 } 422 423 card.innerHTML = ` 424 <div class="user-handle">@${user.handle.replace('.at.aesthetic.computer', '')}</div> 425 <div>${badges.join(' ')}</div> 426 <div class="user-stats">${user.recordCount} record${user.recordCount !== 1 ? 's' : ''}</div> 427 `; 428 429 grid.appendChild(card); 430 }); 431 432 container.innerHTML = ''; 433 container.appendChild(grid); 434 } 435 436 // Search functionality 437 document.getElementById('search').addEventListener('input', (e) => { 438 const query = e.target.value.toLowerCase(); 439 440 if (!query) { 441 displayedUsers = allUsers; 442 } else { 443 displayedUsers = allUsers.filter(user => 444 user.handle.toLowerCase().includes(query) 445 ); 446 } 447 448 renderUsers(displayedUsers); 449 }); 450 451 // Initialize 452 fetchUsers(); 453 </script> 454</body> 455</html>