openvibers/openvibe.rent Commits
Homes' largest cities are bars, like Rent's (#186)
Changes
@@@ -112,7 +112,7 @@ ${home.stats([{ value: Object.keys(US_STATE_NAMES).length, label: 'states with county records', href: '/records' },], { label: 'OpenVibe.Homes in numbers' })}--<section class="sc-sec home-sec home-cities">${home.cityRail({ id: 'cities', title: 'What homes are worth in the largest cities', more: { href: '/in', label: `All ${cities.cities.length} cities` }, list: largest, pick: (c) => ({ corner: c.value != null ? cities.valueText(c.value) : null, peek: c.rent.b2 != null ? `2-bedroom rent ${cities.rentText(c.rent.b2)}` : '' }) })}++<section class="sc-sec home-sec home-cities">${home.cityBars({ id: 'cities', title: 'What homes are worth in the largest cities', more: { href: '/in', label: `All ${cities.cities.length} cities` }, list: largest, value: (c) => (c.value != null ? Number(c.value) : NaN), text: (c) => cities.valueText(c.value) })}<p class="small muted home-under">The median value of owner-occupied homes (Census Bureau, ${cities.SOURCE.years}); each city's page has the price trend, the monthly payment and every for-sale site there.</p>${trend ? home.fold('moves', 'Where home prices moved over the past year', moves.latest, html`<p class="small muted">The FHFA House Price Index, ${moves.latest} against a year before: an index of repeat sales of the same homes, not a price. <a href="${moves.source.url}" rel="noopener">Source: ${moves.source.name}</a></p><div class="home-ranks">${home.ranked('Metro areas rising fastest', moves.metros.slice(0, 6).map(metroRow))}${home.ranked('Metro areas rising slowest or falling', moves.metros.slice(-6).reverse().map(metroRow))}${moves.states.length >= 10 ? home.ranked('States rising fastest', moves.states.slice(0, 6).map(stateRow)) : ''}</div>`) : ''}${big.length >= 10 ? home.fold('range', 'Where homes cost least and most', '', html`<p class="small muted">Big cities (250,000 people or more) by median home value.</p><div class="home-ranks">${home.ranked('Lowest median value', byValue.slice(0, 6).map(valueRow))}${home.ranked('Highest median value', byValue.slice(-6).reverse().map(valueRow))}</div>`) : ''}@@@ -22,21 +22,6 @@ const section = (id, title, lede, body) => (render(body).trim()? html`<section class="sc-sec" aria-labelledby="h-${id}"><h2 id="h-${id}">${title}</h2>${lede ? html`<p class="sc-lede">${lede}</p>` : ''}${body}</section>`: '');--/**-- * The largest cities as a rail of cards (it scrolls sideways and snaps; wide screens wrap it, home.css): the name,-- * its state, the headline number in the card's corner and the rest on hover. `pick(c)` → { corner, peek };-- * `href(c)` is where the card goes (the city page by default).-- */--function cityRail({ id, title, more, list, pick, href = (c) => c.path }) {-- return raw(kit.rail({-- id, title, more, size: 's', wrap: true,-- items: (list || []).map((c) => {-- const p = pick(c);-- return kit.card({ href: href(c), title: c.name, type: 'map', ratio: '4/3', badge: c.state, corner: p.corner || 'not published', peek: p.peek || '', share: false });-- }),-- }));--}--/*** The largest cities as bars, the dearest first (2026-11-01): one look compares them, where a rail of cards drew the* same map icon twelve times. Each row is a link to its city: the name and state, a bar as long as its rent against@@@ -84,4 +69,4 @@ const doors = (list) => html`<p class="home-doors">${raw(list.slice(0, 2).map((d/** Cities with at least `min` people, largest first. */const bigCities = (min) => cities.cities.filter((c) => c.population >= min).sort((a, b) => b.population - a.population);--module.exports = { STYLES, stats, section, cityRail, cityBars, fold, ranked, stateChips, siteList, faq, search, doors, bigCities };++module.exports = { STYLES, stats, section, cityBars, fold, ranked, stateChips, siteList, faq, search, doors, bigCities };@@@ -84,7 +84,9 @@ const housingFetchImpl = async (url) => (new URL(String(url)).pathname.endsWith(// The same front-page shape as Rent's: the search first, the cities as a rail of cards with All N, the parts// read once folded, the site's places as a launcher.assert.ok(r.text.includes('<form class="ovk-find" method="get" action="/search" role="search">'), 'the search is the kit\'s pill');-- assert.ok(/<section class="ovk-rail[^"]*" id="cities"/.test(r.text) && /<a class="ovk-rail-more" href="\/in">All \d+ cities/.test(r.text), 'the cities are a rail with All N');++ // Bars, dearest first, as on Rent's home (2026-11-01): a rail of cards drew the same map icon for every city.++ assert.ok(/<ol class="home-bars" aria-labelledby="h-cities">/.test(r.text) && /<a class="home-more" href="\/in">All \d+ cities/.test(r.text), 'the cities are bars with All N');++ assert.ok(!/ovk-card--map/.test(r.text), 'no placeholder map cards');assert.ok(/<details class="ovk-fold" id="range">/.test(r.text) && /<details class="ovk-fold" id="states">/.test(r.text) && /<details class="ovk-fold" id="sites">/.test(r.text), 'the ranks, the states and the sites are folds');assert.ok(/<section class="sc-sec sc-apps-sec" id="places"/.test(r.text) && r.text.includes('href="/records"') && r.text.includes('href="/mortgage"'), 'the places as a launcher');assert.ok(r.text.includes('Questions people ask') && /"@type":"FAQPage"/.test(r.text) && /"@type":"ItemList"/.test(r.text), 'the questions shown are the FAQPage; the cities are an ItemList');
@@ -112,7 +112,7 @@ ${home.stats([
@@ -112,7 +112,7 @@ ${home.stats([
{ value: Object.keys(US_STATE_NAMES).length, label: 'states with county records', href: '/records' },
{ value: Object.keys(US_STATE_NAMES).length, label: 'states with county records', href: '/records' },
], { label: 'OpenVibe.Homes in numbers' })}
], { label: 'OpenVibe.Homes in numbers' })}
-<section class="sc-sec home-sec home-cities">${home.cityRail({ id: 'cities', title: 'What homes are worth in the largest cities', more: { href: '/in', label: `All ${cities.cities.length} cities` }, list: largest, pick: (c) => ({ corner: c.value != null ? cities.valueText(c.value) : null, peek: c.rent.b2 != null ? `2-bedroom rent ${cities.rentText(c.rent.b2)}` : '' }) })}
+<section class="sc-sec home-sec home-cities">${home.cityBars({ id: 'cities', title: 'What homes are worth in the largest cities', more: { href: '/in', label: `All ${cities.cities.length} cities` }, list: largest, value: (c) => (c.value != null ? Number(c.value) : NaN), text: (c) => cities.valueText(c.value) })}
<p class="small muted home-under">The median value of owner-occupied homes (Census Bureau, ${cities.SOURCE.years}); each city's page has the price trend, the monthly payment and every for-sale site there.</p>
<p class="small muted home-under">The median value of owner-occupied homes (Census Bureau, ${cities.SOURCE.years}); each city's page has the price trend, the monthly payment and every for-sale site there.</p>
${trend ? home.fold('moves', 'Where home prices moved over the past year', moves.latest, html`<p class="small muted">The FHFA House Price Index, ${moves.latest} against a year before: an index of repeat sales of the same homes, not a price. <a href="${moves.source.url}" rel="noopener">Source: ${moves.source.name}</a></p><div class="home-ranks">${home.ranked('Metro areas rising fastest', moves.metros.slice(0, 6).map(metroRow))}${home.ranked('Metro areas rising slowest or falling', moves.metros.slice(-6).reverse().map(metroRow))}${moves.states.length >= 10 ? home.ranked('States rising fastest', moves.states.slice(0, 6).map(stateRow)) : ''}</div>`) : ''}
${trend ? home.fold('moves', 'Where home prices moved over the past year', moves.latest, html`<p class="small muted">The FHFA House Price Index, ${moves.latest} against a year before: an index of repeat sales of the same homes, not a price. <a href="${moves.source.url}" rel="noopener">Source: ${moves.source.name}</a></p><div class="home-ranks">${home.ranked('Metro areas rising fastest', moves.metros.slice(0, 6).map(metroRow))}${home.ranked('Metro areas rising slowest or falling', moves.metros.slice(-6).reverse().map(metroRow))}${moves.states.length >= 10 ? home.ranked('States rising fastest', moves.states.slice(0, 6).map(stateRow)) : ''}</div>`) : ''}
${big.length >= 10 ? home.fold('range', 'Where homes cost least and most', '', html`<p class="small muted">Big cities (250,000 people or more) by median home value.</p><div class="home-ranks">${home.ranked('Lowest median value', byValue.slice(0, 6).map(valueRow))}${home.ranked('Highest median value', byValue.slice(-6).reverse().map(valueRow))}</div>`) : ''}
${big.length >= 10 ? home.fold('range', 'Where homes cost least and most', '', html`<p class="small muted">Big cities (250,000 people or more) by median home value.</p><div class="home-ranks">${home.ranked('Lowest median value', byValue.slice(0, 6).map(valueRow))}${home.ranked('Highest median value', byValue.slice(-6).reverse().map(valueRow))}</div>`) : ''}
@@ -22,21 +22,6 @@ const section = (id, title, lede, body) => (render(body).trim()
@@ -22,21 +22,6 @@ const section = (id, title, lede, body) => (render(body).trim()
? html`<section class="sc-sec" aria-labelledby="h-${id}"><h2 id="h-${id}">${title}</h2>${lede ? html`<p class="sc-lede">${lede}</p>` : ''}${body}</section>`
? html`<section class="sc-sec" aria-labelledby="h-${id}"><h2 id="h-${id}">${title}</h2>${lede ? html`<p class="sc-lede">${lede}</p>` : ''}${body}</section>`
: '');
: '');
-/**
- * The largest cities as a rail of cards (it scrolls sideways and snaps; wide screens wrap it, home.css): the name,
- * its state, the headline number in the card's corner and the rest on hover. `pick(c)` → { corner, peek };
- * `href(c)` is where the card goes (the city page by default).
- */
-function cityRail({ id, title, more, list, pick, href = (c) => c.path }) {
- return raw(kit.rail({
- id, title, more, size: 's', wrap: true,
- items: (list || []).map((c) => {
- const p = pick(c);
- return kit.card({ href: href(c), title: c.name, type: 'map', ratio: '4/3', badge: c.state, corner: p.corner || 'not published', peek: p.peek || '', share: false });
- }),
- }));
-}
-
/**
/**
* The largest cities as bars, the dearest first (2026-11-01): one look compares them, where a rail of cards drew the
* The largest cities as bars, the dearest first (2026-11-01): one look compares them, where a rail of cards drew the
* same map icon twelve times. Each row is a link to its city: the name and state, a bar as long as its rent against
* same map icon twelve times. Each row is a link to its city: the name and state, a bar as long as its rent against
@@ -84,4 +69,4 @@ const doors = (list) => html`<p class="home-doors">${raw(list.slice(0, 2).map((d
@@ -84,4 +69,4 @@ const doors = (list) => html`<p class="home-doors">${raw(list.slice(0, 2).map((d
/** Cities with at least `min` people, largest first. */
/** Cities with at least `min` people, largest first. */
const bigCities = (min) => cities.cities.filter((c) => c.population >= min).sort((a, b) => b.population - a.population);
const bigCities = (min) => cities.cities.filter((c) => c.population >= min).sort((a, b) => b.population - a.population);
-module.exports = { STYLES, stats, section, cityRail, cityBars, fold, ranked, stateChips, siteList, faq, search, doors, bigCities };
+module.exports = { STYLES, stats, section, cityBars, fold, ranked, stateChips, siteList, faq, search, doors, bigCities };
@@ -84,7 +84,9 @@ const housingFetchImpl = async (url) => (new URL(String(url)).pathname.endsWith(
@@ -84,7 +84,9 @@ const housingFetchImpl = async (url) => (new URL(String(url)).pathname.endsWith(
// The same front-page shape as Rent's: the search first, the cities as a rail of cards with All N, the parts
// The same front-page shape as Rent's: the search first, the cities as a rail of cards with All N, the parts
// read once folded, the site's places as a launcher.
// read once folded, the site's places as a launcher.
assert.ok(r.text.includes('<form class="ovk-find" method="get" action="/search" role="search">'), 'the search is the kit\'s pill');
assert.ok(r.text.includes('<form class="ovk-find" method="get" action="/search" role="search">'), 'the search is the kit\'s pill');
- assert.ok(/<section class="ovk-rail[^"]*" id="cities"/.test(r.text) && /<a class="ovk-rail-more" href="\/in">All \d+ cities/.test(r.text), 'the cities are a rail with All N');
+ // Bars, dearest first, as on Rent's home (2026-11-01): a rail of cards drew the same map icon for every city.
+ assert.ok(/<ol class="home-bars" aria-labelledby="h-cities">/.test(r.text) && /<a class="home-more" href="\/in">All \d+ cities/.test(r.text), 'the cities are bars with All N');
+ assert.ok(!/ovk-card--map/.test(r.text), 'no placeholder map cards');
assert.ok(/<details class="ovk-fold" id="range">/.test(r.text) && /<details class="ovk-fold" id="states">/.test(r.text) && /<details class="ovk-fold" id="sites">/.test(r.text), 'the ranks, the states and the sites are folds');
assert.ok(/<details class="ovk-fold" id="range">/.test(r.text) && /<details class="ovk-fold" id="states">/.test(r.text) && /<details class="ovk-fold" id="sites">/.test(r.text), 'the ranks, the states and the sites are folds');
assert.ok(/<section class="sc-sec sc-apps-sec" id="places"/.test(r.text) && r.text.includes('href="/records"') && r.text.includes('href="/mortgage"'), 'the places as a launcher');
assert.ok(/<section class="sc-sec sc-apps-sec" id="places"/.test(r.text) && r.text.includes('href="/records"') && r.text.includes('href="/mortgage"'), 'the places as a launcher');
assert.ok(r.text.includes('Questions people ask') && /"@type":"FAQPage"/.test(r.text) && /"@type":"ItemList"/.test(r.text), 'the questions shown are the FAQPage; the cities are an ItemList');
assert.ok(r.text.includes('Questions people ask') && /"@type":"FAQPage"/.test(r.text) && /"@type":"ItemList"/.test(r.text), 'the questions shown are the FAQPage; the cities are an ItemList');