diff --git a/docs/tickets/an-algo/09-membership-price-history.md b/docs/tickets/an-algo/09-membership-price-history.md new file mode 100644 index 0000000..992275f --- /dev/null +++ b/docs/tickets/an-algo/09-membership-price-history.md @@ -0,0 +1,40 @@ +# Membership tiers that unlock price history for buyers + +## Context + +Price history is now visible to shop owners/editors on product pages (expandable +`
` section). The next step: let shops offer membership tiers that grant +buyers access to price history data. This is valuable for wholesalers dealing in +collectibles (pokemon cards, MTG, vintage vinyl, etc.) who need pricing trend +visibility before committing to purchases. + +## Membership tiers + +- **Monthly** — base price, full price history access +- **Yearly** — suggested ~15% discount over monthly +- **3-Year** — suggested ~30% discount over monthly + +Shop owners set their own prices. The platform suggests discount percentages but +doesn't enforce them. + +## What members unlock + +- Price history table on product pages (same expandable `
` UI that + editors see today) +- Price change notifications (future: email digest of price movements across + followed shops) +- Historical price charts (future: sparkline or simple line chart) + +## Implementation notes + +- New model: `Membership` (user, shop, tier, start/end timestamps, payment ref) +- Gate: `request.user.has_membership(shop)` check alongside the existing + `can_edit_shop` check in `views/product.py` and `views/watch.py` +- Stripe recurring billing integration for membership payments +- Shop settings form section for configuring tier prices and enabling memberships + +## Non-goals + +- No public price history by default (opt-in via membership) +- No price prediction or "best time to buy" features +- No cross-shop price comparison diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css index d430c77..fba8593 100644 --- a/make_post_sell/static/css/common.css +++ b/make_post_sell/static/css/common.css @@ -2721,7 +2721,7 @@ textarea { .ring-header-controls { display: grid; - grid-template-columns: auto auto auto; + grid-template-columns: auto auto auto auto; gap: 16px; justify-content: end; align-items: center; @@ -2808,6 +2808,63 @@ textarea { .related-content-row-watched { opacity: 0.45; } +.related-content.fresh-mode .related-content-row-watched { + display: none; +} + +/* Media type filter — hide rows by data-media-type */ +.related-content.hide-video .related-content-row[data-media-type="video"] { display: none; } +.related-content.hide-audio .related-content-row[data-media-type="audio"] { display: none; } +.related-content.hide-image .related-content-row[data-media-type="image"] { display: none; } +.related-content.hide-document .related-content-row[data-media-type="document"] { display: none; } +.related-content.hide-other .related-content-row[data-media-type="other"] { display: none; } + +.watch-fresh-label { + display: grid; + grid-template-columns: auto auto; + gap: 6px; + align-items: center; + font-size: 0.85em; + cursor: pointer; +} +.watch-fresh-label input { + display: none; +} +.watch-fresh-label input:checked + .autoplay-slider { + background: var(--blue-color, #98b6fa); +} +.watch-fresh-label input:checked + .autoplay-slider::after { + transform: translateX(16px); +} +.fresh-label-text { + font-size: 0.85em; + color: var(--text-muted, #999); +} + +/* Media type filter bar */ +.ring-filter-controls { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(0, auto)); + grid-auto-flow: column; + gap: 6px; + justify-content: end; + align-items: center; +} +.ring-filter-btn { + font-size: 0.7em; + padding: 2px 8px; + border: 1px solid var(--border-color, #ccc); + border-radius: 12px; + background: transparent; + color: var(--text-muted, #999); + cursor: pointer; + white-space: nowrap; +} +.ring-filter-btn.active { + background: var(--blue-color, #5871ad); + color: white; + border-color: var(--blue-color, #5871ad); +} .related-content-row-current { background-color: var(--blue-color, #5871ad); color: white; diff --git a/make_post_sell/static/js/watch.js b/make_post_sell/static/js/watch.js index a8a2053..f461736 100644 --- a/make_post_sell/static/js/watch.js +++ b/make_post_sell/static/js/watch.js @@ -140,6 +140,44 @@ if (saved !== null) autoplayEnabled = saved === '1'; } catch (e) {} + // Fresh mode preference (hide watched items in ring) + var freshMode = false; + try { + var savedFresh = localStorage.getItem('watchFreshMode'); + if (savedFresh !== null) freshMode = savedFresh === '1'; + } catch (e) {} + + // Media type filter preferences (all on by default) + var FILTER_TYPES = ['video', 'audio', 'image', 'document', 'other']; + var FILTER_LABELS = {video: 'Video', audio: 'Audio', image: 'Image', document: 'Docs', other: 'Other'}; + var mediaFilters = {}; + FILTER_TYPES.forEach(function(t) { mediaFilters[t] = true; }); + try { + var savedFilters = localStorage.getItem('watchMediaFilters'); + if (savedFilters) { + var parsed = JSON.parse(savedFilters); + FILTER_TYPES.forEach(function(t) { + if (t in parsed) mediaFilters[t] = parsed[t]; + }); + } + } catch (e) {} + + function saveMediaFilters() { + try { localStorage.setItem('watchMediaFilters', JSON.stringify(mediaFilters)); } catch (e) {} + } + + function applyMediaFilterClasses(container) { + if (!container) container = document.querySelector('.related-content'); + if (!container) return; + FILTER_TYPES.forEach(function(t) { + if (mediaFilters[t]) { + container.classList.remove('hide-' + t); + } else { + container.classList.add('hide-' + t); + } + }); + } + // Set up autoplay toggle var autoplayToggle = document.getElementById('watch-autoplay-toggle'); if (autoplayToggle) { @@ -650,6 +688,34 @@ viewCountEl.textContent = data.human_view_count || ''; } + // Update price history (mod-only) + var priceDetails = document.querySelector('.price-history-details'); + if (data.price_history && data.price_history.length > 1) { + var phHtml = 'Price History (' + data.price_history.length + ')' + + '
' + + ''; + data.price_history.forEach(function(ph) { + phHtml += '' + + '' + + '' + + ''; + }); + phHtml += '
PriceWhen
' + escapeHtml(ph.price_formatted) + '' + escapeHtml(ph.ago) + '
'; + if (priceDetails) { + priceDetails.innerHTML = phHtml; + } else { + var well = document.querySelector('.product-right .well'); + if (well) { + var details = document.createElement('details'); + details.className = 'price-history-details'; + details.innerHTML = phHtml; + well.parentNode.insertBefore(details, well.nextSibling); + } + } + } else if (priceDetails) { + priceDetails.remove(); + } + // Update signal data attributes for signals.js var signalRoot = document.querySelector('[data-signal-product-id]'); if (signalRoot) { @@ -686,6 +752,7 @@ var autoplayChecked = autoplayEnabled ? ' checked' : ''; var directionChecked = ringDirection === -1 ? ' checked' : ''; + var freshChecked = freshMode ? ' checked' : ''; var modBadge = isMod ? 'mod' : ''; var loopsBadge = ringLoops > 0 ? '+' + ringLoops + '' @@ -698,7 +765,11 @@ + '' + '' + '
' - + '' + + '' + '' - + '
'; + + '' + + '' + + '
'; + FILTER_TYPES.forEach(function(t) { + var activeClass = mediaFilters[t] ? ' active' : ''; + headerHtml += ''; + }); + headerHtml += '
'; + + // Apply fresh-mode class to container + if (freshMode) { + container.classList.add('fresh-mode'); + } else { + container.classList.remove('fresh-mode'); + } if (!related || related.length === 0) { container.innerHTML = headerHtml + '

No more items

'; @@ -746,7 +832,8 @@ html += ''; } - html += '