Files
ODOO4projects.com/odoo/upsell.html
T
oliver 5f647c055a fix(analytics): put the Buy class on the <form> so the goal actually fires
Plausible's click handler walks up from the clicked element and bails out the
moment it meets a <form>, so a plausible-event-name=Buy class on a
<button type="submit"> inside a plain form never fires Buy — the tracker sends
its automatic "Form: Submission" event instead and the goal records nothing.
Measured live on /odoo-erp-automation-bootcamp/ before this change:
{"n":"Form: Submission"}.

- odoo-erp-automation-bootcamp/index.html (3 CTAs): class moved from the
  <button type="submit"> to the enclosing <form>.
- australia.html (2 CTAs): same move.
- de|es/odoo-erp-automation-bootcamp/index.html (3 forms each): the buy forms
  carried no Buy marking at all, so the DE and ES bootcamp sales were invisible
  to the goal. Class added to the <form>.
- odoo/upsell.html: the three .plan-btn buttons drive the same JS buy path and
  were unmarked. A class on a plain <button> (not a form submit) does fire, so
  it was added there and browser-verified. The disabled Trial button is left
  unmarked.

Class attribute only: every button label, every form action and every basket
payload is byte-identical. No CSS or JS rule references the class.

Verified in a real browser (fetch shimmed, tracker's localhost/automation gates
bypassed): EN bootcamp 3/3, australia 2/2, DE 3/3, ES 3/3, upsell 1/1 clicks
each send exactly one {"n":"Buy"} to /api/event and never "Form: Submission".
2026-10-28 22:37:32 +00:00

