Track ring loop completions with +N badge and show mod badge for shop owners

When a viewer watches every item in the ring, the loop counter increments,
history resets (un-dims all items), and a +1/+2/+3 badge appears next to
the progress display. Shop owners/editors get a green "mod" badge.

Ring state: ringLoops persisted in localStorage alongside existing ring state.
Server: watch_json now returns is_mod flag. Templates pass data-watch-is-mod.
This commit is contained in:
russell@unturf.com 2026-02-09 10:57:56 -05:00
parent 740001f09b
commit 98efbfe566
5 changed files with 76 additions and 4 deletions

View file

@ -2767,6 +2767,24 @@ textarea {
color: var(--text-muted, #999);
font-variant-numeric: tabular-nums;
}
.ring-loops-badge {
font-size: 0.75em;
font-weight: bold;
padding: 1px 6px;
border-radius: 10px;
background-color: var(--blue-color, #5871ad);
color: white;
}
.ring-mod-badge {
font-size: 0.65em;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 1px 5px;
border-radius: 10px;
background-color: #48bb78;
color: white;
}
.watch-direction-label {
display: grid;

View file

@ -24,6 +24,7 @@
var ringPosition = 0;
var ringDirection = 1; // 1 = forward, -1 = reverse
var ringHistory = {}; // { productId: true } — set of watched IDs
var ringLoops = 0; // completed full circuits of the ring
// Load ring state from localStorage
try {
@ -42,6 +43,10 @@
var savedHistory = localStorage.getItem('watchRingHistory');
if (savedHistory) ringHistory = JSON.parse(savedHistory);
} catch (e) {}
try {
var savedLoops = localStorage.getItem('watchRingLoops');
if (savedLoops !== null) ringLoops = parseInt(savedLoops, 10) || 0;
} catch (e) {}
function saveRingState() {
try {
@ -49,12 +54,31 @@
localStorage.setItem('watchRingPosition', String(ringPosition));
localStorage.setItem('watchRingDirection', String(ringDirection));
localStorage.setItem('watchRingHistory', JSON.stringify(ringHistory));
localStorage.setItem('watchRingLoops', String(ringLoops));
} catch (e) {}
}
function markWatched(productId) {
if (!productId) return;
ringHistory[productId] = true;
// Check for complete loop — all ring items watched
if (ringProductIds.length > 0) {
var allWatched = true;
for (var i = 0; i < ringProductIds.length; i++) {
if (!ringHistory[ringProductIds[i]]) {
allWatched = false;
break;
}
}
if (allWatched) {
ringLoops++;
ringHistory = {};
// Current item starts the new loop as already watched
ringHistory[productId] = true;
}
}
saveRingState();
updateProgressDisplay();
}
@ -73,7 +97,7 @@
}
}
// Update progress display ("23 / 87")
// Update progress display ("23 / 87") and loop badge
function updateProgressDisplay() {
var el = document.getElementById('ring-progress');
if (!el) return;
@ -86,6 +110,17 @@
if (ringProductIds.indexOf(id) !== -1) watched++;
}
el.textContent = watched + ' / ' + ringProductIds.length;
// Loop badge
var badge = document.getElementById('ring-loops-badge');
if (badge) {
if (ringLoops > 0) {
badge.textContent = '+' + ringLoops;
badge.style.display = '';
} else {
badge.style.display = 'none';
}
}
}
// Autoplay preference (persisted in localStorage)
@ -119,12 +154,18 @@
if (savedQueue) queue = JSON.parse(savedQueue);
} catch (e) {}
// Mod status (shop owner/editor)
var isMod = false;
// Mark current item as watched and sync ring position
var initialProductEl = document.querySelector('[data-watch-product-id]');
if (initialProductEl) {
var initialId = initialProductEl.getAttribute('data-watch-product-id');
markWatched(initialId);
syncRingPosition(initialId);
if (initialProductEl.getAttribute('data-watch-is-mod') === '1') {
isMod = true;
}
}
// --- Autoplay with sound (for video/audio) ---
@ -448,10 +489,11 @@
// --- Update page content (title, description, related, URL) ---
function updatePageContent(data) {
// Update ring from server response
// Update ring and mod status from server response
if (data.ring && data.ring.length) {
ringProductIds = data.ring;
}
if (data.is_mod) isMod = true;
markWatched(data.product_id);
syncRingPosition(data.product_id);
@ -492,9 +534,15 @@
var autoplayChecked = autoplayEnabled ? ' checked' : '';
var directionChecked = ringDirection === -1 ? ' checked' : '';
var modBadge = isMod ? '<span class="ring-mod-badge">mod</span>' : '';
var loopsBadge = ringLoops > 0
? '<span id="ring-loops-badge" class="ring-loops-badge">+' + ringLoops + '</span>'
: '<span id="ring-loops-badge" class="ring-loops-badge" style="display:none"></span>';
var headerHtml = '<div class="related-content-header">'
+ '<h3>Up Next</h3>'
+ modBadge
+ '<span id="ring-progress" class="ring-progress"></span>'
+ loopsBadge
+ '<label class="watch-autoplay-label js-only">'
+ '<span class="autoplay-label-text">Autoplay</span>'
+ '<input type="checkbox" id="watch-autoplay-toggle"' + autoplayChecked + ' />'

View file

@ -32,7 +32,7 @@
<section class="two-column{% if request.shop.watch_mode_enabled %} watch-mode{% endif %}">
{% if request.shop.watch_mode_enabled %}<div class="watch-left" data-watch-product-id="{{ product.id }}">{% endif %}
{% if request.shop.watch_mode_enabled %}<div class="watch-left" data-watch-product-id="{{ product.id }}"{% if request.user.can_edit_shop(request.shop) %} data-watch-is-mod="1"{% endif %}>{% endif %}
<div class="product-images">
{% set video_extensions = ["mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v"] %}

View file

@ -30,7 +30,7 @@
<section class="two-column{% if request.shop.watch_mode_enabled %} watch-mode{% endif %}">
{% if request.shop.watch_mode_enabled %}<div class="watch-left" data-watch-product-id="{{ product.id }}">{% endif %}
{% if request.shop.watch_mode_enabled %}<div class="watch-left" data-watch-product-id="{{ product.id }}"{% if request.user.can_edit_shop(request.shop) %} data-watch-is-mod="1"{% endif %}>{% endif %}
<div class="product-images">
<h1>{{ product.title }}<br/><span class="subtitle-text">sold by <a href="{{ product.shop.absolute_about_url(request) }}" rel="nofollow" class="shop-theme-link-color">{{ product.shop.name }}</a></span></h1>

View file

@ -109,6 +109,11 @@ def watch_json(request):
"media_type": r_media,
})
# Check if current user is a shop editor (mod)
is_mod = False
if request.user and request.user.authenticated:
is_mod = request.user.can_edit_shop(request.shop)
return {
"product_id": str(product.id),
"title": product.title,
@ -120,6 +125,7 @@ def watch_json(request):
"is_sellable": product.is_sellable,
"related": related_data,
"ring": ring,
"is_mod": is_mod,
}