live seat availability: fetch from server webhook, orange/red styling when <10/0 seats, disable sold-out locations

This commit is contained in:
Oliver
2026-06-14 16:54:13 -03:00
parent ea58be1d97
commit 19183b42a8
5 changed files with 136 additions and 18 deletions
+49 -1
View File
@@ -299,6 +299,8 @@
} }
.pricing-config-select:hover { border-color: var(--accent); } .pricing-config-select:hover { border-color: var(--accent); }
.pricing-config-select:focus { border-color: var(--accent); } .pricing-config-select:focus { border-color: var(--accent); }
.pricing-config-select.low-seats { border-color: var(--warning); box-shadow: 0 0 0 2px rgba(255, 170, 0, 0.2); }
.pricing-config-select.no-seats { border-color: var(--danger); box-shadow: 0 0 0 2px rgba(255, 59, 59, 0.2); }
.pricing-config-input { .pricing-config-input {
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -363,7 +365,7 @@
<option value="yearly">Yearly</option> <option value="yearly">Yearly</option>
</select> </select>
<select class="pricing-config-select" id="pricing-location"> <select class="pricing-config-select" id="pricing-location">
<option value="france">France</option> <option value="">Loading locations...</option>
</select> </select>
<input type="email" class="pricing-config-input" id="pricing-email" placeholder="Email" autocomplete="email" /> <input type="email" class="pricing-config-input" id="pricing-email" placeholder="Email" autocomplete="email" />
<input type="text" class="pricing-config-input" id="pricing-coupon" placeholder="Coupon code" /> <input type="text" class="pricing-config-input" id="pricing-coupon" placeholder="Coupon code" />
@@ -539,6 +541,51 @@
<script> <script>
// ── Signup Webhook URLs (matching app.derez.ai) ────────── // ── Signup Webhook URLs (matching app.derez.ai) ──────────
const ORDER_WEBHOOK = "https://n8n.derez.ai/webhook/payment"; const ORDER_WEBHOOK = "https://n8n.derez.ai/webhook/payment";
const SERVER_WEBHOOK = "https://n8n.derez.ai/webhook/server";
async function initServerLocations() {
const sel = document.getElementById('pricing-location');
if (!sel) return;
try {
const res = await fetch(SERVER_WEBHOOK);
const data = await res.json();
const servers = Array.isArray(data) ? data : [data];
sel.innerHTML = '';
servers.forEach(function(s) {
const server = s.server || '';
const available = parseInt(s['Seats Available'], 10) || 0;
const label = server.charAt(0).toUpperCase() + server.slice(1);
const opt = document.createElement('option');
opt.value = server.toLowerCase().replace(/\s+/g, '-');
if (available <= 0) {
opt.textContent = label + ' (sold out)';
opt.disabled = true;
opt.style.color = '#ff4444';
opt.style.fontWeight = '600';
sel.classList.remove('low-seats');
sel.classList.add('no-seats');
} else if (available < 10) {
opt.textContent = label + ' (available ' + available + ')';
opt.style.color = '#ff9900';
opt.style.fontWeight = '600';
sel.classList.add('low-seats');
sel.classList.remove('no-seats');
} else {
opt.textContent = label + ' (available ' + available + ')';
opt.style.color = '';
opt.style.fontWeight = '';
sel.classList.remove('low-seats', 'no-seats');
}
sel.appendChild(opt);
});
for (var i = 0; i < sel.options.length; i++) {
if (!sel.options[i].disabled) { sel.selectedIndex = i; break; }
}
} catch (err) {
console.error('Failed to load server locations:', err);
sel.innerHTML = '<option value="france" style="color:#ff9900;font-weight:600">France (available ?)</option>';
}
}
/* ── Period / billing update ──────────────────────────── */ /* ── Period / billing update ──────────────────────────── */
function updatePeriodDisplay() { function updatePeriodDisplay() {
@@ -553,6 +600,7 @@
} }
document.getElementById('pricing-period').addEventListener('change', updatePeriodDisplay); document.getElementById('pricing-period').addEventListener('change', updatePeriodDisplay);
updatePeriodDisplay(); updatePeriodDisplay();
initServerLocations();
/* ── Capture UTM params from URL ──────────────────────── */ /* ── Capture UTM params from URL ──────────────────────── */
(function () { (function () {
+8
View File
@@ -972,6 +972,14 @@ section {
.pricing-config-select:focus { .pricing-config-select:focus {
border-color: var(--accent); border-color: var(--accent);
} }
.pricing-config-select.low-seats {
border-color: var(--warning);
box-shadow: 0 0 0 2px rgba(255, 170, 0, 0.2);
}
.pricing-config-select.no-seats {
border-color: var(--danger);
box-shadow: 0 0 0 2px rgba(255, 59, 59, 0.2);
}
.pricing-config-input { .pricing-config-input {
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
+64
View File
@@ -287,6 +287,7 @@ document
.getElementById("pricing-period") .getElementById("pricing-period")
.addEventListener("change", updatePeriodDisplay); .addEventListener("change", updatePeriodDisplay);
updatePeriodDisplay(); // fire on load so yearly bonuses show by default updatePeriodDisplay(); // fire on load so yearly bonuses show by default
initServerLocations(); // fetch live server availability
/* ── Capture UTM params from URL ──────────────────────────── */ /* ── Capture UTM params from URL ──────────────────────────── */
(function () { (function () {
@@ -638,6 +639,69 @@ async function chatSend() {
}); });
})(); })();
/* ── Server Locations (live seat availability) ──────── */
const SERVER_WEBHOOK = "https://n8n.derez.ai/webhook/server";
async function initServerLocations() {
const sel = document.getElementById("pricing-location");
if (!sel) return;
try {
const res = await fetch(SERVER_WEBHOOK);
const data = await res.json();
const servers = Array.isArray(data) ? data : [data];
// Clear existing options
sel.innerHTML = "";
servers.forEach(function (s) {
const server = s.server || "";
const available = parseInt(s["Seats Available"], 10) || 0;
const label = server.charAt(0).toUpperCase() + server.slice(1);
const opt = document.createElement("option");
opt.value = server.toLowerCase().replace(/\s+/g, "-");
if (available <= 0) {
// No seats — red, disabled, cannot select
opt.textContent = label + " (sold out)";
opt.disabled = true;
opt.style.color = "#ff4444";
opt.style.fontWeight = "600";
sel.classList.remove("low-seats");
sel.classList.add("no-seats");
} else if (available < 10) {
// Low seats — orange with count
opt.textContent = label + " (available " + available + ")";
opt.style.color = "#ff9900";
opt.style.fontWeight = "600";
sel.classList.add("low-seats");
sel.classList.remove("no-seats");
} else {
// Plenty of seats
opt.textContent = label + " (available " + available + ")";
opt.style.color = "";
opt.style.fontWeight = "";
sel.classList.remove("low-seats", "no-seats");
}
sel.appendChild(opt);
});
// Default to first available option
for (var i = 0; i < sel.options.length; i++) {
if (!sel.options[i].disabled) {
sel.selectedIndex = i;
break;
}
}
} catch (err) {
console.error("Failed to load server locations:", err);
// Fallback: leave the placeholder option
sel.innerHTML =
'<option value="france" style="color:#ff9900;font-weight:600">France (available ?)</option>';
}
}
/* ── Coupon Badges ──────────────────────────────────── */ /* ── Coupon Badges ──────────────────────────────────── */
const COUPONS = { const COUPONS = {
// 499 // 499
+14 -16
View File
@@ -57,15 +57,15 @@
], ],
"items": [ "items": [
{ {
"text": "60 YouTube creators collected on Hold", "text": "60 YouTube creators collected — campaign paused until end of July",
"ok": true
},
{
"text": "Need business emails for top 20",
"ok": false "ok": false
}, },
{ {
"text": "Affiliate program TBD", "text": "Focus now: Friends & Family battle-testing first",
"ok": false
},
{
"text": "Influencer affiliate program to be designed after F&F validation",
"ok": false "ok": false
} }
] ]
@@ -92,7 +92,7 @@
] ]
} }
], ],
"state_of_play": "Day 6 of prelaunch (Sunday). Major milestone: 1st agent sold! Traffic: 28 unique visitors / 249 pageviews across 5 days, 7 countries. Unblocked: 12 real YT creator emails found via YouTube about page scrape (NetworkChuck, Wes Roth, Metics Media, Nick Puru, Nate Herk, Bart Slodyczka, Zach Babiarz, Astroquirk, FuturMinds, DaviddTech, Nuno Tavares, CodeHead). Still blocked: Search Console (needs Oliver TXT record), 46 YT creators still need emails. New features: Interactive Architecture Finder, feedback form, YouTube video integration with UTM tracking, countdown screenshot proof, video chip on homepage. Brand: derez.ai now consistently lowercase.", "state_of_play": "Day 7 — Influencer campaign on hold until end of July. Full focus on Friends & Family battle-testing. 1 agent sold, 28 visitors across 5 days, 7 countries. Battle-proofing the system with real users before scaling to influencers. 39 coworking space leads + 6 e-commerce leads on Hold, ready for F&F outreach.",
"comms": { "comms": {
"inbox": { "inbox": {
"total": 0, "total": 0,
@@ -105,14 +105,12 @@
"alerts": 0 "alerts": 0
}, },
"recommendations": [ "recommendations": [
"Search Console submission still #1 blocker \u2014 needs Oliver to add TXT record for domain verification.", "INFLUENCER CAMPAIGN PAUSED until end of July — focus on F&F battle-testing first",
"12 YT creator emails found! Top targets: NetworkChuck (chuck@networkchuck.com), Wes Roth (wesroth@smoothmedia.co), Metics Media (contact@meticsmedia.com). Ready for cold outreach.", "Friends & Family list goes live Monday — start onboarding and collect feedback",
"Cold outreach templates and coupons ready. First batch can go to the 12 creators with real emails.", "Battle-proof the system: fix issues as they come up during F&F usage",
"1 agent sold! First social proof \u2014 publish testimonial/social proof tweet.", "End of July: evaluate F&F results, then re-activate influencer campaign with tested system",
"Submit to BetaList and AlternativeTo directories \u2014 requires browser session (blocked from headless curl).", "1 agent sold already — good sign for F&F conversion expectations"
"Pitch guest post to 5 AI/DevOps blogs, set up HARO/Qwoted alerts.",
"Goal for D7: mid-campaign review, send first batch influencer emails, polish Product Hunt listing."
], ],
"day": 6, "day": 7,
"notes": "Day 6 auto-update at 09:00 PYST \u2014 1 agent sold milestone" "notes": "Day 7 — Influencer campaign paused until end of July. Full focus on F&F battle-testing."
} }
+1 -1
View File
@@ -801,7 +801,7 @@
<option value="yearly">Yearly</option> <option value="yearly">Yearly</option>
</select> </select>
<select class="pricing-config-select" id="pricing-location"> <select class="pricing-config-select" id="pricing-location">
<option value="france">France</option> <option value="">Loading locations...</option>
</select> </select>
<input <input
type="email" type="email"