Compare commits

...

62 Commits

Author SHA1 Message Date
Oliver
d4482e753f mini 2025-11-21 10:44:49 -03:00
Oliver
02f4529bb9 min 2025-11-21 10:41:45 -03:00
Oliver
bc613d668f change 2025-11-21 10:31:08 -03:00
Oliver
6336cc171b scholarix 2025-11-13 08:52:44 -03:00
Oliver
82c97e721e meppel 2025-10-25 05:44:21 -03:00
Oliver
f9b40e6437 unhide 2025-10-24 07:54:46 -03:00
Oliver
1035e0207f text 2025-10-23 12:00:30 -03:00
Oliver
0e624c3dd4 mini 2025-10-23 11:57:51 -03:00
Oliver
170b7e396d hidden 2025-10-23 11:56:33 -03:00
Oliver
95d2c29e7d affiliate Modeal 2025-10-23 11:43:54 -03:00
Oliver
2937c00916 minify 2025-10-21 05:22:17 -03:00
Oliver
7119c7fc28 spelling 2025-10-19 19:46:47 -03:00
Oliver
c949b285d1 saopaulo 2025-10-19 17:41:23 -03:00
Oliver
2a75bb6b40 add uuid to first message 2025-10-18 12:03:03 -03:00
Oliver
08e01e17bb add uuid to first message 2025-10-18 12:00:42 -03:00
Oliver
23c78b3d78 update 2025-10-12 18:04:27 -03:00
Oliver
e1642f421f product id 2025-10-12 15:37:43 -03:00
Oliver
d4bf1cfe3a added upsell 2025-10-12 12:21:49 -03:00
Oliver
1b16ea225e new upsell 2025-10-12 09:06:37 -03:00
Oliver
67db593d33 v 2025-10-09 17:23:37 -03:00
Oliver
8044cd5c2d try 2025-10-09 08:37:53 -03:00
Oliver
34a2ba3b05 try 2025-10-09 08:32:20 -03:00
Oliver
bd26988584 cookie 2025-10-09 08:14:28 -03:00
Oliver
53d61fbaa6 utm 2025-10-08 16:44:39 -03:00
Oliver
f593aca58e hook 2025-10-08 15:46:06 -03:00
Oliver
32a3abaacb defaults 2025-10-08 07:44:41 -03:00
Oliver
e201366360 fix 2025-10-08 07:14:09 -03:00
Oliver
1fc2587518 all 2025-10-08 06:51:18 -03:00
Oliver
f3f58b3371 minify 2025-10-08 06:49:44 -03:00
Oliver
f2aa3a3e1b UTM 2025-10-07 19:48:56 -03:00
Oliver
9d9744a945 Revert "fix"
This reverts commit 9d99cd572a.
2025-10-07 16:41:12 -03:00
Oliver
9d99cd572a fix 2025-10-07 16:39:03 -03:00
Oliver
538f73bbb2 id 2025-10-07 10:17:57 -03:00
Oliver
a01142dfe4 ref 2025-10-07 10:11:14 -03:00
Oliver
fe02208c98 text 2025-10-06 17:14:20 -03:00
Oliver
028a4418ea text 2025-10-06 17:09:58 -03:00
Oliver
8fa4a9467b emojis 2025-10-06 17:08:45 -03:00
Oliver
fad1033a48 FAQ 2025-10-06 17:02:02 -03:00
Oliver
018e60896c FAQ 2025-10-06 16:59:23 -03:00
Oliver
ff60112ddf typo 2025-10-04 17:13:07 -03:00
Oliver
c2ca5fe8e8 fav 2025-10-04 17:08:31 -03:00
Oliver
a6bea0e6c8 UUID 2025-10-04 16:48:57 -03:00
Oliver
3d263f7c4e new workflow 2025-09-30 12:08:50 -03:00
Oliver
ef3d04d284 add cost 2025-09-30 12:03:12 -03:00
Oliver
1258df94c2 . 2025-09-30 07:55:35 -03:00
Oliver
8c20ec66c8 . 2025-09-30 07:51:37 -03:00
Oliver
65e0b2b177 style 2025-09-30 07:00:00 -03:00
Oliver
57e0b94717 style 2025-09-30 06:56:44 -03:00
Oliver
19c65a9326 style 2025-09-30 06:54:45 -03:00
Oliver
049fc274b0 style 2025-09-30 06:52:28 -03:00
Oliver
5735765ac8 style 2025-09-30 06:43:52 -03:00
Oliver
f330f388ce remove 2025-09-29 18:08:59 -03:00
Oliver
bb7fe00605 remove 2025-09-29 18:07:30 -03:00
Oliver
a2ae8f55ac f 2025-09-29 17:16:08 -03:00
Oliver
c01c03da86 f 2025-09-29 17:11:30 -03:00
Oliver
ca5d552a86 credentials 2025-09-29 17:10:02 -03:00
Oliver
7958993195 expires 2025-09-29 13:26:42 -03:00
Oliver
a9a1aa23b0 expires 2025-09-29 13:23:49 -03:00
Oliver
17a79c5343 post 2025-09-29 12:57:38 -03:00
Oliver
d9d45f8dde debug 2025-09-29 12:43:12 -03:00
Oliver
ed8734ff76 debug 2025-09-29 12:33:40 -03:00
Oliver
f59f546d37 new URL 2025-09-29 12:23:13 -03:00
21 changed files with 1878 additions and 208 deletions

96
public/affiliateModal.js Normal file
View File

@@ -0,0 +1,96 @@
// --- Create Affiliate Modal ---
function createAffiliateModal() {
const modal = document.createElement("div");
modal.id = "affiliateBuilder";
Object.assign(modal.style, {
position: "fixed",
top: "0",
left: "0",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.6)",
display: "none",
justifyContent: "center",
alignItems: "center",
zIndex: "1000"
});
modal.innerHTML = `
<div style="background: #fff; padding: 40px 30px; border-radius: 16px; max-width: 500px; width: 90%; position: relative; font-family: 'Arial', sans-serif; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">
<span id="closeAffiliateModal" style="position: absolute; top: 15px; right: 20px; cursor: pointer; font-weight: bold; font-size: 24px; color: #555;">&times;</span>
<h2 style="margin-bottom: 15px; font-size: 24px; color: #333;">Affiliate Builder</h2>
<div style="margin-bottom: 15px;">
<label style="display:block; margin-bottom:5px; color:#555;">Your affiliate code provided by us:</label>
<input type="text" id="affiliateCode" placeholder="Enter code" style="width:100%; padding:12px 15px; border:1px solid #ccc; border-radius:8px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display:block; margin-bottom:5px; color:#555;">For your reference your campaign name:</label>
<input type="text" id="affiliateCampaign" placeholder="Enter campaign" style="width:100%; padding:12px 15px; border:1px solid #ccc; border-radius:8px;">
</div>
<div style="display:flex; gap:10px; align-items:center; margin-bottom: 15px;">
<input type="text" id="affiliateLink" readonly style="flex:1; padding:12px 15px; border:1px solid #ccc; border-radius:8px; background:#f9f9f9;">
<button id="copyAffiliateLink" style="padding:12px 20px; background:#007BFF; color:#fff; border:none; border-radius:8px; cursor:pointer;">Copy</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Close modal
document.getElementById("closeAffiliateModal").onclick = () => modal.style.display = "none";
modal.onclick = e => { if (e.target === modal) modal.style.display = "none"; };
// Build link when inputs change
const codeInput = document.getElementById("affiliateCode");
const campaignInput = document.getElementById("affiliateCampaign");
const linkInput = document.getElementById("affiliateLink");
[codeInput, campaignInput].forEach(input => {
input.addEventListener("input", () => {
const code = codeInput.value.trim() || "affiliate";
const campaign = campaignInput.value.trim() || "default";
linkInput.value = `https://ODOO4projects.com?utm_source=${encodeURIComponent(code)}&utm_campaign=${encodeURIComponent(campaign)}`;
});
});
// Copy to clipboard
document.getElementById("copyAffiliateLink").addEventListener("click", () => {
linkInput.select();
document.execCommand("copy");
alert("Affiliate link copied!");
});
return modal;
}
// --- Open Affiliate Modal ---
function openAffiliateModal() {
const modal = document.getElementById("affiliateBuilder");
modal.style.display = "flex";
// Reset fields
document.getElementById("affiliateCode").value = "";
document.getElementById("affiliateCampaign").value = "";
document.getElementById("affiliateLink").value = "";
}
// --- Attach Affiliate Builder Buttons ---
function attachAffiliateButtons() {
const buttons = Array.from(document.querySelectorAll("button, a"));
buttons.forEach(btn => {
const text = btn.textContent.trim();
if (text === "Start Selling") {
btn.addEventListener("click", (e) => {
e.preventDefault();
openAffiliateModal();
});
}
});
}
// --- Initialize Affiliate Builder ---
document.addEventListener("DOMContentLoaded", () => {
createAffiliateModal();
attachAffiliateButtons();
});

1
public/affiliateModal.min.js vendored Normal file
View File

