split + record
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,837 @@
|
||||
// ── 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
|
||||
|
||||
/* ── 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;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
/* ── 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>"
|
||||
: "";
|
||||
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 +
|
||||
"</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);
|
||||
Reference in New Issue
Block a user