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');