@@ -0,0 +1 @@
function createAffiliateModal(){const e=document.createElement("div");e.id="affiliateBuilder",Object.assign(e.style,{position:"fixed",top:"0",left:"0",width:"100%",height:"100%",backgroundColor:"rgba(0,0,0,0.6)",display:"none",justifyContent:"center",alignItems:"center",zIndex:"1000"}),e.innerHTML='\n <div style="background: #fff; padding: 40px 30px; border-radius: 16px; max-width: 500px; width: 90%; position: relative; font-family: \'Arial\', sans-serif; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">\n <span id="closeAffiliateModal" style="position: absolute; top: 15px; right: 20px; cursor: pointer; font-weight: bold; font-size: 24px; color: #555;">&times;</span>\n <h2 style="margin-bottom: 15px; font-size: 24px; color: #333;">Affiliate Builder</h2>\n \n <div style="margin-bottom: 15px;">\n <label style="display:block; margin-bottom:5px; color:#555;">Your affiliate code provided by us:</label>\n <input type="text" id="affiliateCode" placeholder="Enter code" style="width:100%; padding:12px 15px; border:1px solid #ccc; border-radius:8px;">\n </div>\n <div style="margin-bottom: 15px;">\n <label style="display:block; margin-bottom:5px; color:#555;">For your reference your campaign name:</label>\n <input type="text" id="affiliateCampaign" placeholder="Enter campaign" style="width:100%; padding:12px 15px; border:1px solid #ccc; border-radius:8px;">\n </div>\n\n <div style="display:flex; gap:10px; align-items:center; margin-bottom: 15px;">\n <input type="text" id="affiliateLink" readonly style="flex:1; padding:12px 15px; border:1px solid #ccc; border-radius:8px; background:#f9f9f9;">\n <button id="copyAffiliateLink" style="padding:12px 20px; background:#007BFF; color:#fff; border:none; border-radius:8px; cursor:pointer;">Copy</button>\n </div>\n </div>\n ',document.body.appendChild(e),document.getElementById("closeAffiliateModal").onclick=()=>e.style.display="none",e.onclick=t=>{t.target===e&&(e.style.display="none")};const t=document.getElementById("affiliateCode"),i=document.getElementById("affiliateCampaign"),n=document.getElementById("affiliateLink");return[t,i].forEach(e=>{e.addEventListener("input",()=>{const e=t.value.trim()||"affiliate",o=i.value.trim()||"default";n.value=`https://ODOO4projects.com?utm_source=${encodeURIComponent(e)}&utm_campaign=${encodeURIComponent(o)}`})}),document.getElementById("copyAffiliateLink").addEventListener("click",()=>{n.select(),document.execCommand("copy"),alert("Affiliate link copied!")}),e}function openAffiliateModal(){document.getElementById("affiliateBuilder").style.display="flex",document.getElementById("affiliateCode").value="",document.getElementById("affiliateCampaign").value="",document.getElementById("affiliateLink").value=""}function attachAffiliateButtons(){Array.from(document.querySelectorAll("button, a")).forEach(e=>{"Start Selling"===e.textContent.trim()&&e.addEventListener("click",e=>{e.preventDefault(),openAffiliateModal()})})}document.addEventListener("DOMContentLoaded",()=>{createAffiliateModal(),attachAffiliateButtons()});

View File

@@ -0,0 +1,214 @@
/* ================= Chat Toggle Button ================= */
#cw-chatToggle {
position: fixed;
bottom: 24px;
right: 24px;
width: 100px;
height: 50px;
border: none;
border-radius: 25px;
padding: 0;
background: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
z-index: 100000;
box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px,
rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
transition: transform 0.3s ease;
}
#cw-chatToggle:hover {
transform: translateY(-5px);
}
#cw-chatToggle img {
width: 80%;
height: 80%;
object-fit: contain;
display: block;
user-select: none;
}
/* ================= Chat Widget ================= */
#cw-chatWidget {
display: none;
position: fixed;
bottom: 100px;
right: 24px;
width: 500px;
max-height: 70vh;
background: white;
border: 1px solid #ccc;
border-radius: 12px;
flex-direction: column;
z-index: 100000;
font-family: system-ui, sans-serif;
overflow: hidden;
}
#cw-chatWidget.active {
display: flex;
}
#cw-chatWidget .cw-header {
display: flex;
justify-content: center;
padding: 16px 0 10px 0;
background: #f8f9f9;
}
#cw-chatWidget .cw-header img {
width: 80%;
height: auto;
object-fit: contain;
user-select: none;
}
/* Status Bar */
#cw-status {
padding: 10px;
background-color: #f0f0f0;
border-bottom: 1px solid #ddd;
max-height: 0;
transition: max-height 0.5s ease, padding 0.5s ease;
}
/* Messages */
#cw-chatMessages {
flex: 1;
overflow-y: auto;
padding: 16px;
font-size: 0.875rem;
line-height: 1.4;
background: #fafafa;
scroll-behavior: smooth;
}
/* Form */
#cw-chatForm {
display: flex;
border-top: 1px solid #ddd;
}
#cw-chatInput {
flex: 1;
border: none;
padding: 8px 20px;
font-size: 1rem;
border-radius: 0 0 0 12px;
outline: none;
}
#cw-chatForm button {
background-color: #0070c0;
border: none;
color: white;
padding: 0 16px;
cursor: pointer;
font-weight: 600;
border-radius: 0 0 12px 0;
transition: background-color 0.3s ease;
}
#cw-chatForm button:hover {
background-color: #005a9e;
}
/* Messages Bubbles */
.cw-message {
max-width: 75%;
margin-bottom: 8px;
padding: 8px 12px;
border-radius: 12px;
word-wrap: break-word;
white-space: pre-wrap;
}
.cw-message.user {
background-color: #DCF8C6;
color: #333;
margin-left: auto;
text-align: right;
}
.cw-message.bot {
background-color: #E0E0E0;
color: #333;
margin-right: auto;
text-align: left;
}
/* ================= Wizard ================= */
.wizard-headline {
text-align: center;
font-size: 24px;
font-weight: bold;
padding: 20px 0;
background-color: #f8f9f9;
color: #2c3e50;
border-bottom: 2px solid #ddd;
}
.wizard {
display: flex;
justify-content: space-between;
list-style: none;
padding: 0;
margin: 0;
width: 100%;
background-color: #e0e0e0;
}
.wizard li {
flex: 1;
display: flex;
flex-direction: column;
border-right: 2px solid #fff;
color: white;
box-sizing: border-box;
}
.wizard li:last-child {
border-right: none;
}
.wizard .header {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
padding: 10px;
font-weight: bold;
font-size: 16px;
text-align: center;
}
.wizard .text {
padding: 20px 10px;
text-align: center;
font-size: 14px;
flex-grow: 1;
}
/* Wizard state colors */
.wizard li.open {
background-color: #7f8c8d;
}
.wizard li.open .header {
background-color: #5d6d7e;
}
.wizard li.done {
background-color: #1e8449;
}
.wizard li.done .header {
background-color: #145a32;
}
.wizard i {
font-size: 18px;
}

View File

