static sites for ODOO

This commit is contained in:
oliver
2026-08-22 09:37:27 -03:00
parent b34e001c46
commit 7245e47cac
2 changed files with 750 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ODOO4projects - Support Chat</title>
<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">
<style>
:root{
--odoo-purple: #875A7B;
--odoo-dark: #5a3d52;
--bg: #f6f7fb;
--card: #ffffff;
--muted: #7b7f87;
--accent: #6bbf59;
--radius: 14px;
font-family: 'Rubik', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
}
*{box-sizing:border-box}
body{margin:0;background:linear-gradient(180deg,var(--bg),#eef2f8);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:28px}
.app{width:960px;max-width:96vw;background:transparent;display:grid;grid-template-columns:320px 1fr;gap:20px}
/* 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}
.brand{display:flex;gap:12px;align-items:center}
.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 p{margin:0;font-size:12px;color:var(--muted)}
.sidebar .meta{margin-top:18px;font-size:13px;color:var(--muted)}
.sidebar .meta strong{color:var(--odoo-dark)}
.sidebar .hint{margin-top:14px;padding:10px;border-radius:10px;background:#fbf8ff;color:#5b4160;font-size:13px}
/* Chat area */
.chat-card{background:var(--card);border-radius:var(--radius);display:flex;flex-direction:column;height:70vh;box-shadow:0 10px 30px rgba(24,39,75,0.06);overflow:hidden}
.chat-header{display:flex;align-items:center;gap:12px;padding:18px;border-bottom:1px solid #eef1f6}
.chat-header .title{font-weight:600;color:var(--odoo-dark)}
.chat-header .sub{font-size:13px;color:var(--muted)}
.messages{flex:1;padding:22px;overflow:auto;background:linear-gradient(180deg,#fbfcff, #f7f9fc)}
.messages-inner{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:12px}
.msg{display:flex;gap:10px;align-items:flex-end}
.msg.me{justify-content:flex-end}
.bubble{max-width:68%;padding:12px 14px;border-radius:12px;background:white;border:1px solid #eef1f6;font-size:14px;color:#222}
.bubble.me{background:linear-gradient(90deg,var(--odoo-purple),#9b6fa0);color:white;border:none}
.meta-time{font-size:11px;color:var(--muted);margin-top:6px}
.avatar{width:36px;height:36px;border-radius:10px;background:#eef1f6;display:flex;align-items:center;justify-content:center;color:var(--odoo-dark);font-weight:600}
.composer{padding:14px;border-top:1px solid #eef1f6;display:flex;gap:12px;align-items:center}
.input{flex:1;background:#f2f6fb;border-radius:12px;padding:8px 12px;display:flex;gap:8px;align-items:center}
.input input{border:0;background:transparent;outline:none;padding:10px;font-size:14px;width:100%}
.send{background:var(--odoo-purple);color:white;border:none;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer;box-shadow:0 8px 18px rgba(135,90,123,0.14)}
.send[disabled]{opacity:0.5;cursor:default}
.small{font-size:12px;color:var(--muted)}
/* typing indicator */
.typing{display:flex;gap:6px;align-items:center;padding:6px 10px;border-radius:10px;background:#fff6fb;color:#6b3a55;width:max-content}
.dot{width:7px;height:7px;border-radius:50%;background:#d6a0c8;animation:blink 1.2s linear infinite}
.dot:nth-child(2){animation-delay:0.2s}
.dot:nth-child(3){animation-delay:0.4s}
@keyframes blink{0%{opacity:0.2}50%{opacity:1}100%{opacity:0.2}}
/* small screens */
@media (max-width:760px){
.app{grid-template-columns:1fr;}
.sidebar{display:none}
.chat-card{height:82vh}
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="logo">
<img src="https://static.odoo4projects.com/images/favicon.png" alt="ODOO4projects Logo" />
</div>
<div>
<h1>Odoo Chat</h1>
</div>
</div>
<div class="hint">
This chat is dedicated to support for ODOO4projects hosting services only.<br>
Please note: it does not cover general Odoo usage or functional questions.
</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>
<main class="chat-card" role="main">
<div class="chat-header">
<div style="display:flex;flex-direction:column">
<div class="title">Chat with the ODOO4projects support</div>
</div>
</div>
<div class="messages" id="messages" aria-live="polite">
<div class="messages-inner" id="messagesInner">
<!-- messages go here -->
</div>
</div>
<div class="composer">
<div class="input" role="search">
<input id="messageInput" placeholder="Write a message..." aria-label="Message input" />
</div>
<button id="sendBtn" class="send">Send</button>
</div>
</main>
</div>
<script>
// Configuration - use your webhook
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 input = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const params = new URLSearchParams(window.location.search);
const uuid = params.get("uuid");
// Generate UUID once per page load
function generateChatID(){
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
const chatId = generateChatID();
function formatTime(date){
return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
function appendMessage(text, who='them'){
const wrap = document.createElement('div');
wrap.className = 'msg' + (who==='me' ? ' me' : '');
const avatar = document.createElement('div');
avatar.className = 'avatar';
avatar.textContent = who==='me' ? 'ME' : '4';
const bubble = document.createElement('div');
bubble.className = 'bubble' + (who==='me' ? ' me' : '');
bubble.innerHTML = text.replace(/\n/g,'<br>');
const meta = document.createElement('div');
meta.className = 'meta-time';
meta.textContent = formatTime(new Date());
const col = document.createElement('div');
col.style.display = 'flex';
col.style.flexDirection = 'column';
col.appendChild(bubble);
col.appendChild(meta);
if(who==='me'){
wrap.appendChild(col);
wrap.appendChild(avatar);
} else {
wrap.appendChild(avatar);
wrap.appendChild(col);
}
messagesEl.appendChild(wrap);
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
}
let firstMessage = true;
async function sendMessage(){
let text = input.value.trim();
if(!text) return;
// 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');
input.value = '';
sendBtn.disabled = true;
// show typing indicator
const typingEl = document.createElement('div');
typingEl.className = 'msg';
typingEl.id = 'typing';
typingEl.innerHTML = `
<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>
`;
messagesEl.appendChild(typingEl);
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
try{
// Post to webhook - expecting a JSON response. Adapt to your backend.
const payload = {text: text, uuid: uuid, chatid: chatId};
const res = await fetch(WEBHOOK, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
let data;
const ct = res.headers.get('content-type') || '';
if(ct.includes('application/json')) data = await res.json();
else data = {text: await res.text()};
// remove typing indicator
const t = document.getElementById('typing');
if(t) t.remove();
// Decide where message text is
// Common patterns: {message: '...'}, {text: '...'}, {reply: '...'}, plain text
let replyText = '';
if(typeof data === 'string') replyText = data;
else if(data === null) replyText = 'No response';
else if(data.reply) replyText = data.reply;
else if(data.message) replyText = data.message;
else if(data.text) replyText = data.text;
else if(data.output) replyText = data.output;
else replyText = JSON.stringify(data);
appendMessage(replyText, 'them');
}catch(err){
const t = document.getElementById('typing');
if(t) t.remove();
appendMessage('Error: ' + (err && err.message ? err.message : String(err)), 'them');
} finally {
sendBtn.disabled = false;
}
}
// wire events
sendBtn.addEventListener('click', sendMessage);
input.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMessage(); } });
// friendly welcome
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');
</script>
</body>
</html>
+447
View File
@@ -0,0 +1,447 @@
<!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>
</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">4 weeks 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" 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" 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" 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://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/0c8536be-d175-4740-8e78-123159193b23";
const buy_webhook =
"https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/2366cc41-bfd9-41c0-b9b4-bea1e60726f1";
// ── 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 || "";
// ── Buy handler ───────────────────────────────────────────────────────────
async function buyProduct(productId) {
if (!uuid) {
alert("Missing uuid parameter in URL.");
return;
}
try {
const res = await fetch(buy_webhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uuid, product_id: productId }),
});
if (!res.ok) throw new Error("HTTP " + res.status);
document.body.innerHTML = `
<div style="text-align:center;padding:80px 20px;background:#fff;border-radius:14px;
max-width:560px;margin:60px auto;box-shadow:0 4px 20px rgba(0,0,0,0.1);
font-family:'Inter','Segoe UI',Arial,sans-serif;">
<div style="font-size:3em;margin-bottom:16px;">&#x2705;</div>
<h2 style="color:#262626;font-weight:700;margin-bottom:10px;">Request sent!</h2>
<p style="color:#777;line-height:1.6;">
Please check your email for the payment link and confirmation.<br>
New features will be active as soon as the payment is complete.
</p>
</div>`;
} catch (err) {
console.error(err);
alert(
"Something went wrong. Please try again or contact support.",
);
}
}
// ── 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 : "✅";
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" }]
const record = Array.isArray(payload)
? payload[0]
: payload;
if (record && record.contract_name) {
applyPlanState(record.contract_name, 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>