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: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 {
background: var(--bg);
border: 1px solid var(--border);
@@ -363,7 +365,7 @@
<option value="yearly">Yearly</option>
</select>
<select class="pricing-config-select" id="pricing-location">
<option value="france">France</option>
<option value="">Loading locations...</option>
</select>
<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" />
@@ -539,6 +541,51 @@
<script>
// ── Signup Webhook URLs (matching app.derez.ai) ──────────
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 ──────────────────────────── */
function updatePeriodDisplay() {
@@ -553,6 +600,7 @@
}
document.getElementById('pricing-period').addEventListener('change', updatePeriodDisplay);
updatePeriodDisplay();
initServerLocations();
/* ── Capture UTM params from URL ──────────────────────── */
(function () {
+8
View File
@@ -972,6 +972,14 @@ section {
.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 {
background: var(--bg);
border: 1px solid var(--border);
+64
View File
@@ -287,6 +287,7 @@ document
.getElementById("pricing-period")
.addEventListener("change", updatePeriodDisplay);
updatePeriodDisplay(); // fire on load so yearly bonuses show by default
initServerLocations(); // fetch live server availability
/* ── Capture UTM params from URL ──────────────────────────── */
(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 ──────────────────────────────────── */
const COUPONS = {
// 499
+14 -16
View File
@@ -57,15 +57,15 @@
],
"items": [
{
"text": "60 YouTube creators collected on Hold",
"ok": true
},
{
"text": "Need business emails for top 20",
"text": "60 YouTube creators collected — campaign paused until end of July",
"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
}
]
@@ -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": {
"inbox": {
"total": 0,
@@ -105,14 +105,12 @@
"alerts": 0
},
"recommendations": [
"Search Console submission still #1 blocker \u2014 needs Oliver to add TXT record for domain verification.",
"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.",
"Cold outreach templates and coupons ready. First batch can go to the 12 creators with real emails.",
"1 agent sold! First social proof \u2014 publish testimonial/social proof tweet.",
"Submit to BetaList and AlternativeTo directories \u2014 requires browser session (blocked from headless curl).",
"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."
"INFLUENCER CAMPAIGN PAUSED until end of July — focus on F&F battle-testing first",
"Friends & Family list goes live Monday — start onboarding and collect feedback",
"Battle-proof the system: fix issues as they come up during F&F usage",
"End of July: evaluate F&F results, then re-activate influencer campaign with tested system",
"1 agent sold already — good sign for F&F conversion expectations"
],
"day": 6,
"notes": "Day 6 auto-update at 09:00 PYST \u2014 1 agent sold milestone"
"day": 7,
"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>
</select>
<select class="pricing-config-select" id="pricing-location">
<option value="france">France</option>
<option value="">Loading locations...</option>
</select>
<input
type="email"