Featured rotation -> day-slot booking: 10 slots/day, pick your start day, live occupancy

- Per-day occupancy shown in composer (Today 1/10, Tomorrow 2/10, …), full days unpickable
- Book a specific start day + duration; every covered day must have an open slot
- Slot cap turns dilution disclosure into a hard ceiling; featured skips frame-check (new-tab)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 09:16:24 -05:00
parent c5dd5c685b
commit f30a462ca8
13 changed files with 62 additions and 38 deletions
+20 -8
View File
@@ -514,17 +514,29 @@
if (t === 'video') videoHint();
if (t === 'featured') featHint();
});
function featHint() {
let featStartDay = 0; // selected start-day offset (0 = today)
async function featHint() {
if (!lastRates || !lastRates.featuredDurations) return;
if ($('cFeatDays') && !$('cFeatDays').options.length)
$('cFeatDays').innerHTML = lastRates.featuredDurations.map(dys =>
'<option value="' + dys + '">' + dys + ' day' + (dys > 1 ? 's' : '') + ' — ' + (dys * lastRates.featuredPerDay) + ' credits</option>').join('');
fetch('/api/featured/stats').then(r => r.json()).then(s => {
const dys = Number($('cFeatDays').value) || (lastRates.featuredDurations[0]);
$('cFeatHint').textContent = (s.activeCount || 0) + ' link' + (s.activeCount === 1 ? '' : 's')
+ ' currently share the rotation — yours would make ' + ((s.activeCount || 0) + 1)
+ '. Runs ' + dys + ' day' + (dys > 1 ? 's' : '') + ' for ' + (dys * lastRates.featuredPerDay) + ' credits.';
}).catch(() => {});
let s = null;
try { s = await (await fetch('/api/featured/stats')).json(); } catch (e) {}
if (s && s.occupancy) {
const grid = $('cFeatDaysGrid');
grid.innerHTML = s.occupancy.map(d => {
const label = d.offset === 0 ? 'Today' : d.offset === 1 ? 'Tomorrow' : new Date(d.day + 'T00:00:00Z').toLocaleDateString(undefined, { weekday: 'short', month: 'numeric', day: 'numeric' });
const full = d.open <= 0;
return '<div class="feat-day' + (d.offset === featStartDay ? ' on' : '') + (full ? ' full' : d.open > d.cap / 2 ? ' open2' : '') + '" data-off="' + d.offset + '"' + (full ? '' : '') + '>'
+ '<div class="fd-day">' + label + '</div><div class="fd-occ">' + d.count + '/' + d.cap + (full ? ' full' : ' left ' + d.open) + '</div></div>';
}).join('');
grid.querySelectorAll('.feat-day:not(.full)').forEach(el =>
el.addEventListener('click', () => { featStartDay = Number(el.dataset.off); featHint(); }));
}
const dys = Number($('cFeatDays').value) || (lastRates.featuredDurations[0]);
const dayLabel = featStartDay === 0 ? 'today' : featStartDay === 1 ? 'tomorrow' : 'in ' + featStartDay + ' days';
$('cFeatHint').textContent = 'Runs ' + dys + ' day' + (dys > 1 ? 's' : '') + ' starting ' + dayLabel
+ ' for ' + (dys * lastRates.featuredPerDay) + ' credits. Max ' + (s ? s.slotsPerDay : 10) + ' links share any day.';
}
document.addEventListener('change', e => { if (e.target && e.target.id === 'cFeatDays') featHint(); });
// video composer: tier dropdown + upload + price hint
@@ -566,7 +578,7 @@
title: isVideo ? $('cVideoTitle').value : isFeat ? $('cFeatTitle').value : $('cTitle').value,
body: t === 'solo' ? soloBody : $('cBody').value,
videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value),
days: Number($('cFeatDays').value),
days: Number($('cFeatDays').value), startDay: featStartDay,
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) });
IAP.status('Campaign is live. It starts serving right away.', 'ok');
+9
View File
@@ -259,6 +259,15 @@ textarea{resize:vertical;font:inherit}
#boBurger{display:block}
.bo-content{padding:18px}
}
/* ── featured day-slot picker ── */
.feat-days{display:flex;gap:8px;flex-wrap:wrap}
.feat-day{flex:0 0 auto;min-width:82px;padding:8px 10px;border:1px solid var(--line-strong);border-radius:10px;
background:var(--panel);cursor:pointer;text-align:center}
.feat-day.on{border-color:var(--mint);box-shadow:0 0 0 1px var(--mint)}
.feat-day.full{opacity:.45;cursor:not-allowed}
.feat-day .fd-day{font-weight:700;font-size:12px}
.feat-day .fd-occ{font-family:var(--mono);font-size:11px;color:var(--muted)}
.feat-day.open2 .fd-occ{color:var(--mint)}
/* ── featured rotation strip ── */
.feat-strip{display:flex;flex-direction:column;gap:8px;margin-top:10px}
.feat-strip a{display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px;