feat(bf): Server Location selector beside the deal Buy, carried into both buy forms

Move the homepage's Server Location lockup (label + select) out of section#pricing
and into the HOST27 deal card's CTA row, next to the Buy button. There is still
exactly ONE element with id pricingServerLocation on the page, so buyNow() — which
resolves the selector by id — and every pricing button keep working untouched.
The pricing block keeps only the Odoo Version selector and reads the same global
location selection.

- markup: new .bf-cta-row in the deal form (selector left, brass Buy right);
  location lockup removed from .pricing-server-row
- CSS (page-scoped): .bf-cta-row / .bf-cta-location, and the light-on-ivory twin
  of .pricing-server-select (style.css carries neither; the homepage styles it
  inline for its dark section). Wraps at 375px with no horizontal overflow; the
  Buy stays the one dominant action.
- static/black-friday.js: on submit of the native post_basket forms, write the
  chosen city + version into the basket line's `info`, exactly as buyNow() does.
  Additive only — with JavaScript off the POST is unchanged (no info) and the
  selector is empty because its options are built here (pre-existing behaviour).

Untouched: the hidden redirect=/coupon/HOST27, the single plausible-event-name=Buy
mark, the price, the countdown, #trialForm, the booking link and the consent gate.

Verified: verify-html.py PROBLEMS 0; competitor QC exit 0 on the page (74
pre-existing site-wide, 0 added); node --check OK; impeccable detect 35 findings,
identical set to HEAD (0 introduced); live browser: fastest server auto-selected,
wire body basket=[{"product_id":4,"qty":1,"info":"boston|20.0"}] and it tracks the
selectors on repeat submits; 375px no overflow.
This commit is contained in:
webmaster committed 2026-11-02 09:42:01 +00:00
1 parent 0f8ae45e59
commit 9e38e05aad
2 files changed
+93 -3

No files matched your search

+63 -3
View File
@@ -274,6 +274,52 @@
outline: 3px solid var(--midnight);
outline-offset: 2px;
}
/* The deal's CTA row (2026-11-02): the brass/large Buy keeps its weight and stays the ONE
dominant action; the Server Location selector sits beside it as a compact, quiet form
control, never a second button. The row wraps on a narrow screen — the selector holds a
~190px basis and the Buy takes the line below — so nothing overflows at 375px. */
.bf-cta-row {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0.6rem 0.9rem;
}
.bf-cta-location {
display: flex;
flex-direction: column;
gap: 0.35rem;
flex: 0 1 190px;
min-width: 0;
}
.bf-cta-location label {
font-family: Inter, sans-serif;
font-weight: 600;
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brass-deep);
}
/* The homepage's .pricing-server-select is styled for its dark section and lives only in
index.html's inline CSS — style.css carries neither. On this ivory card the selector
needs the light twin, so it is defined page-scoped here. */
.bf-cta-location .pricing-server-select {
width: 100%;
padding: 0.5rem 1rem;
border-radius: var(--radius);
border: 1px solid var(--line-brass);
background: #fff;
color: var(--ink);
font-family: Inter, sans-serif;
font-size: 0.85rem;
cursor: pointer;
}
.bf-cta-location .pricing-server-select:focus-visible {
outline: 3px solid var(--midnight);
outline-offset: 2px;
}
.bf-cta-row .btn {
flex: 1 1 auto;
}
.bf-cta-secondary {
font-size: 0.85rem;
margin: 0;
@@ -769,7 +815,19 @@
<input type="hidden" name="basket" value='[{"product_id":4,"qty":1}]' />
<input type="hidden" name="clear" value="1" />
<input type="hidden" name="redirect" value="/coupon/HOST27?r=/shop/checkout" />
<button type="submit" class="btn btn-brass btn-lg">Buy On the Rise — $295 first year</button>
<!-- Server Location, MOVED here from the pricing block below (2026-11-02): this is
the ONE element with id pricingServerLocation on the page, so buyNow() — which
reads it by id — and both native buy forms keep working untouched. Its options
are built and its fastest server auto-selected by /static/black-friday.js. It
is a quiet form control (no `name`, so it is not itself submitted), never a
second button; the brass Buy beside it stays the one dominant action. -->
<div class="bf-cta-row">
<div class="bf-cta-location">
<label for="pricingServerLocation">Server Location</label>
<select id="pricingServerLocation" class="pricing-server-select"></select>
</div>
<button type="submit" class="btn btn-brass btn-lg">Buy On the Rise — $295 first year</button>
</div>
</form>
<p class="bf-cta-secondary"><a href="/odoo-hosting-offers/">See everything a year of hosting includes</a></p>
</div>
@@ -955,8 +1013,10 @@
<p class="section-kicker">Order & Pricing</p>
<h2 class="section-title">Your Plan, Your Timing: Upgrade Anytime</h2>
<div class="pricing-server-row">
<label for="pricingServerLocation">Server Location</label>
<select id="pricingServerLocation" class="pricing-server-select"></select>
<!-- The Server Location selector was moved into the Black Friday deal card (#offer):
there must be exactly ONE element with id pricingServerLocation, and the deal card
is where the buyer commits. The pricing buttons read that same global selection by
id via buyNow(). Only the version selector stays here. -->
<label for="pricingOdooVersion">Odoo Version</label>
<select id="pricingOdooVersion" class="pricing-server-select">
<option value="19.0">19.0 — Tons of OCA supported modules</option>
+30
View File
@@ -307,6 +307,36 @@
document.body.removeChild(form);
};
/* ---- Carry the chosen Server Location + Odoo version into the native buy forms ----
* The Black Friday buy buttons are plain form POSTs to /shop/post_basket, so they work with
* JavaScript off. buyNow() builds `info` for the JS buttons; this does exactly the same for
* the native forms, on submit, from the one global selector pair (#pricingServerLocation +
* #pricingOdooVersion). Additive only: with JavaScript off the basket posts exactly as before
* (no info) and the selector is empty anyway because its options are built here. The POST is
* untouched — only the form's own hidden `basket` input is rewritten. */
(function () {
var forms = document.querySelectorAll('form[action*="/shop/post_basket"]');
forms.forEach(function (form) {
form.addEventListener('submit', function () {
var basket = form.querySelector('input[name="basket"]');
if (!basket) return;
var locSelect = document.getElementById('pricingServerLocation');
var verSelect = document.getElementById('pricingOdooVersion');
var location = locSelect ? locSelect.value : '';
var version = verSelect ? verSelect.value : '';
var info = location ? location.replace('.odoo4projects.com', '') : '';
if (version) { info += (info ? '|' : '') + version; }
if (!info) return;
try {
var items = JSON.parse(basket.value);
if (!Array.isArray(items)) return;
items.forEach(function (it) { if (it) { it.info = info; } });
basket.value = JSON.stringify(items);
} catch (e) { /* leave the basket untouched rather than break the purchase */ }
});
});
})();
/* ---- Buy buttons + Custom Quote modal ---- */
(function () {
document.querySelectorAll('.btn-buy').forEach(function (btn) {