@@ -0,0 +1,127 @@
// ================= CONFIG =================
//const baseUrl = 'http://localhost:8000'; // Base URL for all resources
const baseUrl = 'https://static.odoo4projects.com/agent-odoo4projects';
const config = {
cssUrl: `${baseUrl}/agent.css`, // CSS file
api: `https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/702862fd-dd17-4a34-8efb-e9056d2c50df/chat`, // Backend webhook endpoint
buttonImage: `${baseUrl}/images/4.svg`, // Chat toggle button image
logoImage: `${baseUrl}/images/logo.svg`, // Logo image for chat widget
preamble: '🤔 This is the ODOO4Projects sales agent 🛠️. Besides Sales, he can help revert module installations or restart your server using your UUID and a confirmation email. 📬 For human follow-up, leave your email.' // Initial bot message
};
// ================= WIDGET HTML =================
const widgetHTML = `
<div id="cw-chatWidget">
<div class="cw-header">
<img src="${config.logoImage}" alt="Logo" class="cw-logo"/>
</div>
<div id="cw-chatMessages" class="cw-messages"></div>
<form id="cw-chatForm" class="cw-form">
<input type="text" id="cw-chatInput" placeholder="Type your message..." />
<button type="submit">Send</button>
</form>
<div id="cw-status" class="cw-status"></div>
</div>
<button id="cw-chatToggle">
<img src="${config.buttonImage}" alt="Chat"/>
</button>
`;
// ================= UTILS =================
function loadCSS(url) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
}
function appendMessage(chatMessages, text, sender) {
const msg = document.createElement('div');
msg.classList.add('cw-message', sender === 'user' ? 'user' : 'bot');
msg.innerHTML = text;
chatMessages.appendChild(msg);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// ================= MAIN =================
document.addEventListener('DOMContentLoaded', () => {
try {
// Load CSS
loadCSS(config.cssUrl);
// Inject chat widget HTML
document.body.insertAdjacentHTML('beforeend', widgetHTML);
document.dispatchEvent(new CustomEvent('JSsucks'));
// ================= DOM REFERENCES =================
const chatToggle = document.getElementById('cw-chatToggle');
const chatWidget = document.getElementById('cw-chatWidget');
const chatForm = document.getElementById('cw-chatForm');
const chatInput = document.getElementById('cw-chatInput');
const chatMessages = document.getElementById('cw-chatMessages');
const statusDiv = document.getElementById('cw-status');
const chatid = crypto.randomUUID();
let chatOpened = false;
// ================= BOT COMMUNICATION =================
async function sendMessageToBot(messageText) {
chatWidget.classList.add('active');
appendMessage(chatMessages, messageText, 'user');
chatInput.value = '';
chatInput.focus();
try {
const res = await fetch(config.api, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'sendMessage',
chatid,
chatInput: messageText
})
});
const data = await res.json();
if (data.status) {
statusDiv.innerHTML = data.status;
requestAnimationFrame(() => {
statusDiv.style.maxHeight = statusDiv.scrollHeight + 'px';
statusDiv.style.padding = '10px';
});
} else {
statusDiv.style.maxHeight = '0';
statusDiv.style.padding = '0 10px';
}
appendMessage(chatMessages, data.output, 'bot');
} catch (error) {
appendMessage(chatMessages, 'Fehler beim Verbinden mit dem Server. Bitte versuchen Sie es später erneut.', 'bot');
console.error(error);
}
}
window.sendMessageToBot = sendMessageToBot;
// ================= EVENT LISTENERS =================
chatToggle.addEventListener('click', () => {
const isVisible = chatWidget.classList.contains('active');
chatWidget.classList.toggle('active', !isVisible);
if (!isVisible && !chatOpened) {
appendMessage(chatMessages, config.preamble, 'bot');
chatOpened = true;
}
});
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const input = chatInput.value.trim();
if (input) sendMessageToBot(input);
});
} catch (err) {
console.error('Failed to initialize chat widget:', err);
}
});

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="26.458334mm"
height="26.458334mm"
viewBox="0 0 26.458334 26.458334"
version="1.1"
id="svg351"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="4.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview353"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2"
inkscape:cx="141.75"
inkscape:cy="-67.25"
inkscape:window-width="1920"
inkscape:window-height="1127"
inkscape:window-x="1920"
inkscape:window-y="37"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs348" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-27.042028,-166.7327)">
<g
id="g22"
transform="matrix(0.15346171,0,0,0.15347061,-71.664544,106.29597)">
<polygon
class="st1"
points="713.07,486.53 750.82,441.47 742.83,486.53 "
id="polygon14"
style="fill:#ed703e" />
<polygon
class="st1"
points="752.1,393.8 669.3,488.25 663.26,523.19 733.12,523.19 725.6,566.2 643.2,566.2 643.2,393.8 "
id="polygon16"
style="fill:#ed703e" />
<polygon
class="st1"
points="798.47,393.8 815.61,393.8 815.61,486.53 782.29,486.53 "
id="polygon18"
style="fill:#ed703e" />
<polygon
class="st1"
points="767.94,566.2 775.83,523.19 815.61,523.19 815.61,566.2 "
id="polygon20"
style="fill:#ed703e" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 671.21002 172.39999"
xml:space="preserve"
sodipodi:docname="logo-01.svg"
width="671.21002"
height="172.39999"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs47" /><sodipodi:namedview
id="namedview45"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="0.86041667"
inkscape:cx="335.30266"
inkscape:cy="86.004843"
inkscape:window-width="1920"
inkscape:window-height="1007"
inkscape:window-x="0"
inkscape:window-y="37"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:#68696D;}
.st1{fill:#ED703E;}
</style>
<g
id="g42"
transform="translate(-144.4,-393.8)">
<g
id="g8">
<path
class="st0"
d="m 427.65,509.26 c -9.27,-5.19 -16.61,-12.39 -22.02,-21.6 -5.42,-9.21 -8.12,-19.62 -8.12,-31.23 0,-11.61 2.71,-22.02 8.12,-31.23 5.41,-9.21 12.75,-16.38 22.02,-21.52 9.26,-5.13 19.42,-7.7 30.47,-7.7 11.16,0 21.35,2.57 30.56,7.7 9.21,5.14 16.49,12.31 21.85,21.52 5.36,9.21 8.04,19.62 8.04,31.23 0,11.61 -2.68,22.02 -8.04,31.23 -5.36,9.21 -12.67,16.41 -21.93,21.6 -9.27,5.19 -19.42,7.79 -30.47,7.79 -11.06,-0.01 -21.22,-2.6 -30.48,-7.79 z m 50.31,-30.9 c 4.86,-5.47 7.28,-12.78 7.28,-21.93 0,-9.38 -2.43,-16.77 -7.28,-22.19 -4.86,-5.41 -11.47,-8.12 -19.84,-8.12 -8.48,0 -15.13,2.71 -19.93,8.12 -4.8,5.42 -7.2,12.81 -7.2,22.19 0,9.27 2.4,16.61 7.2,22.02 4.8,5.42 11.44,8.12 19.93,8.12 8.37,0 14.99,-2.74 19.84,-8.21 z"
id="path4" />
<path
class="st0"
d="m 528.4,509.26 c -9.27,-5.19 -16.61,-12.39 -22.02,-21.6 -5.42,-9.21 -8.12,-19.62 -8.12,-31.23 0,-11.61 2.71,-22.02 8.12,-31.23 5.41,-9.21 12.75,-16.38 22.02,-21.52 9.26,-5.13 19.42,-7.7 30.47,-7.7 11.16,0 21.35,2.57 30.56,7.7 9.21,5.14 16.49,12.31 21.85,21.52 5.36,9.21 8.04,19.62 8.04,31.23 0,11.61 -2.68,22.02 -8.04,31.23 -5.36,9.21 -12.67,16.41 -21.93,21.6 -9.27,5.19 -19.42,7.79 -30.47,7.79 -11.05,-0.01 -21.21,-2.6 -30.48,-7.79 z m 50.32,-30.9 c 4.86,-5.47 7.28,-12.78 7.28,-21.93 0,-9.38 -2.43,-16.77 -7.28,-22.19 -4.86,-5.41 -11.47,-8.12 -19.84,-8.12 -8.48,0 -15.13,2.71 -19.93,8.12 -4.8,5.42 -7.2,12.81 -7.2,22.19 0,9.27 2.4,16.61 7.2,22.02 4.8,5.42 11.44,8.12 19.93,8.12 8.37,0 14.98,-2.74 19.84,-8.21 z"
id="path6" />
</g>
<path
class="st0"
d="M 260.51,423.82 C 255,414.35 247.52,407 238.07,401.72 c -9.47,-5.28 -19.93,-7.92 -31.39,-7.92 -11.35,0 -21.79,2.64 -31.32,7.92 -9.52,5.28 -17.05,12.64 -22.62,22.1 -5.57,9.47 -8.34,20.16 -8.34,32.08 0,11.94 2.77,22.63 8.34,32.08 5.56,9.47 13.09,16.86 22.62,22.19 9.52,5.33 19.96,8 31.32,8 11.35,0 21.78,-2.67 31.3,-8 9.52,-5.33 17.03,-12.73 22.54,-22.19 5.51,-9.45 8.26,-20.14 8.26,-32.08 -0.01,-11.92 -2.77,-22.61 -8.27,-32.08 z m -33.46,54.62 c -4.99,5.62 -11.79,8.44 -20.38,8.44 -8.72,0 -15.54,-2.78 -20.48,-8.34 -4.93,-5.56 -7.39,-13.11 -7.39,-22.63 0,-9.63 2.46,-17.23 7.39,-22.79 4.93,-5.56 11.75,-8.34 20.48,-8.34 8.6,0 15.39,2.78 20.38,8.34 4.99,5.56 7.47,13.16 7.47,22.79 0.01,9.4 -2.48,16.91 -7.47,22.53 z"
id="path10" />
<path
class="st0"
d="m 381.04,424.95 c -5.15,-9.18 -12.52,-16.32 -22.1,-21.41 -9.58,-5.1 -20.73,-7.66 -33.46,-7.66 h -48 v 121.28 h 48 c 12.61,0 23.7,-2.61 33.28,-7.83 9.58,-5.22 16.97,-12.41 22.19,-21.59 5.22,-9.18 7.83,-19.62 7.83,-31.31 0.01,-11.82 -2.58,-22.31 -7.74,-31.48 z m -34.92,54.35 c -5.62,5.39 -13.36,8.09 -23.22,8.09 h -11.7 v -62.27 h 11.7 c 9.87,0 17.6,2.72 23.22,8.17 5.62,5.45 8.44,13.16 8.44,23.13 -0.01,9.87 -2.82,17.49 -8.44,22.88 z"
id="path12" />
<g
id="g22">
<polygon
class="st1"
points="742.83,486.53 713.07,486.53 750.82,441.47 "
id="polygon14" />
<polygon
class="st1"
points="669.3,488.25 663.26,523.19 733.12,523.19 725.6,566.2 643.2,566.2 643.2,393.8 752.1,393.8 "
id="polygon16" />
<polygon
class="st1"
points="782.29,486.53 798.47,393.8 815.61,393.8 815.61,486.53 "
id="polygon18" />
<polygon
class="st1"
points="815.61,566.2 767.94,566.2 775.83,523.19 815.61,523.19 "
id="polygon20" />
</g>
<g
id="g40">
<path
class="st0"
d="m 162.99,549.74 c -1.81,1.75 -4.58,2.63 -8.3,2.63 h -6.13 v 13.31 H 144.4 V 533.8 h 10.29 c 3.6,0 6.34,0.87 8.21,2.61 1.87,1.74 2.81,3.98 2.81,6.72 0,2.65 -0.91,4.85 -2.72,6.61 z m -3.18,-2.31 c 1.1,-1.01 1.65,-2.44 1.65,-4.3 0,-3.93 -2.26,-5.9 -6.77,-5.9 h -6.13 v 11.71 h 6.13 c 2.31,0 4.02,-0.51 5.12,-1.51 z"
id="path24" />
<path
class="st0"
d="m 224.26,565.68 -7.59,-13.04 h -5.03 v 13.04 h -4.16 V 533.8 h 10.29 c 2.41,0 4.44,0.41 6.11,1.23 1.66,0.82 2.9,1.94 3.73,3.34 0.82,1.4 1.23,3 1.23,4.8 0,2.2 -0.63,4.13 -1.9,5.81 -1.27,1.68 -3.16,2.79 -5.7,3.34 l 8.01,13.36 z M 211.64,549.3 h 6.13 c 2.26,0 3.95,-0.56 5.08,-1.67 1.13,-1.11 1.69,-2.6 1.69,-4.46 0,-1.89 -0.56,-3.35 -1.67,-4.39 -1.11,-1.04 -2.81,-1.56 -5.1,-1.56 h -6.13 z"
id="path26" />
<path
class="st0"
d="m 278.24,563.92 c -2.44,-1.39 -4.37,-3.32 -5.79,-5.81 -1.42,-2.49 -2.13,-5.28 -2.13,-8.39 0,-3.11 0.71,-5.91 2.13,-8.39 1.42,-2.49 3.35,-4.42 5.79,-5.81 2.44,-1.39 5.14,-2.08 8.1,-2.08 2.99,0 5.7,0.69 8.14,2.08 2.44,1.39 4.36,3.32 5.76,5.79 1.4,2.47 2.1,5.28 2.1,8.42 0,3.14 -0.7,5.95 -2.1,8.42 -1.4,2.47 -3.32,4.4 -5.76,5.79 -2.44,1.39 -5.15,2.08 -8.14,2.08 -2.96,-0.02 -5.66,-0.71 -8.1,-2.1 z m 14.12,-3.09 c 1.78,-1.04 3.19,-2.52 4.21,-4.44 1.02,-1.92 1.53,-4.15 1.53,-6.68 0,-2.56 -0.51,-4.8 -1.53,-6.7 -1.02,-1.91 -2.42,-3.38 -4.19,-4.41 -1.77,-1.04 -3.78,-1.56 -6.04,-1.56 -2.26,0 -4.27,0.52 -6.04,1.56 -1.77,1.04 -3.16,2.51 -4.19,4.41 -1.02,1.91 -1.53,4.14 -1.53,6.7 0,2.53 0.51,4.76 1.53,6.68 1.02,1.92 2.42,3.4 4.21,4.44 1.78,1.04 3.79,1.56 6.02,1.56 2.23,-0.01 4.23,-0.52 6.02,-1.56 z"
id="path28" />
<path
class="st0"
d="m 360.5,533.79 v 23.56 c 0,2.62 -0.81,4.72 -2.42,6.29 -1.62,1.57 -3.75,2.36 -6.4,2.36 -2.68,0 -4.83,-0.8 -6.45,-2.4 -1.62,-1.6 -2.42,-3.79 -2.42,-6.56 h 4.16 c 0.03,1.56 0.43,2.82 1.21,3.8 0.78,0.98 1.94,1.46 3.5,1.46 1.56,0 2.71,-0.46 3.48,-1.39 0.76,-0.93 1.14,-2.11 1.14,-3.55 V 533.8 h 4.2 z"
id="path30" />
<path
class="st0"
d="m 409.44,537.18 v 10.66 h 11.62 v 3.43 h -11.62 v 10.98 h 12.99 v 3.43 h -17.15 v -31.93 h 17.15 v 3.43 z"
id="path32" />
<path
class="st0"
d="m 465.89,541.32 c 1.4,-2.49 3.32,-4.43 5.74,-5.83 2.42,-1.4 5.12,-2.1 8.07,-2.1 3.48,0 6.51,0.84 9.1,2.52 2.59,1.68 4.48,4.06 5.67,7.14 h -4.99 c -0.89,-1.92 -2.16,-3.4 -3.82,-4.44 -1.66,-1.04 -3.65,-1.56 -5.97,-1.56 -2.23,0 -4.22,0.52 -5.99,1.56 -1.77,1.04 -3.16,2.51 -4.16,4.41 -1.01,1.91 -1.51,4.14 -1.51,6.7 0,2.53 0.5,4.75 1.51,6.66 1.01,1.91 2.39,3.38 4.16,4.41 1.77,1.04 3.77,1.56 5.99,1.56 2.32,0 4.31,-0.51 5.97,-1.53 1.66,-1.02 2.93,-2.49 3.82,-4.41 h 4.99 c -1.19,3.05 -3.08,5.41 -5.67,7.07 -2.59,1.66 -5.63,2.49 -9.1,2.49 -2.96,0 -5.65,-0.69 -8.07,-2.08 -2.42,-1.39 -4.34,-3.32 -5.74,-5.79 -1.4,-2.47 -2.1,-5.26 -2.1,-8.37 0,-3.11 0.7,-5.92 2.1,-8.41 z"
id="path34" />
<path
class="st0"
d="m 556.88,533.79 v 3.38 h -8.69 v 28.5 h -4.16 v -28.5 h -8.74 v -3.38 z"
id="path36" />
<path
class="st0"
d="m 603.06,564.88 c -1.66,-0.75 -2.97,-1.78 -3.91,-3.11 -0.95,-1.33 -1.43,-2.86 -1.46,-4.6 h 4.44 c 0.15,1.49 0.77,2.75 1.85,3.77 1.08,1.02 2.66,1.53 4.73,1.53 1.98,0 3.55,-0.49 4.69,-1.49 1.14,-0.99 1.72,-2.26 1.72,-3.82 0,-1.22 -0.34,-2.21 -1.01,-2.97 -0.67,-0.76 -1.51,-1.34 -2.52,-1.74 -1.01,-0.4 -2.36,-0.82 -4.07,-1.28 -2.1,-0.55 -3.79,-1.1 -5.06,-1.65 -1.27,-0.55 -2.35,-1.41 -3.25,-2.59 -0.9,-1.17 -1.35,-2.75 -1.35,-4.73 0,-1.74 0.44,-3.28 1.33,-4.62 0.88,-1.34 2.13,-2.38 3.73,-3.11 1.6,-0.73 3.44,-1.1 5.51,-1.1 2.99,0 5.44,0.75 7.34,2.24 1.91,1.5 2.98,3.48 3.22,5.95 h -4.57 c -0.15,-1.22 -0.79,-2.29 -1.92,-3.23 -1.13,-0.93 -2.62,-1.39 -4.48,-1.39 -1.74,0 -3.16,0.45 -4.25,1.35 -1.1,0.9 -1.65,2.16 -1.65,3.77 0,1.16 0.33,2.11 0.98,2.84 0.66,0.73 1.46,1.29 2.42,1.67 0.96,0.38 2.31,0.82 4.05,1.3 2.1,0.58 3.8,1.15 5.08,1.72 1.28,0.57 2.38,1.43 3.29,2.61 0.91,1.17 1.37,2.77 1.37,4.78 0,1.56 -0.41,3.02 -1.24,4.39 -0.83,1.37 -2.04,2.49 -3.66,3.34 -1.62,0.85 -3.52,1.28 -5.72,1.28 -2.08,0.01 -3.97,-0.36 -5.63,-1.11 z"
id="path38" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OD8N - Odoo & n8n Automation Experts</title>
<script type="module" src="agent.js"></script>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
from flask import Flask, send_from_directory
import os
app = Flask(__name__)
directory = os.getcwd() # serve current directory
@app.route("/", defaults={"path": "index.html"})
@app.route("/<path:path>")
def serve_file(path):
# If file exists, serve it
if os.path.isfile(os.path.join(directory, path)):
return send_from_directory(directory, path)
else:
return f"File not found: {path}", 404
if __name__ == "__main__":
# debug=True enables auto-reload when this file changes
app.run(host="0.0.0.0", port=8000, debug=True)

209
public/buyModal.js Normal file
View File

@@ -0,0 +1,209 @@
// --- Category Configuration ---
const CATEGORY_CONFIG = {
3: { showLocation: false, webhook: "" }, // services
4: { showLocation: true, webhook: "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/c76e6b4e-af2f-4bc3-9875-6460d0ffc8e3" }, // hosting
5: { showLocation: false, webhook: "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/fd20e194-b821-4b1f-814f-7bb93ae16046" }, // Workshops
7: { showLocation: false, webhook: "" }, // modules
};
// --- Helper: Get cookie by name ---
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
// --- Helper: Set cookie if not already set ---
function setCookieIfEmpty(name, value, days = 90) {
if (!value) return;
if (getCookie(name)) return; // Don't overwrite
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; path=/; SameSite=Lax`;
}
// --- Add UTM fields to form using cookies ---
function addUtmFields(form) {
const utmParams = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
const defaults = {
utm_source: "homepage",
utm_medium: "direct",
utm_campaign: "none",
utm_term: "",
utm_content: ""
};
utmParams.forEach(param => {
let input = form.querySelector(`input[name="${param}"]`);
if (!input) {
input = document.createElement("input");
input.type = "hidden";
input.name = param;
form.appendChild(input);
}
// First check cookie, then fallback to default
input.value = getCookie(param) || defaults[param];
});
}
// --- Create Modal ---
function createModal() {
const modal = document.createElement("div");
modal.id = "buyNowModal";
Object.assign(modal.style, {
position: "fixed",
top: "0",
left: "0",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.6)",
display: "none",
justifyContent: "center",
alignItems: "center",
zIndex: "1000"
});
modal.innerHTML = `
<div style="background: #fff; padding: 40px 30px; border-radius: 16px; max-width: 500px; width: 90%; position: relative; font-family: 'Arial', sans-serif; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">
<span id="closeModal" style="position: absolute; top: 15px; right: 20px; cursor: pointer; font-weight: bold; font-size: 24px; color: #555;">&times;</span>
<h2 style="margin-bottom: 15px; font-size: 24px; color: #333;">Order Details</h2>
<p id="productText" style="margin-bottom: 25px; font-weight: 500; color: #555;"></p>
<form id="buyForm" style="display: flex; flex-direction: column; gap: 15px;">
<select name="location" id="locationSelect" required style="display: none; padding: 12px 15px; font-size: 16px; border: 1px solid #ccc; border-radius: 8px; outline: none;">
<option value="">Select Location</option>
<option value="Boston">US, Boston</option>
<option value="Manchester">UK, Manchester</option>
<option value="Mumbai">IN, Mumbai</option>
<option value="Saopaulo">BR, Sao Paulo</option>
<option value="Meppel">NL, Meppel</option>
</select>
<input type="text" name="name" placeholder="Name" required style="padding: 12px 15px; border: 1px solid #ccc; border-radius: 8px;">
<input type="text" name="company" placeholder="Company" required style="padding: 12px 15px; border: 1px solid #ccc; border-radius: 8px;">
<input type="text" name="country" placeholder="Country" required style="padding: 12px 15px; border: 1px solid #ccc; border-radius: 8px;">
<input type="text" name="street" placeholder="Street" required style="padding: 12px 15px; border: 1px solid #ccc; border-radius: 8px;">
<div style="display: flex; gap: 10px;">
<input type="text" name="zip" placeholder="ZIP" required style="max-width: 100px; flex: 1 1 0; padding: 12px 10px; border: 1px solid #ccc; border-radius: 8px;">
<input type="text" name="town" placeholder="Town" required style="flex: 2 1 0; padding: 12px 10px; border: 1px solid #ccc; border-radius: 8px;">
</div>
<input type="email" name="email" placeholder="Email" required style="padding: 12px 15px; border: 1px solid #ccc; border-radius: 8px;">
<input type="hidden" name="product">
<input type="hidden" name="price">
<input type="hidden" name="id">
<input type="hidden" name="category">
<button id="submitBuy" type="submit" style="padding: 14px; background: #007BFF; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 18px; font-weight: bold;">Send Order</button>
</form>
<div id="confirmation" style="display: none; margin-top: 20px; padding: 20px; background-color: #e6ffed; color: #056608; border-radius: 12px; text-align: center; font-weight: 500;">
<p>Thank you for your purchase! 🎉</p>
<button id="closeConfirmation" style="margin-top: 10px; padding: 10px 20px; background: #007BFF; color: #fff; border: none; border-radius: 8px; cursor: pointer;">Close</button>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById("closeModal").onclick = () => modal.style.display = "none";
document.getElementById("closeConfirmation").onclick = () => {
document.getElementById("confirmation").style.display = "none";
modal.style.display = "none";
};
modal.onclick = e => { if (e.target === modal) modal.style.display = "none"; };
return modal;
}
// --- Open Modal ---
function openModal(productHref) {
const modal = document.getElementById("buyNowModal");
const form = modal.querySelector("#buyForm");
const confirmation = modal.querySelector("#confirmation");
modal.style.display = "flex";
form.style.display = "flex";
confirmation.style.display = "none";
const parts = productHref.split("/").filter(Boolean);
const [id, price, category, ...productParts] = parts;
const product = decodeURIComponent(productParts.join("/"));
const categoryNum = parseInt(category, 10);
const config = CATEGORY_CONFIG[categoryNum] || { showLocation: false, webhook: "" };
const locationSelect = document.getElementById("locationSelect");
if (config.showLocation) {
locationSelect.style.display = "block";
locationSelect.setAttribute("required", "true");
} else {
locationSelect.style.display = "none";
locationSelect.removeAttribute("required");
locationSelect.value = "";
}
form.querySelector('input[name="id"]').value = id || "";
form.querySelector('input[name="price"]').value = price || "0";
form.querySelector('input[name="category"]').value = category || "";
form.querySelector('input[name="product"]').value = product || "Unknown";
form.dataset.webhook = config.webhook || "";
modal.querySelector("#productText").textContent = `We have send you the payment details for your order ${product} $${price} via mail.`;
addUtmFields(form); // Read from cookie or defaults
}
// --- Handle Form Submit ---
function handleFormSubmit() {
const form = document.getElementById("buyForm");
const confirmation = document.getElementById("confirmation");
form.addEventListener("submit", async (e) => {
e.preventDefault();
addUtmFields(form);
const data = Object.fromEntries(new FormData(form).entries());
const webhookUrl = form.dataset.webhook;
if (!webhookUrl) {
alert("No webhook configured for this category.");
return;
}
try {
const res = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.ok) {
form.style.display = "none";
confirmation.style.display = "block";
form.reset();
} else {
alert("Failed to submit form.");
}
} catch (err) {
console.error(err);
alert("Error submitting form.");
}
});
}
// --- Attach Buttons ---
function attachButtons() {
const buttons = Array.from(document.querySelectorAll("button, a"));
buttons.forEach(btn => {
const text = btn.textContent.trim();
if (text === "Buy Now" || text === "Book Now") {
btn.addEventListener("click", (e) => {
e.preventDefault();
const href = btn.getAttribute("href") || btn.dataset.product || "Unknown/0/0";
openModal(href);
});
}
});
}
// --- Initialize ---
document.addEventListener("DOMContentLoaded", () => {
createModal();
handleFormSubmit();
attachButtons();
});

