The Go90 Scale of Doomed Streaming Services

initial function

Changed files
+407 -5
lexicons
social
+370 -5
index.html
··· 218 218 .hidden { 219 219 display: none !important; 220 220 } 221 + 222 + .rating-form { 223 + display: flex; 224 + flex-direction: column; 225 + gap: 1rem; 226 + } 227 + 228 + .rating-slider-container { 229 + display: flex; 230 + flex-direction: column; 231 + gap: 0.5rem; 232 + } 233 + 234 + .rating-display { 235 + text-align: center; 236 + font-size: 3rem; 237 + font-weight: 700; 238 + color: var(--primary-500); 239 + margin: 0.5rem 0; 240 + } 241 + 242 + .rating-display.defunct { 243 + color: var(--error-text); 244 + } 245 + 246 + .rating-slider { 247 + width: 100%; 248 + height: 8px; 249 + -webkit-appearance: none; 250 + appearance: none; 251 + background: linear-gradient(to right, #32cd32 0%, #ffd700 50%, #ff5722 100%); 252 + border-radius: 4px; 253 + outline: none; 254 + } 255 + 256 + .rating-slider::-webkit-slider-thumb { 257 + -webkit-appearance: none; 258 + appearance: none; 259 + width: 24px; 260 + height: 24px; 261 + background: white; 262 + border: 2px solid var(--primary-500); 263 + border-radius: 50%; 264 + cursor: pointer; 265 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 266 + } 267 + 268 + .rating-slider::-moz-range-thumb { 269 + width: 24px; 270 + height: 24px; 271 + background: white; 272 + border: 2px solid var(--primary-500); 273 + border-radius: 50%; 274 + cursor: pointer; 275 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 276 + } 277 + 278 + .rating-labels { 279 + display: flex; 280 + justify-content: space-between; 281 + font-size: 0.875rem; 282 + color: var(--gray-500); 283 + } 284 + 285 + textarea { 286 + padding: 0.75rem; 287 + border: 1px solid var(--border-color); 288 + border-radius: 0.375rem; 289 + font-size: 1rem; 290 + resize: vertical; 291 + min-height: 80px; 292 + font-family: inherit; 293 + } 294 + 295 + textarea:focus { 296 + outline: none; 297 + border-color: var(--primary-500); 298 + box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.1); 299 + } 300 + 301 + .char-count { 302 + text-align: right; 303 + font-size: 0.75rem; 304 + color: var(--gray-500); 305 + } 306 + 307 + .ratings-list { 308 + display: flex; 309 + flex-direction: column; 310 + gap: 1rem; 311 + } 312 + 313 + .rating-item { 314 + border-bottom: 1px solid var(--border-color); 315 + padding-bottom: 1rem; 316 + } 317 + 318 + .rating-item:last-child { 319 + border-bottom: none; 320 + padding-bottom: 0; 321 + } 322 + 323 + .rating-header { 324 + display: flex; 325 + align-items: center; 326 + gap: 0.75rem; 327 + margin-bottom: 0.5rem; 328 + } 329 + 330 + .service-favicon { 331 + width: 24px; 332 + height: 24px; 333 + border-radius: 4px; 334 + } 335 + 336 + .service-name { 337 + font-weight: 600; 338 + color: var(--gray-900); 339 + flex: 1; 340 + } 341 + 342 + .rating-value { 343 + font-size: 1.25rem; 344 + font-weight: 700; 345 + color: var(--primary-500); 346 + } 347 + 348 + .rating-value.defunct { 349 + color: var(--error-text); 350 + } 351 + 352 + .rating-meta { 353 + font-size: 0.875rem; 354 + color: var(--gray-500); 355 + margin-bottom: 0.5rem; 356 + } 357 + 358 + .rating-comment { 359 + color: var(--gray-700); 360 + font-size: 0.875rem; 361 + } 362 + 363 + .empty-state { 364 + text-align: center; 365 + padding: 2rem; 366 + color: var(--gray-500); 367 + } 368 + 369 + .section-title { 370 + font-size: 1.25rem; 371 + font-weight: 600; 372 + margin-bottom: 0.5rem; 373 + color: var(--gray-900); 374 + } 375 + 376 + .btn-block { 377 + width: 100%; 378 + } 221 379 </style> 222 380 </head> 223 381 <body> ··· 230 388 <ellipse cx="0" cy="28" rx="40" ry="20" fill="#32CD32" /> 231 389 </g> 232 390 </svg> 233 - <h1>Slice Kit</h1> 234 - <p class="tagline">Build your slice of Atmosphere</p> 391 + <h1>Go90 Scale</h1> 392 + <p class="tagline">Rate streaming services on the Go90 scale</p> 235 393 </header> 236 394 <main> 237 395 <div id="auth-section"></div> ··· 251 409 // ============================================================================= 252 410 253 411 const SERVER_URL = "http://127.0.0.1:8080"; 254 - const CLIENT_ID = ""; // Set your OAuth client ID here after registering 412 + const CLIENT_ID = "client_Wpu1V7VdSi1eKI01JTsKOg"; // Set your OAuth client ID here after registering 255 413 256 414 let client; 415 + let serviceMetadataCache = {}; 257 416 258 417 // ============================================================================= 259 418 // INITIALIZATION ··· 342 501 return data?.viewer; 343 502 } 344 503 504 + async function fetchRatings() { 505 + const query = ` 506 + query { 507 + socialGo90Ratings(orderBy: CREATED_AT_DESC, first: 50) { 508 + nodes { 509 + id 510 + serviceDomain 511 + rating 512 + comment 513 + createdAt 514 + author { 515 + handle 516 + appBskyActorProfileByDid { 517 + displayName 518 + } 519 + } 520 + } 521 + } 522 + } 523 + `; 524 + 525 + const data = await client.query(query); 526 + return data?.socialGo90Ratings?.nodes || []; 527 + } 528 + 529 + async function fetchServiceMetadata(domain) { 530 + if (serviceMetadataCache[domain]) { 531 + return serviceMetadataCache[domain]; 532 + } 533 + 534 + const metadata = { 535 + name: domain, 536 + favicon: `https://www.google.com/s2/favicons?domain=${domain}&sz=128`, 537 + }; 538 + 539 + serviceMetadataCache[domain] = metadata; 540 + return metadata; 541 + } 542 + 345 543 // ============================================================================= 346 544 // EVENT HANDLERS 347 545 // ============================================================================= ··· 376 574 } 377 575 } 378 576 577 + async function handleRatingSubmit(event) { 578 + event.preventDefault(); 579 + 580 + const serviceDomain = document.getElementById("serviceDomain").value.trim(); 581 + const rating = parseInt(document.getElementById("rating").value); 582 + const comment = document.getElementById("comment").value.trim(); 583 + 584 + if (!serviceDomain) { 585 + showError("Please enter a service domain"); 586 + return; 587 + } 588 + 589 + // Basic domain validation 590 + if (!serviceDomain.includes(".") || serviceDomain.includes("/")) { 591 + showError("Please enter a valid domain (e.g., netflix.com)"); 592 + return; 593 + } 594 + 595 + try { 596 + const mutation = ` 597 + mutation CreateRating($input: CreateSocialGo90RatingInput!) { 598 + createSocialGo90Rating(input: $input) 599 + } 600 + `; 601 + 602 + const variables = { 603 + input: { 604 + serviceDomain, 605 + rating, 606 + comment: comment || undefined, 607 + createdAt: new Date().toISOString(), 608 + }, 609 + }; 610 + 611 + await client.query(mutation, variables); 612 + 613 + // Clear form 614 + document.getElementById("serviceDomain").value = ""; 615 + document.getElementById("rating").value = "45"; 616 + document.getElementById("comment").value = ""; 617 + updateRatingDisplay(45); 618 + 619 + // Refresh ratings list 620 + const viewer = await fetchViewer(); 621 + await renderContent(viewer); 622 + } catch (error) { 623 + console.error("Failed to submit rating:", error); 624 + showError(`Failed to submit rating: ${error.message}`); 625 + } 626 + } 627 + 628 + function updateRatingDisplay(value) { 629 + const display = document.getElementById("ratingDisplay"); 630 + const isDefunct = value === 90; 631 + display.textContent = value; 632 + display.className = isDefunct ? "rating-display defunct" : "rating-display"; 633 + } 634 + 635 + function updateCharCount() { 636 + const comment = document.getElementById("comment").value; 637 + const count = document.getElementById("charCount"); 638 + count.textContent = `${comment.length}/300`; 639 + } 640 + 379 641 // ============================================================================= 380 642 // UI RENDERING 381 643 // ============================================================================= ··· 456 718 `; 457 719 } 458 720 459 - function renderContent(viewer) { 721 + async function renderContent(viewer) { 460 722 const container = document.getElementById("content"); 723 + 724 + // Render rating form 461 725 container.innerHTML = ` 462 726 <div class="card"> 463 - <p style="color: var(--gray-700);">You're logged in! #getsliced</p> 727 + <h2 class="section-title">Rate a Streaming Service</h2> 728 + <form class="rating-form" onsubmit="handleRatingSubmit(event)"> 729 + <div class="form-group"> 730 + <label for="serviceDomain">Service Domain</label> 731 + <input 732 + type="text" 733 + id="serviceDomain" 734 + placeholder="netflix.com" 735 + required 736 + /> 737 + </div> 738 + 739 + <div class="form-group"> 740 + <label for="rating">Rating</label> 741 + <div class="rating-slider-container"> 742 + <div id="ratingDisplay" class="rating-display">45</div> 743 + <input 744 + type="range" 745 + id="rating" 746 + class="rating-slider" 747 + min="0" 748 + max="90" 749 + value="45" 750 + oninput="updateRatingDisplay(this.value)" 751 + /> 752 + <div class="rating-labels"> 753 + <span>0 (Thriving)</span> 754 + <span>90 (Defunct)</span> 755 + </div> 756 + </div> 757 + </div> 758 + 759 + <div class="form-group"> 760 + <label for="comment">Comment (optional)</label> 761 + <textarea 762 + id="comment" 763 + placeholder="Share your thoughts..." 764 + maxlength="300" 765 + oninput="updateCharCount()" 766 + ></textarea> 767 + <div id="charCount" class="char-count">0/300</div> 768 + </div> 769 + 770 + <button type="submit" class="btn btn-primary btn-block">Submit Rating</button> 771 + </form> 772 + </div> 773 + 774 + <div class="card"> 775 + <h2 class="section-title">Recent Ratings</h2> 776 + <div id="ratingsList"></div> 464 777 </div> 465 778 `; 779 + 780 + // Fetch and render ratings 781 + try { 782 + const ratings = await fetchRatings(); 783 + await renderRatingsList(ratings); 784 + } catch (error) { 785 + console.error("Failed to fetch ratings:", error); 786 + document.getElementById("ratingsList").innerHTML = ` 787 + <div class="empty-state">Failed to load ratings</div> 788 + `; 789 + } 790 + } 791 + 792 + async function renderRatingsList(ratings) { 793 + const container = document.getElementById("ratingsList"); 794 + 795 + if (!ratings || ratings.length === 0) { 796 + container.innerHTML = ` 797 + <div class="empty-state">No ratings yet. Be the first to rate a service!</div> 798 + `; 799 + return; 800 + } 801 + 802 + let html = '<div class="ratings-list">'; 803 + 804 + for (const rating of ratings) { 805 + const metadata = await fetchServiceMetadata(rating.serviceDomain); 806 + const displayName = 807 + rating.author?.appBskyActorProfileByDid?.displayName || 808 + rating.author?.handle || 809 + "Anonymous"; 810 + const isDefunct = rating.rating === 90; 811 + const ratingClass = isDefunct ? "rating-value defunct" : "rating-value"; 812 + const date = new Date(rating.createdAt).toLocaleDateString(); 813 + 814 + html += ` 815 + <div class="rating-item"> 816 + <div class="rating-header"> 817 + <img src="${escapeHtml(metadata.favicon)}" alt="" class="service-favicon" onerror="this.style.display='none'" /> 818 + <span class="service-name">${escapeHtml(metadata.name)}</span> 819 + <span class="${ratingClass}">${rating.rating}</span> 820 + </div> 821 + <div class="rating-meta"> 822 + Rated by ${escapeHtml(displayName)} (@${escapeHtml(rating.author?.handle)}) on ${date} 823 + </div> 824 + ${rating.comment ? `<div class="rating-comment">${escapeHtml(rating.comment)}</div>` : ""} 825 + </div> 826 + `; 827 + } 828 + 829 + html += "</div>"; 830 + container.innerHTML = html; 466 831 } 467 832 468 833 main();
lexicons.zip

This is a binary file and will not be displayed.

+37
lexicons/social/go90/rating.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.go90.rating", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A rating of a streaming service on the Go90 scale", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["serviceDomain", "rating", "createdAt"], 12 + "properties": { 13 + "serviceDomain": { 14 + "type": "string", 15 + "description": "Domain of the service being rated (e.g., netflix.com, dropout.tv)" 16 + }, 17 + "rating": { 18 + "type": "integer", 19 + "minimum": 0, 20 + "maximum": 90, 21 + "description": "Rating on the Go90 scale: 0 = thriving, 1-89 = at risk, 90 = defunct" 22 + }, 23 + "comment": { 24 + "type": "string", 25 + "maxLength": 300, 26 + "maxGraphemes": 300, 27 + "description": "Optional commentary on the rating" 28 + }, 29 + "createdAt": { 30 + "type": "string", 31 + "format": "datetime" 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }