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
Changes
@@@ -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; }@@@ -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,@@@ -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 panelexternalFiles: 0, // 0};
@@ -58,3 +58,14 @@ button.sc-btn.sc-primary { border: 0; }
@@ -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-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; }
.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; }
.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; }
@@ -50,14 +50,17 @@ function createPageRoutes(ctx) {
@@ -50,14 +50,17 @@ function createPageRoutes(ctx) {
// ODbL requires attribution wherever OSM data is shown; every result page carries it, linked back.
// 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>`;
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>`;
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>
<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>`;
</form>`;
}
}
@@ -90,7 +93,7 @@ ${res_.phone ? html`<p class="small">Phone: <a href="tel:${String(res_.phone).re
@@ -90,7 +93,7 @@ ${res_.phone ? html`<p class="small">Phone: <a href="tel:${String(res_.phone).re
{ label: 'The food list', href: '/foods' },
{ label: 'The food list', href: '/foods' },
],
],
note: 'Open source (AGPL-3.0). Places come from OpenStreetMap © contributors (ODbL); prices are typical, not live.',
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, {
page(req, res, {
index: true, cache: signedIn(req) ? null : PUBLIC_CACHE,
index: true, cache: signedIn(req) ? null : PUBLIC_CACHE,
@@ -23,7 +23,7 @@ const BUDGETS = {
@@ -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
// 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.
// cards, the pantry's tick list); app.css itself stayed within a few lines.
cssRawKB: 18.0, // 16.4
cssRawKB: 18.0, // 16.4
- cssBrotliKB: 4.1, // 3.7
+ cssBrotliKB: 4.6, // 4.3 with the hero search panel
externalFiles: 0, // 0
externalFiles: 0, // 0
};
};