openvibers/openvibe.food Commits

Food: the place search sits in a panel beside the home hero (labels above full-width fields, one button), and in a tidy row on /near

OP OpenVibers committed 8 Oct 2026 · 9bfc0858b1 · 3 files, +22 −8


Changes

ddiff --git a/public/css/app.css b/public/css/app.css
iindex 07846bd..9018c76 100644
---- a/public/css/app.css
++++ b/public/css/app.css
@@@ -58,3 +58,14 @@ button.sc-btn.sc-primary { border: 0; }
.pantry-form fieldset { border: 1px solid var(--border, #1f2d47); border-radius: 10px; margin: 10px 0; }
.pantry-item { display: inline-block; min-width: 240px; margin: 2px 10px 2px 0; font-size: .92rem; }
.lede { color: var(--text-secondary, #96a7c2); max-width: 72ch; }
++
++/* ── The place search: a row on /near, a panel beside the home hero ── */
++.food-search .food-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
++.food-search .food-field-where { flex: 1 1 260px; }
++.food-search .food-field input, .food-search .food-field select { width: 100%; min-width: 0; height: 42px; }
++.food-search > .sc-btn { height: 42px; }
++.food-search-panel { flex-direction: column; align-items: stretch; flex-wrap: nowrap; gap: 14px; margin: 0; padding: 22px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: linear-gradient(160deg, color-mix(in srgb, var(--accent, #3b82f6) 9%, var(--bg-card, #131c2e)), var(--bg-card, #131c2e) 60%); box-shadow: 0 18px 40px -24px rgba(0, 0, 0, .7); }
++.food-search-panel .food-field-where { flex: none; }
++.food-search-title { margin: 0; font-size: 1.05rem; font-weight: 750; color: var(--text-primary, #e6edf7); }
++.food-search-panel > .sc-btn { width: 100%; justify-content: center; }
++.food-search-note { margin: -4px 0 0; font-size: .8rem; color: var(--text-muted, #7386a3); text-align: center; }
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 2a8418f..90ce9a8 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -50,14 +50,17 @@ function createPageRoutes(ctx) {
// ODbL requires attribution wherever OSM data is shown; every result page carries it, linked back.
const osmNote = (fetchedAt) => html`<p class="attribution small muted">Food bank and store data © <a href="${ATTRIBUTION.url}" rel="noopener">OpenStreetMap contributors</a>, available under the <a href="${ATTRIBUTION.license_url}" rel="noopener">Open Database License</a>. ${fetchedAt ? html`Fetched ${time(fetchedAt)}.` : ''} OpenVibe.Food asks OSM at most once a second and caches answers for 7 days.</p>`;
-- function searchForm({ q = '', kind = 'foodbank' } = {}) {
++ // The same fields everywhere; on the home page they sit in a panel beside the hero (panel: true), on /near in a row.
++ function searchForm({ q = '', kind = 'foodbank', panel = false } = {}) {
const opt = (value, label) => html`<option value="${value}"${value === kind ? raw(' selected') : ''}>${label}</option>`;
-- return html`<form class="ov-form food-search" method="get" action="/near">
--<label for="q">Where are you?</label>
--<input id="q" name="q" type="search" placeholder="Town, city or postcode" value="${q}" maxlength="120">
--<label for="kind">Looking for</label>
--<select id="kind" name="kind">${Object.entries(KINDS).map(([v, l]) => opt(v, l))}</select>
++ return html`<form class="ov-form food-search${panel ? ' food-search-panel' : ''}" method="get" action="/near">
++${panel ? html`<p class="food-search-title">Find food near you</p>` : ''}
++<div class="food-field food-field-where"><label for="q">Where are you?</label>
++<input id="q" name="q" type="search" placeholder="Town, city or postcode" value="${q}" maxlength="120" autocomplete="address-level2"></div>
++<div class="food-field"><label for="kind">Looking for</label>
++<select id="kind" name="kind">${Object.entries(KINDS).map(([v, l]) => opt(v, l))}</select></div>
<button class="sc-btn sc-primary" type="submit">Find food</button>
++${panel ? html`<p class="food-search-note">Free, no account. Places from OpenStreetMap.</p>` : ''}
</form>`;
}
@@@ -90,7 +93,7 @@ ${res_.phone ? html`<p class="small">Phone: <a href="tel:${String(res_.phone).re
{ label: 'The food list', href: '/foods' },
],
note: 'Open source (AGPL-3.0). Places come from OpenStreetMap © contributors (ODbL); prices are typical, not live.',
-- aside: { html: String(searchForm()) },
++ aside: { html: String(searchForm({ panel: true })) },
});
page(req, res, {
index: true, cache: signedIn(req) ? null : PUBLIC_CACHE,
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex d4a4d0e..8b5bdd8 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -23,7 +23,7 @@ const BUDGETS = {
// 16.4 since the product's own pages brought their stylesheet with them (the search and plan forms, the place
// cards, the pantry's tick list); app.css itself stayed within a few lines.
cssRawKB: 18.0, // 16.4
-- cssBrotliKB: 4.1, // 3.7
++ cssBrotliKB: 4.6, // 4.3 with the hero search panel
externalFiles: 0, // 0
};