lgtm
This commit is contained in:
parent
8708bf000d
commit
11711c57e6
38 changed files with 5361 additions and 265 deletions
|
|
@ -301,24 +301,22 @@ export default function HomePage({
|
|||
{t('home.seeTheDifference')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 sm:gap-x-12 gap-y-4 pt-3 border-t border-white/10">
|
||||
<div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-white">
|
||||
<div className="home-hero-stats flex flex-wrap pt-3 border-t border-white/10">
|
||||
<div className="home-hero-stat">
|
||||
<div className="home-hero-stat-value">
|
||||
<TickerValue text="13M" active={statsActive} />
|
||||
</div>
|
||||
<div className="text-sm text-warm-200">{t('home.statProperties')}</div>
|
||||
<div className="home-hero-stat-label">{t('home.statProperties')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-white">
|
||||
<div className="home-hero-stat">
|
||||
<div className="home-hero-stat-value">
|
||||
<TickerValue text="56" active={statsActive} />
|
||||
</div>
|
||||
<div className="text-sm text-warm-200">{t('home.statFilters')}</div>
|
||||
<div className="home-hero-stat-label">{t('home.statFilters')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-white">
|
||||
{t('home.statEvery')}
|
||||
</div>
|
||||
<div className="text-sm text-warm-200">{t('home.statPostcodeInEngland')}</div>
|
||||
<div className="home-hero-stat">
|
||||
<div className="home-hero-stat-value">{t('home.statEvery')}</div>
|
||||
<div className="home-hero-stat-label">{t('home.statPostcodeInEngland')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -840,7 +840,7 @@ function ScoutScreen({ isActive }: { isActive: boolean }) {
|
|||
<div className="scout-export-action relative cursor-default select-none overflow-hidden rounded-lg border border-teal-300 bg-teal-600 p-2 text-white shadow-lg shadow-teal-900/20 dark:border-teal-500 dark:bg-teal-500 dark:text-navy-950 sm:p-4">
|
||||
<span className="scout-export-ripple" aria-hidden="true" />
|
||||
<div className="relative flex items-center gap-2 sm:gap-3">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-white/15 text-white dark:bg-navy-950/10 dark:text-navy-950 sm:h-10 sm:w-10">
|
||||
<span className="scout-export-icon flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-white/15 text-white dark:bg-navy-950/10 dark:text-navy-950 sm:h-10 sm:w-10">
|
||||
<DownloadIcon className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
|
|
@ -849,8 +849,8 @@ function ScoutScreen({ isActive }: { isActive: boolean }) {
|
|||
{t('home.showcaseDownloadXlsx')}
|
||||
</div>
|
||||
</div>
|
||||
<span className="scout-export-check ml-auto hidden h-6 w-6 shrink-0 items-center justify-center rounded-full bg-white text-teal-700 shadow-sm dark:bg-navy-950 dark:text-teal-300 sm:flex">
|
||||
<CheckIcon className="h-3.5 w-3.5" />
|
||||
<span className="scout-export-check absolute left-4 top-0 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-white text-teal-700 shadow-sm dark:bg-navy-950 dark:text-teal-300 sm:static sm:ml-auto sm:h-6 sm:w-6">
|
||||
<CheckIcon className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { FilterIcon } from '../ui/icons';
|
|||
import { CollapsibleGroupHeader } from '../ui/CollapsibleGroupHeader';
|
||||
import { EmptyState } from '../ui/EmptyState';
|
||||
import type { FeatureMeta } from '../../types';
|
||||
import { groupFeaturesByCategory, orderFilterGroups } from '../../lib/features';
|
||||
import { groupFeaturesByCategory } from '../../lib/features';
|
||||
import { FeatureInfoPopup } from '../ui/FeatureInfoPopup';
|
||||
import { FeatureActions } from '../ui/FeatureIcons';
|
||||
import { FeatureLabel } from '../ui/FeatureLabel';
|
||||
|
|
@ -73,7 +73,7 @@ export default function FeatureBrowser({
|
|||
);
|
||||
}, [availableFeatures, search]);
|
||||
|
||||
const grouped = useMemo(() => orderFilterGroups(groupFeaturesByCategory(filtered)), [filtered]);
|
||||
const grouped = useMemo(() => groupFeaturesByCategory(filtered), [filtered]);
|
||||
|
||||
// When searching, expand all groups so results are visible
|
||||
const isSearching = search.length > 0;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
import {
|
||||
POI_FILTER_NAMES,
|
||||
POI_DISTANCE_FILTER_NAME,
|
||||
TRANSPORT_DISTANCE_FILTER_NAME,
|
||||
POI_COUNT_2KM_FILTER_NAME,
|
||||
POI_COUNT_5KM_FILTER_NAME,
|
||||
getDefaultPoiDistanceFeatureName,
|
||||
|
|
@ -170,6 +171,10 @@ export default memo(function Filters({
|
|||
() => getDefaultPoiDistanceFeatureName(features),
|
||||
[features]
|
||||
);
|
||||
const defaultTransportDistanceFeatureName = useMemo(
|
||||
() => getDefaultPoiFilterFeatureName(features, TRANSPORT_DISTANCE_FILTER_NAME),
|
||||
[features]
|
||||
);
|
||||
const defaultPoiCount2KmFeatureName = useMemo(
|
||||
() => getDefaultPoiFilterFeatureName(features, POI_COUNT_2KM_FILTER_NAME),
|
||||
[features]
|
||||
|
|
@ -179,6 +184,10 @@ export default memo(function Filters({
|
|||
[features]
|
||||
);
|
||||
const poiDistanceMeta = useMemo(() => getPoiDistanceFilterMeta(features), [features]);
|
||||
const transportDistanceMeta = useMemo(
|
||||
() => getPoiFilterMeta(features, TRANSPORT_DISTANCE_FILTER_NAME),
|
||||
[features]
|
||||
);
|
||||
const poiCount2KmMeta = useMemo(
|
||||
() => getPoiFilterMeta(features, POI_COUNT_2KM_FILTER_NAME),
|
||||
[features]
|
||||
|
|
@ -190,18 +199,25 @@ export default memo(function Filters({
|
|||
const poiFilterMetas = useMemo(
|
||||
() => ({
|
||||
[POI_DISTANCE_FILTER_NAME]: poiDistanceMeta,
|
||||
[TRANSPORT_DISTANCE_FILTER_NAME]: transportDistanceMeta,
|
||||
[POI_COUNT_2KM_FILTER_NAME]: poiCount2KmMeta,
|
||||
[POI_COUNT_5KM_FILTER_NAME]: poiCount5KmMeta,
|
||||
}),
|
||||
[poiDistanceMeta, poiCount2KmMeta, poiCount5KmMeta]
|
||||
[poiDistanceMeta, transportDistanceMeta, poiCount2KmMeta, poiCount5KmMeta]
|
||||
);
|
||||
const defaultPoiFilterFeatureNames = useMemo(
|
||||
() => ({
|
||||
[POI_DISTANCE_FILTER_NAME]: defaultPoiDistanceFeatureName,
|
||||
[TRANSPORT_DISTANCE_FILTER_NAME]: defaultTransportDistanceFeatureName,
|
||||
[POI_COUNT_2KM_FILTER_NAME]: defaultPoiCount2KmFeatureName,
|
||||
[POI_COUNT_5KM_FILTER_NAME]: defaultPoiCount5KmFeatureName,
|
||||
}),
|
||||
[defaultPoiDistanceFeatureName, defaultPoiCount2KmFeatureName, defaultPoiCount5KmFeatureName]
|
||||
[
|
||||
defaultPoiDistanceFeatureName,
|
||||
defaultTransportDistanceFeatureName,
|
||||
defaultPoiCount2KmFeatureName,
|
||||
defaultPoiCount5KmFeatureName,
|
||||
]
|
||||
);
|
||||
const schoolFilterItems = useMemo(() => {
|
||||
return Object.keys(filters)
|
||||
|
|
@ -256,7 +272,11 @@ export default memo(function Filters({
|
|||
const backendFeature = backendName
|
||||
? features.find((feature) => feature.name === backendName)
|
||||
: undefined;
|
||||
return { ...(backendFeature ?? poiFilterMetas[filterName]), name, group: 'Amenities' };
|
||||
return {
|
||||
...(backendFeature ?? poiFilterMetas[filterName]),
|
||||
name,
|
||||
group: poiFilterMetas[filterName].group,
|
||||
};
|
||||
});
|
||||
}, [filters, features, poiFilterMetas]);
|
||||
const availableFeatures = useMemo(() => {
|
||||
|
|
@ -266,8 +286,21 @@ export default memo(function Filters({
|
|||
let insertedElectionVoteShareFilter = false;
|
||||
let insertedEthnicityFilter = false;
|
||||
const insertedPoiFilters = new Set<PoiFilterName>();
|
||||
const maybeInsertPoiFilter = (filterName: PoiFilterName | null) => {
|
||||
if (
|
||||
filterName &&
|
||||
defaultPoiFilterFeatureNames[filterName] &&
|
||||
!insertedPoiFilters.has(filterName)
|
||||
) {
|
||||
result.push(poiFilterMetas[filterName]);
|
||||
insertedPoiFilters.add(filterName);
|
||||
}
|
||||
};
|
||||
|
||||
for (const feature of features) {
|
||||
if (feature.group === 'Transport') {
|
||||
maybeInsertPoiFilter(TRANSPORT_DISTANCE_FILTER_NAME);
|
||||
}
|
||||
if (isSchoolFilterName(feature.name)) {
|
||||
if (defaultSchoolFeatureName && !insertedSchoolFilter) {
|
||||
result.push(schoolMeta);
|
||||
|
|
@ -297,15 +330,7 @@ export default memo(function Filters({
|
|||
continue;
|
||||
}
|
||||
if (isPoiFilterFeatureName(feature.name)) {
|
||||
const filterName = getPoiFilterName(feature.name);
|
||||
if (
|
||||
filterName &&
|
||||
defaultPoiFilterFeatureNames[filterName] &&
|
||||
!insertedPoiFilters.has(filterName)
|
||||
) {
|
||||
result.push(poiFilterMetas[filterName]);
|
||||
insertedPoiFilters.add(filterName);
|
||||
}
|
||||
maybeInsertPoiFilter(getPoiFilterName(feature.name));
|
||||
continue;
|
||||
}
|
||||
if (!enabledFeatures.has(feature.name)) result.push(feature);
|
||||
|
|
@ -332,9 +357,19 @@ export default memo(function Filters({
|
|||
let insertedSpecificCrimeFilters = false;
|
||||
let insertedElectionVoteShareFilters = false;
|
||||
let insertedEthnicityFilters = false;
|
||||
let insertedPoiDistanceFilters = false;
|
||||
const insertedPoiFilters = new Set<PoiFilterName>();
|
||||
const insertPoiFilterItems = (filterName: PoiFilterName | null) => {
|
||||
if (!filterName || insertedPoiFilters.has(filterName)) return;
|
||||
result.push(
|
||||
...poiDistanceFilterItems.filter((item) => getPoiFilterName(item.name) === filterName)
|
||||
);
|
||||
insertedPoiFilters.add(filterName);
|
||||
};
|
||||
|
||||
for (const feature of features) {
|
||||
if (feature.group === 'Transport') {
|
||||
insertPoiFilterItems(TRANSPORT_DISTANCE_FILTER_NAME);
|
||||
}
|
||||
if (isSchoolFilterName(feature.name)) {
|
||||
if (!insertedSchoolFilter) {
|
||||
result.push(...schoolFilterItems);
|
||||
|
|
@ -364,10 +399,7 @@ export default memo(function Filters({
|
|||
continue;
|
||||
}
|
||||
if (isPoiFilterFeatureName(feature.name)) {
|
||||
if (!insertedPoiDistanceFilters) {
|
||||
result.push(...poiDistanceFilterItems);
|
||||
insertedPoiDistanceFilters = true;
|
||||
}
|
||||
insertPoiFilterItems(getPoiFilterName(feature.name));
|
||||
continue;
|
||||
}
|
||||
if (enabledFeatures.has(feature.name)) result.push(feature);
|
||||
|
|
@ -583,6 +615,7 @@ export default memo(function Filters({
|
|||
electionVoteShareMeta,
|
||||
ethnicityMeta,
|
||||
poiDistanceMeta,
|
||||
transportDistanceMeta,
|
||||
poiCount2KmMeta,
|
||||
poiCount5KmMeta,
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { SCHOOL_FILTER_NAME, getSchoolBackendFeatureName } from '../../lib/schoo
|
|||
import { getSpecificCrimeFeatureName } from '../../lib/crime-filter';
|
||||
import { getElectionVoteShareFeatureName } from '../../lib/election-filter';
|
||||
import { getEthnicityFeatureName } from '../../lib/ethnicity-filter';
|
||||
import { POI_DISTANCE_FILTER_NAME, getPoiDistanceFeatureName } from '../../lib/poi-distance-filter';
|
||||
import {
|
||||
POI_DISTANCE_FILTER_NAME,
|
||||
getPoiDistanceFeatureName,
|
||||
getPoiFilterName,
|
||||
} from '../../lib/poi-distance-filter';
|
||||
|
||||
interface HoverCardData {
|
||||
count: number;
|
||||
|
|
@ -69,7 +73,7 @@ export default memo(function HoverCard({
|
|||
name: schoolBackendName
|
||||
? SCHOOL_FILTER_NAME
|
||||
: poiDistanceFeatureName
|
||||
? POI_DISTANCE_FILTER_NAME
|
||||
? (getPoiFilterName(name) ?? POI_DISTANCE_FILTER_NAME)
|
||||
: backendName,
|
||||
value: formatValue(val, meta),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ describe('MobileBottomSheet keyboard avoidance', () => {
|
|||
it('reports covered height while the drawer is being dragged', async () => {
|
||||
installViewport({ innerHeight: 800, visualHeight: 800 });
|
||||
const { coveredHeights, sheet } = renderSheet();
|
||||
const handle = sheet.firstElementChild;
|
||||
const handle = sheet.firstElementChild?.firstElementChild;
|
||||
|
||||
if (!(handle instanceof HTMLElement)) throw new Error('Expected bottom sheet drag handle');
|
||||
|
||||
|
|
|
|||
|
|
@ -228,14 +228,18 @@ export default function MobileBottomSheet({
|
|||
: 'height 140ms ease, bottom 180ms ease',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="shrink-0 touch-none px-4 py-2"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
<div className="w-full flex items-center justify-center" role="presentation">
|
||||
<div className="relative shrink-0 px-4 py-2">
|
||||
<div
|
||||
className="absolute inset-x-0 top-1/2 z-10 h-11 -translate-y-1/2 touch-none"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none flex w-full items-center justify-center"
|
||||
role="presentation"
|
||||
>
|
||||
<span className="h-1.5 w-12 rounded-full bg-warm-300 dark:bg-navy-600" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export function ElectionVoteShareFilterCard({
|
|||
return (
|
||||
<div
|
||||
data-filter-name={ELECTION_VOTE_SHARE_FILTER_NAME}
|
||||
className={`space-y-2 rounded-lg border border-warm-200 bg-white px-2 py-2 shadow-sm dark:border-warm-700 dark:bg-warm-800 ${
|
||||
className={`space-y-1.5 px-2 py-1.5 rounded ${
|
||||
isActive
|
||||
? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30'
|
||||
: isPinned
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export function EthnicityFilterCard({
|
|||
return (
|
||||
<div
|
||||
data-filter-name={ETHNICITIES_FILTER_NAME}
|
||||
className={`space-y-2 rounded-lg border border-warm-200 bg-white px-2 py-2 shadow-sm dark:border-warm-700 dark:bg-warm-800 ${
|
||||
className={`space-y-1.5 px-2 py-1.5 rounded ${
|
||||
isActive
|
||||
? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30'
|
||||
: isPinned
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { getFeatureIcon } from '../../../lib/feature-icons';
|
|||
import { getGroupIcon } from '../../../lib/group-icons';
|
||||
import {
|
||||
POI_DISTANCE_FILTER_NAME,
|
||||
TRANSPORT_DISTANCE_FILTER_NAME,
|
||||
clampPoiFilterRange,
|
||||
getDefaultPoiFilterFeatureName,
|
||||
getPoiDistanceFeatureName,
|
||||
|
|
@ -119,7 +120,7 @@ export function PoiDistanceFilterCard({
|
|||
return (
|
||||
<div
|
||||
data-filter-name={filterName}
|
||||
className={`space-y-2 rounded-lg border border-warm-200 bg-white px-2 py-2 shadow-sm dark:border-warm-700 dark:bg-warm-800 ${
|
||||
className={`space-y-1.5 px-2 py-1.5 rounded ${
|
||||
isActive
|
||||
? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30'
|
||||
: isPinned
|
||||
|
|
@ -192,6 +193,10 @@ export function PoiDistanceFilterCard({
|
|||
isAtMax={isAtMax}
|
||||
raw={selectedFeature.raw}
|
||||
feature={selectedFeature}
|
||||
showUnit={
|
||||
filterName === POI_DISTANCE_FILTER_NAME ||
|
||||
filterName === TRANSPORT_DISTANCE_FILTER_NAME
|
||||
}
|
||||
onValueChange={(v) =>
|
||||
onFilterChange(poiFeature.name, clampPoiFilterRange(v, selectedFeature))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export function SpecificCrimeFilterCard({
|
|||
return (
|
||||
<div
|
||||
data-filter-name={SPECIFIC_CRIMES_FILTER_NAME}
|
||||
className={`space-y-2 rounded-lg border border-warm-200 bg-white px-2 py-2 shadow-sm dark:border-warm-700 dark:bg-warm-800 ${
|
||||
className={`space-y-1.5 px-2 py-1.5 rounded ${
|
||||
isActive
|
||||
? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30'
|
||||
: isPinned
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function CollapsibleGroupHeader({
|
|||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`w-full flex items-center justify-between border-b border-warm-300 dark:border-warm-700 ${className}`}
|
||||
className={`w-full cursor-pointer flex items-center justify-between border-b border-warm-300 dark:border-warm-700 ${className}`}
|
||||
>
|
||||
<span>{ts(name)}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue