4fe11e124a
The stats webhook now returns a 'code' field (e.g. 'fr', 'usa') alongside the display 'server' name. The option value now uses s.code and falls back to slugified server name for backward compat.
910 lines
28 KiB
JavaScript
910 lines
28 KiB
JavaScript
// ── Speedrun Slider (inline script from how-it-works section) ──
|
|
(function () {
|
|
const slider = document.getElementById("speedrunSlider");
|
|
const dotsContainer = document.getElementById("speedrunDots");
|
|
if (!slider || !dotsContainer) return;
|
|
const slides = slider.querySelectorAll(".video-slide");
|
|
const total = slides.length;
|
|
if (total === 0) return;
|
|
|
|
for (let i = 0; i < total; i++) {
|
|
const dot = document.createElement("button");
|
|
dot.className = "slider-dot" + (i === 0 ? " active" : "");
|
|
dot.setAttribute("aria-label", "Go to slide " + (i + 1));
|
|
dot.onclick = function () {
|
|
slides[i].scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "nearest",
|
|
inline: "start",
|
|
});
|
|
};
|
|
dotsContainer.appendChild(dot);
|
|
}
|
|
|
|
function updateDots() {
|
|
const threshold = 120;
|
|
let activeIdx = 0;
|
|
for (let i = 0; i < total; i++) {
|
|
const rect = slides[i].getBoundingClientRect();
|
|
const center = rect.left + rect.width / 2;
|
|
const sl = slider.getBoundingClientRect();
|
|
const slCenter = sl.left + sl.width / 2;
|
|
if (Math.abs(center - slCenter) < threshold) {
|
|
activeIdx = i;
|
|
}
|
|
}
|
|
dotsContainer.querySelectorAll(".slider-dot").forEach((d, i) => {
|
|
d.classList.toggle("active", i === activeIdx);
|
|
});
|
|
}
|
|
|
|
slider.addEventListener("scroll", updateDots);
|
|
updateDots();
|
|
})();
|
|
|
|
function slideSpeedrun(dir) {
|
|
plausible("Speedrun Nav", {
|
|
props: { direction: dir > 0 ? "next" : "prev" },
|
|
});
|
|
const slider = document.getElementById("speedrunSlider");
|
|
if (!slider) return;
|
|
const slideWidth = slider.querySelector(".video-slide").offsetWidth + 20;
|
|
slider.scrollBy({
|
|
left: slideWidth * dir,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
|
|
// ── Mobile menu ──────────────────────────────────────────
|
|
function toggleMobile() {
|
|
plausible("Mobile Menu Toggle");
|
|
const nav = document.getElementById("nav-links");
|
|
nav.classList.toggle("mobile-open");
|
|
}
|
|
|
|
// ── FAQ accordion ────────────────────────────────────────
|
|
function toggleFaq(btn) {
|
|
const q = btn.innerText.replace(/[?✦]/g, "").trim();
|
|
plausible("FAQ Toggle", { props: { question: q } });
|
|
const answer = btn.nextElementSibling;
|
|
const isOpen = answer.classList.contains("open");
|
|
// Close all
|
|
document
|
|
.querySelectorAll(".faq-answer.open")
|
|
.forEach((el) => el.classList.remove("open"));
|
|
document
|
|
.querySelectorAll(".faq-question.open")
|
|
.forEach((el) => el.classList.remove("open"));
|
|
if (!isOpen) {
|
|
answer.classList.add("open");
|
|
btn.classList.add("open");
|
|
}
|
|
}
|
|
|
|
// ── Signup Webhook URLs (matching app.derez.ai) ──────────
|
|
const SIGNUP_URL =
|
|
"https://n8n.derez.ai/webhook/4de196b7-2ad2-4f9a-9b4d-dc8d3b30865b";
|
|
const AUTH_URL =
|
|
"https://n8n.derez.ai/webhook/aca1b27f-c18e-4424-b7a0-de01007a3fe6";
|
|
const COUPON_URL =
|
|
"https://n8n.derez.ai/webhook/fd6375cb-6d73-4482-bfa3-ef8365f43a67";
|
|
|
|
let inlineCouponApplied = false;
|
|
|
|
// ── Helper: show/hide auth messages ──────────────────────
|
|
function showAuthError(id, msg) {
|
|
const el = document.getElementById(id);
|
|
el.textContent = msg;
|
|
el.style.display = "flex";
|
|
}
|
|
function hideAuthError(id) {
|
|
document.getElementById(id).style.display = "none";
|
|
}
|
|
|
|
// ── Helper: btn loading state ────────────────────────────
|
|
function btnLoad(btn, text) {
|
|
btn.disabled = true;
|
|
btn.textContent = text;
|
|
}
|
|
function btnReset(btn) {
|
|
btn.disabled = false;
|
|
btn.textContent = "Buy Agent — $9.50/mo";
|
|
}
|
|
|
|
function isValidEmail(e) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e);
|
|
}
|
|
|
|
// ── Coupon ──────────────────────────────────────────────
|
|
function handleCouponKey(e) {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
applyInlineCoupon();
|
|
}
|
|
}
|
|
|
|
async function applyInlineCoupon() {
|
|
const input = document.getElementById("signup-coupon-input");
|
|
const spinner = document.getElementById("signup-coupon-spinner");
|
|
const errorEl = document.getElementById("signup-coupon-error");
|
|
const successEl = document.getElementById("signup-coupon-success");
|
|
|
|
const code = input.value.trim();
|
|
if (!code) return;
|
|
|
|
spinner.style.display = "flex";
|
|
errorEl.style.display = "none";
|
|
successEl.style.display = "none";
|
|
input.classList.remove("signup-coupon-input-error");
|
|
|
|
try {
|
|
const res = await fetch(COUPON_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ code }),
|
|
});
|
|
if (!res.ok) throw new Error("Invalid");
|
|
const data = await res.json();
|
|
const raw = Array.isArray(data) ? data[0] : data;
|
|
const item =
|
|
raw && typeof raw === "object" && "json" in raw ? raw.json : raw;
|
|
if (item?.valid === true || item?.status === "ok") {
|
|
inlineCouponApplied = true;
|
|
successEl.style.display = "flex";
|
|
input.style.display = "none";
|
|
} else {
|
|
throw new Error("Invalid");
|
|
}
|
|
} catch {
|
|
input.classList.add("signup-coupon-input-error");
|
|
errorEl.style.display = "block";
|
|
} finally {
|
|
spinner.style.display = "none";
|
|
}
|
|
}
|
|
|
|
// ── Sign Up (inline on landing page) ────────────────────
|
|
async function doInlineSignUp() {
|
|
const email = document.getElementById("inline-signup-email").value.trim();
|
|
const password = document.getElementById("inline-signup-password").value;
|
|
const agent = document.getElementById("inline-signup-agent").value;
|
|
const location = document.getElementById("inline-signup-location").value;
|
|
|
|
hideAuthError("inline-signup-error");
|
|
document.getElementById("inline-signup-success").style.display = "none";
|
|
|
|
if (!isValidEmail(email)) {
|
|
showAuthError("inline-signup-error", "Please enter a valid email address.");
|
|
return;
|
|
}
|
|
if (password.length < 8) {
|
|
showAuthError(
|
|
"inline-signup-error",
|
|
"Password must be at least 8 characters.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById("inline-signup-btn");
|
|
btnLoad(btn, "Hiring agent\u2026");
|
|
|
|
// Open blank tab synchronously for Stripe redirect
|
|
const stripeTab = window.open("", "_blank");
|
|
|
|
try {
|
|
// Step 1: authenticate
|
|
const authRes = await fetch(AUTH_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
if (!authRes.ok)
|
|
throw new Error("Account creation failed. Please try again later.");
|
|
|
|
// Step 2: create agent
|
|
const createRes = await fetch(SIGNUP_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
email,
|
|
password,
|
|
agent,
|
|
location,
|
|
}),
|
|
});
|
|
if (!createRes.ok) {
|
|
throw new Error("Agent provisioning failed. Please contact support.");
|
|
}
|
|
|
|
const createData = await createRes.json();
|
|
const createRaw = Array.isArray(createData) ? createData[0] : createData;
|
|
const createItem =
|
|
createRaw && typeof createRaw === "object" && "json" in createRaw
|
|
? createRaw.json
|
|
: createRaw;
|
|
|
|
// Navigate the pre-opened tab to Stripe checkout
|
|
const checkoutUrl =
|
|
createItem?.checkout_url || createItem?.checkoutUrl || createItem?.url;
|
|
if (checkoutUrl && checkoutUrl.startsWith("http")) {
|
|
stripeTab.location.href = checkoutUrl;
|
|
} else {
|
|
stripeTab.close();
|
|
}
|
|
|
|
// Show success message
|
|
document.getElementById("inline-signup-success").textContent =
|
|
"Agent created! \u2713 Redirecting to checkout\u2026";
|
|
document.getElementById("inline-signup-success").style.display = "flex";
|
|
} catch (err) {
|
|
stripeTab?.close();
|
|
showAuthError("inline-signup-error", err.message);
|
|
} finally {
|
|
btnReset(btn);
|
|
}
|
|
}
|
|
|
|
// ── Close mobile menu on link click ──────────────────────
|
|
document.querySelectorAll(".nav-links a").forEach((link) => {
|
|
link.addEventListener("click", () => {
|
|
document.getElementById("nav-links").classList.remove("mobile-open");
|
|
});
|
|
});
|
|
|
|
// ── Smooth scroll offset for fixed header ────────────────
|
|
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
|
anchor.addEventListener("click", function (e) {
|
|
const target = document.querySelector(this.getAttribute("href"));
|
|
if (target) {
|
|
e.preventDefault();
|
|
const headerHeight = 60;
|
|
const top =
|
|
target.getBoundingClientRect().top + window.pageYOffset - headerHeight;
|
|
window.scrollTo({ top, behavior: "smooth" });
|
|
}
|
|
});
|
|
});
|
|
|
|
/* ── Period / billing update ──────────────────────────── */
|
|
function updatePeriodDisplay() {
|
|
const sel = document.getElementById("pricing-period");
|
|
const yearly = sel.value === "yearly";
|
|
document.getElementById("bonus-trainee").classList.toggle("show", yearly);
|
|
document.getElementById("bonus-junior").classList.toggle("show", yearly);
|
|
document.getElementById("bonus-senior").classList.toggle("show", yearly);
|
|
|
|
document.getElementById("period-trainee").textContent = yearly
|
|
? "/ month ✦ yearly"
|
|
: "/ month";
|
|
document.getElementById("period-junior").textContent = yearly
|
|
? "/ month ✦ yearly"
|
|
: "/ month";
|
|
document.getElementById("period-senior").textContent = yearly
|
|
? "/ month ✦ yearly"
|
|
: "/ month";
|
|
}
|
|
document
|
|
.getElementById("pricing-period")
|
|
.addEventListener("change", updatePeriodDisplay);
|
|
updatePeriodDisplay(); // fire on load so yearly bonuses show by default
|
|
const SERVER_WEBHOOK = "https://n8n.derez.ai/webhook/server";
|
|
initServerLocations(); // fetch live server availability
|
|
|
|
/* ── Capture UTM params from URL ──────────────────────────── */
|
|
(function () {
|
|
const params = new URLSearchParams(window.location.search);
|
|
["source", "medium", "campaign", "term", "content"].forEach(function (key) {
|
|
const val = params.get("utm_" + key);
|
|
if (val) document.getElementById("utm_" + key).value = val;
|
|
});
|
|
})();
|
|
|
|
/* ── Buy Agent Webhook ───────────────────────────── */
|
|
const ORDER_WEBHOOK = "https://n8n.derez.ai/webhook/payment";
|
|
|
|
function showPricingToast(msg) {
|
|
const el = document.getElementById("pricing-toast");
|
|
el.textContent = msg;
|
|
el.classList.add("show");
|
|
if (window._pricingToastTimer) clearTimeout(window._pricingToastTimer);
|
|
window._pricingToastTimer = setTimeout(function () {
|
|
el.classList.remove("show");
|
|
}, 20000);
|
|
}
|
|
|
|
async function hireAgent(product) {
|
|
const agent = document.getElementById("pricing-agent").value;
|
|
const period = document.getElementById("pricing-period").value;
|
|
const location = document.getElementById("pricing-location").value;
|
|
const email = document.getElementById("pricing-email").value.trim();
|
|
const coupon = document.getElementById("pricing-coupon").value.trim();
|
|
|
|
// Validate email
|
|
const emailInput = document.getElementById("pricing-email");
|
|
emailInput.classList.remove("invalid");
|
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
emailInput.classList.add("invalid");
|
|
showPricingToast(
|
|
"Please configure your agent and provide a valid email address.",
|
|
);
|
|
emailInput.focus();
|
|
return;
|
|
}
|
|
|
|
plausible("Pricing Click", {
|
|
props: { product: product, agent: agent, period: period },
|
|
});
|
|
|
|
try {
|
|
// Use a hidden form POST so the 302 redirect opens naturally in the new tab
|
|
const form = document.createElement("form");
|
|
form.method = "POST";
|
|
form.action = ORDER_WEBHOOK;
|
|
form.target = "_self";
|
|
form.style.display = "none";
|
|
|
|
const fields = {
|
|
product: product,
|
|
agent: agent,
|
|
period: period,
|
|
location: location,
|
|
email: email || "",
|
|
coupon: coupon || "",
|
|
utm_source: document.getElementById("utm_source").value || "",
|
|
utm_medium: document.getElementById("utm_medium").value || "",
|
|
utm_campaign: document.getElementById("utm_campaign").value || "",
|
|
utm_term: document.getElementById("utm_term").value || "",
|
|
utm_content: document.getElementById("utm_content").value || "",
|
|
};
|
|
|
|
Object.keys(fields).forEach(function (key) {
|
|
const inp = document.createElement("input");
|
|
inp.type = "hidden";
|
|
inp.name = key;
|
|
inp.value = fields[key];
|
|
form.appendChild(inp);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
document.body.removeChild(form);
|
|
} catch (err) {
|
|
console.error("Order webhook failed:", err);
|
|
}
|
|
}
|
|
|
|
/* ── Chat Widget ──────────────────────────────────────────── */
|
|
const CHAT_URL =
|
|
"https://n8n.derez.ai/webhook/a58d00c4-f0c9-40cd-bb50-4f45f0442ef0";
|
|
let chatId = null;
|
|
let chatOpen = false;
|
|
let chatBusy = false;
|
|
let greeted = false;
|
|
let probeTimer = null;
|
|
|
|
function chatToggle() {
|
|
plausible("Chat Toggle", {
|
|
props: { action: chatOpen ? "close" : "open" },
|
|
});
|
|
chatOpen = !chatOpen;
|
|
document.getElementById("chat-widget").classList.toggle("open", chatOpen);
|
|
document.getElementById("chat-fab").classList.toggle("open", chatOpen);
|
|
document.getElementById("chat-widget").setAttribute("aria-hidden", !chatOpen);
|
|
if (chatOpen) {
|
|
// User opened chat manually — cancel the auto-open probe
|
|
if (probeTimer) {
|
|
clearTimeout(probeTimer);
|
|
probeTimer = null;
|
|
}
|
|
if (!chatId) {
|
|
chatId = String(Math.floor(100000 + Math.random() * 900000));
|
|
chatAppendMsg(
|
|
"bot",
|
|
"Hi! I'm your Sales Agent. Ask me about plans, setup, pricing, integrations, or anything about your AI agent.",
|
|
);
|
|
}
|
|
setTimeout(() => document.getElementById("chat-input").focus(), 300);
|
|
}
|
|
}
|
|
|
|
function chatClose() {
|
|
if (chatOpen) chatToggle();
|
|
}
|
|
|
|
function chatOpenSilent() {
|
|
if (!chatOpen) {
|
|
chatOpen = true;
|
|
document.getElementById("chat-widget").classList.add("open");
|
|
document.getElementById("chat-widget").setAttribute("aria-hidden", "false");
|
|
}
|
|
}
|
|
|
|
function chatAppendMsg(role, text) {
|
|
const msgs = document.getElementById("chat-msgs");
|
|
const div = document.createElement("div");
|
|
div.className = "chat-msg chat-msg-" + role;
|
|
// strip <script> and on* handlers to prevent XSS, but render safe HTML
|
|
div.innerHTML = text
|
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
|
|
.replace(/\son\w+\s*=\s*"[^"]*"/gi, "")
|
|
.replace(/\son\w+\s*=\s*'[^']*'/gi, "");
|
|
msgs.appendChild(div);
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
return div;
|
|
}
|
|
|
|
function chatKey(e) {
|
|
if (e.key === "Enter") chatSend();
|
|
}
|
|
|
|
async function chatSend() {
|
|
const input = document.getElementById("chat-input");
|
|
const btn = document.getElementById("chat-send-btn");
|
|
const msg = input.value.trim();
|
|
if (!msg || chatBusy) return;
|
|
plausible("Chat Message Sent");
|
|
chatBusy = true;
|
|
chatOpenSilent();
|
|
chatAppendMsg("user", msg);
|
|
input.value = "";
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span style="opacity:0.6">…</span>';
|
|
|
|
const think = chatAppendMsg("bot", "…");
|
|
think.className = "chat-msg chat-msg-thinking";
|
|
|
|
try {
|
|
const res = await fetch(CHAT_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
message: msg,
|
|
sessionId: chatId,
|
|
}),
|
|
});
|
|
const json = await res.json();
|
|
const first = Array.isArray(json) ? json[0] : json;
|
|
const reply =
|
|
typeof json === "string"
|
|
? json
|
|
: (first &&
|
|
(first.output ||
|
|
first.message ||
|
|
first.reply ||
|
|
first.response ||
|
|
first.text)) ||
|
|
JSON.stringify(json);
|
|
think.remove();
|
|
chatAppendMsg("bot", reply);
|
|
} catch (err) {
|
|
think.remove();
|
|
chatAppendMsg(
|
|
"bot",
|
|
"Sorry, I couldn't connect right now. Please try again later.",
|
|
);
|
|
} finally {
|
|
chatBusy = false;
|
|
btn.disabled = false;
|
|
btn.innerHTML = "➤";
|
|
document.getElementById("chat-input").focus();
|
|
}
|
|
}
|
|
|
|
/* ── Visitor Probe — auto-open after 15s ────────────────── */
|
|
(function () {
|
|
let fired = false;
|
|
probeTimer = setTimeout(function () {
|
|
fetch("https://ipinfo.io/json")
|
|
.then(function (r) {
|
|
return r.json();
|
|
})
|
|
.then(function (geo) {
|
|
fireProbe(geo);
|
|
})
|
|
.catch(function () {
|
|
fireProbe(null);
|
|
});
|
|
}, 15000);
|
|
|
|
function fireProbe(geo) {
|
|
if (fired) return;
|
|
fired = true;
|
|
const cid = chatId || String(Math.floor(100000 + Math.random() * 900000));
|
|
const ua = navigator.userAgent;
|
|
|
|
let lines = [
|
|
"NEW VISITOR — 15s Probe",
|
|
"========================================",
|
|
"",
|
|
"- Page section: hero",
|
|
"- Scroll depth: " +
|
|
Math.min(
|
|
100,
|
|
Math.round(
|
|
(window.pageYOffset /
|
|
Math.max(
|
|
1,
|
|
document.documentElement.scrollHeight - window.innerHeight,
|
|
)) *
|
|
100,
|
|
),
|
|
) +
|
|
"%",
|
|
"- Time on page: 15 seconds",
|
|
"- Timestamp: " + new Date().toUTCString(),
|
|
"",
|
|
"--- BROWSER & DEVICE ---",
|
|
"- Browser: " +
|
|
(/Edg\/([\d.]+)/.test(ua)
|
|
? "Edge " + RegExp.$1
|
|
: /Chrome\/([\d.]+)/.test(ua)
|
|
? "Chrome " + RegExp.$1
|
|
: /Firefox\/([\d.]+)/.test(ua)
|
|
? "Firefox " + RegExp.$1
|
|
: /Version\/([\d.]+).*Safari/.test(ua)
|
|
? "Safari " + RegExp.$1
|
|
: "Unknown"),
|
|
"- OS: " +
|
|
(/Windows NT 10/.test(ua)
|
|
? "Windows 10/11"
|
|
: /Mac OS X/.test(ua)
|
|
? "macOS"
|
|
: /Android/.test(ua)
|
|
? "Android"
|
|
: /iPhone/.test(ua)
|
|
? "iOS"
|
|
: /Linux/.test(ua)
|
|
? "Linux"
|
|
: "Unknown"),
|
|
"- Device: " + (/Mobi|Android|iPhone/.test(ua) ? "Mobile" : "Desktop"),
|
|
"- Screen: " + screen.width + "x" + screen.height,
|
|
"- Language: " + navigator.language,
|
|
"- User Agent: " + ua,
|
|
"",
|
|
"--- PAGE ---",
|
|
"- URL: " + window.location.href,
|
|
"- Referrer: " + (document.referrer || "direct / none"),
|
|
"- Title: " + document.title,
|
|
];
|
|
if (geo) {
|
|
lines.push("");
|
|
lines.push("--- LOCATION ---");
|
|
if (geo.country) lines.push("- Country: " + geo.country);
|
|
if (geo.city) lines.push("- City: " + geo.city);
|
|
if (geo.org) lines.push("- ISP: " + geo.org);
|
|
}
|
|
lines.push("");
|
|
lines.push("- Chat ID: " + cid);
|
|
|
|
const payload = {
|
|
message: lines.join("\n"),
|
|
sessionId: cid,
|
|
};
|
|
|
|
fetch(CHAT_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
.then(function (r) {
|
|
const ct = r.headers.get("content-type") || "";
|
|
if (ct.indexOf("json") !== -1) return r.json();
|
|
return r.text().then(function (t) {
|
|
return { output: t };
|
|
});
|
|
})
|
|
.then(function (d) {
|
|
const item = Array.isArray(d) ? d[0] : d;
|
|
const reply =
|
|
(item &&
|
|
(item.output ||
|
|
item.message ||
|
|
item.reply ||
|
|
item.response ||
|
|
item.text)) ||
|
|
null;
|
|
if (reply) {
|
|
if (!chatId) chatId = cid;
|
|
chatAppendMsg("bot", reply);
|
|
greeted = true;
|
|
if (!chatOpen) chatOpenSilent();
|
|
}
|
|
})
|
|
.catch(function () {});
|
|
}
|
|
})();
|
|
|
|
/* ── Scroll Depth Tracking ────────────────────────── */
|
|
(function () {
|
|
const thresholds = [25, 50, 75, 100];
|
|
let sent = {};
|
|
let ticking = false;
|
|
window.addEventListener("scroll", function () {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(function () {
|
|
const scrolled = window.pageYOffset + window.innerHeight;
|
|
const total = document.documentElement.scrollHeight;
|
|
const pct = Math.min(100, Math.round((scrolled / total) * 100));
|
|
thresholds.forEach(function (t) {
|
|
if (pct >= t && !sent[t]) {
|
|
sent[t] = true;
|
|
plausible("Scroll Depth", {
|
|
props: { percent: t + "%" },
|
|
});
|
|
}
|
|
});
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
});
|
|
})();
|
|
|
|
/* ── Server Locations (live seat availability) ──────── */
|
|
|
|
async function initServerLocations() {
|
|
const sel = document.getElementById("pricing-location");
|
|
if (!sel) return;
|
|
|
|
try {
|
|
const res = await fetch(SERVER_WEBHOOK);
|
|
const raw = await res.json();
|
|
// Handle both {data: [...]} and flat array/object
|
|
const list = raw && Array.isArray(raw.data) ? raw.data : Array.isArray(raw) ? raw : [raw].filter(Boolean);
|
|
const servers = list.filter(function (s) { return s && s.server; });
|
|
|
|
// Clear existing options
|
|
sel.innerHTML = "";
|
|
|
|
// Determine overall select styling based on lowest availability
|
|
var minAvail = Infinity;
|
|
servers.forEach(function (s) {
|
|
var a = parseInt(s["Seats Available"], 10) || 0;
|
|
if (a < minAvail) minAvail = a;
|
|
});
|
|
sel.classList.remove("low-seats", "no-seats");
|
|
if (minAvail <= 0) {
|
|
sel.classList.add("no-seats");
|
|
}
|
|
|
|
servers.forEach(function (s) {
|
|
const server = s.server || "";
|
|
const serverCode = s.code || server.toLowerCase().replace(/\s+/g, "-");
|
|
const available = parseInt(s["Seats Available"], 10) || 0;
|
|
const label = server.charAt(0).toUpperCase() + server.slice(1);
|
|
const opt = document.createElement("option");
|
|
opt.value = serverCode;
|
|
|
|
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";
|
|
} else {
|
|
// Plenty of seats
|
|
opt.textContent = label;
|
|
opt.style.color = "";
|
|
opt.style.fontWeight = "";
|
|
}
|
|
|
|
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
|
|
blog499: 499,
|
|
creator499: 499,
|
|
friends499: 499,
|
|
|
|
// 950
|
|
blog950: 950,
|
|
creator950: 950,
|
|
friends950: 950,
|
|
record950: 950,
|
|
|
|
// 1750
|
|
blog1750: 1750,
|
|
friends1750: 1750,
|
|
creator1750: 1750,
|
|
};
|
|
const PLAN_PRICES = { trainee: 499, junior: 950, senior: 1750 };
|
|
function centsToDollar(c) {
|
|
return "$" + (c / 100).toFixed(2);
|
|
}
|
|
function updateCouponBadges() {
|
|
var code = document
|
|
.getElementById("pricing-coupon")
|
|
.value.trim()
|
|
.toLowerCase();
|
|
var discount = COUPONS[code];
|
|
["trainee", "junior", "senior"].forEach(function (plan) {
|
|
var badge = document.getElementById("badge-" + plan);
|
|
if (discount !== undefined) {
|
|
var firstMonth = Math.max(0, PLAN_PRICES[plan] - discount);
|
|
badge.querySelector(".badge-price").textContent =
|
|
centsToDollar(firstMonth);
|
|
badge.classList.add("visible");
|
|
if (firstMonth === 0) badge.classList.add("free");
|
|
else badge.classList.remove("free");
|
|
} else {
|
|
badge.classList.remove("visible", "free");
|
|
badge.querySelector(".badge-price").textContent = "—";
|
|
}
|
|
});
|
|
}
|
|
document
|
|
.getElementById("pricing-coupon")
|
|
.addEventListener("input", updateCouponBadges);
|
|
|
|
/* ── Blog ──────────────────────────────────────────────── */
|
|
const BLOG_POSTS_PER_PAGE = 6;
|
|
let blogAllPosts = [];
|
|
let blogFilteredPosts = [];
|
|
let blogCurrentPage = 0;
|
|
|
|
function blogFormatDate(dateStr) {
|
|
const d = new Date(dateStr);
|
|
const months = [
|
|
"Jan",
|
|
"Feb",
|
|
"Mar",
|
|
"Apr",
|
|
"May",
|
|
"Jun",
|
|
"Jul",
|
|
"Aug",
|
|
"Sep",
|
|
"Oct",
|
|
"Nov",
|
|
"Dec",
|
|
];
|
|
return months[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
|
|
}
|
|
|
|
function blogAreaClass(area) {
|
|
var map = {
|
|
howto: "area-howto",
|
|
news: "area-news",
|
|
"video-scripts": "area-video-scripts",
|
|
"speed-run": "area-speed-run",
|
|
};
|
|
return map[area] || "area-default";
|
|
}
|
|
|
|
function blogRender() {
|
|
const start = blogCurrentPage * BLOG_POSTS_PER_PAGE;
|
|
const pagePosts = blogFilteredPosts.slice(start, start + BLOG_POSTS_PER_PAGE);
|
|
const grid = document.getElementById("blog-grid");
|
|
const totalPages = Math.max(
|
|
1,
|
|
Math.ceil(blogFilteredPosts.length / BLOG_POSTS_PER_PAGE),
|
|
);
|
|
|
|
if (pagePosts.length === 0) {
|
|
grid.innerHTML =
|
|
'<div class="blog-empty">No posts match your search.</div>';
|
|
document.getElementById("blog-page-info").textContent = "";
|
|
document.getElementById("blog-prev").disabled = true;
|
|
document.getElementById("blog-next").disabled = true;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = pagePosts
|
|
.map(function (p) {
|
|
var chip = p.agent
|
|
? '<span class="blog-card-chip">' + p.agent + "</span>"
|
|
: "";
|
|
var videoChip = p.video
|
|
? '<span class="blog-card-chip blog-card-chip-video">▶ video</span>'
|
|
: "";
|
|
return (
|
|
'<a href="' +
|
|
p.link +
|
|
'" class="blog-card" style="text-decoration:none" onclick="plausible(\'Blog Card Click\',{props:{post:\'' +
|
|
p.id.replace(/'/g, "\\'") +
|
|
"',area:'" +
|
|
p.area.replace(/'/g, "\\'") +
|
|
"'}})'>" +
|
|
'<span style="display:flex;gap:6px;flex-wrap:wrap;align-self:flex-start">' +
|
|
'<span class="blog-card-area ' +
|
|
blogAreaClass(p.area) +
|
|
'">' +
|
|
p.area +
|
|
"</span>" +
|
|
chip +
|
|
videoChip +
|
|
"</span>" +
|
|
'<span class="blog-card-date">' +
|
|
blogFormatDate(p.date) +
|
|
"</span>" +
|
|
"<h3>" +
|
|
p.headline +
|
|
"</h3>" +
|
|
"<p>" +
|
|
p.teaser +
|
|
"</p>" +
|
|
'<span class="blog-card-link">Read more →</span>' +
|
|
"</a>"
|
|
);
|
|
})
|
|
.join("");
|
|
|
|
document.getElementById("blog-page-info").textContent =
|
|
"Page " + (blogCurrentPage + 1) + " of " + totalPages;
|
|
document.getElementById("blog-prev").disabled = blogCurrentPage === 0;
|
|
document.getElementById("blog-next").disabled =
|
|
blogCurrentPage >= totalPages - 1;
|
|
}
|
|
|
|
function blogPrev() {
|
|
if (blogCurrentPage > 0) {
|
|
blogCurrentPage--;
|
|
blogRender();
|
|
}
|
|
}
|
|
|
|
function blogNext() {
|
|
const totalPages = Math.ceil(blogFilteredPosts.length / BLOG_POSTS_PER_PAGE);
|
|
if (blogCurrentPage < totalPages - 1) {
|
|
blogCurrentPage++;
|
|
blogRender();
|
|
}
|
|
}
|
|
|
|
function blogFilter() {
|
|
const q = document
|
|
.getElementById("blog-search-input")
|
|
.value.trim()
|
|
.toLowerCase();
|
|
if (!q) {
|
|
blogFilteredPosts = blogAllPosts.slice();
|
|
} else {
|
|
blogFilteredPosts = blogAllPosts.filter(function (p) {
|
|
return (
|
|
p.headline.toLowerCase().indexOf(q) !== -1 ||
|
|
p.teaser.toLowerCase().indexOf(q) !== -1 ||
|
|
p.area.toLowerCase().indexOf(q) !== -1
|
|
);
|
|
});
|
|
}
|
|
blogCurrentPage = 0;
|
|
blogRender();
|
|
}
|
|
|
|
// Load blog index
|
|
fetch("blog/index.json?_=" + Date.now())
|
|
.then(function (r) {
|
|
return r.json();
|
|
})
|
|
.then(function (data) {
|
|
blogAllPosts = data.posts || [];
|
|
blogFilteredPosts = blogAllPosts.slice();
|
|
blogRender();
|
|
})
|
|
.catch(function () {
|
|
document.getElementById("blog-grid").innerHTML =
|
|
'<div class="blog-empty">Blog coming soon.</div>';
|
|
});
|
|
|
|
// Search on input
|
|
document
|
|
.getElementById("blog-search-input")
|
|
.addEventListener("input", blogFilter);
|