view who was fronting when a record was made
at main 20 kB view raw
1<script lang="ts"> 2 import { expect } from "@/lib/result"; 3 import { getFronter, getMemberPublicUri } from "@/lib/utils"; 4 import { isResourceUri } from "@atcute/lexicons"; 5 import type { ResourceUri } from "@atcute/lexicons/syntax"; 6 import FronterList from "@/components/FronterList.svelte"; 7 8 let recordAtUri = $state(""); 9 let queryResult = $state<{ 10 handle: string; 11 fronters: { name: string; uri?: string }[]; 12 } | null>(null); 13 let queryError = $state(""); 14 let isQuerying = $state(false); 15 let fronters = $state<string[]>([]); 16 let pkSystemId = $state<string>(""); 17 let spToken = $state(""); 18 let isFromCurrentTab = $state(false); 19 20 const makeOutput = (record: any) => { 21 const fronters = record.members.map((f: any) => ({ 22 name: f.name, 23 uri: f.uri ? getMemberPublicUri(f.uri) : undefined, 24 })); 25 return { 26 handle: record.handle ?? `handle.invalid (${record.did})`, 27 fronters, 28 }; 29 }; 30 31 const queryRecord = async (recordUri: ResourceUri) => { 32 if (!recordAtUri.trim()) return; 33 34 isQuerying = true; 35 queryResult = null; 36 37 try { 38 if (!isResourceUri(recordUri)) throw "INVALID_RESOURCE_URI"; 39 const result = expect(await getFronter(recordUri)); 40 queryResult = makeOutput(result); 41 } catch (error) { 42 queryResult = null; 43 queryError = `ERROR: ${error}`; 44 } finally { 45 isQuerying = false; 46 } 47 }; 48 49 const updateFronters = (newFronters: string[]) => { 50 fronters = newFronters; 51 storage.setItem("sync:fronters", newFronters); 52 }; 53 54 const updatePkSystem = (event: any) => { 55 pkSystemId = (event.target as HTMLInputElement).value; 56 storage.setItem("sync:pk-system", pkSystemId); 57 }; 58 59 const updateSpToken = (event: any) => { 60 spToken = (event.target as HTMLInputElement).value; 61 storage.setItem("sync:sp_token", spToken); 62 }; 63 64 const handleKeyPress = (event: KeyboardEvent) => { 65 if (event.key === "Enter") { 66 queryRecord(recordAtUri as ResourceUri); 67 } 68 }; 69 70 const clearResult = () => { 71 queryResult = null; 72 queryError = ""; 73 recordAtUri = ""; 74 isFromCurrentTab = false; 75 }; 76 77 onMount(async () => { 78 const frontersArray = await storage.getItem<string[]>("sync:fronters"); 79 if (frontersArray && Array.isArray(frontersArray)) { 80 fronters = frontersArray; 81 } 82 83 const pkSystem = await storage.getItem<string>("sync:pk-system"); 84 if (pkSystem) { 85 pkSystemId = pkSystem; 86 } 87 88 const token = await storage.getItem<string>("sync:sp_token"); 89 if (token) { 90 spToken = token; 91 } 92 93 const tabs = await browser.tabs.query({ 94 active: true, 95 currentWindow: true, 96 }); 97 const tabFronter = await storage.getItem<any>( 98 `local:tab-${tabs[0].id!}-fronter`, 99 ); 100 if (tabFronter) { 101 queryResult = makeOutput(tabFronter); 102 recordAtUri = tabFronter.recordUri; 103 isFromCurrentTab = true; 104 } 105 }); 106</script> 107 108<main> 109 <div class="container"> 110 <div class="content"> 111 <section class="query-panel"> 112 <div class="panel-header"> 113 <span class="panel-title">RECORD QUERY</span> 114 <div class="panel-accent"></div> 115 </div> 116 117 <div class="input-container"> 118 <div class="input-wrapper"> 119 <input 120 type="text" 121 placeholder="input_at_uri (at://repo/collection/rkey)" 122 bind:value={recordAtUri} 123 onkeypress={handleKeyPress} 124 class="record-input" 125 disabled={isQuerying} 126 /> 127 <button 128 onclick={() => 129 queryRecord(recordAtUri as ResourceUri)} 130 class="exec-button" 131 disabled={isQuerying || !recordAtUri.trim()} 132 > 133 <span class="button-text">EXEC</span> 134 <div class="button-accent"></div> 135 </button> 136 </div> 137 </div> 138 139 <div class="output-container"> 140 <div class="output-header"> 141 <div class="output-header-left"> 142 <span>OUTPUT</span> 143 {#if isFromCurrentTab} 144 <div class="tab-indicator"> 145 <span class="tab-indicator-text" 146 >FROM_CURRENT_TAB</span 147 > 148 <div class="tab-indicator-accent"></div> 149 </div> 150 {/if} 151 </div> 152 <div class="clear-button-container"> 153 {#if (queryResult || queryError) && !isQuerying} 154 <button 155 class="clear-button" 156 onclick={clearResult} 157 > 158 <span>CLEAR</span> 159 </button> 160 {/if} 161 </div> 162 </div> 163 <div class="output-display" class:querying={isQuerying}> 164 <div class="output-content"> 165 {#if isQuerying} 166 <div class="loading-indicator"> 167 <span class="loading-text" 168 >PROCESSING REQUEST</span 169 > 170 <div class="loading-bar"></div> 171 </div> 172 {:else if queryError} 173 <div class="result-text error"> 174 {queryError} 175 </div> 176 {:else if queryResult} 177 <div class="result-text"> 178 <div>HANDLE: {queryResult.handle}</div> 179 <div> 180 FRONTER(S): 181 {#each queryResult.fronters as fronter, i} 182 {#if fronter.uri} 183 <a 184 href={fronter.uri} 185 class="fronter-link" 186 >{fronter.name}</a 187 > 188 {:else} 189 {fronter.name + 190 (i < 191 queryResult.fronters 192 .length - 193 1 194 ? ", " 195 : "")} 196 {/if} 197 {/each} 198 </div> 199 </div> 200 {:else} 201 <div class="placeholder-text"> 202 AWAITING INPUT 203 </div> 204 {/if} 205 </div> 206 </div> 207 </div> 208 </section> 209 210 <section class="config-panel"> 211 <div class="panel-header"> 212 <span class="panel-title">CONFIGURATION</span> 213 <div class="panel-accent"></div> 214 </div> 215 <div class="config-card"> 216 <div class="config-row"> 217 <span class="config-label">SP TOKEN</span> 218 <input 219 type="password" 220 placeholder="enter_simply_plural_token" 221 oninput={updateSpToken} 222 bind:value={spToken} 223 class="config-input" 224 class:has-value={spToken} 225 /> 226 </div> 227 <div class="config-note"> 228 <span class="note-text"> 229 when set, pulls fronters from Simply Plural (token 230 only requires read permissions) 231 </span> 232 </div> 233 </div> 234 <div class="config-card"> 235 <div class="config-row"> 236 <span class="config-label">PK SYSTEM</span> 237 <input 238 type="password" 239 placeholder="enter_pk_system_id" 240 oninput={updatePkSystem} 241 bind:value={pkSystemId} 242 class="config-input" 243 class:has-value={pkSystemId} 244 /> 245 </div> 246 <div class="config-note"> 247 <span class="note-text"> 248 when set, pulls fronters from PluralKit (fronters 249 must be public) 250 </span> 251 </div> 252 </div> 253 <FronterList 254 bind:fronters 255 onUpdate={updateFronters} 256 label="FRONTERS" 257 placeholder="enter_fronter_names" 258 note="just names, overrides SP & PK fronters" 259 /> 260 </section> 261 </div> 262 263 <footer class="footer"> 264 <span class="title">AT_FRONTER</span> 265 <span class="footer-separator"></span> 266 <span class="footer-source">SOURCE ON </span> 267 <a 268 href="https://tangled.sh/did:plc:dfl62fgb7wtjj3fcbb72naae/at-fronter" 269 class="footer-link">TANGLED</a 270 > 271 </footer> 272 </div> 273</main> 274 275<style> 276 main { 277 width: 480px; 278 height: 600px; 279 background: #000000; 280 color: #ffffff; 281 font-family: 282 "JetBrains Mono", "SF Mono", "Monaco", "Cascadia Code", 283 "Roboto Mono", monospace; 284 font-size: 13px; 285 position: relative; 286 overflow: hidden; 287 border: 1px solid #2a2a2a; 288 } 289 290 .container { 291 height: 100%; 292 display: flex; 293 flex-direction: column; 294 background: linear-gradient(180deg, #000000 0%, #0a0a0a 100%); 295 } 296 297 .title { 298 font-size: 10px; 299 font-weight: 700; 300 letter-spacing: 2px; 301 color: #999999; 302 line-height: 1; 303 vertical-align: baseline; 304 } 305 306 .content { 307 flex: 1; 308 display: flex; 309 flex-direction: column; 310 gap: 20px; 311 padding: 18px 16px; 312 overflow-y: auto; 313 } 314 315 .query-panel { 316 display: flex; 317 flex-direction: column; 318 gap: 16px; 319 } 320 321 .config-panel { 322 display: flex; 323 flex-direction: column; 324 gap: 12px; 325 } 326 327 .config-card { 328 background: #0d0d0d; 329 border: 1px solid #2a2a2a; 330 border-left: 3px solid #444444; 331 padding: 10px; 332 display: flex; 333 flex-direction: column; 334 gap: 6px; 335 transition: border-left-color 0.2s ease; 336 } 337 338 .config-card:hover { 339 border-left-color: #555555; 340 } 341 342 .config-note { 343 padding: 0; 344 background: transparent; 345 border: none; 346 margin: 0; 347 } 348 349 .note-text { 350 font-size: 11px; 351 color: #bbbbbb; 352 line-height: 1.3; 353 font-weight: 500; 354 letter-spacing: 0.5px; 355 } 356 357 .panel-header { 358 display: flex; 359 align-items: center; 360 gap: 12px; 361 margin-bottom: 8px; 362 } 363 364 .panel-title { 365 font-size: 12px; 366 font-weight: 700; 367 letter-spacing: 2px; 368 color: #e0e0e0; 369 } 370 371 .panel-accent { 372 flex: 1; 373 height: 1px; 374 background: linear-gradient(90deg, #555555, transparent); 375 } 376 377 .input-container { 378 margin-bottom: 8px; 379 } 380 381 .input-wrapper { 382 display: flex; 383 background: #181818; 384 border: 1px solid #333333; 385 transition: border-color 0.2s ease; 386 } 387 388 .input-wrapper:focus-within { 389 border-color: #666666; 390 } 391 392 .record-input { 393 flex: 1; 394 padding: 12px 14px; 395 background: transparent; 396 border: none; 397 outline: none; 398 color: #ffffff; 399 font-family: inherit; 400 font-size: 13px; 401 font-weight: 500; 402 } 403 404 .record-input::placeholder { 405 color: #777777; 406 font-size: 12px; 407 } 408 409 .record-input:disabled { 410 color: #666666; 411 } 412 413 .exec-button { 414 position: relative; 415 padding: 8px 10px; 416 background: #2a2a2a; 417 border: none; 418 border-left: 1px solid #444444; 419 color: #ffffff; 420 font-family: inherit; 421 font-size: 12px; 422 font-weight: 700; 423 letter-spacing: 1.5px; 424 cursor: pointer; 425 transition: all 0.15s ease; 426 overflow: hidden; 427 } 428 429 .exec-button:hover:not(:disabled) { 430 background: #3a3a3a; 431 } 432 433 .exec-button:active:not(:disabled) { 434 background: #444444; 435 } 436 437 .exec-button:disabled { 438 color: #555555; 439 cursor: not-allowed; 440 } 441 442 .button-text { 443 position: relative; 444 z-index: 1; 445 } 446 447 .button-accent { 448 position: absolute; 449 bottom: 0; 450 left: 0; 451 width: 100%; 452 height: 2px; 453 background: #00ff41; 454 transform: scaleX(0); 455 transition: transform 0.2s ease; 456 } 457 458 .exec-button:hover:not(:disabled) .button-accent { 459 transform: scaleX(1); 460 } 461 462 .output-container { 463 display: flex; 464 flex-direction: column; 465 gap: 8px; 466 } 467 468 .output-header { 469 display: flex; 470 align-items: center; 471 justify-content: space-between; 472 font-size: 11px; 473 color: #aaaaaa; 474 font-weight: 600; 475 letter-spacing: 1px; 476 height: 32px; 477 min-height: 32px; 478 } 479 480 .output-header-left { 481 display: flex; 482 align-items: center; 483 gap: 12px; 484 } 485 486 .tab-indicator { 487 display: flex; 488 align-items: center; 489 gap: 6px; 490 padding: 4px 8px; 491 background: #1a1a1a; 492 border: 1px solid #333333; 493 position: relative; 494 overflow: hidden; 495 } 496 497 .tab-indicator-text { 498 font-size: 9px; 499 color: #00ff41; 500 font-weight: 700; 501 letter-spacing: 1px; 502 position: relative; 503 z-index: 1; 504 } 505 506 .tab-indicator-accent { 507 position: absolute; 508 left: 0; 509 bottom: 0; 510 width: 100%; 511 height: 1px; 512 background: #00ff41; 513 animation: pulse 2s ease-in-out infinite; 514 } 515 516 .clear-button-container { 517 width: 60px; 518 display: flex; 519 justify-content: flex-end; 520 } 521 522 .clear-button { 523 background: none; 524 border: 1px solid #444444; 525 color: #aaaaaa; 526 font-family: inherit; 527 font-size: 10px; 528 font-weight: 700; 529 letter-spacing: 1px; 530 padding: 6px 10px; 531 cursor: pointer; 532 transition: all 0.15s ease; 533 } 534 535 .clear-button:hover { 536 border-color: #666666; 537 color: #ffffff; 538 background: #222222; 539 } 540 541 .output-display { 542 background: #111111; 543 border: 1px solid #333333; 544 border-left: 3px solid #555555; 545 min-height: 120px; 546 position: relative; 547 transition: border-left-color 0.2s ease; 548 } 549 550 .output-display.querying { 551 border-left-color: #00ff41; 552 } 553 554 .output-content { 555 padding: 14px; 556 height: 100%; 557 display: flex; 558 align-items: center; 559 } 560 561 .loading-indicator { 562 width: 100%; 563 display: flex; 564 flex-direction: column; 565 gap: 12px; 566 } 567 568 .loading-bar { 569 width: 100%; 570 height: 2px; 571 background: #333333; 572 overflow: hidden; 573 position: relative; 574 } 575 576 .loading-bar::after { 577 content: ""; 578 position: absolute; 579 left: -100%; 580 width: 100%; 581 height: 100%; 582 background: linear-gradient(90deg, transparent, #00ff41, transparent); 583 animation: loading 1.5s ease-in-out infinite; 584 } 585 586 .loading-text { 587 font-size: 12px; 588 color: #00ff41; 589 letter-spacing: 1.5px; 590 font-weight: 700; 591 } 592 593 .result-text { 594 color: #ffffff; 595 font-size: 14px; 596 font-weight: 600; 597 word-break: break-all; 598 line-height: 1.5; 599 } 600 601 .result-text.error { 602 color: #ff4444; 603 } 604 605 .fronter-link { 606 color: #00ff41; 607 text-decoration: none; 608 font-weight: 700; 609 transition: all 0.2s ease; 610 position: relative; 611 border-bottom: 1px solid transparent; 612 } 613 614 .fronter-link:hover { 615 color: #33ff66; 616 border-bottom-color: #00ff41; 617 } 618 619 .fronter-link:active { 620 color: #ffffff; 621 } 622 623 .placeholder-text { 624 color: #888888; 625 font-size: 12px; 626 letter-spacing: 1px; 627 font-style: italic; 628 font-weight: 500; 629 } 630 631 .config-row { 632 display: flex; 633 align-items: center; 634 gap: 12px; 635 margin-bottom: 0; 636 } 637 638 .config-label { 639 font-size: 12px; 640 color: #cccccc; 641 letter-spacing: 1px; 642 font-weight: 700; 643 white-space: nowrap; 644 min-width: 90px; 645 } 646 647 .config-input { 648 flex: 1; 649 padding: 10px 12px; 650 background: #181818; 651 border: 1px solid #333333; 652 color: #ffffff; 653 font-family: inherit; 654 font-size: 12px; 655 font-weight: 500; 656 transition: all 0.2s ease; 657 position: relative; 658 } 659 660 .config-input:focus { 661 outline: none; 662 border-color: #666666; 663 } 664 665 .config-input.has-value { 666 border-bottom-color: #00ff41; 667 } 668 669 .config-input::placeholder { 670 color: #777777; 671 font-size: 12px; 672 } 673 674 .footer { 675 display: flex; 676 align-items: baseline; 677 justify-content: center; 678 gap: 8px; 679 padding: 12px 16px; 680 background: #000000; 681 border-top: 1px solid #222222; 682 font-size: 9px; 683 color: #666666; 684 font-weight: 500; 685 letter-spacing: 0.5px; 686 line-height: 1; 687 position: relative; 688 } 689 690 .footer::before { 691 content: ""; 692 position: absolute; 693 top: 0; 694 left: 0; 695 width: 100%; 696 height: 1px; 697 background: linear-gradient(90deg, transparent, #333333, transparent); 698 } 699 700 .footer-separator { 701 color: #444444; 702 font-weight: 400; 703 line-height: 1; 704 vertical-align: baseline; 705 } 706 707 .footer-source { 708 color: #777777; 709 line-height: 1; 710 vertical-align: baseline; 711 } 712 713 .footer-link { 714 color: #999999; 715 text-decoration: none; 716 font-weight: 700; 717 transition: color 0.2s ease; 718 line-height: 1; 719 vertical-align: baseline; 720 } 721 722 .footer-link:hover { 723 color: #cccccc; 724 } 725 726 /* Animations */ 727 @keyframes pulse { 728 0%, 729 100% { 730 opacity: 1; 731 } 732 50% { 733 opacity: 0.3; 734 } 735 } 736 737 @keyframes loading { 738 0% { 739 left: -100%; 740 } 741 100% { 742 left: 100%; 743 } 744 } 745</style>