Public Access
Compare commits
38
Commits
d440d3fa15
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
078b57795b | ||
|
|
68a388cd78 | ||
|
|
61fd457af0 | ||
|
|
bde9dbc48d | ||
|
|
1fd092a9f0 | ||
|
|
8d1b953c61 | ||
|
|
497002a112 | ||
|
|
b28c002d7d | ||
|
|
59ffde396f | ||
|
|
29ff175e89 | ||
|
|
2b0e89191a | ||
|
|
9d14652a57 | ||
|
|
634f4fcdf1 | ||
|
|
bd0881f882 | ||
|
|
c83186f809 | ||
|
|
1f8179d562 | ||
|
|
cdf166632a | ||
|
|
1b0198913c | ||
|
|
a6ada39784 | ||
|
|
bb963850b1 | ||
|
|
28d95b4dcd | ||
|
|
5d2dc11fbc | ||
|
|
8319a74f99 | ||
|
|
5cf58ffa8a | ||
|
|
7dfd5aec9b | ||
|
|
768dc1151a | ||
|
|
1a1b62136f | ||
|
|
f0a2c8d745 | ||
|
|
53e9ea999d | ||
|
|
092f1e44a2 | ||
|
|
17f171d3d9 | ||
|
|
5535f10e7d | ||
|
|
a0ef97b48d | ||
|
|
f3e2812694 | ||
|
|
80bf4bc019 | ||
|
|
a8c5c1837d | ||
|
|
04199bd606 | ||
|
|
570085cb04 |
No files matched your search
+4
-3
@@ -28,7 +28,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Australia landing page (page-scoped; additive) ---- */
|
/* ---- Australia landing page (page-scoped; additive) ---- */
|
||||||
.cmp-sr-only {
|
.cmp-sr-only {
|
||||||
@@ -391,7 +391,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -742,6 +742,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="support.html">Support</a></li>
|
<li><a href="support.html">Support</a></li>
|
||||||
<li><a href="legal.html">Legal</a></li>
|
<li><a href="legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -765,7 +766,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="cmp-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="cmp-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"posts": [
|
"posts": [
|
||||||
|
{
|
||||||
|
"id": "your-agent-can-run-your-odoo-hosting",
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"area": "news",
|
||||||
|
"headline": "Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live",
|
||||||
|
"teaser": "This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo ho",
|
||||||
|
"link": "blog/posts/your-agent-can-run-your-odoo-hosting.html",
|
||||||
|
"image": "https://image.office.odoo4projects.com/img/20_4server.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "freelancer-directory-soft-launch",
|
||||||
|
"date": "2026-11-04",
|
||||||
|
"area": "news",
|
||||||
|
"headline": "Our freelancer directory opens on 1 December. Help us get the signup right",
|
||||||
|
"teaser": "We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start brows",
|
||||||
|
"link": "blog/posts/freelancer-directory-soft-launch.html",
|
||||||
|
"image": "https://image.office.odoo4projects.com/img/freelance_platform.png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "odoo-20-is-live-on-our-servers-here-is-what-changed",
|
"id": "odoo-20-is-live-on-our-servers-here-is-what-changed",
|
||||||
"date": "2026-11-03",
|
"date": "2026-11-03",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
body { background: #0d1b2e; }
|
body { background: #0d1b2e; }
|
||||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #714b67;
|
--primary: #714b67;
|
||||||
@@ -325,6 +325,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Our freelancer directory opens on 1 December. Help us get the signup right | ODOO4projects</title>
|
||||||
|
<meta name="description" content="We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start brows" />
|
||||||
|
<link rel="canonical" href="https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html" />
|
||||||
|
<meta property="og:title" content="Our freelancer directory opens on 1 December. Help us get the signup right" />
|
||||||
|
<meta property="og:description" content="We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start brows" />
|
||||||
|
<meta property="og:url" content="https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html" />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:image" content="https://image.office.odoo4projects.com/img/freelance_platform.png" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<link rel="icon" href="../../images/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
|
<style>
|
||||||
|
body { background: #0d1b2e; }
|
||||||
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
.blog-post h1 { font-family: 'Marcellus', Georgia, serif; font-size: 2.4rem; font-weight: 400; margin: 0 0 0.5rem; color: #0d1b2e; }
|
||||||
|
.blog-post .post-meta { font-family: Inter, sans-serif; font-size: 0.82rem; color: rgba(23,28,38,0.6); margin-bottom: 2rem; }
|
||||||
|
.blog-post .post-meta .badge { display: inline-block; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.15em; text-transform: uppercase; color: #c9a227; margin-right: 0.75rem; }
|
||||||
|
.blog-post .post-image { width: 100%; border-radius: 0.75rem; margin-bottom: 2rem; box-shadow: 0 2px 8px rgba(13,27,46,0.08); }
|
||||||
|
.blog-post p { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; }
|
||||||
|
.blog-post h2 { font-family: 'Marcellus', Georgia, serif; font-size: 1.6rem; font-weight: 400; margin: 2rem 0 0.75rem; color: #0d1b2e; }
|
||||||
|
.blog-post a { color: #8a6f1f; text-decoration: underline; }
|
||||||
|
.blog-post strong { font-weight: 600; }
|
||||||
|
.back-link { display: inline-block; margin-top: 2rem; font-family: 'Marcellus', Georgia, serif; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; color: #c9a227; text-decoration: none; }
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
@media (max-width: 600px) { .blog-post h1 { font-size: 1.7rem; } .blog-post { padding: 2rem 1rem 4rem; } }
|
||||||
|
</style>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BlogPosting",
|
||||||
|
"headline": "Our freelancer directory opens on 1 December. Help us get the signup right",
|
||||||
|
"datePublished": "2026-11-04",
|
||||||
|
"url": "https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html",
|
||||||
|
"image": "https://image.office.odoo4projects.com/img/freelance_platform.png",
|
||||||
|
"author": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "ODOO4projects",
|
||||||
|
"url": "https://odoo4projects.com/"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "ODOO4projects",
|
||||||
|
"url": "https://odoo4projects.com/",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://odoo4projects.com/images/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
|
<script>
|
||||||
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
|
plausible.init()
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="blog-post">
|
||||||
|
<span class="badge">news</span>
|
||||||
|
<h1>Our freelancer directory opens on 1 December. Help us get the signup right</h1>
|
||||||
|
<div class="post-meta">November 4, 2026 · News</div>
|
||||||
|
<img src="https://image.office.odoo4projects.com/img/freelance_platform.png" alt="Our freelancer directory opens on 1 December. Help us get the signup right" class="post-image" loading="lazy" />
|
||||||
|
<div class="post-body">
|
||||||
|
<p>We need your help!<br><br>We are opening our <b>Odoo Community Freelancer directory</b> on 1 December. Before real clients start browsing it, we want the signup to work, and we would like your help to get it right.</p>
|
||||||
|
<p>So this is a soft launch. Sign up for free and tell us how the signup went: where you got stuck, what you expected to find, what you would change. That feedback is what we fix before opening day.</p>
|
||||||
|
<p>Joining is free, for freelancers and clients both. The directory is where a freelancer describes their work, in their own words.</p>
|
||||||
|
<p>If you are a member of the Odoo Community Association, this is a chance to be seen alongside your community. Your profile card carries an OCA Member Profile badge, and a visitor can filter the directory by OCA membership. We mark it, and the choice of who to contact stays with the visitor.</p>
|
||||||
|
<p>Start here: <a href="https://odoo4projects.com/feedback/">odoo4projects.com/feedback</a></p>
|
||||||
|
</div>
|
||||||
|
<a href="../../index.html#blog" class="back-link">← Back to the Blog</a>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
body { background: #0d1b2e; }
|
body { background: #0d1b2e; }
|
||||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
.blog-header {
|
.blog-header {
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
@@ -275,6 +275,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
body { background: #0d1b2e; }
|
body { background: #0d1b2e; }
|
||||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* Blog post specific overrides — matching homepage light theme */
|
/* Blog post specific overrides — matching homepage light theme */
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #714b67;
|
--primary: #714b67;
|
||||||
@@ -277,6 +277,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #714b67;
|
--primary: #714b67;
|
||||||
@@ -296,6 +296,6 @@ Admin credentials: sent via email</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #714b67;
|
--primary: #714b67;
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
</main>
|
</main>
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
src="/static/agent-odoo4projects/agent.js"
|
src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"
|
||||||
></script>
|
></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
</main>
|
</main>
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
src="/static/agent-odoo4projects/agent.js"
|
src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"
|
||||||
></script>
|
></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
body { background: #0d1b2e; }
|
body { background: #0d1b2e; }
|
||||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #714b67;
|
--primary: #714b67;
|
||||||
@@ -175,6 +175,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
body { background: #0d1b2e; }
|
body { background: #0d1b2e; }
|
||||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
body { background: #0d1b2e; }
|
body { background: #0d1b2e; }
|
||||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
body { background: #0d1b2e; }
|
body { background: #0d1b2e; }
|
||||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* Blog post specific styles — leverages CSS vars from style.css */
|
/* Blog post specific styles — leverages CSS vars from style.css */
|
||||||
:root {
|
:root {
|
||||||
@@ -311,6 +311,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
@@ -250,6 +250,6 @@ Odoo > Settings > Technical > Scheduled Actions > backup job</code><
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #714b67;
|
--primary: #714b67;
|
||||||
@@ -267,6 +267,6 @@ Password: (check your email)</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="../../style.css" />
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #714b67;
|
--primary: #714b67;
|
||||||
@@ -165,6 +165,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live | ODOO4projects</title>
|
||||||
|
<meta name="description" content="This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo ho" />
|
||||||
|
<link rel="canonical" href="https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html" />
|
||||||
|
<meta property="og:title" content="Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live" />
|
||||||
|
<meta property="og:description" content="This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo ho" />
|
||||||
|
<meta property="og:url" content="https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html" />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:image" content="https://image.office.odoo4projects.com/img/20_4server.png" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<link rel="icon" href="../../images/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
|
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||||
|
<style>
|
||||||
|
body { background: #0d1b2e; }
|
||||||
|
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||||
|
.blog-post h1 { font-family: 'Marcellus', Georgia, serif; font-size: 2.4rem; font-weight: 400; margin: 0 0 0.5rem; color: #0d1b2e; }
|
||||||
|
.blog-post .post-meta { font-family: Inter, sans-serif; font-size: 0.82rem; color: rgba(23,28,38,0.6); margin-bottom: 2rem; }
|
||||||
|
.blog-post .post-meta .badge { display: inline-block; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.15em; text-transform: uppercase; color: #c9a227; margin-right: 0.75rem; }
|
||||||
|
.blog-post .post-image { width: 100%; border-radius: 0.75rem; margin-bottom: 2rem; box-shadow: 0 2px 8px rgba(13,27,46,0.08); }
|
||||||
|
.blog-post p { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; }
|
||||||
|
.blog-post h2 { font-family: 'Marcellus', Georgia, serif; font-size: 1.6rem; font-weight: 400; margin: 2rem 0 0.75rem; color: #0d1b2e; }
|
||||||
|
.blog-post ul, .blog-post ol { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; padding-left: 1.5rem; }
|
||||||
|
.blog-post li { margin-bottom: 0.3rem; }
|
||||||
|
.blog-post a { color: #8a6f1f; text-decoration: underline; }
|
||||||
|
.blog-post strong { font-weight: 600; }
|
||||||
|
.back-link { display: inline-block; margin-top: 2rem; font-family: 'Marcellus', Georgia, serif; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; color: #c9a227; text-decoration: none; }
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
@media (max-width: 600px) { .blog-post h1 { font-size: 1.7rem; } .blog-post { padding: 2rem 1rem 4rem; } }
|
||||||
|
</style>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BlogPosting",
|
||||||
|
"headline": "Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live",
|
||||||
|
"datePublished": "2026-11-05",
|
||||||
|
"url": "https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html",
|
||||||
|
"image": "https://image.office.odoo4projects.com/img/20_4server.png",
|
||||||
|
"author": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "ODOO4projects",
|
||||||
|
"url": "https://odoo4projects.com/"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "ODOO4projects",
|
||||||
|
"url": "https://odoo4projects.com/",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://odoo4projects.com/images/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
|
<script>
|
||||||
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
|
plausible.init()
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="blog-post">
|
||||||
|
<span class="badge">news</span>
|
||||||
|
<h1>Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live</h1>
|
||||||
|
<div class="post-meta">November 5, 2026 · News</div>
|
||||||
|
<img src="https://image.office.odoo4projects.com/img/20_4server.png" alt="Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live" class="post-image" loading="lazy" />
|
||||||
|
<div class="post-body">
|
||||||
|
<p>This is the piece of the Odoo 20 update we are asked about most: an <strong>MCP server</strong> that lets your agent work on your Odoo hosting directly, instead of you clicking through menus to do routine things.</p>
|
||||||
|
|
||||||
|
<h2>What your agent can do</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Restart the server</strong> — the task nobody wants to be paged for.</li>
|
||||||
|
<li><strong>Upload modules</strong> — deploy new functionality as part of the same workflow.</li>
|
||||||
|
<li><strong>Add and check domains</strong> — routing handled where the work happens.</li>
|
||||||
|
<li><strong>Manage your backups</strong> — create them, and reinstall one when you need to.</li>
|
||||||
|
<li><strong>Read the logfiles</strong> — investigate a problem without digging through them by hand.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Why we built it this way</h2>
|
||||||
|
|
||||||
|
<p>Hosting chores are short, frequent and interrupt you at the worst moment. Handing them to an agent changes the shape of the day: you stay on the work that moves your project, and the operational tasks happen in the background — under the same account, on the same console you already use.</p>
|
||||||
|
|
||||||
|
<p>The console itself is unchanged. What changed is that the routine half of running an instance no longer needs you in front of it.</p>
|
||||||
|
</div>
|
||||||
|
<a href="../../index.html#blog" class="back-link">← Back to the Blog</a>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+7
-5
@@ -128,7 +128,9 @@
|
|||||||
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
||||||
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
||||||
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
||||||
.hero-notice[hidden]{display:none}
|
/* `.hero-notice[hidden]{display:none}` used to live here; the site-wide `[hidden]`
|
||||||
|
rule in /style.css hides it (`.hero-notice` sets `display:flex`, which outranked
|
||||||
|
the UA rule). */
|
||||||
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
||||||
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||||
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
||||||
@@ -220,14 +222,14 @@
|
|||||||
#customQuoteModal .biz-success p{color:var(--midnight)}
|
#customQuoteModal .biz-success p{color:var(--midnight)}
|
||||||
@media(max-width:600px){.oca-table td,.oca-table th{padding:.5rem .65rem;font-size:.8rem}.ow-panel{left:5%;right:5%}.ow-close{top:.5rem;right:.5rem;font-size:1.6rem}}
|
@media(max-width:600px){.oca-table td,.oca-table th{padding:.5rem .65rem;font-size:.8rem}.ow-panel{left:5%;right:5%}.ow-close{top:.5rem;right:.5rem;font-size:1.6rem}}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<!-- Privacy-friendly analytics by Plausible -->
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!--
|
<!--
|
||||||
@@ -672,7 +674,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||||
<script src="/script.js"></script>
|
<script src="/script.js?v=d2046e3469"></script>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
||||||
.oc-sr-only {
|
.oc-sr-only {
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -869,7 +869,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="oc-sr-only" for="footerContactEmail">Ihre E-Mail-Adresse</label>
|
<label class="oc-sr-only" for="footerContactEmail">Ihre E-Mail-Adresse</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
||||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
||||||
.bc-sr-only {
|
.bc-sr-only {
|
||||||
@@ -1012,15 +1012,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||||
</form>
|
</form>
|
||||||
<noscript><p class="bc-noscript">JavaScript ist deaktiviert. Schreiben Sie uns direkt an <a href="mailto:support@ODOO4projects.com">support@ODOO4projects.com</a>.</p></noscript>
|
<noscript><p class="bc-noscript">JavaScript ist deaktiviert. Schreiben Sie uns direkt an <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a>.</p></noscript>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/buy.js"></script>
|
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||||
<script defer src="/static/bootcamp-availability.js"></script>
|
<script defer src="/static/bootcamp-availability.js?v=94dfd8cd7c"></script>
|
||||||
<script defer src="/static/bootcamp-waitlist.js"></script>
|
<script defer src="/static/bootcamp-waitlist.js?v=6c2e2fbd33"></script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
||||||
.cmp-sr-only {
|
.cmp-sr-only {
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -1013,7 +1013,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="cmp-sr-only" for="footerContactEmail">Ihre E-Mail-Adresse</label>
|
<label class="cmp-sr-only" for="footerContactEmail">Ihre E-Mail-Adresse</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
+3
-3
@@ -29,7 +29,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
||||||
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
@@ -586,6 +586,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+7
-5
@@ -128,7 +128,9 @@
|
|||||||
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
||||||
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
||||||
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
||||||
.hero-notice[hidden]{display:none}
|
/* `.hero-notice[hidden]{display:none}` used to live here; the site-wide `[hidden]`
|
||||||
|
rule in /style.css hides it (`.hero-notice` sets `display:flex`, which outranked
|
||||||
|
the UA rule). */
|
||||||
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
||||||
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||||
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
||||||
@@ -220,14 +222,14 @@
|
|||||||
#customQuoteModal .biz-success p{color:var(--midnight)}
|
#customQuoteModal .biz-success p{color:var(--midnight)}
|
||||||
@media(max-width:600px){.oca-table td,.oca-table th{padding:.5rem .65rem;font-size:.8rem}.ow-panel{left:5%;right:5%}.ow-close{top:.5rem;right:.5rem;font-size:1.6rem}}
|
@media(max-width:600px){.oca-table td,.oca-table th{padding:.5rem .65rem;font-size:.8rem}.ow-panel{left:5%;right:5%}.ow-close{top:.5rem;right:.5rem;font-size:1.6rem}}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<!-- Privacy-friendly analytics by Plausible -->
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!--
|
<!--
|
||||||
@@ -672,7 +674,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||||
<script src="/script.js"></script>
|
<script src="/script.js?v=d2046e3469"></script>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
||||||
.oc-sr-only {
|
.oc-sr-only {
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -869,7 +869,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="oc-sr-only" for="footerContactEmail">Su dirección de correo</label>
|
<label class="oc-sr-only" for="footerContactEmail">Su dirección de correo</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Su dirección de correo" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Su dirección de correo" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
||||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
||||||
.bc-sr-only {
|
.bc-sr-only {
|
||||||
@@ -1012,15 +1012,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||||
</form>
|
</form>
|
||||||
<noscript><p class="bc-noscript">JavaScript está desactivado. Escríbanos directamente a <a href="mailto:support@ODOO4projects.com">support@ODOO4projects.com</a>.</p></noscript>
|
<noscript><p class="bc-noscript">JavaScript está desactivado. Escríbanos directamente a <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a>.</p></noscript>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/buy.js"></script>
|
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||||
<script defer src="/static/bootcamp-availability.js"></script>
|
<script defer src="/static/bootcamp-availability.js?v=94dfd8cd7c"></script>
|
||||||
<script defer src="/static/bootcamp-waitlist.js"></script>
|
<script defer src="/static/bootcamp-waitlist.js?v=6c2e2fbd33"></script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
||||||
.cmp-sr-only {
|
.cmp-sr-only {
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -1012,7 +1012,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="cmp-sr-only" for="footerContactEmail">Su dirección de correo</label>
|
<label class="cmp-sr-only" for="footerContactEmail">Su dirección de correo</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Su dirección de correo" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Su dirección de correo" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
+3
-3
@@ -29,7 +29,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
||||||
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
@@ -586,6 +586,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Freelancer signup feedback — ODOO4projects</title>
|
||||||
|
<meta name="description" content="Tell us how the ODOO4projects freelancer signup went — every answer helps us improve the process before the directory opens." />
|
||||||
|
|
||||||
|
<!-- Private utility page. It is reached ONLY by the link we send, never by search.
|
||||||
|
noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from nowhere
|
||||||
|
on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<!-- Nothing in this page's address is a credential, but the page is private and the
|
||||||
|
feedback is a person's words: keep the URL out of any third party's logs. -->
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="canonical" href="https://odoo4projects.com/feedback/" />
|
||||||
|
<meta property="og:url" content="https://odoo4projects.com/feedback/" />
|
||||||
|
<meta property="og:title" content="Freelancer signup feedback — ODOO4projects" />
|
||||||
|
<meta property="og:description" content="Tell us how the ODOO4projects freelancer signup went — every answer helps us improve the process." />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<link rel="icon" href="/images/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Utility page, same shell as /freelancer-edit/ and the top bar of
|
||||||
|
/odoo-community-developers/: brand line, one card, the site tokens (:root in style.css)
|
||||||
|
and the CI palette only. No hero, no footer, no site chrome beyond the brand line. */
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--cream);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: Inter, "Segoe UI", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.wrap { width: 86%; max-width: 1200px; margin: 0 auto; }
|
||||||
|
.topbar { background: var(--midnight); border-bottom: 1px solid var(--line-brass); padding: 0.85rem 0; }
|
||||||
|
.brand {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.brand span { color: var(--brass); }
|
||||||
|
main { padding: 2.5rem 0 4rem; }
|
||||||
|
.card-wrap { max-width: 760px; margin: 0 auto; }
|
||||||
|
h1 {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
.lede { color: var(--ink-soft); margin: 0 0 1rem; }
|
||||||
|
.panel {
|
||||||
|
background: var(--ivory);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow-rest);
|
||||||
|
}
|
||||||
|
.panel + .panel { margin-top: 1.25rem; }
|
||||||
|
h2 {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
}
|
||||||
|
.state { margin: 0; font-weight: 500; }
|
||||||
|
.state-warn, .state-err { color: var(--brass-deep); }
|
||||||
|
.state-ok { color: var(--success); }
|
||||||
|
.detail { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--ink-soft); }
|
||||||
|
|
||||||
|
/* ---- Forms ---- */
|
||||||
|
.field { margin: 0 0 1.1rem; }
|
||||||
|
.field > label { display: block; font-weight: 600; font-size: 0.9rem; margin: 0 0 0.35rem; }
|
||||||
|
input[type="text"], input[type="email"], textarea, select {
|
||||||
|
width: 100%;
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.6rem 0.7rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
textarea { resize: vertical; }
|
||||||
|
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible,
|
||||||
|
input[type="radio"]:focus-visible, input[type="checkbox"]:focus-visible {
|
||||||
|
outline: 2px solid var(--brass);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.hint { font-size: 0.82rem; color: var(--ink-soft); margin: 0.35rem 0 0; }
|
||||||
|
fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.9rem 1rem 0.4rem; margin: 0 0 1.1rem; }
|
||||||
|
legend { font-weight: 600; font-size: 0.9rem; padding: 0 0.35rem; }
|
||||||
|
.choice { display: flex; align-items: flex-start; gap: 0.55rem; margin: 0 0 0.5rem; font-size: 0.9rem; }
|
||||||
|
.choice input { margin-top: 0.28rem; flex: 0 0 auto; }
|
||||||
|
.field[data-bad="1"] input, .field[data-bad="1"] textarea, .field[data-bad="1"] select,
|
||||||
|
fieldset[data-bad="1"] { border-color: var(--brass-deep); }
|
||||||
|
.err { font-size: 0.82rem; color: var(--brass-deep); font-weight: 600; margin: 0.35rem 0 0; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: var(--brass);
|
||||||
|
color: var(--midnight);
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--brass-bright); }
|
||||||
|
.btn:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||||
|
.btn[disabled] { opacity: 0.6; cursor: default; }
|
||||||
|
.status { margin: 0.9rem 0 0; font-weight: 500; min-height: 1.2em; }
|
||||||
|
.status.state-warn, .status.state-err { color: var(--brass-deep); }
|
||||||
|
.status.state-ok { color: var(--success); }
|
||||||
|
|
||||||
|
/* ---- Progress bar (two steps) ---- */
|
||||||
|
.progress { margin: 0 0 1.15rem; }
|
||||||
|
.steps-bar {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem 0.6rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.steps-bar .step { display: flex; align-items: center; gap: 0.4rem; color: var(--ink-soft); }
|
||||||
|
.steps-bar .step + .step::before { content: "·"; margin-right: 0.6rem; color: var(--line-brass); }
|
||||||
|
.step-mark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.55rem;
|
||||||
|
height: 1.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
/* The current step is the brass pill and the bolder label, and it is said out loud
|
||||||
|
("(current step)", .vh below) — weight and a word, so it is never colour alone. */
|
||||||
|
.steps-bar .step-current { color: var(--ink); font-weight: 600; }
|
||||||
|
.steps-bar .step-current .step-mark { background: var(--brass); border-color: var(--brass); color: var(--midnight); }
|
||||||
|
.link-signup { color: var(--brass-deep); font-weight: 600; }
|
||||||
|
/* Visually hidden, still read aloud. */
|
||||||
|
.vh {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- The one wire name and shape this page uses, in ONE place. POST only — the page reads
|
||||||
|
nothing on load. SHAPE (match this in the n8n workflow):
|
||||||
|
|
||||||
|
POST submitUrl { "form": "feedback", <one key per question, the answer> }
|
||||||
|
-> any 2xx = receipt shown; a non-2xx is reported as a failure, never as success.
|
||||||
|
|
||||||
|
The body carries the answers and nothing else: no name, no email, no identifier of any
|
||||||
|
kind. An unexpected shape is named, never guessed. -->
|
||||||
|
<script>
|
||||||
|
window.O4F = {
|
||||||
|
// The one route that receives the feedback form.
|
||||||
|
submitUrl: "https://n8n.odoo4projects.com/webhook/feedback"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Privacy-friendly analytics by Plausible (verbatim; the page carries no query). -->
|
||||||
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
|
<script>
|
||||||
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
|
plausible.init()
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="wrap">
|
||||||
|
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card-wrap">
|
||||||
|
<h1>Freelancer signup feedback</h1>
|
||||||
|
<p class="lede">
|
||||||
|
We opened a free freelancer portal on ODOO4projects.com. Sign up there, then tell us how
|
||||||
|
it went — every answer helps us improve the process.
|
||||||
|
</p>
|
||||||
|
<!-- The portal link, visible next to the intro and not buried in step copy. It opens in
|
||||||
|
a new window and says so. It is a plain link, not the page's primary action —
|
||||||
|
the feedback form still is. -->
|
||||||
|
<p class="detail">
|
||||||
|
The directory opens on <strong>1 December</strong>. Signing up is free for freelancers
|
||||||
|
and clients, and the feedback you give here is what we fix before then.
|
||||||
|
<a
|
||||||
|
class="link-signup"
|
||||||
|
href="https://odoo4projects.com/odoo-community-developers/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Sign up on the freelancer portal (opens in a new window)</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Filled by the script at the foot of the page. Empty on purpose: with JavaScript off
|
||||||
|
there is no form to render, so an empty container plus the noscript note below is
|
||||||
|
the honest picture. -->
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div class="panel">
|
||||||
|
<p class="state state-warn">This page needs JavaScript to show the feedback form.</p>
|
||||||
|
<p class="detail">
|
||||||
|
Turn JavaScript on in your browser and open this page again — or reply to the message
|
||||||
|
that brought you here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var C = window.O4F;
|
||||||
|
var app = document.getElementById("app");
|
||||||
|
// The freelancer portal — the directory page, at its TOP (its own introduction), not the
|
||||||
|
// directory's apply anchor: that anchor drops the visitor at the application form,
|
||||||
|
// mid-page, past the page's own intro. There is no separate login or account page, so this
|
||||||
|
// is the one target. The static .link-signup link in the markup above is the ONE source of
|
||||||
|
// this URL; the script-written link in step 1 reads it back, so the two can never disagree.
|
||||||
|
var PORTAL_URL = document.querySelector(".link-signup").href;
|
||||||
|
|
||||||
|
function el(tag, cls, text) {
|
||||||
|
var n = document.createElement(tag);
|
||||||
|
if (cls) n.className = cls;
|
||||||
|
if (text != null) n.textContent = text; // textContent only: a reply is never markup
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function show(nodes) {
|
||||||
|
while (app.firstChild) app.removeChild(app.firstChild);
|
||||||
|
for (var i = 0; i < nodes.length; i++) app.appendChild(nodes[i]);
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
function status(text, kind) {
|
||||||
|
var p = el("p", "status" + (kind ? " state-" + kind : ""), text);
|
||||||
|
p.setAttribute("role", "status");
|
||||||
|
p.setAttribute("aria-live", "polite");
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
function fetchJson(url, opts) {
|
||||||
|
return fetch(url, opts).then(function (r) {
|
||||||
|
return r.text().then(function (t) {
|
||||||
|
var body = null;
|
||||||
|
try { body = JSON.parse(t); } catch (e) { body = null; }
|
||||||
|
return { ok: r.ok, status: r.status, body: body };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function msgOf(body) {
|
||||||
|
if (!body || typeof body !== "object") return "";
|
||||||
|
var m = body.message || body.error || body.detail;
|
||||||
|
return typeof m === "string" ? m : "";
|
||||||
|
}
|
||||||
|
// A link to the freelancer portal. Opens in a new window and says so, so a screen-reader
|
||||||
|
// user is not surprised by a new tab.
|
||||||
|
function portalLink() {
|
||||||
|
var p = el("p", "detail");
|
||||||
|
var a = el("a", "link-signup");
|
||||||
|
a.href = PORTAL_URL;
|
||||||
|
a.target = "_blank";
|
||||||
|
a.rel = "noopener noreferrer";
|
||||||
|
a.textContent = "Sign up on the freelancer portal (opens in a new window)";
|
||||||
|
p.appendChild(a);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Progress bar (two steps) ---------------------------------------------------------
|
||||||
|
// Step 1 is the signup on the portal, step 2 is this form. The page has no honest way to
|
||||||
|
// know the visitor has used the portal link, so step 1 stays current and nothing is
|
||||||
|
// guessed: the number, the bolder label and the spoken "(current step)" say which one is
|
||||||
|
// current, so it is never colour alone.
|
||||||
|
var STEP_LABELS = ["Sign up", "Give feedback"];
|
||||||
|
function progressBar(current) {
|
||||||
|
var nav = el("nav", "progress");
|
||||||
|
nav.setAttribute("aria-label", "Progress");
|
||||||
|
var ol = el("ol", "steps-bar");
|
||||||
|
STEP_LABELS.forEach(function (label, i) {
|
||||||
|
var n = i + 1;
|
||||||
|
var li = el("li", "step");
|
||||||
|
var mark = el("span", "step-mark");
|
||||||
|
mark.setAttribute("aria-hidden", "true");
|
||||||
|
mark.textContent = String(n);
|
||||||
|
li.appendChild(mark);
|
||||||
|
li.appendChild(el("span", "step-label", label));
|
||||||
|
if (n === current) {
|
||||||
|
li.className = "step step-current";
|
||||||
|
li.setAttribute("aria-current", "step");
|
||||||
|
li.appendChild(el("span", "vh", " (current step)"));
|
||||||
|
} else {
|
||||||
|
li.className = "step step-next";
|
||||||
|
}
|
||||||
|
ol.appendChild(li);
|
||||||
|
});
|
||||||
|
nav.appendChild(ol);
|
||||||
|
return nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Step 1 — the portal link, again, in the step's own copy -------------------------
|
||||||
|
function signupPanel() {
|
||||||
|
var box = el("div", "panel");
|
||||||
|
box.appendChild(el("h2", "", "Step 1 — sign up on the freelancer portal"));
|
||||||
|
box.appendChild(el("p", "detail",
|
||||||
|
"Sign up on the ODOO4projects freelancer portal and add your profile. The directory opens on 1 December."));
|
||||||
|
box.appendChild(portalLink());
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The feedback form ----------------------------------------------------------------
|
||||||
|
// The n8n form's questions, with the copy corrected. The identity block (name, email,
|
||||||
|
// anonymous tick) and the publish-consent question are gone — do not bring them back.
|
||||||
|
// None of these questions is compulsory: the form submits with any subset answered, or none.
|
||||||
|
// The error nodes (.err) and their CSS are kept, but nothing sets them any more.
|
||||||
|
var FIELDS = [
|
||||||
|
{ id: "finalized", label: "Did you finalize the signup?", type: "select", options: [["yes", "Yes"], ["no", "No"]] },
|
||||||
|
{ id: "difficulty", label: "How hard was the signup?", type: "select", options: [["easy", "Easy"], ["ok", "OK"], ["hard", "Hard"]] },
|
||||||
|
{ id: "signup_feedback", label: "What is your feedback on the signup?", type: "textarea" },
|
||||||
|
{ id: "understood_scale", label: "Did you understand how we ask you to rate your skills? (the 15-point scale)", type: "select", options: [["yes", "Yes"], ["no", "No"]] },
|
||||||
|
{ id: "scale_feedback", label: "What is your feedback on the 15-point skill scale?", type: "textarea" },
|
||||||
|
{ id: "almost_stopped", label: "What almost stopped you from signing up?", type: "textarea", hint: "If nothing did, say so." },
|
||||||
|
{ id: "broke_or_confused", label: "Did anything break or confuse you?", type: "textarea" },
|
||||||
|
{ id: "design_shows_work", label: "Do you think our profile design shows what you do well?", type: "select", options: [["yes", "Yes"], ["so-so", "So-so"], ["no", "No"]] },
|
||||||
|
{ id: "design_feedback", label: "What can we improve about the profile design?", type: "textarea" },
|
||||||
|
{ id: "will_stay", label: "Will you stay on the platform?", type: "select", options: [["yes", "Yes"], ["no", "No"]] },
|
||||||
|
{ id: "improve_offer", label: "How can we improve what we offer you?", type: "textarea" },
|
||||||
|
{ id: "recommend", label: "Would you recommend this platform to a freelance colleague?", type: "select", options: [["yes", "Yes, good idea"], ["maybe", "Maybe"], ["no", "Preferably not"]] },
|
||||||
|
{ id: "expect_next", label: "What do you expect to happen next — and how soon?", type: "textarea" }
|
||||||
|
];
|
||||||
|
|
||||||
|
function fieldBlock(spec) {
|
||||||
|
var f = el("div", "field");
|
||||||
|
f.setAttribute("data-field", spec.id);
|
||||||
|
if (spec.type === "radio") {
|
||||||
|
var fs = el("fieldset");
|
||||||
|
fs.setAttribute("data-field", spec.id);
|
||||||
|
var lg = el("legend", "", spec.label);
|
||||||
|
fs.appendChild(lg);
|
||||||
|
spec.options.forEach(function (pair) {
|
||||||
|
var lab = el("label", "choice");
|
||||||
|
var r = el("input");
|
||||||
|
r.type = "radio";
|
||||||
|
r.name = spec.id;
|
||||||
|
r.value = pair[0];
|
||||||
|
r.id = spec.id + "-" + pair[0];
|
||||||
|
lab.appendChild(r);
|
||||||
|
lab.appendChild(el("span", "", pair[1]));
|
||||||
|
fs.appendChild(lab);
|
||||||
|
});
|
||||||
|
var er = el("p", "err", "");
|
||||||
|
er.hidden = true;
|
||||||
|
fs.appendChild(er);
|
||||||
|
return fs;
|
||||||
|
}
|
||||||
|
var lb = el("label", "", spec.label);
|
||||||
|
lb.setAttribute("for", "fb-" + spec.id);
|
||||||
|
var ctl;
|
||||||
|
if (spec.type === "select") {
|
||||||
|
ctl = el("select");
|
||||||
|
ctl.appendChild(new Option("— choose —", ""));
|
||||||
|
spec.options.forEach(function (pair) { ctl.appendChild(new Option(pair[1], pair[0])); });
|
||||||
|
} else if (spec.type === "textarea") {
|
||||||
|
ctl = el("textarea");
|
||||||
|
ctl.rows = 4;
|
||||||
|
} else {
|
||||||
|
ctl = el("input");
|
||||||
|
ctl.type = "text";
|
||||||
|
}
|
||||||
|
ctl.id = "fb-" + spec.id;
|
||||||
|
ctl.name = spec.id;
|
||||||
|
f.appendChild(lb);
|
||||||
|
f.appendChild(ctl);
|
||||||
|
if (spec.hint) f.appendChild(el("p", "hint", spec.hint));
|
||||||
|
var e = el("p", "err", "");
|
||||||
|
e.hidden = true;
|
||||||
|
f.appendChild(e);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedbackPanel() {
|
||||||
|
var box = el("div", "panel");
|
||||||
|
box.appendChild(el("h2", "", "How did it go?"));
|
||||||
|
box.appendChild(el("p", "detail", "Answer in your own words. Your answers come to us — nothing here is published."));
|
||||||
|
var form = el("form");
|
||||||
|
form.setAttribute("novalidate", "novalidate");
|
||||||
|
|
||||||
|
FIELDS.forEach(function (spec) { form.appendChild(fieldBlock(spec)); });
|
||||||
|
|
||||||
|
var line = status("", "");
|
||||||
|
var btn = el("button", "btn", "Send feedback");
|
||||||
|
btn.type = "submit";
|
||||||
|
form.appendChild(btn);
|
||||||
|
form.appendChild(line);
|
||||||
|
|
||||||
|
form.addEventListener("submit", function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// Nothing here is compulsory, so there is nothing to validate and nothing to announce.
|
||||||
|
// The .err nodes and their CSS are kept in place in case a question is ever made
|
||||||
|
// compulsory again, but nothing here sets them: an empty form submits like any other.
|
||||||
|
|
||||||
|
// Build the body. Only the questions that were answered, and nothing else — no
|
||||||
|
// name, no email, no identifier, and no empty string for a question left blank.
|
||||||
|
var body = { form: "feedback" };
|
||||||
|
FIELDS.forEach(function (spec) {
|
||||||
|
var v = "";
|
||||||
|
if (spec.type === "radio") {
|
||||||
|
var checked = form.querySelector('input[name="' + spec.id + '"]:checked');
|
||||||
|
v = checked ? checked.value : "";
|
||||||
|
} else {
|
||||||
|
var ctl = form.querySelector("#fb-" + spec.id);
|
||||||
|
v = ctl ? ctl.value.trim() : "";
|
||||||
|
}
|
||||||
|
if (v) body[spec.id] = v;
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
line.className = "status";
|
||||||
|
line.textContent = "Sending…";
|
||||||
|
fetchJson(C.submitUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}).then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
btn.disabled = false;
|
||||||
|
line.className = "status state-warn";
|
||||||
|
line.textContent = "We could not send your feedback — the service answered " + res.status +
|
||||||
|
(msgOf(res.body) ? " — " + msgOf(res.body) : "") + ". Your answers are still here; try again.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
line.className = "status state-ok";
|
||||||
|
line.textContent = "Thank you — your feedback reached us.";
|
||||||
|
btn.disabled = true;
|
||||||
|
form.querySelectorAll("input, textarea, select, button").forEach(function (n) {
|
||||||
|
if (n !== btn) n.disabled = true;
|
||||||
|
});
|
||||||
|
}).catch(function () {
|
||||||
|
btn.disabled = false;
|
||||||
|
line.className = "status state-warn";
|
||||||
|
line.textContent = "We could not reach the service. Your answers are still here; try again.";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
box.appendChild(form);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Render ---------------------------------------------------------------------------
|
||||||
|
// One state, always: the form. Nothing is read on load, so there is no fetch here.
|
||||||
|
show([progressBar(1), signupPanel(), feedbackPanel()]);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Answer a question — ODOO4projects</title>
|
||||||
|
<meta name="description" content="Private page: an Odoo freelancer answers a client's question." />
|
||||||
|
|
||||||
|
<!-- Private utility page. It is reached ONLY by a link we email, never by search.
|
||||||
|
noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from nowhere
|
||||||
|
on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<!-- While the link is open the address carries a credential. No claim-by-subdomain of the
|
||||||
|
hash travels to a third party (Google Fonts, and every other cross-origin request). -->
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="canonical" href="https://odoo4projects.com/freelancer-answer/" />
|
||||||
|
<link rel="icon" href="/images/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Utility page. Deliberately small: no hero, no footer, no site chrome beyond a
|
||||||
|
brand line. Reuses the site tokens (:root in style.css) and the CI palette only. */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--cream);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: Inter, "Segoe UI", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
width: 86%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.topbar {
|
||||||
|
background: var(--midnight);
|
||||||
|
border-bottom: 1px solid var(--line-brass);
|
||||||
|
padding: 0.85rem 0;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.brand span {
|
||||||
|
color: var(--brass);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 2.5rem 0 4rem;
|
||||||
|
}
|
||||||
|
.card-wrap {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
.lede {
|
||||||
|
color: var(--ink-soft);
|
||||||
|
margin: 0 0 1.75rem;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
background: var(--ivory);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow-rest);
|
||||||
|
}
|
||||||
|
.state {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.state-warn,
|
||||||
|
.state-err {
|
||||||
|
color: var(--brass-deep);
|
||||||
|
}
|
||||||
|
.state-ok {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
.detail {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
.qcard {
|
||||||
|
background: var(--midnight);
|
||||||
|
color: var(--cream);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.1rem 1.25rem;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
.qcard-label {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--brass);
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
.qcard-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.field label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.7rem 0.8rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--ink);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
textarea:focus-visible {
|
||||||
|
outline: 2px solid var(--brass);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: var(--brass);
|
||||||
|
color: var(--midnight);
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: var(--brass-bright);
|
||||||
|
}
|
||||||
|
.btn:focus-visible {
|
||||||
|
outline: 2px solid var(--midnight);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.btn[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin: 0.9rem 0 0;
|
||||||
|
font-weight: 500;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 1) Read the credential, then take it OUT of the address bar.
|
||||||
|
The hash arrives as ?has=... or ?hash=... (both spellings accepted, 'has' first).
|
||||||
|
The page keeps it in memory for the GET, the PATCH and a retry; it must never sit in
|
||||||
|
the URL, because Plausible reports location.href and a shared/forwarded link would
|
||||||
|
leak it. Stripping the query BEFORE the tracker is loaded is what keeps it out of
|
||||||
|
analytics. All wire names and key lists live here, in one place. -->
|
||||||
|
<script>
|
||||||
|
window.O4P = {
|
||||||
|
// The node webhook: holds the question (GET) and receives the answer (PATCH).
|
||||||
|
webhook: "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_quote",
|
||||||
|
// Query-string spellings to read, in order. The brief wrote "has" twice, so "has"
|
||||||
|
// wins; "hash" is accepted as the identical fallback. Confirm with the owner which
|
||||||
|
// spelling the emailed links will carry.
|
||||||
|
hashKeys: ["has", "hash"],
|
||||||
|
// Wire name used OUTWARD, both directions: GET query param and PATCH body key.
|
||||||
|
hashWireKey: "has",
|
||||||
|
// PATCH body key carrying the freelancer's answer.
|
||||||
|
answerWireKey: "answer",
|
||||||
|
// The ONLY response keys this page may render. Anything not listed is never read,
|
||||||
|
// so a client's email/name/phone sitting in the same object cannot reach the page.
|
||||||
|
questionKeys: ["question", "brief", "message", "text"]
|
||||||
|
};
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var params = new URLSearchParams(location.search);
|
||||||
|
var hash = "";
|
||||||
|
for (var i = 0; i < window.O4P.hashKeys.length; i++) {
|
||||||
|
var v = params.get(window.O4P.hashKeys[i]);
|
||||||
|
if (v) { hash = v; break; }
|
||||||
|
}
|
||||||
|
window.__ANSWER_HASH = hash;
|
||||||
|
// Drop the query string from the address + history. No reload loses anything:
|
||||||
|
// the hash now lives in memory above.
|
||||||
|
if (location.search) history.replaceState(null, "", location.pathname);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 2) Privacy-friendly analytics by Plausible (verbatim; the page now has no query). -->
|
||||||
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
|
<script>
|
||||||
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
|
plausible.init()
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="wrap">
|
||||||
|
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card-wrap">
|
||||||
|
<h1>Answer a question</h1>
|
||||||
|
<p class="lede">
|
||||||
|
A client asked you a question through ODOO4projects. Write your answer below and it
|
||||||
|
goes back to us — we pass it on to them.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Filled by the script at the foot of the page. Empty on purpose: with
|
||||||
|
JavaScript off there is no way to read the question or send an answer, so an
|
||||||
|
empty container + the noscript note below is the honest picture. -->
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div class="panel">
|
||||||
|
<p class="state state-warn">This page needs JavaScript to load the question and to send your answer.</p>
|
||||||
|
<p class="detail">
|
||||||
|
Turn JavaScript on in your browser and open the link again — or reply to the email
|
||||||
|
that brought you here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var C = window.O4P;
|
||||||
|
var app = document.getElementById("app");
|
||||||
|
var hash = window.__ANSWER_HASH || "";
|
||||||
|
|
||||||
|
function el(tag, cls, text) {
|
||||||
|
var n = document.createElement(tag);
|
||||||
|
if (cls) n.className = cls;
|
||||||
|
if (text != null) n.textContent = text; // textContent only: a question is never markup
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function show(nodes) {
|
||||||
|
while (app.firstChild) app.removeChild(app.firstChild);
|
||||||
|
var box = el("div", "panel");
|
||||||
|
for (var i = 0; i < nodes.length; i++) box.appendChild(nodes[i]);
|
||||||
|
app.appendChild(box);
|
||||||
|
}
|
||||||
|
function status(text, kind) {
|
||||||
|
var p = el("p", "state" + (kind ? " state-" + kind : ""), text);
|
||||||
|
p.setAttribute("role", "status");
|
||||||
|
p.setAttribute("aria-live", "polite");
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
function retryButton() {
|
||||||
|
var b = el("button", "btn", "Try again");
|
||||||
|
b.type = "button";
|
||||||
|
b.addEventListener("click", load);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GET reply may be a bare object, an array, or wrapped in .data. Unwrap the
|
||||||
|
// envelope only — the field names inside are decided by questionKeys.
|
||||||
|
function unwrap(payload) {
|
||||||
|
if (Array.isArray(payload)) return payload[0] || null;
|
||||||
|
if (payload && typeof payload === "object") {
|
||||||
|
if (Array.isArray(payload.data)) return payload.data[0] || null;
|
||||||
|
if (Array.isArray(payload.items)) return payload.items[0] || null;
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function pickQuestion(obj) {
|
||||||
|
if (!obj) return "";
|
||||||
|
for (var i = 0; i < C.questionKeys.length; i++) {
|
||||||
|
var v = obj[C.questionKeys[i]];
|
||||||
|
if (typeof v === "string" && v.trim()) return v.trim();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (!hash) {
|
||||||
|
show([
|
||||||
|
status("This link is incomplete — it arrived without its code.", "warn"),
|
||||||
|
el("p", "detail", "Open it again from the email that brought you here.")
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
show([status("Loading your question…")]);
|
||||||
|
|
||||||
|
fetch(C.webhook + "?" + encodeURIComponent(C.hashWireKey) + "=" + encodeURIComponent(hash), {
|
||||||
|
headers: { Accept: "application/json" }
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
// Live behaviour for an unknown hash: 500 {"code":0,"message":"No item to return was found"}.
|
||||||
|
return res.text().then(function (body) {
|
||||||
|
var msg = "";
|
||||||
|
try { msg = (JSON.parse(body) || {}).message || ""; } catch (e) { /* not json */ }
|
||||||
|
show([
|
||||||
|
status("This link is not valid, or it has expired.", "warn"),
|
||||||
|
el("p", "detail", "The service answered " + res.status + (msg ? " — " + msg : "") + "."),
|
||||||
|
el("p", "detail", "If you think that is wrong, reply to the email that brought you here.")
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.json().then(renderQuestion).catch(function () {
|
||||||
|
show([
|
||||||
|
status("We could not read the reply from the service.", "warn"),
|
||||||
|
el("p", "detail", "The reply was not in a form we understand. Please try again."),
|
||||||
|
retryButton()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
// Network failure: the hash is still in memory, so a retry needs no new link.
|
||||||
|
show([
|
||||||
|
status("We could not load your question just now.", "warn"),
|
||||||
|
el("p", "detail", "Your link has been kept, so you can try again."),
|
||||||
|
retryButton()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQuestion(payload) {
|
||||||
|
var obj = unwrap(payload);
|
||||||
|
var question = pickQuestion(obj);
|
||||||
|
if (!question) {
|
||||||
|
// Honest about a shape we did not expect: name the fields, never a value. A
|
||||||
|
// field name cannot identify the client; a value might.
|
||||||
|
var names = obj && typeof obj === "object" ? Object.keys(obj) : [];
|
||||||
|
show([
|
||||||
|
status("We could not read the question from the reply from the service.", "warn"),
|
||||||
|
el("p", "detail",
|
||||||
|
names.length
|
||||||
|
? "The reply carried these fields: " + names.join(", ") + "."
|
||||||
|
: "The reply carried no readable data.")
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var card = el("div", "qcard");
|
||||||
|
card.appendChild(el("p", "qcard-label", "The question"));
|
||||||
|
card.appendChild(el("blockquote", "qcard-text", question));
|
||||||
|
|
||||||
|
var form = el("form", "answer-form");
|
||||||
|
form.setAttribute("novalidate", "novalidate"); // JS does the check, announced below
|
||||||
|
|
||||||
|
var field = el("div", "field");
|
||||||
|
var label = el("label", "", "Your answer");
|
||||||
|
label.setAttribute("for", "answerText");
|
||||||
|
var ta = el("textarea");
|
||||||
|
ta.id = "answerText";
|
||||||
|
ta.name = C.answerWireKey;
|
||||||
|
ta.rows = 8;
|
||||||
|
ta.required = true;
|
||||||
|
ta.setAttribute("aria-describedby", "answerHint");
|
||||||
|
var hint = el("p", "hint", "Your answer goes back to ODOO4projects, who pass it on. Write it as you would to the client.");
|
||||||
|
hint.id = "answerHint";
|
||||||
|
field.appendChild(label);
|
||||||
|
field.appendChild(ta);
|
||||||
|
field.appendChild(hint);
|
||||||
|
|
||||||
|
var btn = el("button", "btn", "Send answer");
|
||||||
|
btn.type = "submit";
|
||||||
|
var line = el("p", "status");
|
||||||
|
line.id = "answerStatus";
|
||||||
|
line.setAttribute("role", "status");
|
||||||
|
line.setAttribute("aria-live", "polite");
|
||||||
|
|
||||||
|
function setLine(text, kind) {
|
||||||
|
line.className = "status" + (kind ? " state-" + kind : "");
|
||||||
|
line.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener("submit", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var answer = ta.value.trim();
|
||||||
|
if (!answer) {
|
||||||
|
ta.setAttribute("aria-invalid", "true");
|
||||||
|
setLine("Please write your answer first.", "warn");
|
||||||
|
ta.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ta.removeAttribute("aria-invalid");
|
||||||
|
|
||||||
|
// The body shape, in one constant (C.hashWireKey + C.answerWireKey):
|
||||||
|
// {"has":"<hash>","answer":"<text>"}
|
||||||
|
// PATCH, not POST: the question record already exists (the client's submission
|
||||||
|
// created it), so this answer is a PARTIAL UPDATE of it — POST risks creating a
|
||||||
|
// second record. Match this method and these two keys in the n8n workflow.
|
||||||
|
var body = {};
|
||||||
|
body[C.hashWireKey] = hash;
|
||||||
|
body[C.answerWireKey] = answer;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
setLine("Sending…", null);
|
||||||
|
fetch(C.webhook, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (res.ok) {
|
||||||
|
// A RECEIPT, not a delivery claim: n8n answers 200 before the workflow
|
||||||
|
// runs, so this page cannot know the answer reached anyone. No time is
|
||||||
|
// promised. The receipt shows ONLY on a 2xx.
|
||||||
|
setLine("Thank you — we have your answer.", "ok");
|
||||||
|
form.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Non-2xx: never show the receipt. A method the route refuses (404/405/501)
|
||||||
|
// is a clear failure, not a transient one — say so plainly and KEEP the
|
||||||
|
// answer in the box so it can be retried.
|
||||||
|
var refused = res.status === 404 || res.status === 405 || res.status === 501;
|
||||||
|
setLine(
|
||||||
|
refused
|
||||||
|
? "We could not save your answer. Please reply to the email that brought you here."
|
||||||
|
: "That did not send. Please try again.",
|
||||||
|
"err"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
// Network failure: the hash stays in memory, the answer stays in the box.
|
||||||
|
setLine("That did not send. Please try again.", "err");
|
||||||
|
})
|
||||||
|
.then(function () { btn.disabled = false; });
|
||||||
|
});
|
||||||
|
|
||||||
|
form.appendChild(field);
|
||||||
|
form.appendChild(btn);
|
||||||
|
form.appendChild(line);
|
||||||
|
|
||||||
|
show([card, form]);
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Your email is confirmed — ODOO4projects</title>
|
||||||
|
<meta name="description" content="Private page: your email is confirmed and your card is in the Odoo Freelancer Directory." />
|
||||||
|
|
||||||
|
<!-- Private utility page. It is reached ONLY by the confirmation link we email, never by
|
||||||
|
search. noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from
|
||||||
|
nowhere on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<!-- The confirmation is completed by the workflow, so this address may carry anything the
|
||||||
|
redirect appends. Nothing from the query string is ever rendered or read: the page is
|
||||||
|
identical with a bare URL and with something appended, and the query is dropped from the
|
||||||
|
address (below) before the tracker loads. -->
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="canonical" href="https://odoo4projects.com/freelancer-confirmed/" />
|
||||||
|
<meta property="og:url" content="https://odoo4projects.com/freelancer-confirmed/" />
|
||||||
|
<meta property="og:title" content="Your email is confirmed — ODOO4projects" />
|
||||||
|
<meta property="og:description" content="Your card is in the Odoo Freelancer Directory." />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<link rel="icon" href="/images/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Utility page, same shell as /freelancer-edit/ and /feedback/: brand line, one card, the
|
||||||
|
site tokens (:root in style.css) and the CI palette only. No hero, no footer, no site
|
||||||
|
chrome beyond the brand line. */
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--cream);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: Inter, "Segoe UI", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.wrap { width: 86%; max-width: 1200px; margin: 0 auto; }
|
||||||
|
.topbar { background: var(--midnight); border-bottom: 1px solid var(--line-brass); padding: 0.85rem 0; }
|
||||||
|
.brand {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.brand span { color: var(--brass); }
|
||||||
|
main { padding: 2.5rem 0 4rem; }
|
||||||
|
.card-wrap { max-width: 760px; margin: 0 auto; }
|
||||||
|
h1 {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
.lede { color: var(--ink-soft); margin: 0 0 1rem; }
|
||||||
|
.panel {
|
||||||
|
background: var(--ivory);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow-rest);
|
||||||
|
}
|
||||||
|
.panel + .panel { margin-top: 1.25rem; }
|
||||||
|
h2 {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
}
|
||||||
|
.detail { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--ink-soft); }
|
||||||
|
.detail:first-of-type { margin-top: 0; }
|
||||||
|
|
||||||
|
/* The page's ONE primary action is the feedback link; the directory link is a plain link. */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: var(--brass);
|
||||||
|
color: var(--midnight);
|
||||||
|
padding: 0.85rem 2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--brass-bright); }
|
||||||
|
/* The anchor needs the text colour kept on hover: the site's global a:hover ({color:
|
||||||
|
var(--brass)}) outranks a lone .btn, and brass on brass-bright is unreadable. */
|
||||||
|
a.btn, a.btn:hover { color: var(--midnight); }
|
||||||
|
.btn:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||||
|
.link-plain { color: var(--brass-deep); font-weight: 600; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- The confirmation redirect may append anything (a name, an id, a tracking token). NOTHING
|
||||||
|
from the query string is read or rendered: this page has one state, whatever it is handed.
|
||||||
|
The query is dropped from the address + the history entry BEFORE the tracker is loaded —
|
||||||
|
Plausible reports location.href, so stripping it first is what keeps anything appended out
|
||||||
|
of the analytics payload (the same care as /freelancer-edit/). With JavaScript off the
|
||||||
|
query stays in the address bar and still nothing renders it. -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (location.search) history.replaceState(null, "", location.pathname);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Privacy-friendly analytics by Plausible (verbatim; the page now carries no query). -->
|
||||||
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
|
<script>
|
||||||
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
|
plausible.init()
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="wrap">
|
||||||
|
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card-wrap">
|
||||||
|
<h1>Your email is confirmed</h1>
|
||||||
|
<p class="lede">
|
||||||
|
Thank you — your email address is confirmed, and your card is now in the Odoo Freelancer
|
||||||
|
Directory.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 1. The page's primary action: the feedback. On its own panel, first, brass and larger. -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>How did the signup go?</h2>
|
||||||
|
<p class="detail">
|
||||||
|
The signup is the part we most want to get right, and you have just been through it.
|
||||||
|
Tell us how it went — every answer helps us improve it.
|
||||||
|
</p>
|
||||||
|
<a class="btn" href="https://odoo4projects.com/feedback/">Give feedback on the signup</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. See yourself in the directory. -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>See your card in the directory</h2>
|
||||||
|
<p class="detail">
|
||||||
|
Everything on a card is the freelancer's own words.
|
||||||
|
</p>
|
||||||
|
<p class="detail">
|
||||||
|
<a
|
||||||
|
class="link-plain"
|
||||||
|
href="https://odoo4projects.com/odoo-community-developers/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Open the Odoo Freelancer Directory (opens in a new window)</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Nobody is stuck with what they typed. One line, and the credential stays where it
|
||||||
|
belongs: the edit link is the one we email, so it is not linked from here. -->
|
||||||
|
<p class="detail">
|
||||||
|
You can change or remove your profile at any time — the link for that is in the email we
|
||||||
|
send you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,813 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Edit your listing — ODOO4projects</title>
|
||||||
|
<meta name="description" content="Private page: a listed Odoo freelancer edits or deletes their own listing." />
|
||||||
|
|
||||||
|
<!-- Private utility page. It is reached ONLY by a link we email, never by search.
|
||||||
|
noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from nowhere
|
||||||
|
on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<!-- While the link is open the address carries a credential. No claim-by-subdomain of the
|
||||||
|
hash travels to a third party (Google Fonts, and every other cross-origin request). -->
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="canonical" href="https://odoo4projects.com/freelancer-edit/" />
|
||||||
|
<!-- The ONE path the emailed link uses; mirrors /freelancer-answer/ deliberately. -->
|
||||||
|
<meta property="og:url" content="https://odoo4projects.com/freelancer-edit/" />
|
||||||
|
<meta property="og:title" content="Edit your listing — ODOO4projects" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<link rel="icon" href="/images/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Utility page. Deliberately small: no hero, no footer, no site chrome beyond a brand
|
||||||
|
line. Reuses the site tokens (:root in style.css) and the CI palette only. */
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--cream);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: Inter, "Segoe UI", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.wrap { width: 86%; max-width: 1200px; margin: 0 auto; }
|
||||||
|
.topbar { background: var(--midnight); border-bottom: 1px solid var(--line-brass); padding: 0.85rem 0; }
|
||||||
|
.brand {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.brand span { color: var(--brass); }
|
||||||
|
main { padding: 2.5rem 0 4rem; }
|
||||||
|
.card-wrap { max-width: 720px; margin: 0 auto; }
|
||||||
|
h1 {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
}
|
||||||
|
.lede { color: var(--ink-soft); margin: 0 0 1.75rem; }
|
||||||
|
.panel {
|
||||||
|
background: var(--ivory);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow-rest);
|
||||||
|
}
|
||||||
|
.state { margin: 0; font-weight: 500; }
|
||||||
|
.state-warn, .state-err { color: var(--brass-deep); }
|
||||||
|
.state-ok { color: var(--success); }
|
||||||
|
.detail { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--ink-soft); }
|
||||||
|
.who {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--brass-deep);
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- The form ---- */
|
||||||
|
.field { margin: 0 0 1.1rem; }
|
||||||
|
.field > label { display: block; font-weight: 600; font-size: 0.9rem; margin: 0 0 0.35rem; }
|
||||||
|
input[type="text"], input[type="url"], input[type="email"], textarea, select {
|
||||||
|
width: 100%;
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.6rem 0.7rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
textarea { resize: vertical; }
|
||||||
|
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible {
|
||||||
|
outline: 2px solid var(--brass);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.hint { font-size: 0.82rem; color: var(--ink-soft); margin: 0.35rem 0 0; }
|
||||||
|
.counter { font-size: 0.78rem; color: var(--ink-soft); margin: 0.3rem 0 0; }
|
||||||
|
.counter[data-over="1"] { color: var(--brass-deep); font-weight: 600; }
|
||||||
|
|
||||||
|
fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.9rem 1rem 0.4rem; margin: 0 0 1.1rem; }
|
||||||
|
legend { font-weight: 600; font-size: 0.9rem; padding: 0 0.35rem; }
|
||||||
|
.row { display: flex; align-items: center; gap: 0.7rem; margin: 0 0 0.7rem; }
|
||||||
|
.row label { flex: 0 0 6.5rem; font-size: 0.85rem; }
|
||||||
|
.row input[type="range"] { flex: 1 1 auto; min-width: 0; }
|
||||||
|
.row output { flex: 0 0 1.6rem; text-align: center; font-variant-numeric: tabular-nums; font-weight: 600; }
|
||||||
|
.total { font-size: 0.85rem; color: var(--ink-soft); margin: 0 0 0.4rem; }
|
||||||
|
.total[data-bad="1"] { color: var(--brass-deep); font-weight: 600; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: var(--brass);
|
||||||
|
color: var(--midnight);
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--brass-bright); }
|
||||||
|
.btn:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||||
|
.btn[disabled] { opacity: 0.6; cursor: default; }
|
||||||
|
.status { margin: 0.9rem 0 0; font-weight: 500; min-height: 1.2em; }
|
||||||
|
|
||||||
|
/* ---- Delete ---- */
|
||||||
|
.danger { margin-top: 1.75rem; border-top: 1px solid var(--line); padding-top: 1.25rem; }
|
||||||
|
.danger h2 { font-family: Marcellus, Georgia, serif; font-weight: 400; font-size: 1.1rem; margin: 0 0 0.4rem; }
|
||||||
|
.danger p { font-size: 0.88rem; color: var(--ink-soft); margin: 0 0 0.8rem; }
|
||||||
|
.confirm { display: flex; align-items: flex-start; gap: 0.55rem; margin: 0 0 0.9rem; font-size: 0.88rem; }
|
||||||
|
.confirm input { margin-top: 0.25rem; }
|
||||||
|
.btn-danger { background: transparent; border: 1px solid var(--brass-deep); color: var(--brass-deep); }
|
||||||
|
.btn-danger:hover { background: var(--brass-deep); color: #fff; }
|
||||||
|
.btn-danger:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||||
|
.photo-readonly { display: flex; align-items: center; gap: 0.8rem; }
|
||||||
|
.photo-readonly img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; }
|
||||||
|
.photo-readonly .mono {
|
||||||
|
width: 56px; height: 56px; border-radius: 50%;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
background: var(--midnight); color: var(--brass);
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- The 15-point rule, in ONE copy for both forms (this one and the application form). -->
|
||||||
|
<script src="/static/focus-rule.js?v=22ac72a8f0"></script>
|
||||||
|
<script src="/static/photo-upload.js?v=c17638457b"></script>
|
||||||
|
<script src="/static/expert-image.js?v=bad5d8c733"></script>
|
||||||
|
|
||||||
|
<!-- 1) Read the credential, then take it OUT of the address bar.
|
||||||
|
The hash arrives as ?has=... or ?hash=... (both spellings accepted, 'has' first — the
|
||||||
|
same list the answer page reads, so the two emailed links behave identically). The page
|
||||||
|
keeps it in memory for the GET, the save, the delete and a retry; it must never sit in
|
||||||
|
the URL, because Plausible reports location.href. Stripping the query BEFORE the tracker
|
||||||
|
is loaded is what keeps it out of analytics. All wire names and key lists live here. -->
|
||||||
|
<script>
|
||||||
|
window.O4E = {
|
||||||
|
// The node webhook: holds this person's listing (GET) and receives the save + delete.
|
||||||
|
webhook: "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_edit",
|
||||||
|
// Query-string spellings to read, in order. Same list as the answer page: the two
|
||||||
|
// emailed links are one shape. Confirm with the owner which spelling the mails carry.
|
||||||
|
hashKeys: ["has", "hash"],
|
||||||
|
// Wire name used OUTWARD on the GET query string.
|
||||||
|
hashWireKey: "has",
|
||||||
|
// SAVE: PATCH the existing record, carrying BOTH the hash (the credential, same key
|
||||||
|
// the delete already sends) and the record id. An id alone authorises nothing: record
|
||||||
|
// ids are small sequential integers, publicly visible on every directory card, so a
|
||||||
|
// request without the hash is refused before it is sent. Match this method in n8n.
|
||||||
|
saveMethod: "PATCH",
|
||||||
|
// DELETE: unverified. A route that refuses it (404/405/501) is a clear failure and
|
||||||
|
// this page never reports success on a non-2xx. Match this method in the workflow.
|
||||||
|
deleteMethod: "DELETE",
|
||||||
|
// How the GET reply is read, in ONE place. The primary name is what the directory
|
||||||
|
// webhook already returns, so the two endpoints agree; a lower-case variant is taken
|
||||||
|
// as the identical fallback. A key NOT listed here is never read.
|
||||||
|
aliases: {
|
||||||
|
id: ["id"],
|
||||||
|
Name: ["Name", "name"],
|
||||||
|
Focus: ["Focus", "focus"],
|
||||||
|
description: ["description", "Description"],
|
||||||
|
Country: ["Country", "country"],
|
||||||
|
OCA_link: ["OCA_link", "oca_link"],
|
||||||
|
// The four profile links. The primary name is the LIVE wire key, casing and all —
|
||||||
|
// "Linkedin", "github", "upwork", "fiverr" are deliberately mixed and must never be
|
||||||
|
// normalised (a normalised key is the silent-drop class that ate odoo_versions). A
|
||||||
|
// same-word casing variant is the identical fallback, the way Name/name already works;
|
||||||
|
// "github" is lower-case on the wire, so it is read exactly as it is, with no variant.
|
||||||
|
Linkedin: ["Linkedin", "linkedin"], github: ["github"], upwork: ["upwork", "Upwork"], fiverr: ["fiverr", "Fiverr"],
|
||||||
|
functional: ["functional"], system: ["system"], modules: ["modules"],
|
||||||
|
integration: ["integration"], ai: ["ai"],
|
||||||
|
english: ["english"], german: ["german"], spanish: ["spanish"]
|
||||||
|
},
|
||||||
|
focusAxes: ["functional", "system", "modules", "integration", "ai"],
|
||||||
|
focusAxisLabels: {
|
||||||
|
functional: "Functional", system: "System", modules: "Modules",
|
||||||
|
integration: "Integration", ai: "AI"
|
||||||
|
},
|
||||||
|
focusTotal: window.O4PFocus.TOTAL,
|
||||||
|
focusMax: 5,
|
||||||
|
focusMaxChars: 70,
|
||||||
|
descMaxChars: 230,
|
||||||
|
langs: [["english", "English"], ["german", "German"], ["spanish", "Spanish"]],
|
||||||
|
// Country VALUE FORMAT the select sends, same as the other forms: the ISO alpha-2 code.
|
||||||
|
countryValueFormat: "code"
|
||||||
|
};
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var params = new URLSearchParams(location.search);
|
||||||
|
var hash = "";
|
||||||
|
for (var i = 0; i < window.O4E.hashKeys.length; i++) {
|
||||||
|
var v = params.get(window.O4E.hashKeys[i]);
|
||||||
|
if (v) { hash = v; break; }
|
||||||
|
}
|
||||||
|
window.__EDIT_HASH = hash;
|
||||||
|
// Drop the query from the address + history. The hash lives in memory above.
|
||||||
|
if (location.search) history.replaceState(null, "", location.pathname);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 2) Privacy-friendly analytics by Plausible (verbatim; the page now has no query). -->
|
||||||
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
|
<script>
|
||||||
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
|
plausible.init()
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="wrap">
|
||||||
|
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card-wrap">
|
||||||
|
<h1>Edit your listing</h1>
|
||||||
|
<p class="lede">
|
||||||
|
This is your own listing in the Odoo Freelancer Directory. Change what you like here
|
||||||
|
and save it — or delete the listing for good.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Filled by the script at the foot of the page. Empty on purpose: with JavaScript
|
||||||
|
off there is no way to load the listing or save a change, so an empty container +
|
||||||
|
the noscript note below is the honest picture. -->
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div class="panel">
|
||||||
|
<p class="state state-warn">This page needs JavaScript to load your listing and to save your changes.</p>
|
||||||
|
<p class="detail">
|
||||||
|
Turn JavaScript on in your browser and open the link again — or reply to the email
|
||||||
|
that brought you here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var COUNTRIES = [["AD", "Andorra"], ["AE", "United Arab Emirates"], ["AF", "Afghanistan"], ["AG", "Antigua and Barbuda"], ["AI", "Anguilla"], ["AL", "Albania"], ["AM", "Armenia"], ["AO", "Angola"], ["AQ", "Antarctica"], ["AR", "Argentina"], ["AS", "American Samoa"], ["AT", "Austria"], ["AU", "Australia"], ["AW", "Aruba"], ["AX", "Aland"], ["AZ", "Azerbaijan"], ["BA", "Bosnia and Herzegovina"], ["BB", "Barbados"], ["BD", "Bangladesh"], ["BE", "Belgium"], ["BF", "Burkina Faso"], ["BG", "Bulgaria"], ["BH", "Bahrain"], ["BI", "Burundi"], ["BJ", "Benin"], ["BL", "Saint Barthelemy"], ["BM", "Bermuda"], ["BN", "Brunei"], ["BO", "Bolivia"], ["BQ", "Bonaire"], ["BR", "Brazil"], ["BS", "Bahamas"], ["BT", "Bhutan"], ["BV", "Bouvet Island"], ["BW", "Botswana"], ["BY", "Belarus"], ["BZ", "Belize"], ["CA", "Canada"], ["CC", "Cocos (Keeling) Islands"], ["CD", "Democratic Republic of the Congo"], ["CF", "Central African Republic"], ["CG", "Republic of the Congo"], ["CH", "Switzerland"], ["CI", "Ivory Coast"], ["CK", "Cook Islands"], ["CL", "Chile"], ["CM", "Cameroon"], ["CN", "China"], ["CO", "Colombia"], ["CR", "Costa Rica"], ["CU", "Cuba"], ["CV", "Cape Verde"], ["CW", "Curacao"], ["CX", "Christmas Island"], ["CY", "Cyprus"], ["CZ", "Czech Republic"], ["DE", "Germany"], ["DJ", "Djibouti"], ["DK", "Denmark"], ["DM", "Dominica"], ["DO", "Dominican Republic"], ["DZ", "Algeria"], ["EC", "Ecuador"], ["EE", "Estonia"], ["EG", "Egypt"], ["EH", "Western Sahara"], ["ER", "Eritrea"], ["ES", "Spain"], ["ET", "Ethiopia"], ["FI", "Finland"], ["FJ", "Fiji"], ["FK", "Falkland Islands"], ["FM", "Micronesia"], ["FO", "Faroe Islands"], ["FR", "France"], ["GA", "Gabon"], ["GB", "United Kingdom"], ["GD", "Grenada"], ["GE", "Georgia"], ["GF", "French Guiana"], ["GG", "Guernsey"], ["GH", "Ghana"], ["GI", "Gibraltar"], ["GL", "Greenland"], ["GM", "Gambia"], ["GN", "Guinea"], ["GP", "Guadeloupe"], ["GQ", "Equatorial Guinea"], ["GR", "Greece"], ["GS", "South Georgia and the South Sandwich Islands"], ["GT", "Guatemala"], ["GU", "Guam"], ["GW", "Guinea-Bissau"], ["GY", "Guyana"], ["HK", "Hong Kong"], ["HM", "Heard Island and McDonald Islands"], ["HN", "Honduras"], ["HR", "Croatia"], ["HT", "Haiti"], ["HU", "Hungary"], ["ID", "Indonesia"], ["IE", "Ireland"], ["IL", "Israel"], ["IM", "Isle of Man"], ["IN", "India"], ["IO", "British Indian Ocean Territory"], ["IQ", "Iraq"], ["IR", "Iran"], ["IS", "Iceland"], ["IT", "Italy"], ["JE", "Jersey"], ["JM", "Jamaica"], ["JO", "Jordan"], ["JP", "Japan"], ["KE", "Kenya"], ["KG", "Kyrgyzstan"], ["KH", "Cambodia"], ["KI", "Kiribati"], ["KM", "Comoros"], ["KN", "Saint Kitts and Nevis"], ["KP", "North Korea"], ["KR", "South Korea"], ["KW", "Kuwait"], ["KY", "Cayman Islands"], ["KZ", "Kazakhstan"], ["LA", "Laos"], ["LB", "Lebanon"], ["LC", "Saint Lucia"], ["LI", "Liechtenstein"], ["LK", "Sri Lanka"], ["LR", "Liberia"], ["LS", "Lesotho"], ["LT", "Lithuania"], ["LU", "Luxembourg"], ["LV", "Latvia"], ["LY", "Libya"], ["MA", "Morocco"], ["MC", "Monaco"], ["MD", "Moldova"], ["ME", "Montenegro"], ["MF", "Saint Martin"], ["MG", "Madagascar"], ["MH", "Marshall Islands"], ["MK", "North Macedonia"], ["ML", "Mali"], ["MM", "Myanmar (Burma)"], ["MN", "Mongolia"], ["MO", "Macao"], ["MP", "Northern Mariana Islands"], ["MQ", "Martinique"], ["MR", "Mauritania"], ["MS", "Montserrat"], ["MT", "Malta"], ["MU", "Mauritius"], ["MV", "Maldives"], ["MW", "Malawi"], ["MX", "Mexico"], ["MY", "Malaysia"], ["MZ", "Mozambique"], ["NA", "Namibia"], ["NC", "New Caledonia"], ["NE", "Niger"], ["NF", "Norfolk Island"], ["NG", "Nigeria"], ["NI", "Nicaragua"], ["NL", "Netherlands"], ["NO", "Norway"], ["NP", "Nepal"], ["NR", "Nauru"], ["NU", "Niue"], ["NZ", "New Zealand"], ["OM", "Oman"], ["PA", "Panama"], ["PE", "Peru"], ["PF", "French Polynesia"], ["PG", "Papua New Guinea"], ["PH", "Philippines"], ["PK", "Pakistan"], ["PL", "Poland"], ["PM", "Saint Pierre and Miquelon"], ["PN", "Pitcairn Islands"], ["PR", "Puerto Rico"], ["PS", "Palestine"], ["PT", "Portugal"], ["PW", "Palau"], ["PY", "Paraguay"], ["QA", "Qatar"], ["RE", "Reunion"], ["RO", "Romania"], ["RS", "Serbia"], ["RU", "Russia"], ["RW", "Rwanda"], ["SA", "Saudi Arabia"], ["SB", "Solomon Islands"], ["SC", "Seychelles"], ["SD", "Sudan"], ["SE", "Sweden"], ["SG", "Singapore"], ["SH", "Saint Helena"], ["SI", "Slovenia"], ["SJ", "Svalbard and Jan Mayen"], ["SK", "Slovakia"], ["SL", "Sierra Leone"], ["SM", "San Marino"], ["SN", "Senegal"], ["SO", "Somalia"], ["SR", "Suriname"], ["SS", "South Sudan"], ["ST", "Sao Tome and Principe"], ["SV", "El Salvador"], ["SX", "Sint Maarten"], ["SY", "Syria"], ["SZ", "Eswatini"], ["TC", "Turks and Caicos Islands"], ["TD", "Chad"], ["TF", "French Southern Territories"], ["TG", "Togo"], ["TH", "Thailand"], ["TJ", "Tajikistan"], ["TK", "Tokelau"], ["TL", "East Timor"], ["TM", "Turkmenistan"], ["TN", "Tunisia"], ["TO", "Tonga"], ["TR", "Turkey"], ["TT", "Trinidad and Tobago"], ["TV", "Tuvalu"], ["TW", "Taiwan"], ["TZ", "Tanzania"], ["UA", "Ukraine"], ["UG", "Line truncated
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var C = window.O4E;
|
||||||
|
var CG = window.O4PFocus; // the shared 15-point rule (static/focus-rule.js)
|
||||||
|
var app = document.getElementById("app");
|
||||||
|
var hash = window.__EDIT_HASH || "";
|
||||||
|
var rec = null; // this person's record, once loaded
|
||||||
|
|
||||||
|
function el(tag, cls, text) {
|
||||||
|
var n = document.createElement(tag);
|
||||||
|
if (cls) n.className = cls;
|
||||||
|
if (text != null) n.textContent = text; // textContent only: a record is never markup
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function show(nodes) {
|
||||||
|
while (app.firstChild) app.removeChild(app.firstChild);
|
||||||
|
var box = el("div", "panel");
|
||||||
|
for (var i = 0; i < nodes.length; i++) box.appendChild(nodes[i]);
|
||||||
|
app.appendChild(box);
|
||||||
|
}
|
||||||
|
function status(text, kind) {
|
||||||
|
var p = el("p", "state" + (kind ? " state-" + kind : ""), text);
|
||||||
|
p.setAttribute("role", "status");
|
||||||
|
p.setAttribute("aria-live", "polite");
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
function retryButton() {
|
||||||
|
var b = el("button", "btn", "Try again");
|
||||||
|
b.type = "button";
|
||||||
|
b.addEventListener("click", load);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
function messageOf(bodyText) {
|
||||||
|
try { return String((JSON.parse(bodyText) || {}).message || ""); }
|
||||||
|
catch (e) { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the states -------------------------------------------------------------------
|
||||||
|
function noCode() {
|
||||||
|
show([
|
||||||
|
status("This link is incomplete — it arrived without its code.", "warn"),
|
||||||
|
el("p", "detail", "Open it again from the email that brought you here.")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// A MISSING WORKFLOW must not read as an expired invitation. This is our problem.
|
||||||
|
function configError(httpStatus, msg) {
|
||||||
|
show([
|
||||||
|
status("The edit service is not available right now.", "warn"),
|
||||||
|
el("p", "detail",
|
||||||
|
"This is a problem at our end, not with your link. Nothing is wrong with your " +
|
||||||
|
"listing. Please try again later, or reply to the email that brought you here."),
|
||||||
|
el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + ".")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// 404 / 500 "No item to return was found" — the hash is wrong or has expired.
|
||||||
|
function invalidLink(httpStatus, msg) {
|
||||||
|
show([
|
||||||
|
status("This link is not valid, or it has expired.", "warn"),
|
||||||
|
el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + "."),
|
||||||
|
el("p", "detail", "If you think that is wrong, reply to the email that brought you here.")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function loadFailed(httpStatus, msg) {
|
||||||
|
show([
|
||||||
|
status("We could not load your listing.", "warn"),
|
||||||
|
el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + "."),
|
||||||
|
retryButton()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Honest about a shape we did not expect: name the fields, never a value.
|
||||||
|
function shapeMismatch(names) {
|
||||||
|
show([
|
||||||
|
status("We could not read your listing from the reply from the service.", "warn"),
|
||||||
|
el("p", "detail",
|
||||||
|
names.length
|
||||||
|
? "The reply carried these fields: " + names.join(", ") + "."
|
||||||
|
: "The reply carried no readable data."),
|
||||||
|
retryButton()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- reading the record -----------------------------------------------------------
|
||||||
|
function unwrap(payload) {
|
||||||
|
if (Array.isArray(payload)) return payload[0] || null;
|
||||||
|
if (payload && typeof payload === "object") {
|
||||||
|
if (Array.isArray(payload.data)) return payload.data[0] || null;
|
||||||
|
if (Array.isArray(payload.items)) return payload.items[0] || null;
|
||||||
|
if (payload.data && typeof payload.data === "object") return payload.data;
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Read one field by its list of accepted names. NEVER a value we did not expect:
|
||||||
|
// anything not in C.aliases is unreadable.
|
||||||
|
function readField(obj, key) {
|
||||||
|
var names = C.aliases[key] || [key];
|
||||||
|
for (var i = 0; i < names.length; i++) {
|
||||||
|
var v = obj[names[i]];
|
||||||
|
if (v !== undefined && v !== null && v !== "") return v;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
function hasAnyMappedField(obj) {
|
||||||
|
for (var key in C.aliases) {
|
||||||
|
if (key === "id") continue;
|
||||||
|
if (readField(obj, key) !== undefined) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function num(value, fallback) {
|
||||||
|
var n = parseInt(value, 10);
|
||||||
|
return isNaN(n) ? fallback : n;
|
||||||
|
}
|
||||||
|
function clamp05(n) { return Math.max(0, Math.min(C.focusMax, n)); }
|
||||||
|
function initials(name) {
|
||||||
|
var parts = String(name || "").trim().split(/\s+/).filter(Boolean);
|
||||||
|
if (!parts.length) return "?";
|
||||||
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||||
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the form ---------------------------------------------------------------------
|
||||||
|
function textField(id, labelText, value, maxLength, hint) {
|
||||||
|
var wrap = el("div", "field");
|
||||||
|
var label = el("label", "", labelText);
|
||||||
|
label.setAttribute("for", id);
|
||||||
|
var input = el("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.id = id;
|
||||||
|
input.name = id;
|
||||||
|
input.value = value == null ? "" : String(value);
|
||||||
|
if (maxLength) { input.maxLength = maxLength; input.setAttribute("aria-describedby", id + "Count"); }
|
||||||
|
wrap.appendChild(label);
|
||||||
|
wrap.appendChild(input);
|
||||||
|
if (maxLength) {
|
||||||
|
var counter = el("p", "counter", input.value.length + " / " + maxLength);
|
||||||
|
counter.id = id + "Count";
|
||||||
|
input.addEventListener("input", function () {
|
||||||
|
var n = input.value.length;
|
||||||
|
counter.textContent = n + " / " + maxLength;
|
||||||
|
counter.setAttribute("data-over", n > maxLength ? "1" : "0");
|
||||||
|
});
|
||||||
|
counter.setAttribute("data-over", input.value.length > maxLength ? "1" : "0");
|
||||||
|
wrap.appendChild(counter);
|
||||||
|
}
|
||||||
|
if (hint) wrap.appendChild(el("p", "hint", hint));
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An optional profile link. A real `type="url"` input with the application form's own
|
||||||
|
// label and placeholder, so the two forms ask for a link the same way. No URL check
|
||||||
|
// lives here: the value is passed through untouched, and the ONLY url predicate in the
|
||||||
|
// fleet is the directory card's isLink() (a prefix allowlist) — one check, not two.
|
||||||
|
function linkField(id, name, labelText, value, placeholder) {
|
||||||
|
var wrap = el("div", "field");
|
||||||
|
var label = el("label", "", labelText);
|
||||||
|
label.setAttribute("for", id);
|
||||||
|
label.appendChild(el("span", "hint", " (optional)"));
|
||||||
|
var input = el("input");
|
||||||
|
input.type = "url";
|
||||||
|
input.id = id;
|
||||||
|
input.name = name;
|
||||||
|
input.value = value == null ? "" : String(value);
|
||||||
|
input.placeholder = placeholder;
|
||||||
|
wrap.appendChild(label);
|
||||||
|
wrap.appendChild(input);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rangeField(fieldset, name, labelText, value) {
|
||||||
|
var row = el("div", "row");
|
||||||
|
var label = el("label", "", labelText);
|
||||||
|
label.setAttribute("for", "ed-" + name);
|
||||||
|
var input = el("input");
|
||||||
|
input.type = "range";
|
||||||
|
input.id = "ed-" + name;
|
||||||
|
input.name = name;
|
||||||
|
input.min = "0";
|
||||||
|
input.max = String(C.focusMax);
|
||||||
|
input.step = "1";
|
||||||
|
input.value = String(clamp05(num(value, 0)));
|
||||||
|
input.setAttribute("aria-valuetext", input.value + " of " + C.focusMax);
|
||||||
|
var out = el("output", "", input.value);
|
||||||
|
out.setAttribute("for", "ed-" + name);
|
||||||
|
input.addEventListener("input", function () {
|
||||||
|
input.setAttribute("aria-valuetext", input.value + " of " + C.focusMax);
|
||||||
|
out.textContent = input.value;
|
||||||
|
updateTotal();
|
||||||
|
});
|
||||||
|
row.appendChild(label);
|
||||||
|
row.appendChild(input);
|
||||||
|
row.appendChild(out);
|
||||||
|
fieldset.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countrySelect(selected) {
|
||||||
|
var sel = el("select");
|
||||||
|
sel.id = "edCountry";
|
||||||
|
sel.name = "Country";
|
||||||
|
sel.appendChild(new Option("— choose —", ""));
|
||||||
|
COUNTRIES.forEach(function (pair) {
|
||||||
|
var o = new Option(pair[1], C.countryValueFormat === "name" ? pair[1] : pair[0]);
|
||||||
|
var want = String(selected == null ? "" : selected).trim();
|
||||||
|
if (want && (want.toUpperCase() === pair[0] || want === pair[1])) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
return sel;
|
||||||
|
}
|
||||||
|
|
||||||
|
var focusTotalLine = null;
|
||||||
|
function focusSum() {
|
||||||
|
var sum = 0;
|
||||||
|
C.focusAxes.forEach(function (axis) {
|
||||||
|
var el2 = document.getElementById("ed-" + axis);
|
||||||
|
sum += el2 ? num(el2.value, 0) : 0;
|
||||||
|
});
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
function updateTotal() {
|
||||||
|
if (!focusTotalLine) return;
|
||||||
|
var sum = focusSum();
|
||||||
|
focusTotalLine.textContent = sum + " of " + C.focusTotal + " points";
|
||||||
|
focusTotalLine.setAttribute("data-bad", sum === C.focusTotal ? "0" : "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderForm(record) {
|
||||||
|
rec = record;
|
||||||
|
var who = readField(record, "Name");
|
||||||
|
var nodes = [];
|
||||||
|
nodes.push(el("p", "who", "Listing: " + (who ? String(who) : "(no name)")));
|
||||||
|
|
||||||
|
var form = el("form", "edit-form");
|
||||||
|
form.setAttribute("novalidate", "novalidate"); // JS announces errors instead
|
||||||
|
|
||||||
|
form.appendChild(textField("edName", "Name", readField(record, "Name"), 0, null));
|
||||||
|
form.appendChild(textField("edFocus", "Focus", readField(record, "Focus"),
|
||||||
|
C.focusMaxChars, "One short line — the kind of work you want, at most " + C.focusMaxChars + " characters."));
|
||||||
|
|
||||||
|
var descWrap = el("div", "field");
|
||||||
|
var descLabel = el("label", "", "About you");
|
||||||
|
descLabel.setAttribute("for", "edDescription");
|
||||||
|
var ta = el("textarea");
|
||||||
|
ta.id = "edDescription";
|
||||||
|
ta.name = "edDescription";
|
||||||
|
ta.rows = 6;
|
||||||
|
ta.maxLength = C.descMaxChars;
|
||||||
|
ta.value = String(readField(record, "description") == null ? "" : readField(record, "description"));
|
||||||
|
ta.setAttribute("aria-describedby", "edDescriptionCount");
|
||||||
|
var descCount = el("p", "counter", ta.value.length + " / " + C.descMaxChars);
|
||||||
|
descCount.id = "edDescriptionCount";
|
||||||
|
ta.addEventListener("input", function () {
|
||||||
|
var n = ta.value.length;
|
||||||
|
descCount.textContent = n + " / " + C.descMaxChars;
|
||||||
|
descCount.setAttribute("data-over", n > C.descMaxChars ? "1" : "0");
|
||||||
|
});
|
||||||
|
descCount.setAttribute("data-over", ta.value.length > C.descMaxChars ? "1" : "0");
|
||||||
|
descWrap.appendChild(descLabel);
|
||||||
|
descWrap.appendChild(ta);
|
||||||
|
descWrap.appendChild(descCount);
|
||||||
|
form.appendChild(descWrap);
|
||||||
|
|
||||||
|
// The five axes. An axis the record does not carry keeps the application form's
|
||||||
|
// default of 3, so the block always totals 15 and can be saved.
|
||||||
|
var axesSet = el("fieldset", "");
|
||||||
|
axesSet.appendChild(el("legend", "", CG.LEGEND));
|
||||||
|
// The same directive the application form carries, so both forms state the rule.
|
||||||
|
axesSet.appendChild(el("p", "hint", CG.DIRECTIVE));
|
||||||
|
C.focusAxes.forEach(function (axis) {
|
||||||
|
var v = readField(record, axis);
|
||||||
|
if (v === undefined || v === null || v === "") v = 3; // → the block totals 15
|
||||||
|
rangeField(axesSet, axis, C.focusAxisLabels[axis] || axis, v);
|
||||||
|
});
|
||||||
|
form.appendChild(axesSet);
|
||||||
|
focusTotalLine = el("p", "total", "");
|
||||||
|
focusTotalLine.id = "edFocusTotal";
|
||||||
|
focusTotalLine.setAttribute("role", "status");
|
||||||
|
focusTotalLine.setAttribute("aria-live", "polite");
|
||||||
|
form.appendChild(focusTotalLine);
|
||||||
|
|
||||||
|
var langsSet = el("fieldset", "");
|
||||||
|
langsSet.appendChild(el("legend", "", "Languages (0 = do not use it)"));
|
||||||
|
C.langs.forEach(function (pair) {
|
||||||
|
rangeField(langsSet, pair[0], pair[1], readField(record, pair[0]));
|
||||||
|
});
|
||||||
|
form.appendChild(langsSet);
|
||||||
|
|
||||||
|
var countryWrap = el("div", "field");
|
||||||
|
var countryLabel = el("label", "", "Country");
|
||||||
|
countryLabel.setAttribute("for", "edCountry");
|
||||||
|
countryWrap.appendChild(countryLabel);
|
||||||
|
countryWrap.appendChild(countrySelect(readField(record, "Country")));
|
||||||
|
form.appendChild(countryWrap);
|
||||||
|
|
||||||
|
form.appendChild(textField("edOcaLink", "OCA member profile link", readField(record, "OCA_link"),
|
||||||
|
0, "Your OCA profile URL, if you have one: https://apps.odoo-community.org/community/…"));
|
||||||
|
|
||||||
|
// The four profile links — parity with the application form. Keys are the LIVE wire
|
||||||
|
// keys verbatim ("Linkedin" capital L; "github", "upwork" and "fiverr" lower) and the
|
||||||
|
// labels and placeholders are the application form's own. Each is optional and
|
||||||
|
// pre-filled from the record.
|
||||||
|
form.appendChild(linkField("edLinkedin", "Linkedin", "LinkedIn URL",
|
||||||
|
readField(record, "Linkedin"), "https://www.linkedin.com/in/…"));
|
||||||
|
form.appendChild(linkField("edGithub", "github", "GitHub URL",
|
||||||
|
readField(record, "github"), "https://github.com/…"));
|
||||||
|
form.appendChild(linkField("edUpwork", "upwork", "Upwork URL",
|
||||||
|
readField(record, "upwork"), "https://www.upwork.com/freelancers/…"));
|
||||||
|
form.appendChild(linkField("edFiverr", "fiverr", "Fiverr URL",
|
||||||
|
readField(record, "fiverr"), "https://www.fiverr.com/…"));
|
||||||
|
|
||||||
|
// The photo block is a LOOK, not a field. The photo does not live in this record
|
||||||
|
// any more — the payload carries no `Image` — it comes from its own endpoint,
|
||||||
|
// addressed by the record id (window.O4PExpertImage). So nothing here edits it,
|
||||||
|
// and the save below sends no `Image` key at all: not the value, not null, not "".
|
||||||
|
var photoWrap = el("div", "field");
|
||||||
|
photoWrap.appendChild(el("label", "", "Photo"));
|
||||||
|
var photoRow = el("div", "photo-readonly");
|
||||||
|
var photoMono = el("span", "mono", initials(who));
|
||||||
|
photoRow.appendChild(photoMono);
|
||||||
|
if (rec && rec.id !== undefined && rec.id !== null && rec.id !== "") {
|
||||||
|
// Same fetch the directory card makes: the route answers JSON, not image bytes,
|
||||||
|
// so the value is read from `Image` and only then put on an <img>. Any failure
|
||||||
|
// (or a value that fails isImageSrc) leaves the monogram standing.
|
||||||
|
O4PExpertImage.load(String(rec.id)).then(function (src) {
|
||||||
|
if (!src) return;
|
||||||
|
var photoImg = el("img");
|
||||||
|
photoImg.id = "edImagePreview";
|
||||||
|
photoImg.alt = "";
|
||||||
|
photoImg.decoding = "async";
|
||||||
|
photoImg.addEventListener("load", function () {
|
||||||
|
if (photoMono.parentNode) photoMono.parentNode.replaceChild(photoImg, photoMono);
|
||||||
|
}, { once: true });
|
||||||
|
photoImg.addEventListener("error", function () {
|
||||||
|
// A failing photo is the monogram: no broken-image icon, no empty box.
|
||||||
|
if (photoImg.parentNode) photoImg.parentNode.removeChild(photoImg);
|
||||||
|
}, { once: true });
|
||||||
|
photoImg.src = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
photoWrap.appendChild(photoRow);
|
||||||
|
photoWrap.appendChild(el("p", "hint",
|
||||||
|
"Your photo comes from your listing and is not editable here — reply to the email " +
|
||||||
|
"that brought you here if it needs to change."));
|
||||||
|
form.appendChild(photoWrap);
|
||||||
|
|
||||||
|
var save = el("button", "btn", "Save my changes");
|
||||||
|
save.type = "submit";
|
||||||
|
var line = el("p", "status", "");
|
||||||
|
line.id = "editStatus";
|
||||||
|
line.setAttribute("role", "status");
|
||||||
|
line.setAttribute("aria-live", "polite");
|
||||||
|
|
||||||
|
form.addEventListener("submit", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (focusSum() !== C.focusTotal) {
|
||||||
|
var sum = focusSum();
|
||||||
|
line.className = "status state-err";
|
||||||
|
line.textContent = CG.message(sum); // names the direction: remove / add / place all
|
||||||
|
focusTotalLine.setAttribute("data-bad", "1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A save is authorised by the HASH, not by the id. If this page holds no hash there
|
||||||
|
// is nothing to prove ownership, so the request is not fired at all — a PATCH
|
||||||
|
// carrying only an id is exactly the hole this closes. Say why, do not fail silently.
|
||||||
|
if (!hash) {
|
||||||
|
line.className = "status state-err";
|
||||||
|
line.textContent = "This link arrived without its code, so we cannot save your " +
|
||||||
|
"changes. Open it again from the email that brought you here.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
save.disabled = true;
|
||||||
|
line.className = "status";
|
||||||
|
line.textContent = "Saving…";
|
||||||
|
// The body shape: READ-MODIFY-WRITE. Start from the record the webhook returned and
|
||||||
|
// overwrite ONLY the fields this form owns. A field-by-field payload is what deletes a
|
||||||
|
// freelancer's own photo and links on save — a partial payload wipes what it leaves
|
||||||
|
// out, so the whole record goes back and only the edited keys are replaced. The
|
||||||
|
// credential rides first: "has" is the SAME key the delete sends (one credential path
|
||||||
|
// for both), then the record's id. Method is C.saveMethod ("PATCH").
|
||||||
|
var payload = { has: hash };
|
||||||
|
if (rec) {
|
||||||
|
Object.keys(rec).forEach(function (k) {
|
||||||
|
if (k === "has") return; // never let the record's own "has" shadow the credential
|
||||||
|
payload[k] = rec[k]; // Image, the links, createdAt/updatedAt and everything else
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload.id = rec && rec.id !== undefined ? rec.id : undefined;
|
||||||
|
payload.Name = document.getElementById("edName").value.trim();
|
||||||
|
payload.Focus = document.getElementById("edFocus").value.trim();
|
||||||
|
payload.description = document.getElementById("edDescription").value.trim();
|
||||||
|
payload.Country = document.getElementById("edCountry").value;
|
||||||
|
payload.OCA_link = document.getElementById("edOcaLink").value.trim();
|
||||||
|
C.focusAxes.forEach(function (axis) { payload[axis] = num(document.getElementById("ed-" + axis).value, 0); });
|
||||||
|
C.langs.forEach(function (pair) { payload[pair[0]] = num(document.getElementById("ed-" + pair[0]).value, 0); });
|
||||||
|
// The four links, read from their inputs. Only a key we actually have a value for is
|
||||||
|
// touched: an empty optional field must NOT invent an empty string on a record that
|
||||||
|
// never had one — the record's own value (null, or absent) is already in the payload
|
||||||
|
// and is left as it is. A link the record DID carry and the owner cleared is a
|
||||||
|
// deliberate clear and sends "".
|
||||||
|
[["edLinkedin", "Linkedin"], ["edGithub", "github"], ["edUpwork", "upwork"], ["edFiverr", "fiverr"]].forEach(function (pair) {
|
||||||
|
var input = document.getElementById(pair[0]);
|
||||||
|
if (!input) return;
|
||||||
|
var v = input.value.trim();
|
||||||
|
var had = rec && rec[pair[1]] !== undefined && rec[pair[1]] !== null && rec[pair[1]] !== "";
|
||||||
|
if (v) payload[pair[1]] = v;
|
||||||
|
else if (had) payload[pair[1]] = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// The photo does NOT ride in this record any more: the payload carries no
|
||||||
|
// `Image` key and this form sends none — not the value, not null, not "". The
|
||||||
|
// delete is the guarantee, whatever a reply may one day carry.
|
||||||
|
delete payload.Image;
|
||||||
|
|
||||||
|
fetch(C.webhook, {
|
||||||
|
method: C.saveMethod,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
// A RECEIPT, not a delivery claim: n8n answers 200 before the workflow runs.
|
||||||
|
line.className = "status state-ok";
|
||||||
|
line.textContent = "Saved — we have your changes. They go live once confirmed.";
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
// Non-2xx or network: never a receipt; the data stays in the form.
|
||||||
|
line.className = "status state-err";
|
||||||
|
line.textContent = "We could not save your changes just now. Your edits are still " +
|
||||||
|
"in the form — please try again, or reply to the email that brought you here.";
|
||||||
|
})
|
||||||
|
.then(function () { save.disabled = false; });
|
||||||
|
});
|
||||||
|
|
||||||
|
form.appendChild(save);
|
||||||
|
form.appendChild(line);
|
||||||
|
nodes.push(form);
|
||||||
|
nodes.push(deleteBlock());
|
||||||
|
show(nodes);
|
||||||
|
// The total needs the fields in the document to sum them — do it after show().
|
||||||
|
updateTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- delete -----------------------------------------------------------------------
|
||||||
|
function deleteBlock() {
|
||||||
|
var box = el("div", "danger");
|
||||||
|
box.appendChild(el("h2", "", "Delete this listing"));
|
||||||
|
box.appendChild(el("p", "",
|
||||||
|
"This removes your public listing for good. It cannot be undone from our side. " +
|
||||||
|
"Tick the box and confirm — nothing is deleted until you do."));
|
||||||
|
var confirmWrap = el("label", "confirm");
|
||||||
|
var box_in = el("input");
|
||||||
|
box_in.type = "checkbox";
|
||||||
|
box_in.id = "edConfirmDelete";
|
||||||
|
confirmWrap.appendChild(box_in);
|
||||||
|
confirmWrap.appendChild(el("span", "",
|
||||||
|
"Yes, delete my listing permanently."));
|
||||||
|
box.appendChild(confirmWrap);
|
||||||
|
|
||||||
|
var del = el("button", "btn btn-danger", "Delete my listing");
|
||||||
|
del.type = "button";
|
||||||
|
del.disabled = true;
|
||||||
|
var line = el("p", "status", "");
|
||||||
|
line.id = "deleteStatus";
|
||||||
|
line.setAttribute("role", "status");
|
||||||
|
line.setAttribute("aria-live", "polite");
|
||||||
|
|
||||||
|
// The deliberate second action: the button cannot fire until the box is ticked.
|
||||||
|
box_in.addEventListener("change", function () { del.disabled = !box_in.checked; });
|
||||||
|
|
||||||
|
del.addEventListener("click", function () {
|
||||||
|
if (!box_in.checked) return;
|
||||||
|
del.disabled = true;
|
||||||
|
box_in.disabled = true;
|
||||||
|
line.className = "status";
|
||||||
|
line.textContent = "Deleting…";
|
||||||
|
// DELETE with the credential and the record id. C.deleteMethod is the one place
|
||||||
|
// the method lives; the id wire key is the record's own "id".
|
||||||
|
fetch(C.webhook, {
|
||||||
|
method: C.deleteMethod,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ has: hash, id: rec && rec.id !== undefined ? rec.id : null })
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
// Only a 2xx is a success. Anything else fails below.
|
||||||
|
line.className = "status state-ok";
|
||||||
|
line.textContent = "Your listing has been deleted. Thank you for having been listed.";
|
||||||
|
del.hidden = true;
|
||||||
|
confirmWrap.hidden = true;
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
box_in.disabled = false;
|
||||||
|
del.disabled = !box_in.checked;
|
||||||
|
line.className = "status state-err";
|
||||||
|
line.textContent = "We could not delete your listing just now — nothing was removed. " +
|
||||||
|
"Please try again, or reply to the email that brought you here.";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
box.appendChild(del);
|
||||||
|
box.appendChild(line);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the GET ----------------------------------------------------------------------
|
||||||
|
function load() {
|
||||||
|
if (!hash) { noCode(); return; }
|
||||||
|
show([status("Loading your listing…")]);
|
||||||
|
|
||||||
|
fetch(C.webhook + "?" + encodeURIComponent(C.hashWireKey) + "=" + encodeURIComponent(hash), {
|
||||||
|
headers: { Accept: "application/json" }
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
return res.text().then(function (body) {
|
||||||
|
var msg = messageOf(body);
|
||||||
|
// A missing workflow is a CONFIGURATION error — never "your link is invalid".
|
||||||
|
if (/is not registered/i.test(msg)) { configError(res.status, msg); return; }
|
||||||
|
if (res.status === 404 || res.status === 500) { invalidLink(res.status, msg); return; }
|
||||||
|
loadFailed(res.status, msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.json().then(function (payload) {
|
||||||
|
var obj = unwrap(payload);
|
||||||
|
if (!obj) { shapeMismatch([]); return; }
|
||||||
|
if (!hasAnyMappedField(obj)) { shapeMismatch(Object.keys(obj)); return; }
|
||||||
|
renderForm(obj);
|
||||||
|
}).catch(function () {
|
||||||
|
shapeMismatch([]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
// Network failure: the hash is still in memory, so a retry needs no new link.
|
||||||
|
show([
|
||||||
|
status("We could not load your listing just now.", "warn"),
|
||||||
|
el("p", "detail", "Your link has been kept, so you can try again."),
|
||||||
|
retryButton()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+8
-5
@@ -128,7 +128,9 @@
|
|||||||
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
||||||
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
||||||
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
||||||
.hero-notice[hidden]{display:none}
|
/* `.hero-notice[hidden]{display:none}` used to live here; the site-wide `[hidden]`
|
||||||
|
rule in /style.css hides it (`.hero-notice` sets `display:flex`, which outranked
|
||||||
|
the UA rule). */
|
||||||
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
||||||
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||||
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
||||||
@@ -230,14 +232,14 @@
|
|||||||
.bf-topbar-cta{color:var(--brass);white-space:nowrap}
|
.bf-topbar-cta{color:var(--brass);white-space:nowrap}
|
||||||
.bf-topbar:hover .bf-topbar-cta,.bf-topbar:focus-visible .bf-topbar-cta{color:var(--brass-bright)}
|
.bf-topbar:hover .bf-topbar-cta,.bf-topbar:focus-visible .bf-topbar-cta{color:var(--brass-bright)}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<!-- Privacy-friendly analytics by Plausible -->
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!--
|
<!--
|
||||||
@@ -662,6 +664,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="support.html">Support</a></li>
|
<li><a href="support.html">Support</a></li>
|
||||||
<li><a href="legal.html">Legal</a></li>
|
<li><a href="legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -700,7 +703,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||||
<script src="script.js"></script>
|
<script src="script.js?v=d2046e3469"></script>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+3
-3
@@ -25,7 +25,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
.legal-hero {
|
.legal-hero {
|
||||||
padding: 4rem 0 2.5rem;
|
padding: 4rem 0 2.5rem;
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
WY 82801, USA
|
WY 82801, USA
|
||||||
</p>
|
</p>
|
||||||
<p style="margin-top: 0.75rem;">
|
<p style="margin-top: 0.75rem;">
|
||||||
Contact: <span class="contact-email">support@ODOO4projects.com</span>
|
Contact: <span class="contact-email">support@odoo4projects.com</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -204,6 +204,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -50,6 +50,8 @@
|
|||||||
|
|
||||||
## Blog posts
|
## Blog posts
|
||||||
|
|
||||||
|
- [Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live](https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html) — 2026-11-05 — This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo hosting directly, instead of you clicking through menus to do routine things.
|
||||||
|
- [Our freelancer directory opens on 1 December. Help us get the signup right](https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html) — 2026-11-04 — We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start browsing it, we want the signup to work, and we would like your help to get it right.
|
||||||
- [Odoo 20 Is Live on Our Servers — Here Is What Changed](https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html) — 2026-11-03 — Two weeks. That is how long it took us to bring up Odoo 20 on our servers, with every new feature that comes with it. It is live now — if your instance runs with us, the engine underneath it just moved forward.
|
- [Odoo 20 Is Live on Our Servers — Here Is What Changed](https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html) — 2026-11-03 — Two weeks. That is how long it took us to bring up Odoo 20 on our servers, with every new feature that comes with it. It is live now — if your instance runs with us, the engine underneath it just moved forward.
|
||||||
- [From a Spreadsheet to a Running ERP in Ten Thursday Sessions](https://odoo4projects.com/blog/posts/from-a-spreadsheet-to-a-running-erp.html) — 2026-11-02 — Most small businesses do not run on an ERP. They run on a spreadsheet, a messaging group, and one person's memory. It works — until that person is on holiday.
|
- [From a Spreadsheet to a Running ERP in Ten Thursday Sessions](https://odoo4projects.com/blog/posts/from-a-spreadsheet-to-a-running-erp.html) — 2026-11-02 — Most small businesses do not run on an ERP. They run on a spreadsheet, a messaging group, and one person's memory. It works — until that person is on holiday.
|
||||||
- [ODOO4projects Supports the OCA Paraguay Internationalization Hackathon](https://odoo4projects.com/blog/posts/oca-paraguay-internationalization-hackathon.html) — 2026-10-29 — Two online days to push Odoo's Paraguayan localisation forward — and we are supporting them.
|
- [ODOO4projects Supports the OCA Paraguay Internationalization Hackathon](https://odoo4projects.com/blog/posts/oca-paraguay-internationalization-hackathon.html) — 2026-10-29 — Two online days to push Odoo's Paraguayan localisation forward — and we are supporting them.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" /></noscript>
|
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Black Friday 2026 page (page-scoped; additive) ---- */
|
/* ---- Odoo Black Friday 2026 page (page-scoped; additive) ---- */
|
||||||
.bf-sr-only {
|
.bf-sr-only {
|
||||||
@@ -525,9 +525,9 @@
|
|||||||
border-top: 2px solid var(--brass, #c9a227);
|
border-top: 2px solid var(--brass, #c9a227);
|
||||||
box-shadow: 0 -8px 30px rgba(13, 27, 46, 0.28);
|
box-shadow: 0 -8px 30px rgba(13, 27, 46, 0.28);
|
||||||
}
|
}
|
||||||
.ads-consent[hidden] {
|
/* `.ads-consent[hidden]{display:none}` used to live here. The banner sets no
|
||||||
display: none;
|
`display` of its own, so the UA rule already hid it — the patch was redundant;
|
||||||
}
|
it is gone so the site-wide `[hidden]` rule in /style.css is the one mechanism. */
|
||||||
.ads-consent-inner {
|
.ads-consent-inner {
|
||||||
width: 86%;
|
width: 86%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
@@ -744,7 +744,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Hero — copied from the homepage (index.html): same component, same behaviour. On load the
|
<!-- Hero — copied from the homepage (index.html): same component, same behaviour. On load the
|
||||||
@@ -1196,6 +1196,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1229,7 +1230,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||||
</form>
|
</form>
|
||||||
<noscript><p>JavaScript is off. Email us directly at <a href="mailto:support@ODOO4projects.com">support@ODOO4projects.com</a>.</p></noscript>
|
<noscript><p>JavaScript is off. Email us directly at <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a>.</p></noscript>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fcopy">Copyright © ODOO4projects LLC · <a href="#" id="adsConsentOpen">Cookie settings</a></div>
|
<div class="fcopy">Copyright © ODOO4projects LLC · <a href="#" id="adsConsentOpen">Cookie settings</a></div>
|
||||||
@@ -1274,7 +1275,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||||
<script defer src="/static/black-friday.js"></script>
|
<script defer src="/static/black-friday.js?v=df1f299dc5"></script>
|
||||||
<script defer src="/static/ads-consent.js"></script>
|
<script defer src="/static/ads-consent.js?v=0cc2bfb809"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- "Odoo Community + AI agent" page (page-scoped; additive).
|
/* ---- "Odoo Community + AI agent" page (page-scoped; additive).
|
||||||
Reuses the top bar, band rhythm, hero, FAQ and final-CTA treatment already
|
Reuses the top bar, band rhythm, hero, FAQ and final-CTA treatment already
|
||||||
@@ -898,6 +898,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -921,7 +922,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="oca-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="oca-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
@@ -936,6 +937,6 @@
|
|||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Freelancer records — admin (page-scoped). Internal page, never linked. ---- */
|
/* ---- Freelancer records — admin (page-scoped). Internal page, never linked. ---- */
|
||||||
.adm-bar {
|
.adm-bar {
|
||||||
@@ -350,8 +350,9 @@
|
|||||||
<span class="focus-counter" id="adFocusCount" aria-live="polite">0 / 70</span>
|
<span class="focus-counter" id="adFocusCount" aria-live="polite">0 / 70</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field full">
|
<div class="form-field full">
|
||||||
<label for="adDescription">Description</label>
|
<label for="adDescription">Description <span class="hint">(at most 230 characters)</span></label>
|
||||||
<textarea id="adDescription" name="description" rows="4"></textarea>
|
<textarea id="adDescription" name="description" rows="4" maxlength="230" aria-describedby="adDescriptionCount"></textarea>
|
||||||
|
<span class="focus-counter" id="adDescriptionCount" aria-live="polite">0 / 230</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="adCountry">Country</label>
|
<label for="adCountry">Country</label>
|
||||||
@@ -727,6 +728,8 @@
|
|||||||
var FOCUS_TOTAL = 15;
|
var FOCUS_TOTAL = 15;
|
||||||
var FOCUS_MAX = 5;
|
var FOCUS_MAX = 5;
|
||||||
var FOCUS_MAX_CHARS = 70;
|
var FOCUS_MAX_CHARS = 70;
|
||||||
|
// The description is capped at 230 characters, same as the public application form.
|
||||||
|
var DESC_MAX_CHARS = 230;
|
||||||
var LANG_FIELDS = [["english", "English"], ["german", "German"], ["spanish", "Spanish"]];
|
var LANG_FIELDS = [["english", "English"], ["german", "German"], ["spanish", "Spanish"]];
|
||||||
// Country VALUE FORMAT — what the Country select sends to the webhook.
|
// Country VALUE FORMAT — what the Country select sends to the webhook.
|
||||||
// "code" → the ISO 3166-1 alpha-2 code, e.g. "DE" (default)
|
// "code" → the ISO 3166-1 alpha-2 code, e.g. "DE" (default)
|
||||||
@@ -874,6 +877,11 @@
|
|||||||
if (focusInput && focusCount) {
|
if (focusInput && focusCount) {
|
||||||
focusCount.textContent = focusInput.value.length + " / " + FOCUS_MAX_CHARS;
|
focusCount.textContent = focusInput.value.length + " / " + FOCUS_MAX_CHARS;
|
||||||
}
|
}
|
||||||
|
var descInput = document.getElementById("adDescription");
|
||||||
|
var descCount = document.getElementById("adDescriptionCount");
|
||||||
|
if (descInput && descCount) {
|
||||||
|
descCount.textContent = descInput.value.length + " / " + DESC_MAX_CHARS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadRecord() {
|
function loadRecord() {
|
||||||
@@ -1001,6 +1009,17 @@
|
|||||||
focusInput.addEventListener("input", refreshCount);
|
focusInput.addEventListener("input", refreshCount);
|
||||||
refreshCount();
|
refreshCount();
|
||||||
}
|
}
|
||||||
|
var descInput = document.getElementById("adDescription");
|
||||||
|
var descCount = document.getElementById("adDescriptionCount");
|
||||||
|
if (descInput && descCount) {
|
||||||
|
var refreshDescCount = function () {
|
||||||
|
var n = descInput.value.length;
|
||||||
|
descCount.textContent = n + " / " + DESC_MAX_CHARS;
|
||||||
|
descCount.setAttribute("data-over", n > DESC_MAX_CHARS ? "1" : "0");
|
||||||
|
};
|
||||||
|
descInput.addEventListener("input", refreshDescCount);
|
||||||
|
refreshDescCount();
|
||||||
|
}
|
||||||
refreshFocus(form);
|
refreshFocus(form);
|
||||||
|
|
||||||
form.addEventListener("submit", function (event) {
|
form.addEventListener("submit", function (event) {
|
||||||
|
|||||||
+1420
-526
File diff suppressed because it is too large.
Load diff
@@ -38,7 +38,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
||||||
.oc-sr-only {
|
.oc-sr-only {
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -996,6 +996,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="support.html">Support</a></li>
|
<li><a href="support.html">Support</a></li>
|
||||||
<li><a href="legal.html">Legal</a></li>
|
<li><a href="legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1019,7 +1020,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
||||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
||||||
.bc-sr-only {
|
.bc-sr-only {
|
||||||
@@ -999,6 +999,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1032,15 +1033,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||||
</form>
|
</form>
|
||||||
<noscript><p class="bc-noscript">JavaScript is off. Email us directly at <a href="mailto:support@ODOO4projects.com">support@ODOO4projects.com</a>.</p></noscript>
|
<noscript><p class="bc-noscript">JavaScript is off. Email us directly at <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a>.</p></noscript>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/buy.js"></script>
|
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||||
<script defer src="/static/bootcamp-availability.js"></script>
|
<script defer src="/static/bootcamp-availability.js?v=94dfd8cd7c"></script>
|
||||||
<script defer src="/static/bootcamp-waitlist.js"></script>
|
<script defer src="/static/bootcamp-waitlist.js?v=6c2e2fbd33"></script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
||||||
.cmp-sr-only {
|
.cmp-sr-only {
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -995,6 +995,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="support.html">Support</a></li>
|
<li><a href="support.html">Support</a></li>
|
||||||
<li><a href="legal.html">Legal</a></li>
|
<li><a href="legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1018,7 +1019,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="cmp-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="cmp-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Odoo Hosting Offers (evergreen hub) page (page-scoped; additive) ---- */
|
/* ---- Odoo Hosting Offers (evergreen hub) page (page-scoped; additive) ---- */
|
||||||
.of-sr-only {
|
.of-sr-only {
|
||||||
@@ -449,7 +449,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||||
@@ -577,6 +577,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -600,7 +601,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="of-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="of-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- ODOO x N8N Masterclass landing page (page-scoped; additive).
|
/* ---- ODOO x N8N Masterclass landing page (page-scoped; additive).
|
||||||
The block below is the bootcamp page's page-scoped CSS, reused so the two
|
The block below is the bootcamp page's page-scoped CSS, reused so the two
|
||||||
@@ -792,6 +792,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -815,7 +816,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
@@ -830,7 +831,7 @@
|
|||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/buy.js"></script>
|
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- "Odoo staging environment" guide page (page-scoped; additive).
|
/* ---- "Odoo staging environment" guide page (page-scoped; additive).
|
||||||
Reuses the top bar, band rhythm, hero and FAQ treatment already shipped on
|
Reuses the top bar, band rhythm, hero and FAQ treatment already shipped on
|
||||||
@@ -783,6 +783,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -806,7 +807,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
@@ -821,6 +822,6 @@
|
|||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+5
-4
@@ -27,8 +27,8 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" media="print" onload="this.media='all'" />
|
||||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
<noscript><link rel="stylesheet" href="/style.css?v=061517baef" /></noscript>
|
||||||
<style>
|
<style>
|
||||||
/* ---- Partner briefing for Andrew (page-scoped; additive) ---- */
|
/* ---- Partner briefing for Andrew (page-scoped; additive) ---- */
|
||||||
.andrew-sr-only {
|
.andrew-sr-only {
|
||||||
@@ -661,6 +661,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -684,7 +685,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
@@ -699,6 +700,6 @@
|
|||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+4
-4
@@ -25,7 +25,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
.legal-hero {
|
.legal-hero {
|
||||||
padding: 4rem 0 2.5rem;
|
padding: 4rem 0 2.5rem;
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
30 N Gould St, STE R, Sheridan<br />
|
30 N Gould St, STE R, Sheridan<br />
|
||||||
WY 82801, USA
|
WY 82801, USA
|
||||||
</p>
|
</p>
|
||||||
<p>Contact: <span class="contact-email">support@ODOO4projects.com</span></p>
|
<p>Contact: <span class="contact-email">support@odoo4projects.com</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legal-card">
|
<div class="legal-card">
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
|
|
||||||
<div class="legal-card">
|
<div class="legal-card">
|
||||||
<h2>Your requests</h2>
|
<h2>Your requests</h2>
|
||||||
<p>Ask us for a copy of your data, a correction, or deletion at <span class="contact-email">support@ODOO4projects.com</span>. If your account is active, deletion ends the service. We keep invoice records for as long as accounting rules require them.</p>
|
<p>Ask us for a copy of your data, a correction, or deletion at <span class="contact-email">support@odoo4projects.com</span>. If your account is active, deletion ends the service. We keep invoice records for as long as accounting rules require them.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legal-card">
|
<div class="legal-card">
|
||||||
@@ -205,6 +205,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+12
@@ -84,6 +84,18 @@
|
|||||||
<changefreq>yearly</changefreq>
|
<changefreq>yearly</changefreq>
|
||||||
<priority>0.4</priority>
|
<priority>0.4</priority>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html</loc>
|
||||||
|
<lastmod>2026-11-05</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html</loc>
|
||||||
|
<lastmod>2026-11-04</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html</loc>
|
<loc>https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html</loc>
|
||||||
<lastmod>2026-11-03</lastmod>
|
<lastmod>2026-11-03</lastmod>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/* The freelancer PHOTO — one endpoint, one constant, one fetch.
|
||||||
|
|
||||||
|
The photo does NOT live in the profile payload any more. It used to ride there as
|
||||||
|
inline base64 under the key `Image`; the webhook no longer sends that key, so the
|
||||||
|
payload is small and the photo is fetched separately, per card, by record id.
|
||||||
|
|
||||||
|
ONE constant holds BOTH halves of the address: the route and the query-parameter
|
||||||
|
name. Changing either is a one-line edit here, and both pages pick it up.
|
||||||
|
|
||||||
|
The parameter name is `id`, CONFIRMED by measurement against the live route:
|
||||||
|
`?id=<n>` answers 200 with the photo for that record, while `/4`, `?profile_id=`,
|
||||||
|
`?image_id=` and a missing id all fail.
|
||||||
|
|
||||||
|
THE ENDPOINT RETURNS JSON, NOT IMAGE BYTES. Measured on the live route:
|
||||||
|
|
||||||
|
GET …/odoo_experts_image?id=10 → 200 application/json
|
||||||
|
body: {"Image":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ…"}
|
||||||
|
|
||||||
|
So a plain `<img src="…?id=N">` receives JSON, cannot decode it and fires `onerror`
|
||||||
|
— the monogram. That is why the card FETCHES the route, reads `Image` and puts the
|
||||||
|
DATA URI on an `<img>` only when it passes the `isImageSrc` allowlist. n8n sometimes
|
||||||
|
answers an array instead of an object, so both shapes are accepted.
|
||||||
|
|
||||||
|
`load(id)` resolves to a usable image source, or `null` on ANY failure — a non-2xx, an
|
||||||
|
empty `{}`, a missing/blank `Image`, a value that fails `isImageSrc`, or a request that
|
||||||
|
does not answer in `timeoutMs`. A `null` answer means "show the monogram": the caller
|
||||||
|
never sees a broken image. The decoded URI is cached by id (and a failure is remembered
|
||||||
|
too), so a re-render can never re-fetch a photo already in memory.
|
||||||
|
|
||||||
|
THE TIMEOUT. A request that neither succeeds nor errors leaves the fallback unable to
|
||||||
|
fire (measured live: an <img> pointed at this route sat `complete=false · naturalWidth=0`
|
||||||
|
for 20 s). Each fetch is therefore bounded by `timeoutMs` (default 8000): an
|
||||||
|
`AbortController` is aborted by a `setTimeout`, the abort rejects the fetch, and the
|
||||||
|
rejection lands in the same catch that returns `null` — the monogram. The timer is
|
||||||
|
cleared on every completion, so a fast answer never pays for the guard. `AbortController`
|
||||||
|
is universal in the browsers this site targets (it already needs `fetch`, `Promise` and
|
||||||
|
`IntersectionObserver`); if it were missing there is nothing to abort with, but the
|
||||||
|
abort is what turns a hang into the fallback, so it is the one capability worth naming.
|
||||||
|
|
||||||
|
The route answers with `Access-Control-Allow-Origin` echoing the caller and
|
||||||
|
`Cache-Control: public, max-age=3600`, so the browser may also serve a repeat from its
|
||||||
|
own HTTP cache. */
|
||||||
|
window.O4PExpertImage = {
|
||||||
|
endpoint: "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_image",
|
||||||
|
idParam: "id",
|
||||||
|
// A photo request that hangs is a card with no photo AND no fallback: bound it.
|
||||||
|
timeoutMs: 8000,
|
||||||
|
|
||||||
|
/* The ONLY allowlist for an image source in this codebase — `https://` or a
|
||||||
|
`data:image/` URI. Both forms are what this endpoint has served across its life. */
|
||||||
|
isImageSrc: function (v) {
|
||||||
|
return typeof v === "string" &&
|
||||||
|
(v.indexOf("https://") === 0 || v.indexOf("data:image/") === 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
url: function (id) {
|
||||||
|
return this.endpoint + "?" + this.idParam + "=" + encodeURIComponent(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
// id -> decoded image source (or "" when the record has no usable photo)
|
||||||
|
cache: Object.create(null),
|
||||||
|
// id -> the in-flight Promise, so two observers of one id share ONE request
|
||||||
|
inflight: Object.create(null),
|
||||||
|
|
||||||
|
/* Fetch the photo for `id`. Resolves to an image source, or null when there is none.
|
||||||
|
Never rejects — a caller only has to check for a value. Every request is bounded by
|
||||||
|
`timeoutMs`: on expiry the fetch is ABORTED (not merely abandoned) and the rejection
|
||||||
|
resolves null, so a hung endpoint becomes the monogram instead of an empty box. */
|
||||||
|
load: function (id) {
|
||||||
|
var self = this;
|
||||||
|
var key = String(id);
|
||||||
|
if (key in self.cache) return Promise.resolve(self.cache[key] || null);
|
||||||
|
if (self.inflight[key]) return self.inflight[key];
|
||||||
|
|
||||||
|
var ctrl = (typeof AbortController !== "undefined") ? new AbortController() : null;
|
||||||
|
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, self.timeoutMs) : 0;
|
||||||
|
|
||||||
|
var req = fetch(self.url(key), {
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
signal: ctrl ? ctrl.signal : undefined
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function (body) {
|
||||||
|
// n8n sometimes wraps the record in an array — accept both shapes.
|
||||||
|
var d = Array.isArray(body) ? body[0] : body;
|
||||||
|
var img = d && d.Image;
|
||||||
|
if (!self.isImageSrc(img)) throw new Error("no image");
|
||||||
|
self.cache[key] = img;
|
||||||
|
return img;
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
// Every failure — non-2xx, no image, a decode-hostile value, or the timeout's
|
||||||
|
// abort — is the monogram. Remember it, so a shared id under a re-render does not
|
||||||
|
// ask the network twice.
|
||||||
|
self.cache[key] = "";
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(function (out) {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
delete self.inflight[key];
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.inflight[key] = req;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* The ONE source of truth for the "skill profile" rule — the 15-point budget.
|
||||||
|
Two forms render this rule: the directory application form
|
||||||
|
(/odoo-community-developers/) and the private self-service edit form
|
||||||
|
(/freelancer-edit/). Both take the legend, the directive and the wrong-total
|
||||||
|
message from here, so the two cannot word the same rule differently again.
|
||||||
|
Loaded before each page's own script. */
|
||||||
|
(function () {
|
||||||
|
var TOTAL = 15;
|
||||||
|
window.O4PFocus = {
|
||||||
|
TOTAL: TOTAL,
|
||||||
|
MAX: 5,
|
||||||
|
LEGEND: "Skill profile — 15 points to distribute",
|
||||||
|
DIRECTIVE: "Distribute all 15 points across the five areas — they cannot all be 0.",
|
||||||
|
/* The wrong-total message, naming the direction to move. "" when the sum is right. */
|
||||||
|
message: function (sum) {
|
||||||
|
if (sum === TOTAL) return "";
|
||||||
|
if (sum === 0) return "You have 0 of " + TOTAL + " — place all " + TOTAL + " points.";
|
||||||
|
if (sum > TOTAL) return "You have " + sum + " of " + TOTAL + " — remove " + (sum - TOTAL) + ".";
|
||||||
|
return "You have " + sum + " of " + TOTAL + " — add " + (TOTAL - sum) + ".";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# static/icons — the profile-link icon set
|
||||||
|
|
||||||
|
Four monochrome brand marks shown in the freelancer directory card's links row
|
||||||
|
(`/odoo-community-developers/`): LinkedIn, GitHub, Upwork, Fiverr.
|
||||||
|
|
||||||
|
**Source:** [Simple Icons](https://simpleicons.org) v13 — one uniform set, so the four
|
||||||
|
share one visual language by construction: the same 24 × 24 grid, the same internal
|
||||||
|
padding, the same monochrome mass, single-path art. Nothing here is hand-drawn and
|
||||||
|
nothing is fetched from a third party at runtime.
|
||||||
|
|
||||||
|
**How the page uses them.** The card inlines the same path data (see `LINK_ICONS` in the
|
||||||
|
page's script and in `work/build-directory.py`), so the links row makes **zero requests**
|
||||||
|
and each mark inherits the link's `currentColor`. These files are the self-hosted,
|
||||||
|
drop-in copy of that set — identical geometry — for any host that prefers a file over an
|
||||||
|
inline `<svg>`.
|
||||||
|
|
||||||
|
**Brand rule.** A brand mark is never redrawn and never restyled beyond a single
|
||||||
|
monochrome fill. The one styling the page applies is `fill="currentColor"`.
|
||||||
|
|
||||||
|
The `role`/`aria-label` here are for standalone use; the card strips them and marks the
|
||||||
|
inline SVG `aria-hidden="true" focusable="false"`, putting the accessible name on the
|
||||||
|
link (`aria-label` + `title`, e.g. "GitHub profile").
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Fiverr"><path fill="currentColor" d="M23.004 15.588a.995.995 0 1 0 .002-1.99.995.995 0 0 0-.002 1.99zm-.996-3.705h-.85c-.546 0-.84.41-.84 1.092v2.466h-1.61v-3.558h-.684c-.547 0-.84.41-.84 1.092v2.466h-1.61v-4.874h1.61v.74c.264-.574.626-.74 1.163-.74h1.972v.74c.264-.574.625-.74 1.162-.74h.527v1.316zm-6.786 1.501h-3.359c.088.546.43.858 1.006.858.43 0 .732-.175.83-.487l1.425.4c-.351.848-1.22 1.364-2.255 1.364-1.748 0-2.549-1.355-2.549-2.515 0-1.14.703-2.505 2.45-2.505 1.856 0 2.471 1.384 2.471 2.408 0 .224-.01.37-.02.477zm-1.562-.945c-.04-.42-.342-.81-.889-.81-.508 0-.81.225-.908.81h1.797zM7.508 15.44h1.416l1.767-4.874h-1.62l-.86 2.837-.878-2.837H5.72l1.787 4.874zm-6.6 0H2.51v-3.558h1.524v3.558h1.591v-4.874H2.51v-.302c0-.332.235-.536.606-.536h.918V8.412H2.85c-1.162 0-1.943.712-1.943 1.755v.4H0v1.316h.908v3.558z"/></svg>
|
||||||
|
After Width: | Height: | Size: 912 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="GitHub"><path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||||
|
After Width: | Height: | Size: 842 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="LinkedIn"><path fill="currentColor" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
||||||
|
After Width: | Height: | Size: 630 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Upwork"><path fill="currentColor" d="M18.561 13.158c-1.102 0-2.135-.467-3.074-1.227l.228-1.076.008-.042c.207-1.143.849-3.06 2.839-3.06 1.492 0 2.703 1.212 2.703 2.703-.001 1.489-1.212 2.702-2.704 2.702zm0-8.14c-2.539 0-4.51 1.649-5.31 4.366-1.22-1.834-2.148-4.036-2.687-5.892H7.828v7.112c-.002 1.406-1.141 2.546-2.547 2.548-1.405-.002-2.543-1.143-2.545-2.548V3.492H0v7.112c0 2.914 2.37 5.303 5.281 5.303 2.913 0 5.283-2.389 5.283-5.303v-1.19c.529 1.107 1.182 2.229 1.974 3.221l-1.673 7.873h2.797l1.213-5.71c1.063.679 2.285 1.109 3.686 1.109 3 0 5.439-2.452 5.439-5.45 0-3-2.439-5.439-5.439-5.439z"/></svg>
|
||||||
|
After Width: | Height: | Size: 689 B |
@@ -0,0 +1,141 @@
|
|||||||
|
/* The ONE source of truth for photo uploads — limits, validation and downscale.
|
||||||
|
Two forms call this: the directory application form (/odoo-community-developers/)
|
||||||
|
and the private self-service edit form (/freelancer-edit/). The limits live here
|
||||||
|
once, so the two cannot drift. Loaded before each page's own script.
|
||||||
|
|
||||||
|
Why this exists beyond page weight: re-encoding to a canvas DROPS EXIF entirely.
|
||||||
|
A phone photo carries GPS coordinates, so an un-stripped portrait publishes the
|
||||||
|
photographer's home location. Re-encoding is also a security step — a file whose
|
||||||
|
declared type was spoofed fails to decode and is refused here.
|
||||||
|
|
||||||
|
NOTE: the <input accept> attribute is a picker HINT, never validation. Only
|
||||||
|
validate() below decides, and it runs on the File object before any read. */
|
||||||
|
(function () {
|
||||||
|
var MAX_INPUT_BYTES = 8 * 1024 * 1024; // 8 MB — refuse before reading a byte
|
||||||
|
var ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
||||||
|
var MAX_SIDE = 384; // longest side of the stored image, px
|
||||||
|
var MAX_OUTPUT_BYTES = 80 * 1024; // 80 KB — the encoded JPEG target
|
||||||
|
var QUALITY = 0.82; // starting JPEG quality
|
||||||
|
var QUALITY_STEP = 0.05; // step down until the target is met
|
||||||
|
var QUALITY_FLOOR = 0.4; // never go below this
|
||||||
|
|
||||||
|
function kb(bytes) { return Math.round(bytes / 1024); }
|
||||||
|
function mb(bytes) { return (bytes / (1024 * 1024)).toFixed(1); }
|
||||||
|
|
||||||
|
/* Decide on the File's own metadata — no read happens here. */
|
||||||
|
function validate(file) {
|
||||||
|
if (!file) return { ok: false, reason: "No file was chosen." };
|
||||||
|
if (ALLOWED_TYPES.indexOf(file.type) === -1) {
|
||||||
|
return { ok: false, reason: "That file type is not allowed. Use a JPEG, PNG or WebP image." };
|
||||||
|
}
|
||||||
|
if (file.size > MAX_INPUT_BYTES) {
|
||||||
|
return { ok: false, reason: "That photo is " + mb(file.size) + " MB. The limit is 8 MB — please choose a smaller one." };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAsDataUrl(file) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function () { resolve(reader.result); };
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(url) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var img = new Image();
|
||||||
|
img.onload = function () { resolve(img); };
|
||||||
|
img.onerror = reject; // a spoofed "image/jpeg" that is not an image lands here
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function byteLength(dataUrl) {
|
||||||
|
return atob(dataUrl.slice(dataUrl.indexOf(",") + 1)).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read → centre-crop square → cap longest side → encode JPEG → step quality down
|
||||||
|
until it fits. Resolves { dataUrl, width, height, bytes, quality }. */
|
||||||
|
function downscale(file) {
|
||||||
|
return readAsDataUrl(file).then(loadImage).then(function (img) {
|
||||||
|
var w = img.naturalWidth, h = img.naturalHeight;
|
||||||
|
var side = Math.min(w, h);
|
||||||
|
var sx = (w - side) / 2, sy = (h - side) / 2;
|
||||||
|
var out = Math.min(MAX_SIDE, side);
|
||||||
|
var canvas = document.createElement("canvas");
|
||||||
|
canvas.width = out;
|
||||||
|
canvas.height = out;
|
||||||
|
canvas.getContext("2d").drawImage(img, sx, sy, side, side, 0, 0, out, out);
|
||||||
|
var quality = QUALITY;
|
||||||
|
var dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||||
|
var bytes = byteLength(dataUrl);
|
||||||
|
while (bytes > MAX_OUTPUT_BYTES && quality > QUALITY_FLOOR) {
|
||||||
|
quality = Math.max(QUALITY_FLOOR, quality - QUALITY_STEP);
|
||||||
|
dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||||
|
bytes = byteLength(dataUrl);
|
||||||
|
}
|
||||||
|
return { dataUrl: dataUrl, width: out, height: out, bytes: bytes, quality: quality };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The one honest line the person sees. No "optimised for the web" — only what we can show. */
|
||||||
|
function resultLine(res) {
|
||||||
|
return "Resized to " + res.width + " × " + res.height + ", " + kb(res.bytes) + " KB";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wire one file input to a preview <img> and a status line. onResult receives the
|
||||||
|
downscaled data URL, or null when there is no (valid) file. Both forms use this,
|
||||||
|
so the outcome shown is identical. */
|
||||||
|
function wire(input, preview, result, onResult) {
|
||||||
|
if (!input) return;
|
||||||
|
function say(text, state) {
|
||||||
|
if (!result) return;
|
||||||
|
result.textContent = text || "";
|
||||||
|
if (state) result.setAttribute("data-state", state);
|
||||||
|
else result.removeAttribute("data-state");
|
||||||
|
}
|
||||||
|
input.addEventListener("change", function () {
|
||||||
|
var file = input.files && input.files[0];
|
||||||
|
if (!file) {
|
||||||
|
if (preview) preview.hidden = true;
|
||||||
|
say("", null);
|
||||||
|
onResult(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var v = validate(file);
|
||||||
|
if (!v.ok) {
|
||||||
|
if (preview) preview.hidden = true;
|
||||||
|
input.value = ""; // a refused file must never be submitted
|
||||||
|
say(v.reason, "err");
|
||||||
|
onResult(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
say("Processing…", null);
|
||||||
|
downscale(file).then(function (res) {
|
||||||
|
if (preview) { preview.src = res.dataUrl; preview.hidden = false; }
|
||||||
|
say(resultLine(res), "ok");
|
||||||
|
onResult(res.dataUrl, res);
|
||||||
|
}).catch(function () {
|
||||||
|
if (preview) preview.hidden = true;
|
||||||
|
input.value = "";
|
||||||
|
say("That file could not be read as an image. Please choose a JPEG, PNG or WebP.", "err");
|
||||||
|
onResult(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.O4PPhoto = {
|
||||||
|
MAX_INPUT_BYTES: MAX_INPUT_BYTES,
|
||||||
|
ALLOWED_TYPES: ALLOWED_TYPES,
|
||||||
|
MAX_SIDE: MAX_SIDE,
|
||||||
|
MAX_OUTPUT_BYTES: MAX_OUTPUT_BYTES,
|
||||||
|
QUALITY: QUALITY,
|
||||||
|
validate: validate,
|
||||||
|
downscale: downscale,
|
||||||
|
resultLine: resultLine,
|
||||||
|
kb: kb,
|
||||||
|
wire: wire
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -6,6 +6,19 @@
|
|||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
/* `hidden` must really hide — everywhere, not one element at a time. The UA's own
|
||||||
|
`[hidden] { display: none }` is the weakest possible declaration: any page rule that
|
||||||
|
sets `display` on the same element outranks it. A class rule (`.dir-pager`,
|
||||||
|
`.dev-card`, `.hero-notice`) already beats it, and an id-based state selector such as
|
||||||
|
`.js #directory[data-dir-state="ready"] #dirGrid > .dev-card` (two ids) beat the old
|
||||||
|
per-element patch — so setting `el.hidden = true` set the attribute and the element
|
||||||
|
stayed on screen (measured on /odoo-community-developers/: four cards visible, the
|
||||||
|
count line reading "Showing 1 of 4"). This one rule, with `!important`, beats every
|
||||||
|
class- or id-based `display`, so the attribute hides its element no matter which
|
||||||
|
later rule sets `display`. It is the single mechanism for `[hidden]` site-wide; the
|
||||||
|
per-element `[hidden]` rules are gone so nothing disagrees. */
|
||||||
|
[hidden] { display: none !important; }
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Inter, 'Segoe UI', sans-serif;
|
font-family: Inter, 'Segoe UI', sans-serif;
|
||||||
|
|||||||
+3
-3
@@ -29,7 +29,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
||||||
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
plausible.init()
|
plausible.init()
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
@@ -590,6 +590,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+3
-3
@@ -25,7 +25,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
.legal-hero {
|
.legal-hero {
|
||||||
padding: 4rem 0 2.5rem;
|
padding: 4rem 0 2.5rem;
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="legal-card">
|
<div class="legal-card">
|
||||||
<h2>Who we are</h2>
|
<h2>Who we are</h2>
|
||||||
<p>ODOO4projects LLC, 30 N Gould St, STE R, Sheridan, WY 82801, USA. Contact: <span class="contact-email">support@ODOO4projects.com</span></p>
|
<p>ODOO4projects LLC, 30 N Gould St, STE R, Sheridan, WY 82801, USA. Contact: <span class="contact-email">support@odoo4projects.com</span></p>
|
||||||
<p>These terms cover the hosting service we sell on this website.</p>
|
<p>These terms cover the hosting service we sell on this website.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,6 +195,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- "What Can I Use Odoo For?" guide page (page-scoped; additive).
|
/* ---- "What Can I Use Odoo For?" guide page (page-scoped; additive).
|
||||||
The block below is odoo-community-upgrade.html's page-scoped CSS, reused so
|
The block below is odoo-community-upgrade.html's page-scoped CSS, reused so
|
||||||
@@ -913,6 +913,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -936,7 +937,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
@@ -951,6 +952,6 @@
|
|||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
onload="this.media='all'"
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||||
<style>
|
<style>
|
||||||
/* ---- Workshops and Classes hub page (page-scoped; additive).
|
/* ---- Workshops and Classes hub page (page-scoped; additive).
|
||||||
The block below is the bootcamp page's page-scoped CSS, reused so the two
|
The block below is the bootcamp page's page-scoped CSS, reused so the two
|
||||||
@@ -749,6 +749,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||||
|
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||||
<li><a href="/support.html">Support</a></li>
|
<li><a href="/support.html">Support</a></li>
|
||||||
<li><a href="/legal.html">Legal</a></li>
|
<li><a href="/legal.html">Legal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -772,7 +773,7 @@
|
|||||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||||
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
||||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||||
<div class="footer-textarea-wrap">
|
<div class="footer-textarea-wrap">
|
||||||
@@ -787,6 +788,6 @@
|
|||||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script defer src="/static/lang.js"></script>
|
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user