The Go90 Scale of Doomed Streaming Services

arc shaped scale interface

Changed files
+393 -237
+327 -117
drag-fix.js
··· 1 - // New unified drag system 2 - console.log("Drag fix script loaded"); 1 + // Arc-based drag system for Go90 Scale 2 + console.log("Arc drag system loaded"); 3 + 4 + // Arc configuration 5 + const ARC_CONFIG = { 6 + centerX: 0, // Will be set based on canvas width 7 + centerY: 0, // Will be set based on canvas height 8 + radius: 300, 9 + startAngle: -Math.PI / 2, // -90° (top, 0 rating) 10 + endAngle: 0, // 0° (right, 90 rating) 11 + baseDistance: 50, // Base distance from arc for service icons 12 + crowdingOffset: 120, // Distance to push out for each overlapping service 13 + iconSize: 80, 14 + }; 15 + 16 + let canvas, ctx, canvasContainer; 17 + let servicePositions = {}; // Store { domain: { rating, angle, distance } } 18 + let currentDrag = null; 3 19 4 20 function initDragAndDrop() { 5 - console.log("initDragAndDrop called"); 6 - const canvas = document.getElementById("dragCanvas"); 7 - const servicesBar = document.getElementById("servicesBar"); 8 - const serviceItems = document.querySelectorAll(".service-item"); 21 + console.log("initDragAndDrop called - arc mode"); 9 22 10 - console.log("Found service items:", serviceItems.length); 23 + canvasContainer = document.getElementById("dragCanvas"); 24 + canvas = document.getElementById("arcCanvas"); 25 + ctx = canvas.getContext("2d"); 11 26 12 - // Setup all service items for dragging 27 + // Set canvas size 28 + resizeCanvas(); 29 + window.addEventListener("resize", resizeCanvas); 30 + 31 + // Draw the arc scale 32 + drawArcScale(); 33 + 34 + // Setup service items for dragging 35 + const serviceItems = document.querySelectorAll(".service-item"); 13 36 serviceItems.forEach(setupServiceDrag); 37 + } 14 38 15 - // Make canvas accept drops 16 - canvas.addEventListener("dragover", (e) => e.preventDefault()); 17 - canvas.addEventListener("drop", handleCanvasDrop); 39 + function resizeCanvas() { 40 + const rect = canvasContainer.getBoundingClientRect(); 41 + canvas.width = rect.width; 42 + canvas.height = rect.height; 43 + 44 + // Set arc center 45 + ARC_CONFIG.centerX = canvas.width / 2; 46 + ARC_CONFIG.centerY = canvas.height - 100; 47 + 48 + drawArcScale(); 49 + redrawServices(); 18 50 } 19 51 20 - let currentDrag = null; 52 + function drawArcScale() { 53 + if (!ctx) return; 54 + 55 + ctx.clearRect(0, 0, canvas.width, canvas.height); 56 + 57 + const { centerX, centerY, radius, startAngle, endAngle } = ARC_CONFIG; 58 + 59 + // Draw gradient arc 60 + const gradient = ctx.createLinearGradient( 61 + centerX, 62 + centerY + radius * Math.sin(startAngle), 63 + centerX + radius * Math.cos(endAngle), 64 + centerY + radius * Math.sin(endAngle), 65 + ); 66 + 67 + gradient.addColorStop(0, "#32cd32"); 68 + gradient.addColorStop(0.2, "#adff2f"); 69 + gradient.addColorStop(0.4, "#ffff00"); 70 + gradient.addColorStop(0.6, "#ffa500"); 71 + gradient.addColorStop(0.8, "#ff4500"); 72 + gradient.addColorStop(1, "#8b0000"); 73 + 74 + ctx.strokeStyle = gradient; 75 + ctx.lineWidth = 6; 76 + ctx.beginPath(); 77 + ctx.arc(centerX, centerY, radius, startAngle, endAngle); 78 + ctx.stroke(); 79 + 80 + // Draw tick marks and labels 81 + const totalRatings = 91; // 0 to 90 82 + for (let rating = 0; rating <= 90; rating += 10) { 83 + const angle = ratingToAngle(rating); 84 + const tickLength = rating % 10 === 0 ? 20 : 10; 85 + 86 + // Outer point on arc 87 + const x1 = centerX + radius * Math.cos(angle); 88 + const y1 = centerY + radius * Math.sin(angle); 89 + 90 + // Inner point (tick mark) 91 + const x2 = centerX + (radius - tickLength) * Math.cos(angle); 92 + const y2 = centerY + (radius - tickLength) * Math.sin(angle); 93 + 94 + ctx.strokeStyle = "white"; 95 + ctx.lineWidth = 3; 96 + ctx.beginPath(); 97 + ctx.moveTo(x1, y1); 98 + ctx.lineTo(x2, y2); 99 + ctx.stroke(); 100 + 101 + // Draw rating label every 10 102 + if (rating % 10 === 0) { 103 + const labelDistance = radius - 50; 104 + const labelX = centerX + labelDistance * Math.cos(angle); 105 + const labelY = centerY + labelDistance * Math.sin(angle); 106 + 107 + ctx.fillStyle = rating === 90 ? "#ff5555" : "white"; 108 + ctx.font = "bold 32px 'Innovator Grotesk', sans-serif"; 109 + ctx.textAlign = "center"; 110 + ctx.textBaseline = "middle"; 111 + ctx.fillText(rating.toString(), labelX, labelY); 112 + } 113 + } 114 + 115 + // Draw indicator lines for placed services 116 + Object.entries(servicePositions).forEach(([domain, data]) => { 117 + drawIndicatorLine(data.angle, data.distance); 118 + }); 119 + } 120 + 121 + function drawIndicatorLine(angle, distance) { 122 + const { centerX, centerY, radius } = ARC_CONFIG; 123 + 124 + // Point on arc 125 + const arcX = centerX + radius * Math.cos(angle); 126 + const arcY = centerY + radius * Math.sin(angle); 127 + 128 + // Point at service icon 129 + const iconX = centerX + distance * Math.cos(angle); 130 + const iconY = centerY + distance * Math.sin(angle); 131 + 132 + ctx.strokeStyle = "white"; 133 + ctx.lineWidth = 2; 134 + ctx.setLineDash([5, 5]); 135 + ctx.beginPath(); 136 + ctx.moveTo(arcX, arcY); 137 + ctx.lineTo(iconX, iconY); 138 + ctx.stroke(); 139 + ctx.setLineDash([]); 140 + } 141 + 142 + function ratingToAngle(rating) { 143 + const { startAngle, endAngle } = ARC_CONFIG; 144 + const t = rating / 90; 145 + return startAngle + t * (endAngle - startAngle); 146 + } 147 + 148 + function angleToRating(angle) { 149 + const { startAngle, endAngle } = ARC_CONFIG; 150 + const t = (angle - startAngle) / (endAngle - startAngle); 151 + return Math.max(0, Math.min(90, Math.round(t * 90))); 152 + } 21 153 22 154 function setupServiceDrag(element) { 23 155 element.style.cursor = "grab"; ··· 25 157 } 26 158 27 159 function startDrag(e) { 28 - const serviceItem = e.target.closest(".service-item"); 160 + const serviceItem = e.target.closest(".service-item, .service-on-canvas"); 29 161 if (!serviceItem) return; 30 162 31 163 e.preventDefault(); 32 164 33 - const clone = serviceItem.cloneNode(true); 34 - clone.style.position = "fixed"; 35 - clone.style.pointerEvents = "none"; 36 - clone.style.zIndex = "10000"; 37 - clone.style.width = "80px"; 38 - clone.style.height = "80px"; 165 + const domain = serviceItem.dataset.domain || serviceItem.dataset.canvasDomain; 166 + const name = serviceItem.dataset.name; 39 167 40 168 currentDrag = { 169 + domain, 170 + name, 41 171 element: serviceItem, 42 - clone: clone, 43 - domain: serviceItem.dataset.domain, 44 - name: serviceItem.dataset.name, 45 - startX: e.clientX, 46 - startY: e.clientY, 172 + isFromCanvas: serviceItem.classList.contains("service-on-canvas"), 47 173 }; 48 174 49 - document.body.appendChild(clone); 50 - updateClonePosition(e); 51 - 52 - serviceItem.style.opacity = "0.3"; 53 - 175 + serviceItem.style.opacity = "0.5"; 54 176 document.addEventListener("mousemove", onDragMove); 55 177 document.addEventListener("mouseup", onDragEnd); 56 178 } 57 179 58 180 function onDragMove(e) { 59 181 if (!currentDrag) return; 60 - updateClonePosition(e); 61 - } 182 + 183 + // Update visual feedback based on mouse position relative to arc 184 + const canvasRect = canvas.getBoundingClientRect(); 185 + const mouseX = e.clientX - canvasRect.left; 186 + const mouseY = e.clientY - canvasRect.top; 187 + 188 + // Calculate angle from center to mouse 189 + const dx = mouseX - ARC_CONFIG.centerX; 190 + const dy = mouseY - ARC_CONFIG.centerY; 191 + const angle = Math.atan2(dy, dx); 192 + 193 + // Constrain to arc range 194 + const { startAngle, endAngle } = ARC_CONFIG; 195 + const constrainedAngle = Math.max(startAngle, Math.min(endAngle, angle)); 196 + 197 + // Show preview by updating element position if it's on canvas 198 + if (currentDrag.isFromCanvas) { 199 + const distance = calculateDistance(currentDrag.domain, constrainedAngle); 200 + const x = ARC_CONFIG.centerX + distance * Math.cos(constrainedAngle); 201 + const y = ARC_CONFIG.centerY + distance * Math.sin(constrainedAngle); 62 202 63 - function updateClonePosition(e) { 64 - if (!currentDrag) return; 65 - currentDrag.clone.style.left = e.clientX - 40 + "px"; 66 - currentDrag.clone.style.top = e.clientY - 40 + "px"; 203 + currentDrag.element.style.left = x - ARC_CONFIG.iconSize / 2 + "px"; 204 + currentDrag.element.style.top = y - ARC_CONFIG.iconSize / 2 + "px"; 205 + } 67 206 } 68 207 69 208 function onDragEnd(e) { ··· 72 211 document.removeEventListener("mousemove", onDragMove); 73 212 document.removeEventListener("mouseup", onDragEnd); 74 213 75 - const domain = currentDrag.domain; 76 - const name = currentDrag.name; 214 + const { domain, name, element, isFromCanvas } = currentDrag; 77 215 78 - // Check where it was dropped 79 - const canvas = document.getElementById("dragCanvas"); 216 + // Check if dropped on services bar (to remove rating) 80 217 const servicesBar = document.getElementById("servicesBar"); 81 - const scaleBar = document.getElementById("scaleBar"); 82 - 83 218 const servicesBarRect = servicesBar.getBoundingClientRect(); 84 - const canvasRect = canvas.getBoundingClientRect(); 85 - const scaleBarRect = scaleBar.getBoundingClientRect(); 86 - 87 - const dropX = e.clientX; 88 - const dropY = e.clientY; 89 219 90 - // Clean up 91 - currentDrag.clone.remove(); 92 - currentDrag.element.style.opacity = "1"; 93 - 94 - // Check if dropped on services bar 95 220 if ( 96 - dropY >= servicesBarRect.top && 97 - dropY <= servicesBarRect.bottom && 98 - dropX >= servicesBarRect.left && 99 - dropX <= servicesBarRect.right 221 + e.clientY >= servicesBarRect.top && 222 + e.clientY <= servicesBarRect.bottom && 223 + e.clientX >= servicesBarRect.left && 224 + e.clientX <= servicesBarRect.right 100 225 ) { 101 - // Return to bar - remove rating 102 - removeServiceFromCanvas(domain); 103 - delete pendingRatings[domain]; 104 - updateSaveButton(); 226 + // Remove from canvas and add back to bar 227 + removeServiceFromCanvas(domain, name); 228 + element.style.opacity = "1"; 105 229 currentDrag = null; 106 230 return; 107 231 } 108 232 109 - // Calculate rating from horizontal position relative to scale bar 110 - const scaleX = dropX - scaleBarRect.left; 111 - const percentage = Math.max(0, Math.min(100, (scaleX / scaleBarRect.width) * 100)); 112 - const rating = Math.round((percentage / 100) * 90); 233 + // Calculate position on arc 234 + const canvasRect = canvas.getBoundingClientRect(); 235 + const mouseX = e.clientX - canvasRect.left; 236 + const mouseY = e.clientY - canvasRect.top; 113 237 114 - // Position on canvas 115 - const canvasX = dropX - canvasRect.left; 116 - const canvasY = dropY - canvasRect.top; 238 + const dx = mouseX - ARC_CONFIG.centerX; 239 + const dy = mouseY - ARC_CONFIG.centerY; 240 + const angle = Math.atan2(dy, dx); 117 241 118 - // Store pending rating 119 - pendingRatings[domain] = { 120 - domain, 121 - name, 122 - rating, 123 - x: canvasX, 124 - y: canvasY, 125 - }; 242 + // Constrain to arc range 243 + const { startAngle, endAngle } = ARC_CONFIG; 244 + const constrainedAngle = Math.max(startAngle, Math.min(endAngle, angle)); 245 + 246 + const rating = angleToRating(constrainedAngle); 126 247 127 248 // Place on canvas 128 - placeServiceOnCanvas(domain, name, rating, canvasX, canvasY); 129 - updateSaveButton(); 249 + placeServiceOnArc(domain, name, rating); 130 250 251 + element.style.opacity = "1"; 131 252 currentDrag = null; 132 253 } 133 254 134 - function placeServiceOnCanvas(domain, name, rating, x, y) { 135 - const canvas = document.getElementById("dragCanvas"); 136 - const servicesBar = document.getElementById("servicesBar"); 255 + function calculateDistance(domain, angle) { 256 + // Find the furthest "valence ring" occupied within 5 degrees BEFORE this angle 257 + // This prevents recursive offsetting - only look backwards along the arc 258 + // Only services at the current ring level count as crowding 259 + const angularTolerance = 10 * (Math.PI / 180); // 5 degrees in radians 260 + const baseDistance = ARC_CONFIG.radius + ARC_CONFIG.baseDistance; 261 + 262 + // Find all occupied rings in the angular range 263 + let occupiedRings = new Set(); 264 + Object.entries(servicePositions).forEach(([d, data]) => { 265 + if (d !== domain) { 266 + const angleDiff = angle - data.angle; // Positive if data.angle is before this angle 267 + if (angleDiff >= 0 && angleDiff <= angularTolerance) { 268 + // Found a service in the 5 degrees before 269 + occupiedRings.add(data.distance); 270 + } 271 + } 272 + }); 273 + 274 + // Find the first unoccupied ring 275 + let testDistance = baseDistance; 276 + while (occupiedRings.has(testDistance)) { 277 + testDistance += ARC_CONFIG.crowdingOffset; 278 + } 279 + 280 + return testDistance; 281 + } 282 + 283 + function placeServiceOnArc(domain, name, rating) { 284 + const angle = ratingToAngle(rating); 285 + const distance = calculateDistance(domain, angle); 286 + 287 + // Store position 288 + servicePositions[domain] = { rating, angle, distance }; 137 289 138 - // Remove from services bar if it's there 290 + // Store in pending ratings 291 + if (typeof pendingRatings !== "undefined") { 292 + pendingRatings[domain] = { domain, name, rating }; 293 + } 294 + 295 + // Remove from services bar 296 + const servicesBar = document.getElementById("servicesBar"); 139 297 const inBar = servicesBar.querySelector(`[data-domain="${domain}"]`); 140 - if (inBar) { 141 - inBar.remove(); 142 - } 298 + if (inBar) inBar.remove(); 143 299 144 - // Remove existing placement on canvas 145 - const existing = canvas.querySelector(`[data-canvas-domain="${domain}"]`); 300 + // Remove existing on canvas 301 + const existing = canvasContainer.querySelector(`[data-canvas-domain="${domain}"]`); 146 302 if (existing) existing.remove(); 147 303 304 + // Calculate pixel position 305 + const x = ARC_CONFIG.centerX + distance * Math.cos(angle); 306 + const y = ARC_CONFIG.centerY + distance * Math.sin(angle); 307 + 148 308 // Create element 149 309 const el = document.createElement("div"); 150 - el.className = "service-item on-canvas"; 310 + el.className = "service-on-canvas"; 311 + el.dataset.canvasDomain = domain; 151 312 el.dataset.domain = domain; 152 313 el.dataset.name = name; 153 - el.dataset.canvasDomain = domain; 154 - el.style.position = "absolute"; 155 - el.style.left = x - 40 + "px"; 156 - el.style.top = y - 40 + "px"; 314 + el.style.left = x - ARC_CONFIG.iconSize / 2 + "px"; 315 + el.style.top = y - ARC_CONFIG.iconSize / 2 + "px"; 157 316 el.innerHTML = ` 158 317 <img src="https://www.google.com/s2/favicons?domain=${domain}&sz=128" 159 318 alt="${name}" 160 - class="service-logo" 319 + class="service-icon" 161 320 draggable="false"> 162 321 <div class="service-badge ${rating === 90 ? "defunct" : ""}">${rating}</div> 163 322 `; 164 323 165 324 setupServiceDrag(el); 166 - canvas.appendChild(el); 325 + canvasContainer.appendChild(el); 326 + 327 + // Redraw canvas to show indicator line 328 + drawArcScale(); 329 + 330 + // Reposition any crowded services 331 + repositionCrowdedServices(); 332 + 333 + if (typeof updateSaveButton !== "undefined") { 334 + updateSaveButton(); 335 + } 167 336 } 168 337 169 - function removeServiceFromCanvas(domain) { 170 - const canvas = document.getElementById("dragCanvas"); 338 + function repositionCrowdedServices() { 339 + // Recalculate distances for all services to handle crowding 340 + Object.entries(servicePositions).forEach(([domain, data]) => { 341 + const newDistance = calculateDistance(domain, data.angle); 342 + if (newDistance !== data.distance) { 343 + data.distance = newDistance; 344 + 345 + // Update element position 346 + const el = canvasContainer.querySelector(`[data-canvas-domain="${domain}"]`); 347 + if (el) { 348 + const x = ARC_CONFIG.centerX + newDistance * Math.cos(data.angle); 349 + const y = ARC_CONFIG.centerY + newDistance * Math.sin(data.angle); 350 + el.style.left = x - ARC_CONFIG.iconSize / 2 + "px"; 351 + el.style.top = y - ARC_CONFIG.iconSize / 2 + "px"; 352 + } 353 + } 354 + }); 355 + 356 + drawArcScale(); 357 + } 358 + 359 + function removeServiceFromCanvas(domain, name) { 360 + // Remove from positions 361 + delete servicePositions[domain]; 362 + 363 + // Remove from canvas 364 + const existing = canvasContainer.querySelector(`[data-canvas-domain="${domain}"]`); 365 + if (existing) existing.remove(); 366 + 367 + // Add back to services bar 171 368 const servicesBar = document.getElementById("servicesBar"); 369 + const rating = window.existingRatings ? window.existingRatings[domain] : undefined; 172 370 173 - // Remove from canvas 174 - const existing = canvas.querySelector(`[data-canvas-domain="${domain}"]`); 175 - if (existing) { 176 - const name = existing.dataset.name; 177 - const rating = window.existingRatings ? window.existingRatings[domain] : undefined; 178 - existing.remove(); 371 + const serviceEl = document.createElement("div"); 372 + serviceEl.className = "service-item"; 373 + serviceEl.dataset.domain = domain; 374 + serviceEl.dataset.name = name; 375 + serviceEl.innerHTML = ` 376 + <img src="https://www.google.com/s2/favicons?domain=${domain}&sz=128" 377 + alt="${name}" 378 + class="service-logo" 379 + draggable="false"> 380 + ${rating !== undefined ? `<div class="service-badge ${rating === 90 ? "defunct" : ""}">${rating}</div>` : ""} 381 + `; 382 + setupServiceDrag(serviceEl); 383 + servicesBar.appendChild(serviceEl); 179 384 180 - // Add back to services bar 181 - const serviceEl = document.createElement("div"); 182 - serviceEl.className = "service-item"; 183 - serviceEl.dataset.domain = domain; 184 - serviceEl.dataset.name = name; 185 - serviceEl.innerHTML = ` 186 - <img src="https://www.google.com/s2/favicons?domain=${domain}&sz=128" 187 - alt="${name}" 188 - class="service-logo" 189 - draggable="false"> 190 - ${rating !== undefined ? `<div class="service-badge ${rating === 90 ? "defunct" : ""}">${rating}</div>` : ""} 191 - `; 192 - setupServiceDrag(serviceEl); 193 - servicesBar.appendChild(serviceEl); 385 + // Remove from pending 386 + if (typeof pendingRatings !== "undefined") { 387 + delete pendingRatings[domain]; 388 + } 389 + 390 + // Redraw and reposition 391 + drawArcScale(); 392 + repositionCrowdedServices(); 393 + 394 + if (typeof updateSaveButton !== "undefined") { 395 + updateSaveButton(); 194 396 } 195 397 } 196 398 197 - function handleCanvasDrop(e) { 198 - // This is here for compatibility but we use mouse events instead 199 - e.preventDefault(); 399 + function redrawServices() { 400 + // Redraw all services at their current positions 401 + Object.entries(servicePositions).forEach(([domain, data]) => { 402 + const el = canvasContainer.querySelector(`[data-canvas-domain="${domain}"]`); 403 + if (el) { 404 + const x = ARC_CONFIG.centerX + data.distance * Math.cos(data.angle); 405 + const y = ARC_CONFIG.centerY + data.distance * Math.sin(data.angle); 406 + el.style.left = x - ARC_CONFIG.iconSize / 2 + "px"; 407 + el.style.top = y - ARC_CONFIG.iconSize / 2 + "px"; 408 + } 409 + }); 200 410 }
+66 -120
index.html
··· 399 399 /* Go90 Scale Interface */ 400 400 .scale-container { 401 401 background: var(--go90-blue); 402 - border: 4px solid var(--go90-yellow); 403 - padding: 3rem 2rem; 402 + padding: 2rem; 404 403 border-radius: 8px; 405 404 margin-bottom: 2rem; 406 - min-height: 600px; 407 405 position: relative; 406 + display: flex; 407 + flex-direction: column; 408 + align-items: center; 408 409 } 409 410 410 411 .drag-canvas { 411 412 position: relative; 412 - min-height: 500px; 413 - } 414 - 415 - .scale-bar-wrapper { 416 - margin-bottom: 3rem; 413 + width: 100%; 414 + height: 700px; 415 + margin-bottom: 2rem; 417 416 } 418 417 419 - .scale-labels { 420 - display: flex; 421 - justify-content: space-between; 422 - margin-bottom: 1rem; 423 - } 424 - 425 - .scale-label { 426 - font-size: 3rem; 427 - font-weight: 900; 428 - color: var(--go90-yellow); 429 - } 430 - 431 - .scale-label.red { 432 - color: #ff5555; 433 - } 434 - 435 - .scale-bar { 436 - position: relative; 437 - height: 80px; 438 - background: linear-gradient( 439 - to right, 440 - #32cd32 0%, 441 - #7fff00 10%, 442 - #adff2f 20%, 443 - #ffff00 30%, 444 - #ffd700 40%, 445 - #ffa500 50%, 446 - #ff8c00 60%, 447 - #ff6347 70%, 448 - #ff4500 80%, 449 - #dc143c 90%, 450 - #8b0000 100% 451 - ); 452 - border-radius: 12px; 453 - border: 3px solid white; 454 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 455 - margin-bottom: 120px; 456 - margin-top: 120px; 457 - } 458 - 459 - .scale-bar.drag-over { 460 - box-shadow: 0 0 20px rgba(255, 255, 0, 0.8); 461 - border-color: var(--go90-yellow); 462 - } 463 - 464 - .drop-zone { 418 + #arcCanvas { 465 419 position: absolute; 466 420 top: 0; 467 421 left: 0; 468 - right: 0; 469 - bottom: 0; 470 - border-radius: 12px; 422 + pointer-events: none; 471 423 } 472 424 473 - .service-on-scale { 425 + .service-on-canvas { 474 426 position: absolute; 475 - top: 50%; 476 - transform: translate(-50%, -50%); 477 - cursor: move; 427 + cursor: grab; 478 428 transition: transform 0.1s; 479 429 } 480 430 481 - .service-on-scale:hover { 482 - transform: translate(-50%, -50%) scale(1.1); 431 + .service-on-canvas:active { 432 + cursor: grabbing; 483 433 } 484 434 485 - .service-logo-large { 435 + .service-on-canvas:hover { 436 + transform: scale(1.05); 437 + } 438 + 439 + .service-icon { 486 440 width: 80px; 487 441 height: 80px; 488 442 border-radius: 12px; 489 443 background: white; 490 444 padding: 8px; 491 445 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); 492 - border: 3px solid white; 446 + border: 3px solid var(--go90-yellow); 447 + display: block; 493 448 } 494 449 495 450 .service-badge { ··· 513 468 } 514 469 515 470 .services-bar { 516 - background: black; 471 + background: transparent; 517 472 padding: 1.5rem; 518 473 border-radius: 8px; 519 474 display: flex; 520 475 gap: 1rem; 521 476 align-items: center; 477 + justify-content: center; 522 478 flex-wrap: wrap; 523 - min-height: 100px; 479 + border: 3px solid rgba(255, 255, 255, 0.3); 524 480 } 525 481 526 482 .service-item { 527 483 width: 80px; 528 484 height: 80px; 529 485 border-radius: 8px; 530 - background: white; 486 + background: black; 531 487 padding: 8px; 532 488 cursor: grab; 533 489 transition: ··· 565 521 display: flex; 566 522 gap: 0.5rem; 567 523 align-items: center; 568 - margin-top: 1rem; 524 + justify-content: center; 525 + width: 100%; 526 + max-width: 600px; 569 527 } 570 528 571 529 .add-service-input { 572 530 flex: 1; 573 - padding: 0.75rem; 574 - border: 2px solid var(--gray-500); 575 - border-radius: 4px; 576 - background: var(--gray-900); 531 + padding: 1rem 1.5rem; 532 + border: 3px solid rgba(255, 255, 255, 0.3); 533 + border-radius: 8px; 534 + background: transparent; 577 535 color: white; 578 - font-size: 1rem; 536 + font-size: 1.25rem; 537 + font-weight: 500; 538 + } 539 + 540 + .add-service-input::placeholder { 541 + color: rgba(255, 255, 255, 0.5); 579 542 } 580 543 581 544 .add-service-input:focus { 582 545 outline: none; 583 - border-color: var(--go90-yellow); 546 + border-color: white; 584 547 } 585 548 586 549 .add-service-btn { 587 - padding: 0.75rem 1.5rem; 588 - background: var(--go90-yellow); 589 - color: black; 590 - border: none; 591 - border-radius: 4px; 550 + padding: 1rem 1.5rem; 551 + background: transparent; 552 + color: white; 553 + border: 3px solid rgba(255, 255, 255, 0.3); 554 + border-radius: 8px; 592 555 font-weight: 700; 593 556 cursor: pointer; 594 - font-size: 1rem; 557 + font-size: 1.25rem; 558 + transition: all 0.2s; 595 559 } 596 560 597 561 .add-service-btn:hover { 598 - background: #ffff44; 562 + border-color: white; 563 + background: rgba(255, 255, 255, 0.1); 599 564 } 600 565 601 566 .instructions { ··· 1219 1184 const container = document.getElementById("content"); 1220 1185 1221 1186 container.innerHTML = ` 1222 - <div class="instructions"> 1223 - Drag services anywhere to rate them! Drop on the bar to remove rating. 1224 - </div> 1225 - 1226 - <div class="scale-container drag-canvas" id="dragCanvas"> 1227 - <div class="scale-bar-wrapper"> 1228 - <div class="scale-labels"> 1229 - <span class="scale-label">0</span> 1230 - <span class="scale-label red">90</span> 1231 - </div> 1232 - <div class="scale-bar" id="scaleBar"></div> 1187 + <div class="scale-container"> 1188 + <div class="drag-canvas" id="dragCanvas"> 1189 + <canvas id="arcCanvas"></canvas> 1233 1190 </div> 1234 1191 1235 1192 <div class="services-bar" id="servicesBar"> ··· 1253 1210 <input type="text" 1254 1211 id="customServiceDomain" 1255 1212 class="add-service-input" 1256 - placeholder="Enter a domain (e.g., dropout.tv)"> 1257 - <button type="submit" class="add-service-btn">Add Service</button> 1213 + placeholder="add service"> 1214 + <button type="submit" class="add-service-btn">add service</button> 1258 1215 </form> 1259 1216 </div> 1260 1217 `; ··· 1310 1267 const data = await client.query(ratingsQuery); 1311 1268 const allEdges = data?.socialGo90Rating?.edges || []; 1312 1269 1313 - // Filter to only viewer's ratings 1314 - const myRatings = allEdges 1270 + // Filter to only viewer's ratings and deduplicate by domain (keep latest) 1271 + const ratingsByDomain = {}; 1272 + allEdges 1315 1273 .filter((edge) => edge.node.did === viewerDid) 1316 - .map((edge) => edge.node); 1274 + .forEach((edge) => { 1275 + const node = edge.node; 1276 + ratingsByDomain[node.serviceDomain] = node; 1277 + }); 1278 + const myRatings = Object.values(ratingsByDomain); 1317 1279 1318 1280 if (myRatings && myRatings.length > 0) { 1319 1281 // Store ratings in global object ··· 1324 1286 // Update badges on services in the bar 1325 1287 updateServiceBadges(); 1326 1288 1327 - const scaleBar = document.getElementById("scaleBar"); 1328 - const scaleBarRect = scaleBar.getBoundingClientRect(); 1329 - const canvas = document.getElementById("dragCanvas"); 1330 - const canvasRect = canvas.getBoundingClientRect(); 1331 - 1332 - // Load viewer's ratings onto canvas 1289 + // Load viewer's ratings onto arc canvas 1333 1290 for (const rating of myRatings) { 1334 - // Calculate position based on rating value 1335 - const percentageX = (rating.rating / 90) * 100; 1336 - const scaleX = scaleBarRect.left + (percentageX / 100) * scaleBarRect.width; 1337 - const scaleY = scaleBarRect.top + scaleBarRect.height / 2; 1338 - 1339 - // Convert to canvas coordinates 1340 - const canvasX = scaleX - canvasRect.left; 1341 - const canvasY = scaleY - canvasRect.top; 1291 + console.log(`Loading ${rating.serviceDomain} at rating ${rating.rating}`); 1342 1292 1343 - // Place on canvas 1344 - placeServiceOnCanvas( 1345 - rating.serviceDomain, 1346 - rating.serviceDomain, 1347 - rating.rating, 1348 - canvasX, 1349 - canvasY, 1350 - ); 1293 + // Use arc-based placement from drag-fix.js 1294 + if (typeof placeServiceOnArc !== "undefined") { 1295 + placeServiceOnArc(rating.serviceDomain, rating.serviceDomain, rating.rating); 1296 + } 1351 1297 } 1352 1298 } 1353 1299 } catch (error) {