+370
-5
index.html
+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
lexicons.zip
This is a binary file and will not be displayed.