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);
|
||||||
+63
-2807
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user