1
public/buyModal.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
public/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

7
public/minify Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
npm install -g terser
terser buyModal.js -o buyModal.min.js -c -m
terser tryModal.js -o tryModal.min.js -c -m
terser setCookie.js -o setCookie.min.js -c -m
terser affiliateModal.js -o affiliateModal.min.js -c -m

36
public/setCookie.js Normal file
View File

@@ -0,0 +1,36 @@
function setUtmCookies() {
// Helper: get query param from URL
function getQueryParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
}
// Helper: get cookie by name
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
// Helper: set cookie if not already set
function setCookieIfEmpty(name, value, days = 90) {
if (!value) return;
if (getCookie(name)) return; // Don't overwrite existing cookie
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; path=/; SameSite=Lax`;
}
// Get UTM params
const utmSource = getQueryParam('utm_source');
const utmMedium = getQueryParam('utm_medium');
const utmCampaign = getQueryParam('utm_campaign');
// Set cookies only if not already present
setCookieIfEmpty('utm_source', utmSource, 90);
setCookieIfEmpty('utm_medium', utmMedium, 90);
setCookieIfEmpty('utm_campaign', utmCampaign, 90);
}
// Run on page load
setUtmCookies();

1
public/setCookie.min.js vendored Normal file
View File

@@ -0,0 +1 @@
function setUtmCookies(){function e(e){return new URLSearchParams(window.location.search).get(e)}function t(e,t,n=90){if(!t)return;if(function(e){const t=document.cookie.match(new RegExp("(^| )"+e+"=([^;]+)"));return t?decodeURIComponent(t[2]):null}(e))return;const o=new Date;o.setTime(o.getTime()+24*n*60*60*1e3),document.cookie=`${e}=${encodeURIComponent(t)}; expires=${o.toUTCString()}; path=/; SameSite=Lax`}const n=e("utm_source"),o=e("utm_medium"),m=e("utm_campaign");t("utm_source",n,90),t("utm_medium",o,90),t("utm_campaign",m,90)}setUtmCookies();

20
public/start Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
#!/bin/bash
# Simple static file server with live reload
# Requires Node.js and live-server
# Check if live-server is installed
if ! command -v live-server &> /dev/null
then
echo "Installing live-server..."
npm install -g live-server
fi
# Serve the current directory with auto-reload
echo "Starting live-server on http://localhost:8080 ..."
live-server .

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Odoo-style Chat</title> <title>ODOO4projects - Support Chat</title>
<link rel="preconnect" href="https://fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;700&display=swap" rel="stylesheet">
<style> <style>
@@ -25,7 +25,27 @@
/* Left column - user list / details */ /* Left column - user list / details */
.sidebar{background:linear-gradient(180deg,rgba(255,255,255,0.9),var(--card));border-radius:var(--radius);padding:20px;box-shadow:0 6px 20px rgba(24,39,75,0.06);overflow:hidden} .sidebar{background:linear-gradient(180deg,rgba(255,255,255,0.9),var(--card));border-radius:var(--radius);padding:20px;box-shadow:0 6px 20px rgba(24,39,75,0.06);overflow:hidden}
.brand{display:flex;gap:12px;align-items:center} .brand{display:flex;gap:12px;align-items:center}
.logo{width:44px;height:44px;border-radius:9px;background:linear-gradient(135deg,var(--odoo-purple),#b07fa9);display:flex;align-items:center;justify-content:center;color:white;font-weight:700;box-shadow:0 6px 18px rgba(135,90,123,0.12)}
.logo {
width: 44px;
height: 44px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 18px rgba(135,90,123,0.12);
overflow: hidden; /* ensures image respects border radius */
}
.logo img {
width: 100%;
height: 100%;
object-fit: cover; /* ensures the image fills the container */
}
.brand h1{margin:0;font-size:18px;color:var(--odoo-dark)} .brand h1{margin:0;font-size:18px;color:var(--odoo-dark)}
.brand p{margin:0;font-size:12px;color:var(--muted)} .brand p{margin:0;font-size:12px;color:var(--muted)}
@@ -78,28 +98,52 @@
<div class="app"> <div class="app">
<aside class="sidebar"> <aside class="sidebar">
<div class="brand"> <div class="brand">
<div class="logo">OD</div> <div class="logo">
<img src="https://static.odoo4projects.com/images/favicon.png" alt="ODOO4projects Logo" />
</div>
<div> <div>
<h1>Odoo Chat</h1> <h1>Odoo Chat</h1>
<p>Connected: <span id="status" class="small">disconnected</span></p>
</div> </div>
</div> </div>
<div class="meta">Backend: <strong>Custom n8n Chat Webhook</strong></div> <div class="hint">
<div class="hint">This is a lightweight Odoo-styled chat UI. Messages are posted to your webhook and responses (JSON) are displayed here.</div> This chat is dedicated to support for ODOO4projects hosting services only.<br>
<div style="margin-top:18px;font-size:13px;color:var(--muted)"> Please note: it does not cover general Odoo usage or functional questions.
Usage tips:
<ul style="padding-left:18px;margin:8px 0;color:var(--muted)">
<li>Type a message and press <strong>Enter</strong> or click Send.</li>
<li>Expect JSON response with a simple text field (see docs).</li>
</ul>
</div> </div>
<div style="margin-top:18px; font-size:13px; color:var(--muted); line-height:1.5;">
<strong>Usage Tips:</strong>
<ul style="padding-left:20px; margin:8px 0; color:var(--muted);">
<li>
<strong>Operations Agent</strong><br>
Our agent can analyze the odoo and git log files for you. We are currently working on other tasks for him.
</li>
<li>
<strong>Support Ticket</strong><br>
Type <code>/ticket</code> to get in touch with our support team.
</li>
<li>
<strong>Feature Requests</strong><br>
Suggest a new feature for our services by leaving a comment in the chat using this format:<br>
<code>/feature: [Your description here]</code><br>
<em>Example:</em><br>
<code>/feature: Add an automatic backup notification email</code>
</li>
</ul>
<div style="margin-top:10px;">
⚠️ <strong>Important:</strong> If you cannot access Odoo due to a Git change, you can easily <a href="https://ODOO4projects.com" style="color:inherit; text-decoration:underline;">revert the last change via our homepage</a>.
</div>
</div>
</aside> </aside>
<main class="chat-card" role="main"> <main class="chat-card" role="main">
<div class="chat-header"> <div class="chat-header">
<div style="display:flex;flex-direction:column"> <div style="display:flex;flex-direction:column">
<div class="title">Chat with N8N Backend</div> <div class="title">Chat with the ODOO4projects support</div>
<div class="sub">Webhook: <span id="webhook" class="small">https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/702862fd-dd17-4a34-8efb-e9056d2c50df/chat</span></div>
</div> </div>
</div> </div>
@@ -123,18 +167,19 @@
const WEBHOOK = 'https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/702862fd-dd17-4a34-8efb-e9056d2c50df/chat'; const WEBHOOK = 'https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/702862fd-dd17-4a34-8efb-e9056d2c50df/chat';
const messagesEl = document.getElementById('messagesInner'); const messagesEl = document.getElementById('messagesInner');
const statusEl = document.getElementById('status');
const input = document.getElementById('messageInput'); const input = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn'); const sendBtn = document.getElementById('sendBtn');
document.getElementById('webhook').textContent = WEBHOOK;
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
// Generate UUID once per page load // Generate UUID once per page load
function generateUUID(){ function generateChatID(){
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
); );
} }
const CLIENT_UUID = generateUUID(); const chatId = generateChatID();
function formatTime(date){ function formatTime(date){
return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
@@ -146,7 +191,7 @@
const avatar = document.createElement('div'); const avatar = document.createElement('div');
avatar.className = 'avatar'; avatar.className = 'avatar';
avatar.textContent = who==='me' ? 'ME' : 'AI'; avatar.textContent = who==='me' ? 'ME' : '4';
const bubble = document.createElement('div'); const bubble = document.createElement('div');
bubble.className = 'bubble' + (who==='me' ? ' me' : ''); bubble.className = 'bubble' + (who==='me' ? ' me' : '');
@@ -174,26 +219,30 @@
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight; messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
} }
function setStatus(connected){ let firstMessage = true;
statusEl.textContent = connected ? 'ready' : 'disconnected';
statusEl.style.color = connected ? 'var(--accent)' : 'var(--muted)';
}
async function sendMessage(){ async function sendMessage(){
const text = input.value.trim(); let text = input.value.trim();
if(!text) return; if(!text) return;
// show user message // show user message
if (firstMessage) {
text = `${uuid}: ${text}`;
firstMessage = false;
}
if(text.toLowerCase().startsWith('/ticket')){
text = `/ticket UUID:${uuid} chatId: ${chatId}`; // Add UUID after /ticket
}
appendMessage(text, 'me'); appendMessage(text, 'me');
input.value = ''; input.value = '';
sendBtn.disabled = true; sendBtn.disabled = true;
setStatus(false);
// show typing indicator // show typing indicator
const typingEl = document.createElement('div'); const typingEl = document.createElement('div');
typingEl.className = 'msg'; typingEl.className = 'msg';
typingEl.id = 'typing'; typingEl.id = 'typing';
typingEl.innerHTML = ` typingEl.innerHTML = `
<div class="avatar">AI</div> <div class="avatar">4</div>
<div class="typing" style="margin-left:6px"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div> <div class="typing" style="margin-left:6px"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
`; `;
messagesEl.appendChild(typingEl); messagesEl.appendChild(typingEl);
@@ -201,7 +250,7 @@
try{ try{
// Post to webhook - expecting a JSON response. Adapt to your backend. // Post to webhook - expecting a JSON response. Adapt to your backend.
const payload = {text: text, uuid: CLIENT_UUID}; const payload = {text: text, uuid: uuid, chatid: chatId};
const res = await fetch(WEBHOOK, { const res = await fetch(WEBHOOK, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -231,13 +280,11 @@
else replyText = JSON.stringify(data); else replyText = JSON.stringify(data);
appendMessage(replyText, 'them'); appendMessage(replyText, 'them');
setStatus(true);
}catch(err){ }catch(err){
const t = document.getElementById('typing'); const t = document.getElementById('typing');
if(t) t.remove(); if(t) t.remove();
appendMessage('Error: ' + (err && err.message ? err.message : String(err)), 'them'); appendMessage('Error: ' + (err && err.message ? err.message : String(err)), 'them');
setStatus(false);
} finally { } finally {
sendBtn.disabled = false; sendBtn.disabled = false;
} }
@@ -247,17 +294,8 @@
sendBtn.addEventListener('click', sendMessage); sendBtn.addEventListener('click', sendMessage);
input.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMessage(); } }); input.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMessage(); } });
// optimistic check to see if webhook is reachable via HEAD
(async function ping(){
try{
const r = await fetch(WEBHOOK, {method:'HEAD'});
setStatus(r.ok);
}catch(e){ setStatus(false); }
})();
// friendly welcome // friendly welcome
appendMessage('Hello! This chat will post your message to the configured webhook and show the response here. Try sending a message. Your session id is ' + CLIENT_UUID, 'them'); appendMessage('I am here to help with all your hosting-related questions.Type /ticket to contact our support team directly. Your UUID is '+uuid, 'them');
setStatus(false);
</script> </script>
</body> </body>
</html> </html>

211
public/tryModal.js Normal file
View File

@@ -0,0 +1,211 @@
const TRYNOW_WEBHOOK_URL = "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/c25169c6-4234-4b47-8e74-612b9539da0a";
// --- Helper: Get cookie value ---
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// --- Add UTM fields to form ---
function addUtmFields(form) {
const utmParams = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
const defaults = {
utm_source: "homepage",
utm_medium: "direct",
utm_campaign: "none"
};
utmParams.forEach(param => {
let input = form.querySelector(`input[name="${param}"]`);
if (!input) {
input = document.createElement("input");
input.type = "hidden";
input.name = param;
form.appendChild(input);
}
const cookieValue = getCookie(param);
input.value = cookieValue || defaults[param] || "";
});
}
// --- Create modal ---
function tryNow_createModal() {
const modal = document.createElement("div");
modal.id = "trynowModal";
modal.style.position = "fixed";
modal.style.top = "0";
modal.style.left = "0";
modal.style.width = "100%";
modal.style.height = "100%";
modal.style.backgroundColor = "rgba(0,0,0,0.6)";
modal.style.display = "none";
modal.style.justifyContent = "center";
modal.style.alignItems = "center";
modal.style.zIndex = "1000";
modal.innerHTML = `
<div style="
background: #ffffff;
padding: 40px 30px;
border-radius: 16px;
max-width: 400px;
width: 90%;
position: relative;
font-family: 'Arial', sans-serif;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
">
<span id="trynowCloseModal" style="
position: absolute;
top: 15px;
right: 20px;
cursor: pointer;
font-weight: bold;
font-size: 24px;
color: #555;
">&times;</span>
<h2 style="margin-bottom: 20px; font-size: 24px; color: #333;">Order Details</h2>
<form id="trynowForm" style="display: flex; flex-direction: column; gap: 15px;">
<input type="email" name="email" placeholder="Email" required style="padding: 12px 15px; font-size: 16px; border: 1px solid #ccc; border-radius: 8px; outline: none;">
<select name="location" id="trynowLocationSelect" required style="padding: 12px 15px; font-size: 16px; border: 1px solid #ccc; border-radius: 8px; outline: none;">
<option value="">Select Location</option>
<option value="Boston">US, Boston</option>
<option value="Manchester">UK, Manchester</option>
<option value="Mumbai">IN, Mumbai</option>
<option value="saopaulo">BR, Sao Paulo</option>
<option value="Meppel">NL, Meppel</option>
</select>
<select name="product" required style="padding: 12px 15px; font-size: 16px; border: 1px solid #ccc; border-radius: 8px; outline: none;">
<option value="odoo_19" selected>ODOO</option>
<option value="N8N">n8n</option>
</select>
<button type="submit" style="
padding: 14px;
background: #007BFF;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
transition: background 0.3s;
">Start free Trial</button>
</form>
<div id="trynowConfirmation" style="
display: none;
margin-top: 20px;
padding: 20px;
background-color: #e6ffed;
color: #056608;
border-radius: 12px;
text-align: center;
font-weight: 500;
">
<p>Thank you for your submission! 🎉</p>
<button id="trynowCloseConfirmation" style="
margin-top: 10px;
padding: 10px 20px;
background: #007BFF;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
">Close</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Add UTM fields after form exists
const form = modal.querySelector("#trynowForm");
addUtmFields(form);
// Close modal handlers
document.getElementById("trynowCloseModal").onclick = () => { modal.style.display = "none"; };
document.getElementById("trynowCloseConfirmation").onclick = () => {
document.getElementById("trynowConfirmation").style.display = "none";
modal.style.display = "none";
};
modal.onclick = (e) => { if (e.target === modal) modal.style.display = "none"; };
return modal;
}
// --- Handle form submission ---
function tryNow_handleFormSubmit() {
const form = document.getElementById("trynowForm");
const confirmation = document.getElementById("trynowConfirmation");
form.addEventListener("submit", async (e) => {
e.preventDefault();
// Update UTM fields from cookies
addUtmFields(form);
const data = {};
new FormData(form).forEach((value, key) => (data[key] = value));
try {
const res = await fetch(TRYNOW_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
if (res.ok) {
form.style.display = "none";
confirmation.style.display = "block";
form.reset();
} else {
alert("Failed to submit form.");
}
} catch (err) {
console.error(err);
alert("Error submitting form.");
}
});
}
// --- Attach "Try Now" buttons ---
function tryNow_attachButtons() {
const buttons = Array.from(document.querySelectorAll("a, button"));
buttons.forEach(btn => {
if (btn.textContent && btn.textContent.trim() === "Try Now") {
btn.addEventListener("click", (e) => {
e.preventDefault();
const modal = document.getElementById("trynowModal");
if (modal) {
modal.style.display = "flex";
// Reset modal state
const form = document.getElementById("trynowForm");
const confirmation = document.getElementById("trynowConfirmation");
if (form && confirmation) {
form.style.display = "flex";
confirmation.style.display = "none";
addUtmFields(form); // refresh UTM fields each open
}
}
});
}
});
}
// --- Initialize ---
document.addEventListener("DOMContentLoaded", () => {
tryNow_createModal();
tryNow_handleFormSubmit();
tryNow_attachButtons();
});

1
public/tryModal.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,224 +2,265 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Upgrade Form</title> <title>Check and Upgrade Your Plan</title>
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: "Inter", "Segoe UI", Arial, sans-serif;
padding: 20px; background-color: #f4f5f7;
max-width: 700px; color: #333;
margin: auto; margin: 0;
background: #f9f9f9; padding: 40px;
display: flex;
justify-content: center;
} }
.summary-container {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
padding: 40px;
max-width: 720px;
width: 100%;
}
h2 { h2 {
text-align: center; text-align: center;
color: #333; color: #262626;
margin-bottom: 4px;
font-weight: 600;
} }
form {
/* New small centered contract name */
.contract-name {
text-align: center;
font-size: 0.85em;
color: #666;
margin-bottom: 25px;
}
.subtitle {
text-align: center;
font-size: 0.9em;
color: #777;
margin-bottom: 30px;
line-height: 1.5;
}
.grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 20px; gap: 20px;
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
} }
label {
display: block; .grid-item {
font-weight: bold; background: #fafafa;
border: 1px solid #e2e2e2;
border-radius: 8px;
padding: 15px 20px;
}
.grid-item.full {
grid-column: 1 / -1;
}
.label {
font-weight: 600;
font-size: 0.9em;
color: #555;
margin-bottom: 6px; margin-bottom: 6px;
} }
input[type="number"], input[type="text"] {
width: 100%; .value {
padding: 8px; font-size: 1.1em;
border-radius: 6px; color: #222;
border: 1px solid #ccc;
} }
input[type="checkbox"] {
transform: scale(1.2); .footer {
margin-right: 6px; text-align: center;
font-size: 0.9em;
color: #888;
margin-top: 30px;
} }
.full-width {
grid-column: 1 / -1; /* Button styling */
} .actions {
.submit-section {
grid-column: 1 / -1;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 12px; margin-top: 40px;
margin-top: 20px; gap: 15px;
} }
button {
padding: 10px 20px; .action-row {
background-color: #4CAF50; display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
}
.action-btn {
background-color: #875A7B; /* Odoo purple */
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 10px 22px;
font-size: 15px;
cursor: pointer; cursor: pointer;
font-size: 16px; transition: background-color 0.2s ease, transform 0.1s ease;
min-width: 180px;
} }
button:hover {
background-color: #45a049; .action-btn:hover {
background-color: #744e6a;
transform: translateY(-1px);
} }
.cost {
font-weight: bold; .action-btn.secondary {
font-size: 18px; background-color: #6c757d;
} }
.readonly-text {
padding: 8px; .action-btn.secondary:hover {
background: #eee; background-color: #5a636a;
border-radius: 6px; }
border: 1px solid #ccc;
.action-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
transform: none;
} }
</style> </style>
</head> </head>
<body> <body>
<h2>Upgrade Your Plan</h2> <div class="summary-container">
<div id="uuidDisplay" style="text-align:center; font-size: 0.9em; color: #666; margin-bottom: 10px;"></div> <h2>Check and Upgrade Your Plan</h2>
<form id="upgradeForm"> <div class="contract-name" id="contractName">Loading...</div>
<div> <div class="subtitle">
<label><input type="checkbox" id="git" name="git"> GIT</label> Choose an upgrade. With this upgade you only pay for the <b>remaining days</b> of your current contract. Due to technical reasons, the <b>total price</b> will be included in the payment link sent to your email. If you dont want to upgrade, simply dont complete the payment. Current yearly prices are listed on our homepage, and new features will apply when you extend your contract next time.
</div> </div>
<div> <div class="grid">
<label for="domains">Domains</label> <div class="grid-item">
<input type="number" id="domains" name="domains" min="1" max="10"> <div class="label">GIT</div>
<div class="value" id="gitValue">Loading...</div>
</div>
<div class="grid-item">
<div class="label">Domains</div>
<div class="value" id="domainsValue">Loading...</div>
</div>
<div class="grid-item">
<div class="label">Backup Slots</div>
<div class="value" id="backupValue">Loading...</div>
</div>
<div class="grid-item">
<div class="label">Workers</div>
<div class="value" id="workersValue">Loading...</div>
</div>
<div class="grid-item full">
<div class="label">HDD (MB)</div>
<div class="value" id="hddValue">Loading...</div>
</div>
<div class="grid-item full">
<div class="label">Expires</div>
<div class="value" id="expiresValue">Loading...</div>
</div>
</div> </div>
<div> <!-- Buttons -->
<label for="backupSlots">Backup Slots</label> <div class="actions">
<input type="number" id="backupSlots" name="backupSlots" min="2" max="10"> <div class="action-row">
<button class="action-btn" id="upgradeRiseBtn">Upgrade to "On the Rise"</button>
<button class="action-btn" id="upgradePowerhouseBtn">Upgrade to "Powerhouse"</button>
</div>
<div class="action-row">
<button class="action-btn secondary" id="addDomainBtn">Add a Domain</button>
<button class="action-btn secondary" id="addBackupsBtn">Add 5 Backup Slots</button>
</div>
</div> </div>
<div> <div class="footer" id="uuidDisplay">
<label for="workers">Workers</label> UUID: <span id="uuidText">Loading...</span>
<input type="number" id="workers" name="workers" min="1" max="3">
</div> </div>
</div>
<div class="full-width">
<label for="hdd">HDD (MB)</label>
<input type="number" id="hdd" name="hdd" min="250" max="2048" step="250">
</div>
<div class="full-width">
<label>Valid Until</label>
<div id="validUntil" class="readonly-text">Loading...</div>
</div>
<div class="submit-section">
<button type="submit">Upgrade</button>
<div class="cost" id="cost">$0,00</div>
</div>
</form>
<script> <script>
const webhookUrl = "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/0c8536be-d175-4740-8e78-123159193b23";
const webhook_buy = "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/2366cc41-bfd9-41c0-b9b4-bea1e60726f1";
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid"); const uuid = params.get("uuid");
document.getElementById("uuidDisplay").textContent = uuid ? uuid.toLowerCase() : "-";
async function loadData() {
if (!uuid) {
alert("Missing uuid parameter in URL");
return;
}
const webhookBase = "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/0c8536be-d175-4740-8e78-123159193b23"; document.getElementById("uuidText").textContent = uuid;
const webhookGet = `/get-data?uuid=${uuid}`;
const webhookPost = `${webhookBase}/submit-data`;
async function loadData() {
if (!uuid) {
alert("Missing uuid parameter in URL");
return;
}
try {
const res = await fetch(webhookBase); // Fetching the webhook JSON
if (!res.ok) throw new Error("Failed to fetch data");
const data = await res.json();
// Save original data globally
window.originalData = data;
// Save prices globally
window.prices = data.prices || {
git: 10,
domain: 1,
backupSlot: 1,
worker: 50,
hddUnit: 10
};
// Fill form fields
document.getElementById("git").checked = data.git || false;
document.getElementById("domains").value = data.domains || 1;
document.getElementById("backupSlots").value = data.backupSlots || 2;
document.getElementById("workers").value = data.workers || 1;
document.getElementById("hdd").value = data.hdd || 250;
document.getElementById("validUntil").textContent = data.validUntil || "-";
calculateCost();
} catch (err) {
console.error(err);
alert("Error loading data.");
}
}
function calculateCost() {
if (!window.originalData || !window.prices) return;
const domains = parseInt(document.getElementById("domains").value) || 0;
const backups = parseInt(document.getElementById("backupSlots").value) || 0;
const workers = parseInt(document.getElementById("workers").value) || 0;
const hdd = parseInt(document.getElementById("hdd").value) || 0;
const gitChecked = document.getElementById("git").checked;
const baselineDomains = parseInt(window.originalData.domains) || 0;
const baselineBackups = parseInt(window.originalData.backupSlots) || 0;
const baselineWorkers = parseInt(window.originalData.workers) || 0;
const baselineHDD = parseInt(window.originalData.hdd) || 0;
const baselineGit = window.originalData.git || false;
const extraDomains = Math.max(domains - baselineDomains, 0);
const extraBackups = Math.max(backups - baselineBackups, 0);
const extraWorkers = Math.max(workers - baselineWorkers, 0);
const extraHDDUnits = Math.max(Math.floor((hdd - baselineHDD) / 250), 0);
const extraGit = gitChecked && !baselineGit ? 1 : 0;
const cost =
extraGit * window.prices.git +
extraDomains * window.prices.domain +
extraBackups * window.prices.backupSlot +
extraWorkers * window.prices.worker +
extraHDDUnits * window.prices.hddUnit;
document.getElementById("cost").textContent = `$${cost.toFixed(2)}`;
}
document.querySelectorAll("input").forEach(input => {
input.addEventListener("input", calculateCost);
});
document.getElementById("upgradeForm").addEventListener("submit", async (e) => {
e.preventDefault();
const payload = {
uuid: uuid,
git: document.getElementById("git").checked,
domains: parseInt(document.getElementById("domains").value),
backupSlots: parseInt(document.getElementById("backupSlots").value),
workers: parseInt(document.getElementById("workers").value),
hdd: parseInt(document.getElementById("hdd").value)
};
try { try {
const res = await fetch(webhookPost, { const res = await fetch(webhookUrl, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload) body: JSON.stringify({ uuid })
}); });
if (!res.ok) throw new Error("Failed to submit data");
alert("Upgrade submitted successfully!"); if (!res.ok) throw new Error("Failed to fetch data");
const data = await res.json();
document.getElementById("gitValue").textContent = data.git ? "Enabled" : "Disabled";
document.getElementById("domainsValue").textContent = data.domains || "-";
document.getElementById("backupValue").textContent = data.backupSlots || "-";
document.getElementById("workersValue").textContent = data.workers || "-";
document.getElementById("hddValue").textContent = data.hdd ? `${data.hdd} MB` : "-";
document.getElementById("expiresValue").textContent = data.expires || "-";
document.getElementById("contractName").textContent = data.contract_name || "-";
// Disable buttons based on contract_name
if (data.contract_name === "On the Rise") {
document.getElementById("upgradeRiseBtn").disabled = true;
} else if (data.contract_name === "Powerhouse") {
document.getElementById("upgradeRiseBtn").disabled = true;
document.getElementById("upgradePowerhouseBtn").disabled = true;
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert("Error submitting form."); alert("Error loading data.");
} }
}); }
async function buyProduct(product_id) {
if (!uuid) return;
try {
const res = await fetch(webhook_buy, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uuid, product_id })
});
if (!res.ok) throw new Error("Failed to send purchase request");
document.body.innerHTML = `
<div style="text-align:center;padding:80px;background:#fff;border-radius:12px;max-width:700px;margin:auto;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<h2>✅ Your request has been sent</h2>
<p>Please check your email for confirmation.</p>
</div>
`;
} catch (err) {
console.error(err);
alert("Error sending request.");
}
}
document.getElementById("upgradeRiseBtn").addEventListener("click", () => buyProduct("ontherise"));
document.getElementById("upgradePowerhouseBtn").addEventListener("click", () => buyProduct("powerhouse"));
document.getElementById("addDomainBtn").addEventListener("click", () => buyProduct("1-domain"));
document.getElementById("addBackupsBtn").addEventListener("click", () => buyProduct("5-backup"));
loadData(); loadData();
</script> </script>

