Monorepo for Aesthetic.Computer aesthetic.computer
at main 2131 lines 90 kB view raw
1<!-- 2ATProto User Page for *.at.aesthetic.computer 3Shows all records for a specific user's handle 4Uses ONLY ATProto APIs - no aesthetic.computer backend dependencies 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 id="page-title">Loading...</title> 12 <meta name="description" content="Personal ATProto data page"> 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 <script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script> 16 17 <style> 18 * { 19 box-sizing: border-box; 20 } 21 22 ::-webkit-scrollbar { 23 display: none; 24 } 25 26 body { 27 margin: 0; 28 font-size: 14px; 29 font-family: monospace; 30 -webkit-text-size-adjust: none; 31 background: #f5f5f5; 32 color: #000; 33 line-height: 1.4; 34 } 35 36 .container { 37 max-width: 1400px; 38 margin: 0 auto; 39 padding: 1em 0.5em; 40 } 41 42 header { 43 text-align: center; 44 padding: 1em 0; 45 border-bottom: 2px solid rgb(205, 92, 155); 46 margin-bottom: 1em; 47 } 48 49 h1 { 50 font-size: 1.5em; 51 font-weight: normal; 52 margin: 0 0 0.3em 0; 53 color: rgb(205, 92, 155); 54 } 55 56 .subtitle { 57 font-size: 0.85em; 58 opacity: 0.7; 59 margin: 0.3em 0; 60 } 61 62 .stats { 63 display: flex; 64 gap: 1em; 65 justify-content: center; 66 flex-wrap: wrap; 67 margin: 0.5em 0; 68 font-size: 0.75em; 69 } 70 71 .stat { 72 padding: 0.3em 0.6em; 73 background: rgba(205, 92, 155, 0.1); 74 border-radius: 3px; 75 } 76 77 .stat strong { 78 color: rgb(205, 92, 155); 79 } 80 81 .loading { 82 text-align: center; 83 padding: 4em 2em; 84 font-size: 1.2em; 85 opacity: 0.6; 86 } 87 88 .error { 89 text-align: center; 90 padding: 4em 2em; 91 color: #d32f2f; 92 font-size: 1.1em; 93 } 94 95 .section { 96 margin: 1.5em 0; 97 } 98 99 .section-title { 100 font-size: 1.2em; 101 font-weight: normal; 102 margin: 0 0 0.5em 0; 103 padding-bottom: 0.3em; 104 border-bottom: 1px solid rgba(205, 92, 155, 0.3); 105 display: flex; 106 justify-content: space-between; 107 align-items: center; 108 } 109 110 .section-count { 111 font-size: 0.7em; 112 color: rgb(205, 92, 155); 113 font-weight: bold; 114 } 115 116 .records-grid { 117 display: grid; 118 grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); 119 gap: 0.5em; 120 } 121 122 .record-card { 123 background: white; 124 padding: 0.75em; 125 border-radius: 4px; 126 box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); 127 transition: transform 0.2s, box-shadow 0.2s; 128 } 129 130 .record-card:hover { 131 transform: translateY(-1px); 132 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 133 } 134 135 .kidlisp-preview-card { 136 position: relative; 137 border-radius: 8px; 138 overflow: hidden; 139 background: #0d0d1a; 140 aspect-ratio: 1; 141 display: flex; 142 align-items: stretch; 143 justify-content: stretch; 144 margin: 0.5em 0; 145 } 146 147 .kidlisp-preview-card img.kidlisp-webp { 148 width: 100%; 149 height: 100%; 150 object-fit: cover; 151 image-rendering: pixelated; 152 opacity: 0.6; 153 transform: scale(1.05); 154 } 155 156 .kidlisp-preview-overlay { 157 position: absolute; 158 inset: 0; 159 padding: 0.7em 0.8em; 160 color: #fff; 161 font-family: monospace; 162 font-size: 0.75em; 163 line-height: 1.35; 164 text-shadow: 0 1px 2px rgba(0,0,0,0.9); 165 pointer-events: none; 166 display: flex; 167 flex-direction: column; 168 justify-content: space-between; 169 background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.5) 80%); 170 } 171 172 .kidlisp-preview-code { 173 overflow: hidden; 174 max-height: 60%; 175 white-space: pre-wrap; 176 word-break: break-word; 177 } 178 179 .kidlisp-preview-qr { 180 align-self: flex-end; 181 display: flex; 182 flex-direction: column; 183 align-items: flex-end; 184 line-height: 0; 185 } 186 187 .kidlisp-preview-qr img { 188 display: block; 189 image-rendering: pixelated; 190 width: 44px; 191 height: 44px; 192 background: #fff; 193 padding: 2px; 194 } 195 196 .kidlisp-preview-label { 197 font-family: monospace; 198 font-size: 1em; 199 color: #fff; 200 background: #000; 201 padding: 0.15em 0.4em; 202 margin-bottom: 4px; 203 line-height: 1; 204 } 205 206 .record-header { 207 display: flex; 208 justify-content: space-between; 209 align-items: start; 210 margin-bottom: 0.5em; 211 gap: 0.5em; 212 } 213 214 .lexicon-badge { 215 font-size: 0.65em; 216 padding: 0.3em 0.6em; 217 background: transparent; 218 border: none; 219 color: rgb(150, 150, 150); 220 border-radius: 3px; 221 cursor: pointer; 222 transition: all 0.2s; 223 white-space: nowrap; 224 } 225 226 .lexicon-badge:hover { 227 color: rgb(200, 200, 200); 228 text-decoration: underline; 229 } 230 231 .color-cycle-a { 232 animation: colorCycleA 1.8s steps(1) infinite; 233 } 234 235 .color-cycle-r { 236 animation: colorCycleR 1.8s steps(1) infinite; 237 } 238 239 .color-cycle-t { 240 animation: colorCycleT 1.8s steps(1) infinite; 241 } 242 243 @keyframes colorCycleA { 244 0% { color: rgb(205, 92, 155); } 245 16.6% { color: rgb(255, 120, 100); } 246 33.3% { color: rgb(255, 200, 100); } 247 50% { color: rgb(150, 230, 150); } 248 66.6% { color: rgb(100, 180, 255); } 249 83.3% { color: rgb(180, 130, 230); } 250 100% { color: rgb(205, 92, 155); } 251 } 252 253 @keyframes colorCycleR { 254 0% { color: rgb(255, 120, 100); } 255 16.6% { color: rgb(255, 200, 100); } 256 33.3% { color: rgb(150, 230, 150); } 257 50% { color: rgb(100, 180, 255); } 258 66.6% { color: rgb(180, 130, 230); } 259 83.3% { color: rgb(205, 92, 155); } 260 100% { color: rgb(255, 120, 100); } 261 } 262 263 @keyframes colorCycleT { 264 0% { color: rgb(255, 200, 100); } 265 16.6% { color: rgb(150, 230, 150); } 266 33.3% { color: rgb(100, 180, 255); } 267 50% { color: rgb(180, 130, 230); } 268 66.6% { color: rgb(205, 92, 155); } 269 83.3% { color: rgb(255, 120, 100); } 270 100% { color: rgb(255, 200, 100); } 271 } 272 273 /* Generic color cycle for handle characters */ 274 .color-cycle { 275 animation: colorCycleGeneric 1.8s steps(1) infinite; 276 } 277 278 @keyframes colorCycleGeneric { 279 0% { color: rgb(205, 92, 155); } 280 16.6% { color: rgb(255, 120, 100); } 281 33.3% { color: rgb(255, 200, 100); } 282 50% { color: rgb(150, 230, 150); } 283 66.6% { color: rgb(100, 180, 255); } 284 83.3% { color: rgb(180, 130, 230); } 285 100% { color: rgb(205, 92, 155); } 286 } 287 288 .record-type { 289 font-size: 0.65em; 290 padding: 0.2em 0.5em; 291 background: rgb(205, 92, 155); 292 color: white; 293 border-radius: 2px; 294 text-transform: uppercase; 295 letter-spacing: 0.05em; 296 } 297 298 .record-date { 299 font-size: 0.7em; 300 opacity: 0.6; 301 white-space: nowrap; 302 } 303 304 .record-content { 305 margin: 0.5em 0; 306 } 307 308 .record-image { 309 width: 100%; 310 max-width: 200px; 311 margin: 0.5em 0; 312 border-radius: 2px; 313 border: none; 314 image-rendering: pixelated; 315 image-rendering: crisp-edges; 316 } 317 318 .record-text { 319 font-size: 0.85em; 320 line-height: 1.3; 321 white-space: pre-wrap; 322 word-wrap: break-word; 323 } 324 325 .record-meta { 326 margin-top: 0.5em; 327 padding-top: 0.5em; 328 border-top: 1px solid #e0e0e0; 329 font-size: 0.7em; 330 opacity: 0.7; 331 display: flex; 332 gap: 0.5em; 333 flex-wrap: wrap; 334 } 335 336 .record-link { 337 color: rgb(205, 92, 155); 338 text-decoration: none; 339 font-size: 0.75em; 340 display: inline-block; 341 margin-top: 0.3em; 342 transition: color 0.2s; 343 } 344 345 .record-link:hover { 346 text-decoration: underline; 347 color: rgb(255, 120, 200); 348 } 349 350 /* Style all links */ 351 a { 352 color: rgb(205, 92, 155); 353 text-decoration: none; 354 transition: all 0.2s; 355 } 356 357 a:hover { 358 color: rgb(255, 120, 200); 359 text-decoration: underline; 360 } 361 362 a:visited { 363 color: rgb(180, 70, 135); 364 } 365 366 #pals { 367 position: fixed; 368 bottom: 5px; 369 right: 16px; 370 user-select: none; 371 } 372 373 /* Preview box scrolling animation */ 374 .source-preview { 375 position: relative; 376 max-height: 120px; 377 overflow: hidden; 378 background: linear-gradient(135deg, rgba(205, 92, 155, 0.05), rgba(100, 180, 255, 0.05)); 379 border: 1px solid rgba(205, 92, 155, 0.2); 380 } 381 382 .source-preview:hover .preview-scroll-container { 383 animation-play-state: paused !important; 384 } 385 386 .preview-scroll-container { 387 display: flex; 388 animation: previewScrollLoop 40s linear infinite; 389 will-change: transform; 390 } 391 392 .line-numbers { 393 flex-shrink: 0; 394 width: 3em; 395 text-align: right; 396 padding-right: 0.5em; 397 user-select: none; 398 font-size: 0.9em; 399 line-height: inherit; 400 background: rgba(0, 0, 0, 0.1); 401 border-right: 1px solid rgba(0, 0, 0, 0.15); 402 } 403 404 .line-numbers div { 405 padding: 0.1em 0; 406 } 407 408 /* Striped color palette for line numbers */ 409 .line-numbers div:nth-child(6n+1) { color: rgb(205, 92, 155); opacity: 0.8; } 410 .line-numbers div:nth-child(6n+2) { color: rgb(255, 120, 100); opacity: 0.8; } 411 .line-numbers div:nth-child(6n+3) { color: rgb(255, 200, 100); opacity: 0.8; } 412 .line-numbers div:nth-child(6n+4) { color: rgb(150, 230, 150); opacity: 0.8; } 413 .line-numbers div:nth-child(6n+5) { color: rgb(100, 180, 255); opacity: 0.8; } 414 .line-numbers div:nth-child(6n+6) { color: rgb(180, 130, 230); opacity: 0.8; } 415 416 .preview-content { 417 display: block; 418 flex: 1; 419 padding-left: 0.5em; 420 } 421 422 /* Infinite scroll - content is duplicated so it wraps seamlessly */ 423 @keyframes previewScrollLoop { 424 0% { 425 transform: translateY(0); 426 } 427 100% { 428 transform: translateY(-50%); 429 } 430 } 431 432 .empty-state { 433 text-align: center; 434 padding: 2em 1em; 435 opacity: 0.5; 436 font-style: italic; 437 } 438 439 .tabs { 440 display: flex; 441 gap: 0.3em; 442 margin-bottom: 1em; 443 border-bottom: 1px solid #e0e0e0; 444 flex-wrap: wrap; 445 } 446 447 .tab { 448 padding: 0.5em 1em; 449 background: none; 450 border: none; 451 font-family: monospace; 452 font-size: 0.85em; 453 cursor: pointer; 454 border-bottom: 2px solid transparent; 455 transition: all 0.2s; 456 color: #666; 457 } 458 459 .tab:hover { 460 color: rgb(205, 92, 155); 461 background: rgba(205, 92, 155, 0.05); 462 } 463 464 .tab.active { 465 color: rgb(205, 92, 155); 466 border-bottom-color: rgb(205, 92, 155); 467 font-weight: bold; 468 } 469 470 .tab.disabled { 471 opacity: 0.3; 472 cursor: not-allowed; 473 } 474 475 #scroll-top:hover { 476 transform: translateY(-2px); 477 box-shadow: 0 6px 16px rgba(0,0,0,0.4); 478 } 479 480 #scroll-top:active { 481 transform: translateY(0); 482 } 483 484 @media (prefers-color-scheme: dark) { 485 body { 486 background: rgb(40, 35, 45); 487 color: rgba(255, 255, 255, 0.9); 488 } 489 490 .record-card { 491 background: rgba(255, 255, 255, 0.05); 492 color: rgba(255, 255, 255, 0.9); 493 } 494 495 .stat { 496 background: rgba(205, 92, 155, 0.2); 497 } 498 499 .tabs { 500 border-bottom-color: rgba(255, 255, 255, 0.1); 501 } 502 503 .tab { 504 color: rgba(255, 255, 255, 0.6); 505 } 506 507 .record-meta { 508 border-top-color: rgba(255, 255, 255, 0.1); 509 } 510 } 511 512 @media (max-width: 600px) { 513 body { 514 font-size: 16px; 515 } 516 517 h1 { 518 font-size: 1.5em; 519 } 520 521 .stats { 522 flex-direction: column; 523 align-items: center; 524 } 525 526 .tab { 527 padding: 0.6em 1em; 528 } 529 } 530 </style> 531</head> 532 533<body> 534 <div class="container"> 535 <header> 536 <a href="https://at.aesthetic.computer" style="text-decoration: none; color: inherit;"><div style="font-size: 0.75em; color: rgb(205, 92, 155); margin-bottom: 0.3em;">← at.aesthetic.computer</div></a> 537 <h1 id="handle-display">Loading...</h1> 538 <div class="subtitle" id="did-display"></div> 539 <div class="stats" id="stats"></div> 540 </header> 541 542 <div id="loading" class="loading"> 543 🔍 Loading ATProto records... 544 </div> 545 546 <div id="error" class="error" style="display: none;"></div> 547 548 <div id="content" style="display: none;"> 549 <div class="tabs" id="tabs"></div> 550 <div id="records-container"></div> 551 </div> 552 553 <!-- Floating AC logo --> 554 <svg 555 id="pals" 556 width="64" 557 height="64" 558 viewBox="0 0 24 24" 559 fill="none" 560 xmlns="http://www.w3.org/2000/svg" 561 onclick="window.scrollTo({ top: 0, behavior: 'smooth' })" 562 style="cursor: pointer;" 563 > 564 <path fill-rule="evenodd" clip-rule="evenodd" d="M14.8982 5.10335C15.5333 4.92226 16.0802 4.97918 16.6196 5.27392L16.6294 5.27925L16.6386 5.28543C16.8852 5.45034 17.0637 5.6336 17.1768 5.8569C17.2893 6.07886 17.3264 6.31921 17.3264 6.57986C17.3264 6.8465 17.3041 7.09444 17.2269 7.3334C17.2091 7.38846 17.1886 7.44241 17.1652 7.4955C17.23 7.47358 17.2711 7.4571 17.2859 7.44936C17.3941 7.3926 17.5907 7.24475 17.8166 7.06782C17.8604 7.03348 17.9051 6.99825 17.9497 6.96304C18.1213 6.82774 18.2924 6.69283 18.4123 6.61077C18.6212 6.46789 18.9896 6.23185 19.3662 6.01043C19.7366 5.79269 20.1374 5.57551 20.4022 5.48361C20.7689 5.35632 21.2081 5.38009 21.5334 5.72086C21.7339 5.93084 21.8023 6.15795 21.7913 6.36941C21.7808 6.57 21.7001 6.73725 21.6399 6.84298L21.6299 6.86056L21.6171 6.87629C21.4157 7.12388 21.1869 7.28577 20.956 7.41907C20.851 7.47973 20.743 7.53584 20.6386 7.59007L20.6124 7.60369C20.4982 7.66307 20.3871 7.72144 20.2759 7.78716C20.0167 7.94039 19.4561 8.36643 19.1861 8.59807C18.854 8.88299 18.5291 9.22697 18.2969 9.64591C18.2562 9.71926 18.2357 9.85967 18.246 10.0459C18.2558 10.2216 18.2902 10.397 18.3192 10.5068C18.3474 10.6137 18.369 10.7073 18.39 10.7987C18.4496 11.0574 18.5052 11.2984 18.696 11.7739C18.8086 12.0543 18.9341 12.2641 19.0783 12.5052C19.1295 12.5907 19.183 12.6802 19.2391 12.7781C19.2555 12.8067 19.2708 12.8346 19.2856 12.8619C19.3428 12.9667 19.3941 13.0606 19.4805 13.1372C19.5653 13.2123 19.6973 13.2788 19.9584 13.218L19.9665 13.2161L19.9748 13.2147C20.347 13.1537 20.6475 13.0147 20.955 12.8725C20.9664 12.8672 20.9778 12.862 20.9891 12.8567C21.298 12.714 21.636 12.5602 22.0258 12.5602C22.3606 12.5602 22.6158 12.7005 22.7802 12.9167C22.9376 13.1238 23 13.384 23 13.6229C23 13.9455 22.7996 14.1926 22.6034 14.3582C22.4028 14.5276 22.1652 14.6481 21.9994 14.718C21.48 14.9369 20.4859 15.2891 19.7384 15.3685C19.7042 15.3721 19.661 15.3789 19.6105 15.3867C19.1991 15.4509 18.3 15.591 17.7518 14.7545C17.6407 14.585 17.4006 14.2594 17.1498 13.9515C17.0248 13.7981 16.8997 13.6521 16.7888 13.5341C16.6729 13.4106 16.5881 13.3346 16.5416 13.305C16.38 13.2022 16.262 13.2217 16.1713 13.2721C16.0628 13.3325 15.9743 13.4514 15.9373 13.5606C15.8308 13.8749 15.691 14.2874 15.5776 14.6879C15.4273 15.2186 15.2045 16.0282 15.0393 16.6782C15.0149 16.7741 14.9905 16.8863 14.963 17.0127C14.957 17.0402 14.9509 17.0685 14.9445 17.0974C14.9098 17.2562 14.8705 17.4305 14.8234 17.6033C14.7321 17.9384 14.6013 18.3097 14.3836 18.556C14.0202 18.9669 13.4846 19.0727 13.0429 18.9548C12.6009 18.8368 12.2125 18.4785 12.2125 17.943C12.2125 17.6301 12.3162 17.124 12.4343 16.6504C12.5535 16.1718 12.6958 15.6946 12.7899 15.416C12.9757 14.749 13.2116 13.8949 13.3866 13.1212C13.3979 13.0613 13.4099 12.9976 13.4226 12.9309C13.4978 12.5343 13.5932 12.031 13.6699 11.5717C13.7149 11.3024 13.7529 11.0514 13.7766 10.8478C13.8015 10.633 13.8065 10.5012 13.7992 10.4515C13.7844 10.3497 13.751 10.315 13.729 10.2987C13.699 10.2766 13.6441 10.2559 13.5426 10.2494C13.4417 10.2429 13.3238 10.2517 13.1852 10.2649C13.1692 10.2664 13.1529 10.268 13.1363 10.2696C13.0172 10.2811 12.8839 10.294 12.7597 10.294C12.6223 10.294 12.472 10.2977 12.3149 10.3015C11.9756 10.3098 11.605 10.3188 11.2661 10.2933C11.1149 10.2819 10.9352 10.258 10.7544 10.2337L10.7295 10.2304C10.5535 10.2067 10.3742 10.1825 10.2029 10.1659C10.0225 10.1485 9.86134 10.1404 9.7317 10.1484C9.59167 10.157 9.53289 10.1823 9.51826 10.1932C9.40563 10.2772 9.36293 10.3344 9.34618 10.3683C9.33318 10.3946 9.32868 10.4202 9.3367 10.4673C9.34595 10.5217 9.36711 10.5816 9.40163 10.6792C9.40461 10.6877 9.40769 10.6964 9.41087 10.7054C9.44816 10.8111 9.49265 10.9424 9.52178 11.0999C9.55533 11.2814 9.5886 11.4647 9.62198 11.6487C9.7153 12.1629 9.80952 12.6821 9.91334 13.1798C9.9259 13.24 9.93878 13.3014 9.95186 13.3637C10.1143 14.1372 10.3081 15.0602 10.3081 15.8244C10.3081 15.9889 10.3121 16.1554 10.3162 16.3235C10.3297 16.8792 10.3436 17.4525 10.2136 18.0277C10.0889 18.5797 9.55124 18.8059 9.10572 18.8008C8.66723 18.7957 8.13248 18.5567 8.08837 17.9935C8.06444 17.6878 8.09368 17.4281 8.12633 17.2004C8.13244 17.1578 8.13853 17.1171 8.1444 17.0777C8.17087 16.9005 8.19298 16.7524 8.19298 16.5993C8.19298 15.8848 8.00978 15.083 7.83775 14.4909C7.75439 14.204 7.63364 14.1146 7.55668 14.0888C7.47435 14.0612 7.35495 14.0767 7.21784 14.1711C7.12371 14.2358 7.02 14.3956 6.91136 14.6516C6.83797 14.8246 6.77436 15.01 6.71076 15.1953C6.68322 15.2755 6.65569 15.3557 6.62737 15.4349C6.44732 15.9385 6.18242 16.5995 5.96374 17.0833C5.90321 17.2173 5.85247 17.3496 5.80243 17.4829C5.79639 17.4991 5.79033 17.5152 5.78426 17.5315C5.74091 17.6474 5.69659 17.7658 5.64795 17.8792C5.53571 18.1408 5.39094 18.3993 5.13767 18.6146L5.12889 18.6221L5.11943 18.6287C4.88967 18.7898 4.54244 18.8806 4.21559 18.8642C3.88604 18.8477 3.51569 18.7163 3.3221 18.3609C3.22342 18.1798 3.20996 17.9754 3.22908 17.7902C3.24839 17.6032 3.3033 17.4128 3.36603 17.2414C3.42919 17.0687 3.50363 16.9062 3.56667 16.7745C3.58947 16.7269 3.60969 16.6854 3.62724 16.6494C3.6617 16.5786 3.68592 16.5289 3.69936 16.495C3.8768 16.0476 4.01924 15.64 4.17636 15.1904C4.26827 14.9274 4.3652 14.65 4.47711 14.3419C4.60715 13.9838 4.95824 12.8655 5.14491 12.0888C5.19511 11.8799 5.24781 11.7216 5.28981 11.5955C5.30443 11.5516 5.31775 11.5115 5.32922 11.4746C5.37289 11.3342 5.40057 11.2104 5.40057 11.0093C5.40057 10.9784 5.3803 10.9448 5.35211 10.9295C5.34057 10.9232 5.32949 10.921 5.31788 10.9221C5.30669 10.9232 5.28313 10.9285 5.24908 10.9554C5.15797 11.0275 5.02885 11.1337 4.88463 11.2523C4.62733 11.4639 4.32194 11.715 4.09846 11.8826C3.78743 12.1158 3.44918 12.4103 3.10959 12.706C2.99451 12.8062 2.87926 12.9066 2.76487 13.0047C2.58673 13.1575 2.3447 13.326 2.06932 13.376C1.92662 13.4019 1.77378 13.3961 1.62075 13.3392C1.46845 13.2826 1.33057 13.1809 1.20826 13.0366C0.991479 12.781 0.95847 12.4944 1.04269 12.2282C1.12188 11.9778 1.30054 11.7539 1.49196 11.5725C1.60625 11.4642 1.75653 11.3228 1.92542 11.164C2.46962 10.6521 3.20715 9.9583 3.55739 9.60275C3.70303 9.4549 3.82293 9.3268 3.93164 9.21066C4.21275 8.91035 4.41901 8.68999 4.80176 8.415C4.86144 8.35575 4.92154 8.30978 4.97317 8.27381C4.96626 8.26895 4.95902 8.26391 4.95145 8.2587C4.53475 7.97188 4.10253 7.21461 4.52248 6.34306C4.68856 5.9984 4.89668 5.74448 5.14374 5.56752C5.39101 5.39041 5.6638 5.30006 5.94419 5.26279C6.4907 5.19016 7.04612 5.30816 7.42088 5.58929C7.72296 5.8159 7.96946 6.15062 8.06084 6.52803C8.16526 6.95934 8.07218 7.30168 7.98025 7.53605C7.96274 7.58069 7.94515 7.6217 7.92956 7.65735C8.38866 7.7876 8.68645 7.8138 9.01858 7.84303C9.0943 7.8497 9.17182 7.85652 9.25343 7.86477C9.31219 7.8707 9.39816 7.87955 9.50086 7.89011C9.89664 7.93083 10.5409 7.99711 10.8338 8.02111C11.262 8.05621 12.0343 8.11934 12.7225 8.02302C13.095 7.96395 13.3919 7.92769 13.6272 7.90177C13.7148 7.89213 13.7918 7.8841 13.8602 7.87697C13.9759 7.86491 14.067 7.85542 14.1427 7.84501C14.088 7.79376 14.027 7.72972 13.9687 7.65108C13.7716 7.38488 13.6315 6.98552 13.759 6.39462C13.8933 5.77205 14.2887 5.27713 14.8982 5.10335ZM14.3079 7.98743C14.3079 7.98742 14.3077 7.98718 14.3072 7.98672C14.3077 7.9872 14.3079 7.98743 14.3079 7.98743ZM16.3689 5.69496C15.9552 5.4716 15.5479 5.42594 15.0363 5.57182C14.6356 5.68607 14.3482 6.01348 14.2442 6.49583C14.1451 6.9551 14.2576 7.21261 14.3697 7.36393C14.4296 7.44486 14.4963 7.50454 14.5562 7.55437C14.562 7.55923 14.5685 7.56462 14.5755 7.57035C14.5985 7.58926 14.6259 7.61178 14.6458 7.63026C14.6596 7.64302 14.6809 7.66378 14.7001 7.69016C14.7164 7.7125 14.7547 7.77006 14.7547 7.85171C14.7547 7.96533 14.7273 8.10948 14.587 8.20991C14.482 8.28514 14.3415 8.30979 14.2213 8.32669C14.1353 8.33878 14.0269 8.3501 13.8989 8.36346C13.8319 8.37045 13.7596 8.37799 13.6824 8.3865C13.4524 8.41184 13.163 8.44719 12.7992 8.50491L12.797 8.50526L12.7948 8.50558C12.0497 8.61026 11.2306 8.5431 10.8053 8.50823L10.7926 8.50719C10.4937 8.48269 9.83874 8.4153 9.4439 8.37468C9.34318 8.36432 9.25938 8.35569 9.20274 8.34997C9.12713 8.34233 9.05328 8.33587 8.97943 8.32941C8.59427 8.29573 8.20917 8.26205 7.57537 8.06072L7.54617 8.05145L7.52016 8.03546C7.41937 7.97351 7.37992 7.87416 7.37749 7.78687C7.37557 7.71796 7.39598 7.6556 7.40927 7.6188C7.4236 7.57911 7.44274 7.5356 7.45971 7.49703L7.46158 7.4928C7.48022 7.45041 7.49907 7.4075 7.5175 7.36051C7.58929 7.1775 7.65106 6.94132 7.57835 6.641C7.51721 6.38847 7.34484 6.14571 7.12007 5.9771C6.8714 5.79055 6.45604 5.68696 6.01061 5.74616C5.79502 5.77481 5.60406 5.84123 5.43573 5.96179C5.26721 6.0825 5.10792 6.26713 4.97069 6.55193C4.67103 7.17382 4.98354 7.68542 5.23585 7.85909C5.34648 7.93524 5.44869 8.01423 5.50733 8.10184C5.54069 8.1517 5.57312 8.22342 5.56708 8.3111C5.56098 8.39945 5.51857 8.46412 5.48267 8.50442C5.44885 8.54238 5.40977 8.57112 5.38263 8.59003C5.36351 8.60336 5.34006 8.61864 5.31952 8.63202C5.31133 8.63736 5.30361 8.64239 5.2968 8.64688C5.24196 8.68306 5.19124 8.71952 5.14493 8.76759L5.12909 8.78404L5.11044 8.79733C4.75876 9.04799 4.5912 9.22704 4.32176 9.51494C4.21003 9.63433 4.08078 9.77243 3.91362 9.94213C3.55595 10.3052 2.80487 11.0117 2.26019 11.5241C2.09389 11.6805 1.94683 11.8188 1.83608 11.9238L1.83607 11.9238C1.67225 12.079 1.56004 12.2347 1.51628 12.373C1.47755 12.4955 1.4895 12.6067 1.58913 12.7242L1.58914 12.7242C1.66719 12.8163 1.73782 12.8613 1.79609 12.8829C1.85363 12.9043 1.91343 12.9083 1.97935 12.8963C2.12123 12.8706 2.28041 12.7731 2.43882 12.6372C2.5461 12.5451 2.6567 12.4488 2.76895 12.3511C3.11216 12.0522 3.47086 11.7398 3.79775 11.4947C4.01563 11.3313 4.29585 11.1007 4.54485 10.8957C4.69383 10.7731 4.83163 10.6597 4.93822 10.5753C5.14798 10.4094 5.39517 10.3956 5.59199 10.5026C5.77395 10.6014 5.89654 10.7962 5.89654 11.0093C5.89654 11.2684 5.85837 11.4408 5.80353 11.6172C5.78813 11.6668 5.77232 11.7142 5.75615 11.7628C5.71555 11.8848 5.67258 12.0138 5.62758 12.201C5.4365 12.9961 5.0801 14.1317 4.94419 14.5059C4.83695 14.8012 4.7417 15.0737 4.65024 15.3353C4.49033 15.7927 4.34202 16.2169 4.16143 16.6723C4.14095 16.7239 4.10332 16.8012 4.06288 16.8842C4.0472 16.9164 4.0311 16.9495 4.01541 16.9823C3.95473 17.109 3.88802 17.2553 3.83273 17.4065C3.77699 17.5588 3.73614 17.7075 3.72252 17.8395C3.7087 17.9733 3.72513 18.0679 3.75929 18.1306C3.84089 18.2804 4.01052 18.3656 4.24086 18.3771C4.46841 18.3885 4.69552 18.3225 4.82254 18.2377C4.98849 18.0935 5.09436 17.9148 5.19099 17.6896C5.23478 17.5875 5.27479 17.4806 5.31861 17.3635C5.3247 17.3472 5.33086 17.3308 5.33712 17.3141C5.38755 17.1797 5.44298 17.0347 5.51051 16.8852C5.72338 16.4142 5.98345 15.7655 6.15948 15.2732C6.18339 15.2063 6.20838 15.1335 6.23446 15.0575C6.30034 14.8656 6.37323 14.6533 6.45363 14.4638C6.56239 14.2075 6.71075 13.9247 6.93342 13.7715C7.15058 13.622 7.43534 13.5329 7.71667 13.6272C8.00337 13.7232 8.20577 13.9822 8.31465 14.3569C8.49077 14.9631 8.68895 15.8166 8.68895 16.5993C8.68895 16.7915 8.66032 16.9819 8.63345 17.1605C8.62795 17.197 8.62253 17.2331 8.61745 17.2685C8.5865 17.4843 8.56307 17.703 8.58288 17.956C8.5982 18.1516 8.79173 18.3094 9.11151 18.313C9.42426 18.3166 9.67486 18.1635 9.72945 17.9218C9.84509 17.4101 9.83345 16.9177 9.82064 16.376C9.81644 16.1982 9.81212 16.0152 9.81212 15.8244C9.81212 15.1129 9.62868 14.2379 9.46366 13.4507C9.45148 13.3926 9.43939 13.3349 9.42748 13.2778C9.32227 12.7734 9.22637 12.245 9.13272 11.7289C9.09957 11.5463 9.0667 11.3651 9.0338 11.1872C9.01181 11.0682 8.97786 10.9661 8.94228 10.8652C8.93868 10.855 8.93495 10.8445 8.93114 10.8339C8.90026 10.7471 8.8642 10.6458 8.84752 10.5478C8.82676 10.4258 8.83197 10.2929 8.90009 10.1551C8.96445 10.0248 9.07427 9.9122 9.21849 9.8046C9.35612 9.70192 9.54184 9.6714 9.70072 9.66162C9.86998 9.6512 10.0621 9.66217 10.2516 9.68053C10.4325 9.69806 10.6203 9.72334 10.7939 9.74672L10.8216 9.75045C11.006 9.77525 11.1703 9.79689 11.3039 9.80695C11.6184 9.83063 11.9425 9.82254 12.2673 9.81444C12.4316 9.81035 12.5961 9.80624 12.7597 9.80624C12.8578 9.80624 12.9647 9.79597 13.0871 9.7842C13.1036 9.78261 13.1205 9.78099 13.1376 9.77937C13.2736 9.76649 13.4288 9.75328 13.5749 9.76267C13.7205 9.77202 13.8865 9.80514 14.0267 9.90866C14.1749 10.0181 14.2609 10.1809 14.2902 10.3825C14.308 10.5047 14.2931 10.6992 14.2693 10.9032C14.2443 11.1184 14.2048 11.3785 14.1593 11.6507C14.0817 12.1157 13.9851 12.625 13.91 13.0213C13.8971 13.0893 13.8848 13.1539 13.8734 13.2144L13.8726 13.2187L13.8717 13.2228C13.693 14.0134 13.4526 14.8832 13.2665 15.5512L13.2647 15.5576L13.2626 15.5639C13.1732 15.8277 13.0333 16.2957 12.916 16.7665C12.7965 17.2461 12.7085 17.6974 12.7085 17.943C12.7085 18.2088 12.894 18.4096 13.1729 18.4841C13.4521 18.5586 13.7844 18.4902 14.0093 18.2359C14.1458 18.0815 14.2541 17.8083 14.3444 17.477C14.3881 17.3167 14.4252 17.1524 14.4596 16.9949C14.4656 16.9678 14.4714 16.9407 14.4773 16.9139C14.5048 16.7873 14.5314 16.6648 14.5581 16.5599C14.7248 15.904 14.9488 15.0899 15.0998 14.557C15.2169 14.1438 15.3601 13.721 15.4661 13.4085L15.4668 13.4064C15.5361 13.202 15.695 12.9767 15.9271 12.8476C16.1771 12.7085 16.4931 12.6932 16.811 12.8955C16.9151 12.9617 17.0368 13.0792 17.1532 13.2032C17.2747 13.3326 17.4078 13.4881 17.5368 13.6466C17.7942 13.9624 18.0454 14.3021 18.1687 14.4903C18.529 15.0401 19.0678 14.9663 19.5043 14.9065C19.567 14.898 19.6276 14.8897 19.6852 14.8836C20.3572 14.8122 21.2962 14.4837 21.8041 14.2697C21.9448 14.2104 22.1323 14.1132 22.2803 13.9882C22.4326 13.8596 22.504 13.7357 22.504 13.6229C22.504 13.4595 22.4602 13.3104 22.3829 13.2087C22.3125 13.1161 22.2047 13.048 22.0258 13.048C21.7624 13.048 21.5204 13.1501 21.2 13.2982C21.1848 13.3052 21.1693 13.3124 21.1538 13.3196C20.8554 13.4578 20.5021 13.6215 20.0644 13.6945C19.6566 13.7872 19.3585 13.6856 19.1484 13.4995C18.9898 13.3588 18.889 13.1701 18.8331 13.0654C18.823 13.0465 18.8143 13.0303 18.8071 13.0176C18.7602 12.9358 18.7123 12.8558 18.6642 12.7754C18.5145 12.5254 18.3627 12.2719 18.2347 11.953C18.0289 11.4402 17.964 11.1584 17.9028 10.8925C17.8827 10.8056 17.8631 10.7204 17.8391 10.6294C17.8037 10.4951 17.7627 10.2871 17.7508 10.0724C17.7395 9.86822 17.7512 9.61118 17.8614 9.41243C18.1313 8.92552 18.5018 8.53787 18.8601 8.23051C19.1397 7.99063 19.7252 7.54356 20.0204 7.3691C20.1445 7.29577 20.2664 7.23185 20.3806 7.1725L20.4049 7.15984C20.5114 7.1045 20.6099 7.05333 20.7049 6.99846C20.9004 6.88556 21.0698 6.76333 21.2168 6.58743C21.2585 6.51101 21.2916 6.42806 21.2959 6.34449C21.3001 6.26538 21.2799 6.16786 21.1719 6.05471C21.0165 5.89202 20.8027 5.86182 20.5672 5.94356C20.3557 6.01698 19.9948 6.20913 19.6207 6.42908C19.2529 6.64535 18.8941 6.87534 18.6955 7.01117C18.5909 7.08275 18.4392 7.20237 18.2698 7.33588C18.2229 7.37287 18.1746 7.41091 18.1256 7.4493C17.9134 7.61561 17.6767 7.79714 17.5193 7.87974C17.4582 7.91177 17.362 7.94576 17.2726 7.97451C17.1769 8.00529 17.0675 8.03675 16.9677 8.06233C16.8714 8.08699 16.7721 8.10934 16.7016 8.11789C16.6834 8.1201 16.6606 8.12222 16.6371 8.12214H16.6358C16.6227 8.12216 16.5634 8.12225 16.5035 8.09117C16.465 8.07123 16.3993 8.02361 16.3756 7.93227C16.3529 7.84493 16.3841 7.77575 16.4022 7.74494C16.4208 7.71319 16.4444 7.6894 16.4629 7.67362C16.613 7.50624 16.7012 7.34969 16.7542 7.18571C16.8097 7.01388 16.8305 6.82185 16.8305 6.57986C16.8305 6.36355 16.7994 6.20559 16.7329 6.07444C16.6682 5.94668 16.5594 5.82392 16.3689 5.69496Z" fill="rgb(205, 92, 155)"/> 565 </svg> 566 </div> 567 568 <script src="/media-modal.js"></script> 569 <script> 570 // Determine base URLs based on current environment 571 function getBaseURL() { 572 const hostname = window.location.hostname; 573 574 // Dev mode: localhost or 127.0.0.1 575 if (hostname === '127.0.0.1' || hostname === 'localhost') { 576 // Always use localhost:8888 for dev (not 127.0.0.1 due to SSL cert) 577 return 'https://localhost:8888'; 578 } 579 580 // Production 581 return 'https://aesthetic.computer'; 582 } 583 584 const API_BASE_URL = getBaseURL(); 585 const PDS_URL = 'https://at.aesthetic.computer'; 586 const RECORDS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes 587 const FORCE_REFRESH = (() => { 588 const params = new URLSearchParams(window.location.search); 589 return params.has('refresh') || params.has('nocache'); 590 })(); 591 592 console.log('🌐 API Base URL:', API_BASE_URL); 593 594 // Shared modal functions 595 function openModal(url) { 596 if (!url) return; 597 if (!window.ACMediaModal?.open) { 598 window.open(url, '_blank', 'noopener,noreferrer'); 599 return; 600 } 601 window.ACMediaModal.open({ 602 title: 'Aesthetic Computer', 603 subtitle: 'Embedded view', 604 iframeUrl: url, 605 bodyHtml: '', 606 actions: [{ label: 'Open In New Tab', url }], 607 }); 608 } 609 610 function closeModal() { 611 if (!window.ACMediaModal?.close) return; 612 window.ACMediaModal.close(); 613 } 614 615 // Intercept aesthetic.computer and pdsls.dev links (left-click only) 616 document.addEventListener('click', (e) => { 617 const link = e.target.closest('a'); 618 if (link && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.button === 0) { 619 const href = link.getAttribute('href'); 620 // Intercept aesthetic.computer URLs (not at.aesthetic.computer) and pdsls.dev URLs 621 if (href && 622 ((href.includes('aesthetic.computer') && !href.includes('at.aesthetic.computer')) || 623 href.includes('pdsls.dev'))) { 624 e.preventDefault(); 625 openModal(href); 626 } 627 } 628 }); 629 630 function normalizeHandle(handle) { 631 if (!handle) return null; 632 if (handle.includes('.')) return handle; 633 return `${handle}.at.aesthetic.computer`; 634 } 635 636 function getHandleFromQuery() { 637 const params = new URLSearchParams(window.location.search); 638 const handle = params.get('handle') || params.get('h'); 639 return normalizeHandle(handle); 640 } 641 642 // Extract handle from subdomain 643 function getHandleFromSubdomain() { 644 const hostname = window.location.hostname; 645 646 // Query param override (local testing) 647 const queryHandle = getHandleFromQuery(); 648 if (queryHandle) { 649 console.log(`🔧 Using handle from query param: ${queryHandle}`); 650 return queryHandle; 651 } 652 653 // Dev mode: default to 'art' handle when running on localhost 654 if (hostname === '127.0.0.1' || hostname === 'localhost') { 655 console.log('🔧 Dev mode: defaulting to art.at.aesthetic.computer'); 656 return 'art.at.aesthetic.computer'; 657 } 658 659 // Match: handle.at.aesthetic.computer 660 const match = hostname.match(/^([^.]+)\.at\.aesthetic\.computer$/); 661 if (match) { 662 return normalizeHandle(match[1]); 663 } 664 return null; 665 } 666 667 function getCacheKey(handle, collection) { 668 return `ac:at-records:${handle}:${collection}`; 669 } 670 671 function loadCachedRecords(handle, collection) { 672 if (FORCE_REFRESH) return null; 673 try { 674 const key = getCacheKey(handle, collection); 675 const raw = localStorage.getItem(key); 676 if (!raw) return null; 677 const parsed = JSON.parse(raw); 678 if (!parsed || !parsed.ts || !Array.isArray(parsed.records)) return null; 679 if (Date.now() - parsed.ts > RECORDS_CACHE_TTL_MS) return null; 680 return parsed.records; 681 } catch (error) { 682 return null; 683 } 684 } 685 686 function saveCachedRecords(handle, collection, records) { 687 try { 688 const key = getCacheKey(handle, collection); 689 localStorage.setItem(key, JSON.stringify({ ts: Date.now(), records })); 690 } catch (error) { 691 // Ignore storage errors (quota, etc.) 692 } 693 } 694 695 function clearRecordsCache(handle) { 696 try { 697 const prefix = `ac:at-records:${handle}:`; 698 Object.keys(localStorage) 699 .filter(key => key.startsWith(prefix)) 700 .forEach(key => localStorage.removeItem(key)); 701 } catch (error) { 702 // Ignore storage errors 703 } 704 } 705 706 // Resolve handle to DID 707 async function resolveDID(handle) { 708 try { 709 const response = await fetch(`${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); 710 if (!response.ok) throw new Error(`Failed to resolve handle: ${response.status}`); 711 const data = await response.json(); 712 return data.did; 713 } catch (error) { 714 console.error('Error resolving DID:', error); 715 throw error; 716 } 717 } 718 719 // List all records for a collection 720 async function listRecords(did, collection, handle, limit = 100) { 721 const cached = handle ? loadCachedRecords(handle, collection) : null; 722 if (cached) return cached; 723 724 const records = []; 725 let cursor = undefined; 726 727 try { 728 do { 729 const url = new URL(`${PDS_URL}/xrpc/com.atproto.repo.listRecords`); 730 url.searchParams.append('repo', did); 731 url.searchParams.append('collection', collection); 732 url.searchParams.append('limit', limit); 733 if (cursor) url.searchParams.append('cursor', cursor); 734 735 const response = await fetch(url); 736 if (!response.ok) { 737 if (response.status === 400) { 738 // Collection might not exist for this user 739 return []; 740 } 741 throw new Error(`Failed to list records: ${response.status}`); 742 } 743 744 const data = await response.json(); 745 records.push(...(data.records || [])); 746 cursor = data.cursor; 747 } while (cursor); 748 749 if (handle) saveCachedRecords(handle, collection, records); 750 return records; 751 } catch (error) { 752 console.error(`Error listing ${collection}:`, error); 753 return []; 754 } 755 } 756 757 // Format date 758 function formatDate(dateString) { 759 const date = new Date(dateString); 760 return date.toLocaleString('en-US', { 761 year: 'numeric', 762 month: 'short', 763 day: 'numeric', 764 hour: '2-digit', 765 minute: '2-digit' 766 }); 767 } 768 769 // Get record key from URI 770 function getRkey(uri) { 771 return uri.split('/').pop(); 772 } 773 774 function getLexicon(uri) { 775 // URI format: at://did:plc:xyz/computer.aesthetic.painting/rkey 776 const parts = uri.split('//')[1].split('/'); 777 return parts[1]; // computer.aesthetic.painting 778 } 779 780 function getLexiconTypeName(lexicon) { 781 // Get just the type name (e.g., "painting" from "computer.aesthetic.painting") 782 return lexicon.split('.').pop(); 783 } 784 785 // Parse kidlisp color escape codes (like \red\, \blue\, etc.) and convert to HTML 786 function parseColoredKidlisp(coloredString) { 787 if (!coloredString) return ''; 788 789 const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 790 791 // Color mapping from kidlisp names to CSS colors 792 const colorMap = { 793 'red': '#ff0000', 794 'darkred': '#8b0000', 795 'orange': '#ff6600', 796 'yellow': isDark ? '#ffff00' : '#d4a000', 797 'lime': isDark ? '#00ff00' : '#00a000', 798 'limegreen': '#32cd32', 799 'green': '#008000', 800 'cyan': isDark ? '#00ffff' : '#008b8b', 801 'blue': '#0000ff', 802 'purple': '#800080', 803 'magenta': '#ff00ff', 804 'hotpink': '#ff69b4', 805 'white': isDark ? '#ffffff' : '#333333', 806 'black': isDark ? '#000000' : '#ffffff', 807 'gray': '#808080', 808 'grey': '#808080', 809 'mediumseagreen': '#3cb371', 810 'pink': '#ffc0cb', 811 'brown': '#a52a2a', 812 'violet': '#ee82ee', 813 'teal': '#008080', 814 'navy': '#000080', 815 'maroon': '#800000', 816 'olive': '#808000', 817 'silver': '#c0c0c0' 818 }; 819 820 const result = []; 821 let i = 0; 822 let currentColor = isDark ? '#ffffff' : '#333333'; 823 let textBuffer = ''; 824 825 function flushBuffer() { 826 if (textBuffer) { 827 const escaped = textBuffer 828 .replace(/&/g, '&amp;') 829 .replace(/</g, '&lt;') 830 .replace(/>/g, '&gt;'); 831 result.push(`<span style="color: ${currentColor}">${escaped}</span>`); 832 textBuffer = ''; 833 } 834 } 835 836 while (i < coloredString.length) { 837 if (coloredString[i] === '\\' && i + 1 < coloredString.length) { 838 // Find the closing \ 839 let j = i + 1; 840 while (j < coloredString.length && coloredString[j] !== '\\') { 841 j++; 842 } 843 if (j < coloredString.length) { 844 const colorCode = coloredString.substring(i + 1, j); 845 846 // Flush any buffered text before changing color 847 flushBuffer(); 848 849 // Parse the color code 850 if (colorMap[colorCode.toLowerCase()]) { 851 currentColor = colorMap[colorCode.toLowerCase()]; 852 } else if (/^\d+,\d+,\d+(,[\d.]+)?$/.test(colorCode)) { 853 // RGB or RGBA value like "255,20,147" or "255,255,255,0.5" 854 currentColor = `rgb(${colorCode})`; 855 } else { 856 // Unknown color code - try as CSS color name 857 currentColor = colorCode; 858 } 859 i = j + 1; 860 continue; 861 } 862 } 863 864 // Regular character - add to buffer 865 textBuffer += coloredString[i]; 866 i++; 867 } 868 869 // Flush any remaining text 870 flushBuffer(); 871 872 return result.join(''); 873 } 874 875 function buildQrDataUrl(text, cellSize = 4, margin = 0) { 876 try { 877 if (typeof qrcode === 'undefined') return ''; 878 const qr = qrcode(0, 'M'); 879 qr.addData(text); 880 qr.make(); 881 return qr.createDataURL(cellSize, margin); 882 } catch (error) { 883 return ''; 884 } 885 } 886 887 // Load kidlisp.mjs and generate colored string 888 let kidlispModule = null; 889 let highlightCache = new Map(); // Cache highlighted results 890 let sourceMap = window.sourceMap || (window.sourceMap = {}); // Cache source code for modal 891 892 const kidlispModuleUrls = [ 893 'https://aesthetic.computer/aesthetic.computer/lib/kidlisp.mjs', 894 'https://aesthetic.computer/lib/kidlisp.mjs', 895 `${window.location.origin}/aesthetic.computer/lib/kidlisp.mjs`, 896 `${window.location.origin}/lib/kidlisp.mjs`, 897 ]; 898 899 async function loadKidlispModule() { 900 if (kidlispModule) return kidlispModule; 901 902 for (const url of kidlispModuleUrls) { 903 try { 904 const module = await import(url); 905 if (module && module.KidLisp) { 906 kidlispModule = module; 907 console.log(`✅ Kidlisp module loaded successfully from ${url}`); 908 return kidlispModule; 909 } 910 } catch (error) { 911 console.warn(`⚠️ Failed to load kidlisp module from ${url}:`, error); 912 } 913 } 914 915 console.error('❌ Failed to load kidlisp module from all known locations'); 916 return null; 917 } 918 919 async function highlightKidlisp(source) { 920 if (!source) return ''; 921 922 // Check cache first 923 if (highlightCache.has(source)) { 924 return highlightCache.get(source); 925 } 926 927 try { 928 const module = await loadKidlispModule(); 929 if (module && module.KidLisp) { 930 // Create a kidlisp instance for syntax highlighting 931 const kid = new module.KidLisp(); 932 kid.initializeSyntaxHighlighting(source); 933 const coloredString = kid.buildColoredKidlispString(); 934 935 if (coloredString && coloredString.trim()) { 936 console.log('🎨 KidLisp colored string sample:', coloredString.substring(0, 100)); 937 938 const result = parseColoredKidlisp(coloredString); 939 940 // Cache the result 941 highlightCache.set(source, result); 942 return result; 943 } 944 } else { 945 console.warn('⚠️ Kidlisp module or KidLisp class not found'); 946 } 947 } catch (error) { 948 console.error('❌ Syntax highlighting failed:', error); 949 } 950 951 // Fallback: simple syntax highlighting 952 const fallback = highlightKidLispSimple(source); 953 highlightCache.set(source, fallback); 954 return fallback; 955 } 956 957 async function openLexiconModal(lexicon) { 958 // Load the lexicon JSON file 959 const lexiconPath = lexicon.replace(/\./g, '/'); 960 try { 961 const response = await fetch(`/lexicons/${lexiconPath}.json`); 962 if (!response.ok) { 963 throw new Error(`Failed to load lexicon: ${response.status}`); 964 } 965 const lexiconData = await response.json(); 966 967 // Create a formatted HTML view of the lexicon 968 const formattedJson = JSON.stringify(lexiconData, null, 2); 969 const htmlContent = ` 970 <!DOCTYPE html> 971 <html> 972 <head> 973 <style> 974 body { 975 margin: 0; 976 padding: 2em; 977 background: black; 978 color: rgb(200, 200, 200); 979 font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; 980 font-size: 11px; 981 line-height: 1.4; 982 } 983 h1 { 984 color: rgb(205, 92, 155); 985 font-size: 1.2em; 986 margin-bottom: 0.8em; 987 font-weight: normal; 988 } 989 pre { 990 margin: 0; 991 white-space: pre-wrap; 992 word-break: break-word; 993 } 994 .json-key { color: rgb(100, 150, 200); } 995 .json-string { color: rgb(152, 195, 121); } 996 .json-number { color: rgb(209, 154, 102); } 997 .json-boolean { color: rgb(198, 120, 221); } 998 .json-null { color: rgb(229, 192, 123); } 999 </style> 1000 </head> 1001 <body> 1002 <h1>${lexicon}</h1> 1003 <pre>${syntaxHighlight(formattedJson)}</pre> 1004 </body> 1005 </html> 1006 `; 1007 1008 // Create blob URL and open in modal 1009 const blob = new Blob([htmlContent], { type: 'text/html' }); 1010 const blobUrl = URL.createObjectURL(blob); 1011 openModal(blobUrl); 1012 1013 // Clean up blob URL after modal opens 1014 setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); 1015 } catch (error) { 1016 console.error('Failed to load lexicon:', error); 1017 alert(`Failed to load lexicon: ${error.message}`); 1018 } 1019 } 1020 1021 function syntaxHighlight(json) { 1022 return json 1023 .replace(/&/g, '&amp;') 1024 .replace(/</g, '&lt;') 1025 .replace(/>/g, '&gt;') 1026 .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { 1027 let cls = 'json-number'; 1028 if (/^"/.test(match)) { 1029 if (/:$/.test(match)) { 1030 cls = 'json-key'; 1031 } else { 1032 cls = 'json-string'; 1033 } 1034 } else if (/true|false/.test(match)) { 1035 cls = 'json-boolean'; 1036 } else if (/null/.test(match)) { 1037 cls = 'json-null'; 1038 } 1039 return '<span class="' + cls + '">' + match + '</span>'; 1040 }); 1041 } 1042 1043 // Simple JavaScript syntax highlighter 1044 function highlightJavaScript(code) { 1045 // Escape HTML first 1046 const escaped = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); 1047 1048 // Parse and highlight in order of precedence to avoid conflicts 1049 let result = ''; 1050 let i = 0; 1051 1052 while (i < escaped.length) { 1053 let matched = false; 1054 1055 // Multi-line comments 1056 if (escaped.substr(i, 2) === '/*') { 1057 let end = escaped.indexOf('*/', i + 2); 1058 if (end === -1) end = escaped.length; 1059 else end += 2; 1060 result += '<span style="color: rgb(120, 120, 120);">' + escaped.substring(i, end) + '</span>'; 1061 i = end; 1062 continue; 1063 } 1064 1065 // Single-line comments 1066 if (escaped.substr(i, 2) === '//') { 1067 let end = escaped.indexOf('\n', i); 1068 if (end === -1) end = escaped.length; 1069 else end += 1; 1070 result += '<span style="color: rgb(120, 120, 120);">' + escaped.substring(i, end) + '</span>'; 1071 i = end; 1072 continue; 1073 } 1074 1075 // Strings 1076 if (escaped[i] === '"' || escaped[i] === "'" || escaped[i] === '`') { 1077 const quote = escaped[i]; 1078 let end = i + 1; 1079 while (end < escaped.length && escaped[end] !== quote) { 1080 if (escaped[end] === '\\') end += 2; 1081 else end++; 1082 } 1083 end++; // Include closing quote 1084 result += '<span style="color: rgb(150, 255, 150);">' + escaped.substring(i, end) + '</span>'; 1085 i = end; 1086 continue; 1087 } 1088 1089 // Numbers 1090 if (/\d/.test(escaped[i])) { 1091 let end = i; 1092 while (end < escaped.length && /[\d.]/.test(escaped[end])) end++; 1093 result += '<span style="color: rgb(255, 200, 100);">' + escaped.substring(i, end) + '</span>'; 1094 i = end; 1095 continue; 1096 } 1097 1098 // Keywords and identifiers 1099 if (/[a-zA-Z_$]/.test(escaped[i])) { 1100 let end = i; 1101 while (end < escaped.length && /[a-zA-Z0-9_$]/.test(escaped[end])) end++; 1102 const word = escaped.substring(i, end); 1103 1104 const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'async', 'await', 'class', 'extends', 'import', 'export', 'from', 'default', 'new', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'delete', 'in', 'of', 'void', 'yield', 'static', 'get', 'set', 'super', 'this']; 1105 1106 if (keywords.includes(word)) { 1107 result += '<span style="color: rgb(100, 150, 255);">' + word + '</span>'; 1108 } else { 1109 // Check if it's a function call (followed by opening paren) 1110 let j = end; 1111 while (j < escaped.length && /\s/.test(escaped[j])) j++; 1112 if (escaped[j] === '(') { 1113 result += '<span style="color: rgb(255, 220, 100);">' + word + '</span>'; 1114 } else { 1115 result += word; 1116 } 1117 } 1118 i = end; 1119 continue; 1120 } 1121 1122 // Default: just add the character 1123 result += escaped[i]; 1124 i++; 1125 } 1126 1127 return result; 1128 } 1129 1130 // KidLisp syntax highlighter - simple fallback 1131 function highlightKidLispSimple(code) { 1132 // Escape HTML first 1133 code = code.replace(/</g, '&lt;').replace(/>/g, '&gt;'); 1134 1135 // Comments (semicolons) 1136 code = code.replace(/(;.*$)/gm, 1137 '<span style="color: rgb(120, 120, 120);">$1</span>'); 1138 1139 // Strings 1140 code = code.replace(/("(?:\\.|[^"\\])*")/g, 1141 '<span style="color: rgb(150, 255, 150);">$1</span>'); 1142 1143 // Numbers 1144 code = code.replace(/\b(-?\d+\.?\d*)\b/g, 1145 '<span style="color: rgb(255, 200, 100);">$1</span>'); 1146 1147 // Common KidLisp forms/functions (after opening paren) 1148 code = code.replace(/\(([a-zA-Z_][a-zA-Z0-9_-]*)/g, 1149 '(<span style="color: rgb(147, 51, 234);">$1</span>'); 1150 1151 // Parentheses 1152 code = code.replace(/([()])/g, 1153 '<span style="color: rgb(200, 200, 200);">$1</span>'); 1154 1155 return code; 1156 } 1157 1158 async function openSourceModal(uri, source, slug) { 1159 // Detect file type from slug 1160 const isJavaScript = slug && slug.endsWith('.mjs'); 1161 const isKidLisp = slug && slug.endsWith('.lisp'); 1162 1163 // Apply syntax highlighting 1164 let highlightedSource; 1165 if (isJavaScript) { 1166 highlightedSource = highlightJavaScript(source); 1167 } else if (isKidLisp) { 1168 // Use the proper KidLisp highlighter with color codes 1169 highlightedSource = await highlightKidlisp(source); 1170 if (!highlightedSource || highlightedSource.trim() === '') { 1171 // Fallback to simple highlighter if the full one fails 1172 highlightedSource = highlightKidLispSimple(source); 1173 } 1174 } else { 1175 highlightedSource = source.replace(/</g, '&lt;').replace(/>/g, '&gt;'); 1176 } 1177 1178 // Create a formatted HTML view of the source code with animated scrolling 1179 const htmlContent = ` 1180 <!DOCTYPE html> 1181 <html> 1182 <head> 1183 <style> 1184 * { 1185 box-sizing: border-box; 1186 } 1187 body { 1188 margin: 0; 1189 padding: 0; 1190 background: black; 1191 color: rgb(200, 200, 200); 1192 font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; 1193 font-size: 11px; 1194 line-height: 1.4; 1195 overflow: hidden; 1196 height: 100vh; 1197 display: flex; 1198 flex-direction: column; 1199 } 1200 .header { 1201 padding: 1.5em 2em; 1202 background: rgba(0, 0, 0, 0.8); 1203 border-bottom: 1px solid rgb(205, 92, 155); 1204 flex-shrink: 0; 1205 } 1206 h1 { 1207 color: rgb(205, 92, 155); 1208 font-size: 1.2em; 1209 margin: 0 0 0.5em 0; 1210 font-weight: normal; 1211 } 1212 .controls { 1213 display: flex; 1214 gap: 1em; 1215 align-items: center; 1216 margin-top: 0.8em; 1217 } 1218 button { 1219 background: rgba(205, 92, 155, 0.2); 1220 border: 1px solid rgb(205, 92, 155); 1221 color: rgb(205, 92, 155); 1222 padding: 0.4em 0.8em; 1223 cursor: pointer; 1224 font-family: inherit; 1225 font-size: 0.9em; 1226 border-radius: 3px; 1227 } 1228 button:hover { 1229 background: rgba(205, 92, 155, 0.3); 1230 } 1231 button.active { 1232 background: rgb(205, 92, 155); 1233 color: black; 1234 } 1235 .scroll-speed { 1236 display: flex; 1237 gap: 0.5em; 1238 align-items: center; 1239 } 1240 .scroll-speed label { 1241 font-size: 0.85em; 1242 color: rgb(150, 150, 150); 1243 } 1244 .scroll-speed input { 1245 width: 80px; 1246 } 1247 a { 1248 color: rgb(205, 92, 155); 1249 text-decoration: none; 1250 } 1251 a:hover { 1252 text-decoration: underline; 1253 } 1254 .code-container { 1255 flex: 1; 1256 overflow: hidden; 1257 position: relative; 1258 } 1259 .code-scroll { 1260 padding: 2em; 1261 overflow-y: auto; 1262 height: 100%; 1263 position: relative; 1264 } 1265 .code-scroll.auto-scrolling { 1266 overflow-y: hidden; /* Hide scrollbar during animation */ 1267 animation: autoScroll var(--scroll-duration, 60s) linear infinite; 1268 } 1269 .code-scroll.auto-scrolling pre { 1270 animation: smoothScroll var(--scroll-duration, 60s) linear infinite; 1271 } 1272 pre { 1273 margin: 0; 1274 white-space: pre-wrap; 1275 word-break: break-word; 1276 } 1277 @keyframes autoScroll { 1278 0% { 1279 scroll-behavior: smooth; 1280 } 1281 100% { 1282 scroll-behavior: smooth; 1283 } 1284 } 1285 @keyframes smoothScroll { 1286 from { 1287 transform: translateY(0); 1288 } 1289 to { 1290 transform: translateY(calc(-100% + 100vh - 250px)); 1291 } 1292 } 1293 .badge { 1294 display: inline-block; 1295 padding: 0.2em 0.6em; 1296 background: rgba(100, 150, 255, 0.2); 1297 border: 1px solid rgb(100, 150, 255); 1298 color: rgb(100, 150, 255); 1299 border-radius: 3px; 1300 font-size: 0.75em; 1301 margin-left: 0.5em; 1302 } 1303 </style> 1304 </head> 1305 <body> 1306 <div class="header"> 1307 <h1> 1308 ${slug ? `piece: ${slug}` : 'piece source'} 1309 ${isJavaScript ? '<span class="badge">.mjs</span>' : ''} 1310 ${isKidLisp ? '<span class="badge">.lisp</span>' : ''} 1311 <span class="badge" style="background: rgba(100, 100, 100, 0.2); border-color: rgb(150, 150, 150); color: rgb(150, 150, 150);">${source.split('\\n').length} lines</span> 1312 </h1> 1313 ${slug ? `<p style="margin: 0;"><a href="https://aesthetic.computer/${slug}" target="_blank">→ open on aesthetic.computer</a></p>` : ''} 1314 <div class="controls"> 1315 <button id="scrollBtn" onclick="toggleScroll()">▶ Auto-scroll</button> 1316 <div class="scroll-speed"> 1317 <label for="speedRange">Speed:</label> 1318 <input type="range" id="speedRange" min="10" max="120" value="60" 1319 oninput="updateSpeed(this.value)"> 1320 <span id="speedLabel">60s</span> 1321 </div> 1322 </div> 1323 </div> 1324 <div class="code-container"> 1325 <div class="code-scroll" id="codeScroll"> 1326 <pre>${highlightedSource}</pre> 1327 </div> 1328 </div> 1329 <script> 1330 let isScrolling = true; // Start with auto-scroll enabled 1331 const scrollContainer = document.getElementById('codeScroll'); 1332 const scrollBtn = document.getElementById('scrollBtn'); 1333 const speedLabel = document.getElementById('speedLabel'); 1334 1335 // Initialize auto-scroll on load 1336 window.addEventListener('load', () => { 1337 scrollContainer.classList.add('auto-scrolling'); 1338 scrollBtn.classList.add('active'); 1339 scrollBtn.textContent = '⏸ Pause'; 1340 }); 1341 1342 function toggleScroll() { 1343 isScrolling = !isScrolling; 1344 if (isScrolling) { 1345 scrollContainer.classList.add('auto-scrolling'); 1346 scrollBtn.classList.add('active'); 1347 scrollBtn.textContent = '⏸ Pause'; 1348 } else { 1349 scrollContainer.classList.remove('auto-scrolling'); 1350 scrollBtn.classList.remove('active'); 1351 scrollBtn.textContent = '▶ Auto-scroll'; 1352 } 1353 } 1354 1355 function updateSpeed(seconds) { 1356 document.documentElement.style.setProperty('--scroll-duration', seconds + 's'); 1357 speedLabel.textContent = seconds + 's'; 1358 } 1359 1360 // Pause on hover 1361 scrollContainer.addEventListener('mouseenter', () => { 1362 if (isScrolling) { 1363 scrollContainer.style.animationPlayState = 'paused'; 1364 } 1365 }); 1366 1367 scrollContainer.addEventListener('mouseleave', () => { 1368 if (isScrolling) { 1369 scrollContainer.style.animationPlayState = 'running'; 1370 } 1371 }); 1372 1373 // Manual scroll disables auto-scroll 1374 scrollContainer.addEventListener('wheel', () => { 1375 if (isScrolling) { 1376 toggleScroll(); 1377 } 1378 }); 1379 <\/script> 1380 </body> 1381 </html> 1382 `; 1383 1384 // Create blob URL and open in modal 1385 const blob = new Blob([htmlContent], { type: 'text/html' }); 1386 const blobUrl = URL.createObjectURL(blob); 1387 openModal(blobUrl); 1388 1389 // Clean up blob URL after modal opens 1390 setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); 1391 } 1392 1393 // Helper function to open source modal from DOM element 1394 async function openSourceModalFromElement(element) { 1395 const sourceId = element.dataset.sourceId; 1396 const uri = element.dataset.uri; 1397 const slug = element.dataset.slug; 1398 const language = element.dataset.language || 'mjs'; 1399 1400 const sourceHolder = document.getElementById(sourceId); 1401 if (sourceHolder) { 1402 const source = sourceHolder.textContent; 1403 // Add language extension to slug for proper detection 1404 const slugWithExtension = slug.includes('.') ? slug : `${slug}.${language}`; 1405 await openSourceModal(uri, source, slugWithExtension); 1406 } else { 1407 console.error('Source not found for ID:', sourceId); 1408 } 1409 } 1410 1411 // Render a painting record 1412 function renderPainting(record) { 1413 const { value, uri } = record; 1414 const rkey = getRkey(uri); 1415 const lexicon = getLexicon(uri); 1416 const typeName = getLexiconTypeName(lexicon); 1417 1418 const card = document.createElement('div'); 1419 card.className = 'record-card'; 1420 1421 let imageHtml = ''; 1422 if (value.thumbnail?.ref) { 1423 const did = uri.split('/')[2]; 1424 const thumbnailUrl = `${PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${value.thumbnail.ref.$link}`; 1425 imageHtml = `<img src="${thumbnailUrl}" alt="Painting thumbnail" class="record-image">`; 1426 } 1427 1428 card.innerHTML = ` 1429 <div class="record-header"> 1430 <span class="record-date">${formatDate(value.createdAt || value.when)}</span> 1431 <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> 1432 </div> 1433 <div class="record-content"> 1434 ${imageHtml} 1435 <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> 1436 ${value.code ? `<a href="${value.acUrl || `https://aesthetic.computer/#${value.code}`}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>#</strong><strong>${value.code}</strong></a>` : ''} 1437 <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> 1438 </div> 1439 </div> 1440 `; 1441 1442 return card; 1443 } 1444 1445 // Render a mood record 1446 function renderMood(record) { 1447 const { value, uri } = record; 1448 const rkey = getRkey(uri); 1449 const lexicon = getLexicon(uri); 1450 const typeName = getLexiconTypeName(lexicon); 1451 1452 const card = document.createElement('div'); 1453 card.className = 'record-card'; 1454 1455 card.innerHTML = ` 1456 <div class="record-header"> 1457 <span class="record-date">${formatDate(value.when || value.createdAt)}</span> 1458 <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> 1459 </div> 1460 <div class="record-content"> 1461 <div class="record-text">${value.mood || value.text || ''}</div> 1462 </div> 1463 `; 1464 1465 return card; 1466 } 1467 1468 // Render a tape record 1469 function renderTape(record) { 1470 const { value, uri } = record; 1471 const rkey = getRkey(uri); 1472 const lexicon = getLexicon(uri); 1473 const typeName = getLexiconTypeName(lexicon); 1474 1475 const card = document.createElement('div'); 1476 card.className = 'record-card'; 1477 1478 let videoHtml = ''; 1479 if (value.video?.ref) { 1480 const did = uri.split('/')[2]; 1481 const videoUrl = `${PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${value.video.ref.$link}`; 1482 videoHtml = `<video controls loop autoplay muted playsinline webkit-playsinline class="record-image" style="max-width: 100%; height: auto; border: none;"> 1483 <source src="${videoUrl}" type="video/mp4"> 1484 Your browser does not support the video tag. 1485 </video>`; 1486 } 1487 1488 card.innerHTML = ` 1489 <div class="record-header"> 1490 <span class="record-date">${formatDate(value.when || value.createdAt)}</span> 1491 <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> 1492 </div> 1493 <div class="record-content"> 1494 ${videoHtml} 1495 <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> 1496 ${value.code ? `<a href="${value.acUrl || `https://aesthetic.computer/!${value.code}`}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>!</strong><strong>${value.code}</strong></a>` : ''} 1497 <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> 1498 </div> 1499 </div> 1500 `; 1501 1502 return card; 1503 } 1504 1505 // Render a kidlisp record 1506 async function renderKidlisp(record) { 1507 const { value, uri } = record; 1508 const rkey = getRkey(uri); 1509 const lexicon = getLexicon(uri); 1510 const typeName = getLexiconTypeName(lexicon); 1511 1512 const card = document.createElement('div'); 1513 card.className = 'record-card'; 1514 1515 // Create initial card HTML with preview container 1516 const previewCode = value.code ? value.code.replace(/[^a-zA-Z0-9_-]/g, '') : ''; 1517 const kidlispPreviewHtml = previewCode 1518 ? `<a href="https://aesthetic.computer/$${previewCode}" target="_blank" rel="noreferrer" class="kidlisp-preview-card"> 1519 <img class="kidlisp-webp" src="https://oven.aesthetic.computer/grab/webp/200/200/$${previewCode}?duration=4000&fps=8&quality=80&density=1&nowait=true" alt="KidLisp preview" loading="lazy" /> 1520 <div class="kidlisp-preview-overlay" data-kidlisp-code="${previewCode}"> 1521 <div class="kidlisp-preview-code">${value.source ? value.source.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : ''}</div> 1522 <div class="kidlisp-preview-qr"> 1523 <div class="kidlisp-preview-label">$${previewCode}</div> 1524 <img alt="KidLisp QR" /> 1525 </div> 1526 </div> 1527 </a>` 1528 : ''; 1529 1530 card.innerHTML = ` 1531 <div class="record-header"> 1532 <span class="record-date">${formatDate(value.when || value.createdAt)}</span> 1533 <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> 1534 </div> 1535 <div class="record-content"> 1536 ${kidlispPreviewHtml} 1537 <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> 1538 ${value.code ? `<a href="${value.acUrl || `https://aesthetic.computer/$${value.code}`}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>$</strong><strong>${value.code}</strong></a>` : ''} 1539 <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> 1540 </div> 1541 </div> 1542 `; 1543 1544 // Highlight and animate the source code asynchronously 1545 if (value.source) { 1546 highlightKidlisp(value.source).then(highlightedSource => { 1547 // Store source for modal 1548 const sourceId = `kidlisp-${rkey}`; 1549 sourceMap = window.sourceMap || (window.sourceMap = sourceMap || {}); 1550 sourceMap[sourceId] = value.source; 1551 1552 // Update overlay code in WebP preview 1553 const overlay = card.querySelector('.kidlisp-preview-overlay'); 1554 if (overlay) { 1555 const overlayCode = overlay.querySelector('.kidlisp-preview-code'); 1556 if (overlayCode) { 1557 overlayCode.innerHTML = highlightedSource; 1558 } 1559 const qrImg = overlay.querySelector('.kidlisp-preview-qr img'); 1560 if (qrImg && value.code) { 1561 const qrUrl = buildQrDataUrl(`https://aesthetic.computer/$${value.code}`, 3, 0); 1562 if (qrUrl) qrImg.src = qrUrl; 1563 } 1564 } 1565 }).catch(error => { 1566 console.error('Highlighting error:', error); 1567 }); 1568 } 1569 1570 if (value.code) { 1571 const overlay = card.querySelector('.kidlisp-preview-overlay'); 1572 if (overlay) { 1573 const qrImg = overlay.querySelector('.kidlisp-preview-qr img'); 1574 if (qrImg) { 1575 const qrUrl = buildQrDataUrl(`https://aesthetic.computer/$${value.code}`, 3, 0); 1576 if (qrUrl) qrImg.src = qrUrl; 1577 } 1578 } 1579 } 1580 1581 return card; 1582 } 1583 1584 // Render a piece record 1585 async function renderPiece(record, handle) { 1586 const { value, uri } = record; 1587 const rkey = getRkey(uri); 1588 const lexicon = getLexicon(uri); 1589 const typeName = getLexiconTypeName(lexicon); 1590 1591 const card = document.createElement('div'); 1592 card.className = 'record-card'; 1593 1594 // Create initial card HTML 1595 card.innerHTML = ` 1596 <div class="record-header"> 1597 <span class="record-date">${formatDate(value.when || value.createdAt)}</span> 1598 <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> 1599 </div> 1600 <div class="record-content"> 1601 <div class="source-preview-container" style="margin: 0.5em 0;"> 1602 <div style="font-size: 0.75em; color: rgb(150, 150, 150);">loading source...</div> 1603 </div> 1604 <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> 1605 ${value.slug ? `<a href="https://prompt.ac/@${handle ? handle.split('.at.aesthetic.computer')[0] : 'art'}/${value.slug}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>@${handle ? handle.split('.at.aesthetic.computer')[0] : 'art'}</strong>/<strong>${value.slug}</strong></a>` : ''} 1606 <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> 1607 </div> 1608 </div> 1609 `; 1610 1611 // Fetch source code from record or media URL if available 1612 if (value.slug && handle) { 1613 try { 1614 let source = null; 1615 let language = 'mjs'; // Default to JavaScript 1616 1617 // Use embedded source if present 1618 if (typeof value.source === 'string' && value.source.trim()) { 1619 source = value.source; 1620 language = value.language || (value.source.trim().startsWith('(') ? 'lisp' : 'mjs'); 1621 } else if (typeof value.sourceCode === 'string' && value.sourceCode.trim()) { 1622 source = value.sourceCode; 1623 language = value.language || (value.sourceCode.trim().startsWith('(') ? 'lisp' : 'mjs'); 1624 } 1625 1626 if (!source) { 1627 // Prefer same-origin media proxy to avoid CORS issues 1628 const baseHandle = handle.replace(/^@/, ''); 1629 const shortHandle = baseHandle.split('.at.aesthetic.computer')[0]; 1630 const mediaHandles = Array.from(new Set([baseHandle, shortHandle].filter(Boolean))); 1631 const rawSlug = value.slug || ''; 1632 const slug = rawSlug.replace(/\.(mjs|js|lisp)$/i, ''); 1633 1634 async function tryFetchPieceSource(mediaHandle) { 1635 const bases = [ 1636 `/media/@${mediaHandle}/piece/${slug}`, 1637 `/media/@${mediaHandle}/code/${slug}` 1638 ]; 1639 const extensions = ['.lisp', '.mjs', '.js', '']; 1640 1641 for (const base of bases) { 1642 for (const ext of extensions) { 1643 const url = `${base}${ext}`; 1644 const response = await fetch(url); 1645 if (response.ok) { 1646 const text = await response.text(); 1647 const isLisp = ext === '.lisp' || text.trim().startsWith('('); 1648 return { source: text, language: isLisp ? 'lisp' : 'mjs' }; 1649 } 1650 } 1651 } 1652 1653 return null; 1654 } 1655 1656 for (const mediaHandle of mediaHandles) { 1657 const result = await tryFetchPieceSource(mediaHandle); 1658 if (result) { 1659 source = result.source; 1660 language = result.language; 1661 break; 1662 } 1663 } 1664 1665 // Fallback for art handle legacy URLs 1666 if (!source && handle === 'art.at.aesthetic.computer') { 1667 const legacyLispUrl = `https://art.aesthetic.computer/${slug}.lisp`; 1668 let response = await fetch(legacyLispUrl); 1669 if (response.ok) { 1670 source = await response.text(); 1671 language = 'lisp'; 1672 } else { 1673 const legacyMjsUrl = `https://art.aesthetic.computer/${slug}.mjs`; 1674 response = await fetch(legacyMjsUrl); 1675 if (response.ok) { 1676 source = await response.text(); 1677 language = 'mjs'; 1678 } 1679 } 1680 } 1681 } 1682 1683 // Render source if found 1684 if (source) { 1685 const previewContainer = card.querySelector('.source-preview-container'); 1686 if (previewContainer) { 1687 // Create a unique ID for this source 1688 const sourceId = `source-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 1689 1690 // Store source in a hidden element to avoid escaping issues 1691 const sourceHolder = document.createElement('div'); 1692 sourceHolder.id = sourceId; 1693 sourceHolder.style.display = 'none'; 1694 sourceHolder.textContent = source; 1695 sourceHolder.dataset.language = language; 1696 document.body.appendChild(sourceHolder); 1697 1698 // Apply syntax highlighting to full source (not just preview) 1699 let highlightedSource; 1700 if (language === 'lisp') { 1701 // Use async highlighter for KidLisp 1702 highlightKidlisp(source).then(highlighted => { 1703 if (highlighted && highlighted.trim()) { 1704 // Update the preview with highlighted code (duplicated for seamless loop) 1705 const previewContent = previewContainer.querySelector('.preview-content'); 1706 if (previewContent) { 1707 previewContent.innerHTML = ` 1708 ${highlighted} 1709 <div style="margin: 2em 0; border-top: 1px dashed rgba(100,100,100,0.3); padding-top: 2em;"></div> 1710 ${highlighted} 1711 `; 1712 } 1713 } 1714 }); 1715 // Start with plain text while loading 1716 highlightedSource = source.replace(/</g, '&lt;').replace(/>/g, '&gt;'); 1717 } else if (language === 'mjs') { 1718 highlightedSource = highlightJavaScript(source); 1719 } else { 1720 highlightedSource = source.replace(/</g, '&lt;').replace(/>/g, '&gt;'); 1721 } 1722 1723 // Language badge colors 1724 const languageBadge = language === 'lisp' 1725 ? '<span style="display: inline-block; padding: 0.15em 0.4em; background: rgba(147, 51, 234, 0.1); color: rgb(147, 51, 234); border-radius: 3px; font-size: 0.85em; font-weight: 600; margin-left: 0.5em;">KidLisp</span>' 1726 : '<span style="display: inline-block; padding: 0.15em 0.4em; background: rgba(234, 179, 8, 0.1); color: rgb(234, 179, 8); border-radius: 3px; font-size: 0.85em; font-weight: 600; margin-left: 0.5em;">JavaScript</span>'; 1727 1728 // Calculate dynamic font size and animation speed based on content 1729 const lineCount = source.split('\n').length; 1730 const fontSize = lineCount > 50 ? '0.55em' : lineCount > 30 ? '0.6em' : lineCount > 15 ? '0.7em' : '0.8em'; 1731 1732 // Shorter programs scroll faster - scale duration based on line count 1733 const animationDuration = Math.max(15, Math.min(60, lineCount * 0.8)); 1734 1735 // Generate line numbers (doubled for the duplicated content) 1736 const lineNumbers = Array.from({length: lineCount * 2}, (_, i) => 1737 `<div>${(i % lineCount) + 1}</div>` 1738 ).join(''); 1739 1740 previewContainer.innerHTML = ` 1741 <div class="source-preview" 1742 data-source-id="${sourceId}" 1743 data-uri="${uri.replace(/"/g, '&quot;')}" 1744 data-slug="${value.slug}" 1745 data-language="${language}" 1746 onclick="openSourceModalFromElement(this)" 1747 style="padding: 0.5em; border-radius: 2px; cursor: pointer; font-family: monospace; font-size: ${fontSize}; color: rgb(100, 100, 100);"> 1748 <div class="preview-scroll-container" style="animation: previewScrollLoop ${animationDuration}s linear infinite;"> 1749 <div class="line-numbers">${lineNumbers}</div> 1750 <div class="preview-content" style="white-space: pre; overflow-wrap: normal;"> 1751 ${highlightedSource} 1752 <div style="margin: 2em 0; border-top: 1px dashed rgba(100,100,100,0.3); padding-top: 2em;"></div> 1753 ${highlightedSource} 1754 </div> 1755 </div> 1756 </div> 1757 `; 1758 } 1759 } else { 1760 const previewContainer = card.querySelector('.source-preview-container'); 1761 if (previewContainer) previewContainer.innerHTML = ''; 1762 } 1763 } catch (error) { 1764 console.error('Failed to fetch piece source:', error); 1765 const previewContainer = card.querySelector('.source-preview-container'); 1766 if (previewContainer) previewContainer.innerHTML = ''; 1767 } 1768 } else { 1769 const previewContainer = card.querySelector('.source-preview-container'); 1770 if (previewContainer) previewContainer.innerHTML = ''; 1771 } 1772 1773 return card; 1774 } 1775 1776 // Render a generic record 1777 function renderGenericRecord(record, typeName) { 1778 const { value, uri } = record; 1779 const rkey = getRkey(uri); 1780 1781 const card = document.createElement('div'); 1782 card.className = 'record-card'; 1783 1784 card.innerHTML = ` 1785 <div class="record-header"> 1786 <span class="record-type">${typeName}</span> 1787 <span class="record-date">${formatDate(value.createdAt || value.when || new Date().toISOString())}</span> 1788 </div> 1789 <div class="record-content"> 1790 <pre class="record-text" style="font-size: 0.85em; overflow-x: auto;">${JSON.stringify(value, null, 2)}</pre> 1791 </div> 1792 <div class="record-meta"> 1793 <span>rkey: ${rkey}</span> 1794 </div> 1795 <a href="https://pdsls.dev/at://${uri.split('//')[1]}" target="_blank" class="record-link"> 1796 View on pdsls.dev → 1797 </a> 1798 `; 1799 1800 return card; 1801 } 1802 1803 // Global state for lazy loading 1804 let currentRecords = []; 1805 let currentCollection = ''; 1806 let currentHandle = ''; 1807 let currentIndex = 0; 1808 const BATCH_SIZE = 50; // Load 50 records at a time 1809 let isLoading = false; 1810 let loadingSentinel = null; 1811 1812 // Render a batch of records 1813 async function renderBatch(startIndex, endIndex) { 1814 if (isLoading || startIndex >= currentRecords.length) return; 1815 1816 isLoading = true; 1817 const grid = document.querySelector('.records-grid'); 1818 if (!grid) return; 1819 1820 const batch = currentRecords.slice(startIndex, endIndex); 1821 1822 for (const record of batch) { 1823 let card; 1824 const collectionKey = currentCollection === 'all' 1825 ? (record._collection || '') 1826 : currentCollection; 1827 1828 if (collectionKey.includes('painting')) { 1829 card = renderPainting(record); 1830 } else if (collectionKey.includes('mood')) { 1831 card = renderMood(record); 1832 } else if (collectionKey.includes('tape')) { 1833 card = renderTape(record); 1834 } else if (collectionKey.includes('kidlisp')) { 1835 card = await renderKidlisp(record); 1836 } else if (collectionKey.includes('piece')) { 1837 card = await renderPiece(record, currentHandle); 1838 } else { 1839 card = renderGenericRecord(record, collectionKey.split('.').pop()); 1840 } 1841 1842 // Insert before the sentinel 1843 if (loadingSentinel && loadingSentinel.parentNode) { 1844 grid.insertBefore(card, loadingSentinel); 1845 } else { 1846 grid.appendChild(card); 1847 } 1848 } 1849 1850 currentIndex = endIndex; 1851 isLoading = false; 1852 1853 // Update sentinel text 1854 if (loadingSentinel) { 1855 if (currentIndex >= currentRecords.length) { 1856 loadingSentinel.textContent = `✓ All ${currentRecords.length} records loaded`; 1857 loadingSentinel.style.opacity = '0.5'; 1858 } else { 1859 loadingSentinel.textContent = `Loaded ${currentIndex} of ${currentRecords.length} records... (scroll to load more)`; 1860 } 1861 } 1862 } 1863 1864 // Render records for a collection with lazy loading 1865 async function renderRecords(records, collection, handle) { 1866 const container = document.getElementById('records-container'); 1867 container.innerHTML = ''; 1868 1869 if (records.length === 0) { 1870 container.innerHTML = '<div class="empty-state">No records found</div>'; 1871 return; 1872 } 1873 1874 // Sort by date (newest first) 1875 const sorted = [...records].sort((a, b) => { 1876 const dateA = new Date(a.value.createdAt || a.value.when || 0); 1877 const dateB = new Date(b.value.createdAt || b.value.when || 0); 1878 return dateB - dateA; 1879 }); 1880 1881 // Update global state 1882 currentRecords = sorted; 1883 currentCollection = collection; 1884 currentHandle = handle; 1885 currentIndex = 0; 1886 isLoading = false; 1887 1888 // Create grid 1889 const grid = document.createElement('div'); 1890 grid.className = 'records-grid'; 1891 1892 // Create loading sentinel 1893 loadingSentinel = document.createElement('div'); 1894 loadingSentinel.className = 'loading-sentinel'; 1895 loadingSentinel.style.cssText = 'text-align: center; padding: 2em 1em; opacity: 0.6; font-size: 0.9em; grid-column: 1 / -1;'; 1896 loadingSentinel.textContent = `Loading records...`; 1897 grid.appendChild(loadingSentinel); 1898 1899 container.appendChild(grid); 1900 1901 // Set up Intersection Observer for lazy loading 1902 const observer = new IntersectionObserver( 1903 async (entries) => { 1904 const entry = entries[0]; 1905 if (entry.isIntersecting && !isLoading && currentIndex < currentRecords.length) { 1906 await renderBatch(currentIndex, currentIndex + BATCH_SIZE); 1907 } 1908 }, 1909 { 1910 rootMargin: '400px', // Start loading 400px before sentinel is visible 1911 threshold: 0.1 1912 } 1913 ); 1914 1915 observer.observe(loadingSentinel); 1916 1917 // Load initial batch 1918 await renderBatch(0, BATCH_SIZE); 1919 } 1920 1921 // Initialize the page 1922 async function init() { 1923 try { 1924 const handle = getHandleFromSubdomain(); 1925 if (!handle) { 1926 throw new Error('Invalid subdomain format. Expected: handle.at.aesthetic.computer'); 1927 } 1928 1929 document.getElementById('handle-display').textContent = `@${handle}`; 1930 document.title = `@${handle} · ATProto`; 1931 1932 // Add subtitle for art.at.aesthetic.computer 1933 const didDisplay = document.getElementById('did-display'); 1934 const handleDisplay = document.getElementById('handle-display'); 1935 1936 if (handle === 'art.at.aesthetic.computer') { 1937 didDisplay.innerHTML = '<div style="font-size: 0.9em; margin-top: 0.5em; opacity: 0.8; line-height: 1.5; max-width: 600px; margin-left: auto; margin-right: auto;">Anonymously recorded tapes and other media on <a href="https://aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">Aesthetic Computer</a>, synced to <a href="https://atproto.com" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">ATProto</a> for open syndication via the <a href="https://at.aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">at.aesthetic.computer</a> PDS instance.</div>'; 1938 1939 // Make handle clickable and add color cycling to "art" 1940 handleDisplay.innerHTML = '<a href="https://at.aesthetic.computer" style="color: inherit; text-decoration: none;"><span class="color-cycle-a">a</span><span class="color-cycle-r">r</span><span class="color-cycle-t">t</span>.at.aesthetic.computer</a>'; 1941 handleDisplay.style.cursor = 'pointer'; 1942 } else { 1943 // For user pages, extract username part (before .at.aesthetic.computer) 1944 const username = handle.split('.at.aesthetic.computer')[0]; 1945 1946 // Add color cycling only to username part 1947 const usernameParts = username.split(''); 1948 const colorCycledUsername = usernameParts.map((char, index) => { 1949 const delay = (index * 0.3).toFixed(1); 1950 return `<span class="color-cycle" style="animation-delay: -${delay}s">${char}</span>`; 1951 }).join(''); 1952 1953 handleDisplay.innerHTML = `${colorCycledUsername}.at.aesthetic.computer`; 1954 1955 // Simple subtitle with username and Aesthetic Computer link 1956 didDisplay.innerHTML = `<div style="font-size: 0.9em; margin-top: 0.5em; opacity: 0.8; line-height: 1.5; max-width: 600px; margin-left: auto; margin-right: auto;">All media by <span style="font-weight: bold;">@${username}</span> on <a href="https://aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">Aesthetic Computer</a>, synced to <a href="https://atproto.com" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">ATProto</a> for open syndication via the <a href="https://at.aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">at.aesthetic.computer</a> PDS instance.</div>`; 1957 } 1958 1959 // Resolve DID (but don't display it - we're showing subtitles instead) 1960 const did = await resolveDID(handle); 1961 1962 // Fetch all collections (tapes first) 1963 const collections = [ 1964 'computer.aesthetic.tape', 1965 'computer.aesthetic.painting', 1966 'computer.aesthetic.mood', 1967 'computer.aesthetic.piece', 1968 'computer.aesthetic.kidlisp' 1969 ]; 1970 1971 const recordsByCollection = {}; 1972 const allRecords = []; 1973 let totalRecords = 0; 1974 let firstTab = null; 1975 let firstRecords = null; 1976 let firstCollection = null; 1977 let loadingHidden = false; 1978 1979 function hideLoading() { 1980 if (loadingHidden) return; 1981 document.getElementById('loading').style.display = 'none'; 1982 document.getElementById('content').style.display = 'block'; 1983 loadingHidden = true; 1984 } 1985 1986 // Stats section removed - counts now in tabs 1987 document.getElementById('stats').innerHTML = ''; 1988 1989 // Create tabs 1990 const tabsContainer = document.getElementById('tabs'); 1991 1992 // Helper to get display name for collection 1993 function getDisplayName(collection) { 1994 const name = collection.split('.').pop(); 1995 if (name === 'kidlisp') return 'KidLisp'; 1996 if (name === 'painting') return 'Paintings'; 1997 if (name === 'mood') return 'Moods'; 1998 if (name === 'piece') return 'Pieces'; 1999 if (name === 'tape') return 'Tapes'; 2000 return name.charAt(0).toUpperCase() + name.slice(1); 2001 } 2002 2003 const tabs = new Map(); 2004 2005 function setTabLabel(tab, label, count, loading) { 2006 tab.innerHTML = `${label} (${count}${loading ? '…' : ''})`; 2007 } 2008 2009 function activateTab(tab, records, collection) { 2010 document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); 2011 tab.classList.add('active'); 2012 renderRecords(records, collection, handle); 2013 } 2014 2015 const allTab = document.createElement('button'); 2016 allTab.className = 'tab disabled'; 2017 allTab.disabled = true; 2018 setTabLabel(allTab, 'All Media', 0, true); 2019 allTab.onclick = () => activateTab(allTab, allRecords, 'all'); 2020 tabsContainer.appendChild(allTab); 2021 tabs.set('all', allTab); 2022 2023 // Collection tabs (initialize with loading state) 2024 collections.forEach(collection => { 2025 const displayName = getDisplayName(collection); 2026 const tab = document.createElement('button'); 2027 tab.className = 'tab disabled'; 2028 tab.disabled = true; 2029 setTabLabel(tab, displayName, 0, true); 2030 tab.onclick = () => activateTab(tab, recordsByCollection[collection] || [], collection); 2031 tabsContainer.appendChild(tab); 2032 tabs.set(collection, tab); 2033 }); 2034 2035 const refreshButton = document.createElement('button'); 2036 refreshButton.className = 'tab'; 2037 refreshButton.style.opacity = '0.7'; 2038 refreshButton.innerHTML = '↻ Refresh'; 2039 refreshButton.onclick = () => { 2040 clearRecordsCache(handle); 2041 window.location.search = window.location.search.includes('refresh') 2042 ? window.location.search 2043 : window.location.search + (window.location.search ? '&refresh=1' : '?refresh=1'); 2044 }; 2045 tabsContainer.appendChild(refreshButton); 2046 2047 // Fetch collections concurrently and update tabs as they resolve 2048 const pending = new Set(collections); 2049 await Promise.all(collections.map(async (collection) => { 2050 const records = await listRecords(did, collection, handle); 2051 recordsByCollection[collection] = records; 2052 totalRecords += records.length; 2053 2054 const withCollection = records.map(record => ({ ...record, _collection: collection })); 2055 allRecords.push(...withCollection); 2056 allRecords.sort((a, b) => { 2057 const dateA = new Date(a.value.createdAt || a.value.when || 0); 2058 const dateB = new Date(b.value.createdAt || b.value.when || 0); 2059 return dateB - dateA; 2060 }); 2061 2062 const tab = tabs.get(collection); 2063 if (tab) { 2064 setTabLabel(tab, getDisplayName(collection), records.length, false); 2065 if (records.length > 0) { 2066 tab.disabled = false; 2067 tab.classList.remove('disabled'); 2068 } 2069 } 2070 2071 pending.delete(collection); 2072 2073 const allTabRef = tabs.get('all'); 2074 if (allTabRef) { 2075 setTabLabel(allTabRef, 'All Media', totalRecords, pending.size > 0); 2076 if (totalRecords > 0) { 2077 allTabRef.disabled = false; 2078 allTabRef.classList.remove('disabled'); 2079 } 2080 } 2081 2082 if (!firstTab && allRecords.length > 0) { 2083 firstTab = allTabRef; 2084 firstRecords = allRecords; 2085 firstCollection = 'all'; 2086 hideLoading(); 2087 activateTab(firstTab, firstRecords, firstCollection); 2088 } else if (currentCollection === 'all') { 2089 // Update active All Media view when new records arrive 2090 renderRecords(allRecords, 'all', handle); 2091 } 2092 })); 2093 2094 if (!loadingHidden) { 2095 hideLoading(); 2096 } 2097 2098 if (!firstTab) { 2099 document.getElementById('records-container').innerHTML = '<div class="empty-state">No records found</div>'; 2100 } 2101 2102 } catch (error) { 2103 console.error('Error loading page:', error); 2104 document.getElementById('loading').style.display = 'none'; 2105 document.getElementById('error').style.display = 'block'; 2106 document.getElementById('error').textContent = `❌ Error: ${error.message}`; 2107 } 2108 } 2109 2110 // Auto-refresh placeholder images (baking indicator) 2111 document.addEventListener('load', (e) => { 2112 if (e.target.tagName === 'IMG' && e.target.src.includes('nowait=true')) { 2113 const img = e.target; 2114 if (!img.dataset.retried) { 2115 // Schedule a retry after 10-15 seconds to fetch the real baked image 2116 const retryDelay = 10000 + Math.random() * 5000; 2117 setTimeout(() => { 2118 img.dataset.retried = 'true'; 2119 const url = new URL(img.src); 2120 url.searchParams.set('t', Date.now()); 2121 img.src = url.toString(); 2122 }, retryDelay); 2123 } 2124 } 2125 }, true); 2126 2127 // Start 2128 init(); 2129 </script> 2130</body> 2131</html>