This commit is contained in:
Andras Schmelczer 2026-07-12 15:03:33 +01:00
parent 982e0cc89c
commit cfaf58dfba
44 changed files with 793 additions and 201 deletions

View file

@ -9,7 +9,7 @@ export const QUALIFICATIONS_FILTER_KEY_PREFIX = `${QUALIFICATIONS_FILTER_NAME}:`
* 100% per neighbourhood and render as a single stacked "Qualifications"
* composition in the area pane (see STACKED_GROUPS["Neighbours"] in consts.ts).
* In the filter browser they fold into one "Qualifications" filter whose
* dropdown selects a band including "% Degree or higher" rather than seven
* dropdown selects a band (including "% Degree or higher") rather than seven
* separate sliders.
*/
export const QUALIFICATION_FEATURE_NAMES = [

View file

@ -77,9 +77,9 @@ export const SEO_LANDING_PAGES: Record<SeoLandingKey, SeoLandingContent> = {
title: 'Compare property prices across every postcode in England',
metaTitle: 'Property price map for England - Compare postcodes before viewing',
metaDescription:
'Compare sold prices, estimated current value, price per square metre and local context across English postcodes before searching listings.',
'Compare sold prices, estimated current value, price per square metre and local context across English postcodes to find the underpriced ones.',
intro:
'Perfect Postcode maps sold prices, estimated current value, price per square metre, property type, floor area, tenure, and local context so buyers can find realistic search areas before opening listing portals.',
'Perfect Postcode maps sold prices, estimated current value, price per square metre, property type, floor area, tenure, and local context so buyers can rank postcodes by value and find the underpriced areas the market has overlooked.',
points: [
'Screen historical sale prices and current-value estimates by postcode.',
'Compare value with commute, schools, broadband, crime, noise, and amenities.',
@ -714,7 +714,7 @@ export const SEO_CONTENT_PAGES: Record<SeoContentKey, SeoContentPage> = {
sections: [
{
title: 'Your account stays separate from the public site',
body: 'The guides, methodology, and support pages are public. Everything tied to your account — your dashboard, saved searches, and invitations — is kept out of public view and out of search engines.',
body: 'The guides, methodology, and support pages are public. Everything tied to your account (your dashboard, saved searches, and invitations) is kept out of public view and out of search engines.',
},
{
title: 'Saved searches belong to your account',

View file

@ -8,8 +8,8 @@ export const TENURE_FILTER_KEY_PREFIX = `${TENURE_FILTER_NAME}:`;
* The Census 2021 housing tenure categories (TS054). They sum to 100% per
* neighbourhood and render as a single stacked "Tenure" composition in the area
* pane (see STACKED_GROUPS["Neighbours"] in consts.ts). In the filter browser
* they fold into one "Tenure" filter whose dropdown selects a category
* owner-occupied, social rent or private rent rather than three separate
* they fold into one "Tenure" filter whose dropdown selects a category
* (owner-occupied, social rent or private rent) rather than three separate
* sliders.
*/
export const TENURE_FEATURE_NAMES = [

View file

@ -11,6 +11,7 @@ import { createElectionVoteShareFilterKey } from './election-filter';
import { createEthnicityFilterKey } from './ethnicity-filter';
import { createQualificationFilterKey } from './qualification-filter';
import { createTenureFilterKey } from './tenure-filter';
import { createCouncilFilterKey } from './council-filter';
import {
POI_COUNT_2KM_FILTER_NAME,
TRANSPORT_DISTANCE_FILTER_NAME,
@ -285,6 +286,52 @@ describe('url-state', () => {
expect(parseUrlState().colorOpacity).toBe(0.1);
});
it('round-trips a non-default listings mode and defaults to "all"', () => {
// Default mode emits no param, so a fresh reload restores the default.
const defaultParams = stateToParams(
null,
{},
[],
new Set(),
'area',
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
'all'
);
expect(defaultParams.has('listings')).toBe(false);
const params = stateToParams(
null,
{},
[],
new Set(),
'area',
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
'new'
);
expect(params.get('listings')).toBe('new');
window.history.replaceState({}, '', `/?${params.toString()}`);
expect(parseUrlState().listings).toBe('new');
// A bare URL falls back to the default; an unknown value is ignored.
window.history.replaceState({}, '', '/');
expect(parseUrlState().listings).toBe('all');
window.history.replaceState({}, '', '/?listings=bogus');
expect(parseUrlState().listings).toBe('all');
});
it('round-trips a crime-type subset when the crime overlay is active', () => {
const params = stateToParams(
null,
@ -581,6 +628,63 @@ describe('url-state', () => {
});
});
it('round-trips council filters (incl. the shared social-rent column) via the council param', () => {
const ex = createCouncilFilterKey('% Ex-council', 1);
const current = createCouncilFilterKey('% Social rent', 2);
const params = stateToParams(
null,
{
[ex]: [10, 90],
[current]: [30, 100],
},
[],
new Set(),
'area'
);
expect(params.getAll('council')).toEqual(['% Ex-council:10:90', '% Social rent:30:100']);
expect(params.getAll('tenure')).toEqual([]);
expect(params.getAll('filter')).toEqual([]);
window.history.replaceState({}, '', `/?${params.toString()}`);
const state = parseUrlState();
expect(state.filters).toEqual({
[createCouncilFilterKey('% Ex-council', 0)]: [10, 90],
[createCouncilFilterKey('% Social rent', 1)]: [30, 100],
});
});
it('keeps a Tenure social-rent filter and a Council "current" filter as distinct params', () => {
// The shared "% Social rent" column must serialise under BOTH params without
// colliding: the prefixes keep the keys distinct on the way back in.
const tenureSocial = createTenureFilterKey('% Social rent', 0);
const councilCurrent = createCouncilFilterKey('% Social rent', 0);
const params = stateToParams(
null,
{
[tenureSocial]: [40, 100],
[councilCurrent]: [10, 50],
},
[],
new Set(),
'area'
);
expect(params.getAll('tenure')).toEqual(['% Social rent:40:100']);
expect(params.getAll('council')).toEqual(['% Social rent:10:50']);
window.history.replaceState({}, '', `/?${params.toString()}`);
const state = parseUrlState();
expect(state.filters).toEqual({
[createTenureFilterKey('% Social rent', 0)]: [40, 100],
[createCouncilFilterKey('% Social rent', 0)]: [10, 50],
});
});
it('round-trips repeated amenity distance filters with dedicated URL params', () => {
const park = createPoiDistanceFilterKey('Distance to nearest amenity (Park) (km)', 3);
const cafe = createPoiDistanceFilterKey('Distance to nearest amenity (Café) (km)', 4);