251
public/upsell.html_ Normal file
View File

@@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upgrade Form</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 700px;
margin: auto;
background: #f9f9f9;
}
h2 {
text-align: center;
color: #333;
}
form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
label {
display: block;
font-weight: bold;
margin-bottom: 6px;
}
input[type="number"], input[type="text"] {
width: 100%;
padding: 8px;
border-radius: 6px;
border: 1px solid #ccc;
}
input[type="checkbox"] {
transform: scale(1.2);
margin-right: 6px;
}
.full-width {
grid-column: 1 / -1;
}
.submit-section {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 12px;
margin-top: 20px;
}
button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
}
button:disabled {
background-color: #aaa;
cursor: not-allowed;
}
.cost {
font-weight: bold;
font-size: 18px;
}
.readonly-text {
padding: 8px;
background: #eee;
border-radius: 6px;
border: 1px solid #ccc;
}
.confirmation {
text-align: center;
font-size: 1.2em;
padding: 40px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
color: #333;
}
</style>
</head>
<body>
<h2>Upgrade Your Plan</h2>
<div id="uuidDisplay" style="text-align:center; font-size: 0.9em; color: #666; margin-bottom: 10px;">Enhance your Odoo experience by upgrading your feature set here. Youll only pay for the remaining duration of your current Odoo contract.
When its time for your annual renewal, you will receive a reminder email with all the details.</div>
<form id="upgradeForm">
<div>
<label><input type="checkbox" id="git" name="git"> GIT</label>
</div>
<div>
<label for="domains">Domains</label>
<input type="number" id="domains" name="domains" min="1" max="10">
</div>
<div>
<label for="backupSlots">Backup Slots</label>
<input type="number" id="backupSlots" name="backupSlots" min="2" max="10">
</div>
<div>
<label for="workers">Workers</label>
<input type="number" id="workers" name="workers" min="1" max="3">
</div>
<div class="full-width">
<label for="hdd">HDD (MB)</label>
<input type="number" id="hdd" name="hdd" min="250" max="2048" step="250">
</div>
<div class="full-width">
<label>Expires</label>
<div id="expires" class="readonly-text">Loading...</div>
</div>
<div class="submit-section">
<button type="submit" id="submitBtn" disabled>Upgrade</button>
<div class="cost" id="cost">$0.00</div>
</div>
</form>
<script>
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
const webhookUrl = "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/0c8536be-d175-4740-8e78-123159193b23";
const webhookPost = "https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/3709b60f-935e-43e4-834a-5060a40182dd";
async function loadData() {
if (!uuid) {
alert("Missing uuid parameter in URL");
return;
}
try {
const res = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ uuid })
});
if (!res.ok) throw new Error("Failed to fetch data");
const data = await res.json();
window.originalData = data;
window.prices = data.prices || {
git: 10,
domain: 1,
backupSlot: 1,
worker: 50,
hddUnit: 10
};
document.getElementById("git").checked = data.git || false;
document.getElementById("domains").value = data.domains || 1;
document.getElementById("backupSlots").value = data.backupSlots || 2;
document.getElementById("workers").value = data.workers || 1;
document.getElementById("hdd").value = data.hdd || 250;
document.getElementById("expires").textContent = data.expires || "-";
calculateCost();
} catch (err) {
console.error(err);
alert("Error loading data.");
}
}
function calculateCost() {
if (!window.originalData || !window.prices) return;
const domains = parseInt(document.getElementById("domains").value) || 0;
const backups = parseInt(document.getElementById("backupSlots").value) || 0;
const workers = parseInt(document.getElementById("workers").value) || 0;
const hdd = parseInt(document.getElementById("hdd").value) || 0;
const gitChecked = document.getElementById("git").checked;
const baselineDomains = parseInt(window.originalData.domains) || 0;
const baselineBackups = parseInt(window.originalData.backupSlots) || 0;
const baselineWorkers = parseInt(window.originalData.workers) || 0;
const baselineHDD = parseInt(window.originalData.hdd) || 0;
const baselineGit = window.originalData.git || false;
const extraDomains = Math.max(domains - baselineDomains, 0);
const extraBackups = Math.max(backups - baselineBackups, 0);
const extraWorkers = Math.max(workers - baselineWorkers, 0);
const extraHDDUnits = Math.max(Math.floor((hdd - baselineHDD) / 250), 0);
const extraGit = gitChecked && !baselineGit ? 1 : 0;
const cost =
extraGit * window.prices.git +
extraDomains * window.prices.domain +
extraBackups * window.prices.backupSlot +
extraWorkers * window.prices.worker +
extraHDDUnits * window.prices.hddUnit;
document.getElementById("cost").textContent = `$${cost.toFixed(2)}`;
// Enable/disable button based on cost
document.getElementById("submitBtn").disabled = cost <= 0;
}
document.querySelectorAll("input").forEach(input => {
input.addEventListener("input", calculateCost);
});
document.getElementById("upgradeForm").addEventListener("submit", async (e) => {
e.preventDefault();
const payload = {
uuid: uuid,
git: document.getElementById("git").checked,
domains: parseInt(document.getElementById("domains").value),
backupSlots: parseInt(document.getElementById("backupSlots").value),
workers: parseInt(document.getElementById("workers").value),
hdd: parseInt(document.getElementById("hdd").value),
cost: document.getElementById("cost").textContent.replace('$','')
};
try {
const res = await fetch(webhookPost, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error("Failed to submit data");
// Replace page content with confirmation message
document.body.innerHTML = `
<div class="confirmation">
<h2>✅ Your upgrade request has been sent</h2>
<p>Please check your email for confirmation.</p>
</div>
`;
} catch (err) {
console.error(err);
alert("Error submitting form.");
}
});
loadData();
</script>
</body>
</html>

186
public/widget.js Normal file
View File

@@ -0,0 +1,186 @@
(function() {
console.log("WIDGET JS LOADED");
const container = document.getElementById("my-signup-widget");
if (!container) return;
// --- Hardcoded tracking values ---
const utm_source = "scholarix";
const utm_campaign = "trial_widget";
const default_product = "odoo_19";
const shadow = container.attachShadow({ mode: "open" });
const styles = `
:host {
all: initial;
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Inter,Arial,sans-serif;
display: block;
max-width: 420px;
margin: 0 auto;
color: #cffaff;
}
.card {
background: #084574;
border: 1px solid #cffaff33;
border-radius: 16px;
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
padding: 24px;
}
.header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: #cffaff22;
display: flex;
align-items: center;
justify-content: center;
}
.title {
font-size: 18px;
font-weight: 800;
color: #cffaff;
}
.subtitle {
font-size: 13px;
color: #a9d8f3;
}
label, .label {
display: block;
font-size: 12px;
color: #cffaff;
font-weight: 600;
margin: 12px 0 6px;
}
select, input[type="email"] {
width: 100%;
background: #08395f;
border: 1px solid #cffaff44;
border-radius: 10px;
padding: 10px 12px;
font-size: 14px;
color: #cffaff;
outline: none;
box-sizing: border-box;
}
button {
margin-top: 16px;
width: 100%;
border: none;
border-radius: 12px;
padding: 12px 14px;
font-size: 15px;
font-weight: 800;
background: linear-gradient(135deg,#cffaff,#6ad7ff);
color: #084574;
cursor: pointer;
box-shadow: 0 8px 18px rgba(0,0,0,0.3);
}
button:hover {
background: linear-gradient(135deg,#b3f2ff,#84e2ff);
}
.footnote {
margin-top: 10px;
font-size: 11px;
color: #a9d8f3;
text-align: center;
}
.status {
padding: 20px;
font-size: 15px;
color: #cffaff;
background: #08395f;
border: 1px solid #cffaff55;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
word-break: break-word;
}
.status.success { background: #065f46; color: #cffaff; }
.status.error { background: #991b1b; color: #fff; }
`;
const html = `
<form class="card">
<div class="header">
<img class="icon" src="images/logo.jpg">
<div>
<div class="title">Try Managed ODOO - 4 Weeks Free</div>
<div class="subtitle">Full-featured 4-week trial. No credit card required.</div>
</div>
</div>
<label for="location">Location</label>
<select id="location" name="location" required>
<option value="" disabled selected>Select a region</option>
<option value="manchester">UK, Manchester</option>
<option value="boston">US, Boston</option>
<option value="mumbai">IN, Mumbai</option>
<option value="saopaulo">BR, Sao Paulo</option>
<option value="Meppel">NL, Meppel</option>
</select>
<label for="email">Work Email</label>
<input id="email" name="email" type="email" required placeholder="you@company.com">
<input type="hidden" name="product" value="${default_product}">
<input type="hidden" name="utm_source" value="${utm_source}">
<input type="hidden" name="utm_campaign" value="${utm_campaign}">
<button type="submit" class="plausible-event-name=Trial">Start 4-Week Free Trial</button>
<div class="footnote">
By submitting, you agree to the T&C of Scholarix.
</div>
</form>
<div id="status" class="status" style="display:none;"></div>
`;
shadow.innerHTML = `<style>${styles}</style>${html}`;
const form = shadow.querySelector("form");
const statusEl = shadow.getElementById("status");
form.addEventListener("submit", async (e) => {
e.preventDefault();
statusEl.style.display = "block";
statusEl.textContent = "Submitting...";
statusEl.className = "status";
try {
const formData = new FormData(form);
const res = await fetch("https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/47129739-e60b-4944-b6c2-d3fd5ce0991b", {
method: "POST",
body: formData,
});
const text = await res.text();
form.style.display = "none";
statusEl.textContent = text;
statusEl.className = "status success";
} catch (err) {
statusEl.textContent = "❌ Failed to submit. Please try again.";
statusEl.className = "status error";
}
});
})();