497 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Your Plan</title>
<style>
body {
font-family: "Inter", "Segoe UI", Arial, sans-serif;
background-color: #f4f5f7;
color: #333;
margin: 0;
padding: 32px 16px;
}
.page-title {
text-align: center;
color: #262626;
font-size: 1.45em;
font-weight: 700;
margin-bottom: 6px;
}
.page-subtitle {
text-align: center;
font-size: 1.05em;
color: #444;
margin-bottom: 32px;
letter-spacing: 0.01em;
line-height: 1.7;
font-weight: 400;
}
.plans-row {
display: flex;
flex-wrap: wrap;
gap: 18px;
justify-content: center;
align-items: stretch;
}
.plan-card {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 26px 22px 20px;
width: 200px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
transition:
box-shadow 0.2s,
border-color 0.2s,
transform 0.2s;
}
.plan-card:hover {
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.11);
transform: scale(1.03);
}
#card-ontherise {
border: 3px dotted #e0e0e0;
}
.plan-card.current {
border-color: #4caf50;
box-shadow:
0 0 0 3px rgba(76, 175, 80, 0.2),
0 4px 16px rgba(76, 175, 80, 0.15);
}
.plan-badge {
position: absolute;
top: -13px;
left: 50%;
transform: translateX(-50%);
background: #4caf50;
color: #fff;
font-size: 0.68em;
font-weight: 700;
padding: 3px 11px;
border-radius: 20px;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(76, 175, 80, 0.35);
letter-spacing: 0.01em;
}
.plan-name {
font-size: 1.05em;
font-weight: 700;
color: #262626;
margin-bottom: 14px;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.plan-price {
font-size: 1.55em;
font-weight: 800;
color: #875a7b;
line-height: 1.1;
margin-bottom: 2px;
}
.plan-price-period {
font-size: 0.75em;
color: #aaa;
margin-bottom: 14px;
}
.plan-tagline {
font-size: 0.8em;
color: #666;
font-style: italic;
margin-bottom: 16px;
line-height: 1.5;
min-height: 52px;
display: flex;
align-items: center;
justify-content: center;
}
.plan-btn {
background-color: #875a7b;
color: #fff;
border: none;
border-radius: 7px;
padding: 9px 0;
font-size: 0.88em;
font-weight: 600;
cursor: pointer;
width: 100%;
margin-bottom: 18px;
transition:
background-color 0.2s,
transform 0.1s;
letter-spacing: 0.01em;
}
.plan-btn:hover:not(:disabled) {
background-color: #744e6a;
transform: translateY(-1px);
}
.plan-btn:disabled {
background-color: #bdbdbd;
color: #fff;
cursor: not-allowed;
transform: none;
}
.plan-btn.is-current-plan:disabled {
background-color: #4caf50;
}
.plan-features {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
text-align: left;
}
.plan-features li {
font-size: 0.78em;
color: #555;
padding: 5px 0 5px 18px;
border-top: 1px solid #f2f2f2;
position: relative;
line-height: 1.4;
}
.plan-features li:first-child {
border-top: none;
}
.plan-features li::before {
content: "–";
position: absolute;
left: 0;
color: #875a7b;
font-weight: 700;
}
.plan-guarantee {
font-size: 0.68em;
color: #bbb;
margin-top: auto;
padding-top: 14px;
font-style: italic;
min-height: 18px;
}
.footer-uuid {
text-align: center;
font-size: 0.75em;
color: #ccc;
margin-top: 40px;
user-select: all;
}
</style>
<!-- Privacy-friendly analytics by Plausible -->
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
<script>
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
plausible.init()
</script>
</head>
<body>
<div class="page-subtitle">
You <b>immediately</b> unlock all new features and higher limits.<br />
Any extra time is <b>added</b> to your existing subscription.
</div>
<div class="plans-row" id="plansRow">
<!-- ── Trial ─────────────────────────────────────────────────── -->
<div class="plan-card" id="card-trial">
<div class="plan-name">Trial</div>
<div class="plan-price">30 days free</div>
<div class="plan-price-period">&nbsp;</div>
<div class="plan-tagline">
"I want to see what ODOO can do for my company."
</div>
<button
class="plan-btn is-current-plan"
id="btn-trial"
disabled
>
Trial
</button>
<ul class="plan-features">
<li>ODOO CE hosting</li>
<li>0 Backup Slots</li>
<li>No Domain Mapping</li>
<li>No module installation</li>
</ul>
<div class="plan-guarantee">&nbsp;</div>
</div>
<!-- ── Side Hustle ───────────────────────────────────────────── -->
<div class="plan-card" id="card-sidehustle">
<div class="plan-name">Side Hustle</div>
<div class="plan-price">$214.00</div>
<div class="plan-price-period">/ year</div>
<div class="plan-tagline">
"ODOO Standard .. that is all my business needs"
</div>
<button class="plan-btn plausible-event-name=Buy" id="btn-sidehustle">
Upgrade Now
</button>
<ul class="plan-features">
<li>ODOO CE hosting</li>
<li>Space for 1 Backup</li>
<li>Connect 1 domain with SSL</li>
<li>No module installation</li>
<li>One Year of Sercice</li>
</ul>
</div>
<!-- ── On the Rise ───────────────────────────────────────────── -->
<div class="plan-card" id="card-ontherise">
<div class="plan-name">On the Rise</div>
<div class="plan-price">$395.00</div>
<div class="plan-price-period">/ year</div>
<div class="plan-tagline">
"I need some modules and professional reports"
</div>
<button class="plan-btn plausible-event-name=Buy" id="btn-ontherise">Upgrade Now</button>
<ul class="plan-features">
<li>ODOO CE hosting</li>
<li>Space for 4 Backups</li>
<li>Connect 3 domain with SSL</li>
<li>Install custom modules</li>
<li>One Year of Sercice</li>
</ul>
</div>
<!-- ── Powerhouse ────────────────────────────────────────────── -->
<div class="plan-card" id="card-powerhouse">
<div class="plan-name">Powerhouse</div>
<div class="plan-price">$595.00</div>
<div class="plan-price-period">/ year</div>
<div class="plan-tagline">
"ODOO Enterprise is a must have!"
</div>
<button class="plan-btn plausible-event-name=Buy" id="btn-powerhouse">
Upgrade Now
</button>
<ul class="plan-features">
<li>All of On the Rise</li>
<li>ODOO Enterprise Ready</li>
<li>2 Workers</li>
<li>One Year of Sercice</li>
</ul>
</div>
</div>
<div class="footer-uuid">UUID: <span id="uuidText">–</span></div>
<script>
// ── Webhooks ─────────────────────────────────────────────────────────────
const getContract_Webhook =
"https://n8n.odoo4projects.com/webhook/ODOO4projects/upsell";
// ── Plan hierarchy (index = tier, lower = cheaper) ────────────────────────
const PLAN_ORDER = [
"Trial",
"Side Hustle",
"On the Rise",
"Powerhouse",
];
const PLAN_META = {
Trial: {
cardId: "card-trial",
btnId: "btn-trial",
productId: null,
},
"Side Hustle": {
cardId: "card-sidehustle",
btnId: "btn-sidehustle",
productId: "sidehustle",
},
"On the Rise": {
cardId: "card-ontherise",
btnId: "btn-ontherise",
productId: "ontherise",
},
Powerhouse: {
cardId: "card-powerhouse",
btnId: "btn-powerhouse",
productId: "powerhouse",
},
};
// ── UUID from URL ─────────────────────────────────────────────────────────
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid") || "";
document.getElementById("uuidText").textContent = uuid || "–";
// ── Product ID mapping ─────────────────────────────────────────
const PRODUCT_IDS = {
sidehustle: 6,
ontherise: 4,
powerhouse: 5,
};
// ── UTM tracking (source / medium / campaign) ─────────────────────────────
const UTM_KEYS = ["source", "medium", "campaign"];
function getCookie(name) {
const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
return match ? decodeURIComponent(match[2]) : null;
}
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie =
name + "=" + encodeURIComponent(value) +
"; expires=" + date.toUTCString() +
"; path=/; SameSite=Lax";
}
UTM_KEYS.forEach(function (key) {
const val = params.get("utm_" + key);
if (val) setCookie("utm_" + key, val, 90);
});
function appendUTM(form) {
UTM_KEYS.forEach(function (key) {
const val = getCookie("utm_" + key);
if (val) {
const input = document.createElement("input");
input.type = "hidden";
input.name = "utm_" + key;
input.value = val;
form.appendChild(input);
}
});
}
// ── Buy handler ───────────────────────────────────────────────────────────
function buyProduct(productId) {
if (!uuid) {
alert("Missing uuid parameter in URL.");
return;
}
var pid = PRODUCT_IDS[productId];
if (!pid) {
alert("Unknown product.");
return;
}
var form = document.createElement("form");
form.method = "POST";
form.action = "https://20.odoo4projects.com/shop/post_basket";
form.target = "_blank";
form.style.display = "none";
var basket = document.createElement("input");
basket.type = "hidden";
basket.name = "basket";
var item = { product_id: pid, qty: 1, info: uuid };
basket.value = JSON.stringify([item]);
form.appendChild(basket);
var clear = document.createElement("input");
clear.type = "hidden";
clear.name = "clear";
clear.value = "1";
form.appendChild(clear);
appendUTM(form);
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}
// ── Wire buy buttons ──────────────────────────────────────────────────────
document
.getElementById("btn-sidehustle")
.addEventListener("click", () => buyProduct("sidehustle"));
document
.getElementById("btn-ontherise")
.addEventListener("click", () => buyProduct("ontherise"));
document
.getElementById("btn-powerhouse")
.addEventListener("click", () => buyProduct("powerhouse"));
// ── Apply current-plan state ──────────────────────────────────────────────
function applyPlanState(contractName, expires) {
const currentIdx = PLAN_ORDER.indexOf(contractName);
if (currentIdx === -1) return; // unknown plan – leave defaults
const badgeLabel = expires ? "✅ Expires " + expires : "✅";
PLAN_ORDER.forEach(function (plan, idx) {
var meta = PLAN_META[plan];
if (!meta) return;
var btn = document.getElementById(meta.btnId);
var card = document.getElementById(meta.cardId);
if (!btn || !card) return;
if (idx === currentIdx) {
// Current plan – green frame, badge with expiry date
card.classList.add("current");
var badge = document.createElement("div");
badge.className = "plan-badge";
badge.textContent = badgeLabel;
card.insertBefore(badge, card.firstChild);
// Trial is always green with no text – skip button changes
if (plan !== "Trial") {
btn.textContent = "Add 1 Year";
btn.disabled = false;
btn.classList.add("is-current-plan");
}
} else if (idx < currentIdx) {
// Lower tier – hide entirely
card.style.display = "none";
}
// Higher tiers: leave as "Upgrade Now" (enabled)
});
}
// ── Load contract from webhook ────────────────────────────────────────────
async function loadContract() {
if (!uuid) return;
try {
const res = await fetch(getContract_Webhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uuid }),
});
if (!res.ok) throw new Error("HTTP " + res.status);
const payload = await res.json();
// Webhook returns: { "expires": "YYYY-MM-DD", "contractName": "Trial" }
// (tolerate an array wrapper and snake_case keys as well)
const record = Array.isArray(payload)
? payload[0]
: payload;
const contractName =
record && (record.contractName || record.contract_name);
if (contractName) {
applyPlanState(contractName, record.expires);
}
} catch (err) {
console.error("Could not load contract data:", err);
// Graceful degradation: page remains usable with default button states
}
}
loadContract();
</script>
</body>
</html>