diff --git a/.gitignore b/.gitignore index 31f21e7..abfeff5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ frontend/public/assets/* !frontend/public/assets/poi-icons/** frontend/public/assets/.done server-rs/logs +video/auth.* diff --git a/frontend/public/video/recording.jpg b/frontend/public/video/recording.jpg new file mode 100644 index 0000000..a44e905 Binary files /dev/null and b/frontend/public/video/recording.jpg differ diff --git a/frontend/public/video/recording.mp4 b/frontend/public/video/recording.mp4 index a3c6321..c73241f 100644 Binary files a/frontend/public/video/recording.mp4 and b/frontend/public/video/recording.mp4 differ diff --git a/frontend/scripts/check-translations.mjs b/frontend/scripts/check-translations.mjs index 0ec2a1c..49638b9 100644 --- a/frontend/scripts/check-translations.mjs +++ b/frontend/scripts/check-translations.mjs @@ -76,7 +76,7 @@ const SAME_AS_EN_VALUE_ALLOWLIST = new Set([ ]); const FORBIDDEN_VISIBLE_STRINGS = [ - ['without this filter', 'filters.withoutThisFilter'], + ['without this filter', 'filters.filtersOut'], ['Connecting to server...', 'common.connectingToServer'], ['Property saved!', 'toasts.propertySaved'], ['View saved', 'toasts.viewSaved'], diff --git a/frontend/src/components/home/ProductShowcase.tsx b/frontend/src/components/home/ProductShowcase.tsx index 85691e5..b0d46ca 100644 --- a/frontend/src/components/home/ProductShowcase.tsx +++ b/frontend/src/components/home/ProductShowcase.tsx @@ -330,7 +330,7 @@ function FilterPreviewRow({ feature, value, rangeLabel, - withoutCount, + filteredOutCount, index, isTightened, onValueChange, @@ -338,7 +338,7 @@ function FilterPreviewRow({ feature: FeatureMeta; value: [number, number]; rangeLabel: string; - withoutCount: number; + filteredOutCount: number; index: number; isTightened: boolean; onValueChange: (value: [number, number]) => void; @@ -374,8 +374,8 @@ function FilterPreviewRow({ - {t('filters.withoutThisFilter', { - value: withoutCount.toLocaleString(), + {t('filters.filtersOut', { + value: filteredOutCount.toLocaleString(), })} @@ -413,7 +413,11 @@ function formatDemoRange(feature: FeatureMeta, value: [number, number], t: TFunc return `${value[0]} - ${value[1]}`; } -function randomWithoutCount(feature: FeatureMeta, value: [number, number], index: number): number { +function randomFilteredOutCount( + feature: FeatureMeta, + value: [number, number], + index: number +): number { const min = feature.min ?? 0; const max = feature.max ?? 100; const range = Math.max(max - min, 1); @@ -456,7 +460,7 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) { [285000, 610000], [0, 650000], ] as [number, number][], - without: [41820, 50622, 24860, 18645, 29796, 41820], + filteredOut: [41820, 50622, 24860, 18645, 29796, 41820], }, { feature: DEMO_FEATURES[3], @@ -465,7 +469,7 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) { [43, 52], [40, 58], ] as [number, number][], - without: [19412, 8706, 19412], + filteredOut: [19412, 8706, 19412], }, { feature: DEMO_FEATURES[4], @@ -474,7 +478,7 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) { [5, 25], [0, 60], ] as [number, number][], - without: [11209, 4118, 11209], + filteredOut: [11209, 4118, 11209], }, { feature: DEMO_FEATURES[2], @@ -483,34 +487,34 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) { [2, 6], [1, 8], ] as [number, number][], - without: [13608, 6944, 13608], + filteredOut: [13608, 6944, 13608], }, ], [] ); const [filterState, setFilterState] = useState(() => - rows.map((row) => ({ value: row.values[0], without: row.without[0] })) + rows.map((row) => ({ value: row.values[0], filteredOut: row.filteredOut[0] })) ); useEffect(() => { if (!isActive || hasUserAdjusted) return; let frame = 0; const start = window.performance.now(); - setFilterState(rows.map((row) => ({ value: row.values[0], without: row.without[0] }))); + setFilterState(rows.map((row) => ({ value: row.values[0], filteredOut: row.filteredOut[0] }))); const animate = (timestamp: number) => { const progress = ((timestamp - start) % FILTER_ANIMATION_MS) / FILTER_ANIMATION_MS; setFilterState( rows.map((row) => { const value = interpolateRangePath(row.values, progress); - const without = interpolateRangePath( - row.without.map((count) => [count, count]), + const filteredOut = interpolateRangePath( + row.filteredOut.map((count) => [count, count]), progress )[0]; return { value, - without: Math.round(without), + filteredOut: Math.round(filteredOut), }; }) ); @@ -528,7 +532,7 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) { itemIndex === index ? { value, - without: randomWithoutCount(rows[index].feature, value, index), + filteredOut: randomFilteredOutCount(rows[index].feature, value, index), } : item ) @@ -544,7 +548,7 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) { feature={row.feature} value={filterState[index].value} rangeLabel={formatDemoRange(row.feature, filterState[index].value, t)} - withoutCount={filterState[index].without} + filteredOutCount={filterState[index].filteredOut} index={index} isTightened onValueChange={(value) => updateFilter(index, value)} diff --git a/frontend/src/components/learn/LearnPage.tsx b/frontend/src/components/learn/LearnPage.tsx index fcd3762..d37f1b3 100644 --- a/frontend/src/components/learn/LearnPage.tsx +++ b/frontend/src/components/learn/LearnPage.tsx @@ -57,6 +57,11 @@ const DATA_SOURCE_DEFS: DataSourceDef[] = [ url: 'https://osdatahub.os.uk/downloads/open/OpenGreenspace', license: 'Open Government Licence v3.0', }, + { + id: 'forest-research-tow', + url: 'https://www.forestresearch.gov.uk/tools-and-resources/national-trees-outside-woodland-map/', + license: 'Open Government Licence v3.0', + }, { id: 'naptan', url: 'https://naptan.dft.gov.uk/naptan/schema/2.4/doc/NaPTANSchemaGuide-2.4-v0.57.pdf', @@ -121,6 +126,11 @@ const DS_KEYS: Record = { 'learnPage.dsGreenspaceOrigin', 'learnPage.dsGreenspaceUse', ], + 'forest-research-tow': [ + 'learnPage.dsTowName', + 'learnPage.dsTowOrigin', + 'learnPage.dsTowUse', + ], naptan: ['learnPage.dsNaptanName', 'learnPage.dsNaptanOrigin', 'learnPage.dsNaptanUse'], noise: ['learnPage.dsNoiseName', 'learnPage.dsNoiseOrigin', 'learnPage.dsNoiseUse'], ofsted: ['learnPage.dsOfstedName', 'learnPage.dsOfstedOrigin', 'learnPage.dsOfstedUse'], diff --git a/frontend/src/components/map/FeatureBrowser.tsx b/frontend/src/components/map/FeatureBrowser.tsx index e2d148e..7d70b21 100644 --- a/frontend/src/components/map/FeatureBrowser.tsx +++ b/frontend/src/components/map/FeatureBrowser.tsx @@ -6,8 +6,8 @@ import { SearchInput } from '../ui/SearchInput'; import { FilterIcon } from '../ui/icons'; import { CollapsibleGroupHeader } from '../ui/CollapsibleGroupHeader'; import { EmptyState } from '../ui/EmptyState'; -import type { FeatureGroup, FeatureMeta } from '../../types'; -import { groupFeaturesByCategory } from '../../lib/features'; +import type { FeatureMeta } from '../../types'; +import { groupFeaturesByCategory, orderFilterGroups } from '../../lib/features'; import { FeatureInfoPopup } from '../ui/FeatureInfoPopup'; import { FeatureActions } from '../ui/FeatureIcons'; import { FeatureLabel } from '../ui/FeatureLabel'; @@ -35,16 +35,6 @@ interface FeatureBrowserProps { onAddTravelTimeEntry: (mode: TransportMode) => void; } -function moveTransportFirst(groups: FeatureGroup[]): FeatureGroup[] { - const transportIdx = groups.findIndex((group) => group.name === 'Transport'); - if (transportIdx <= 0) return groups; - return [ - groups[transportIdx], - ...groups.slice(0, transportIdx), - ...groups.slice(transportIdx + 1), - ]; -} - export default function FeatureBrowser({ availableFeatures, allFeatures, @@ -83,7 +73,7 @@ export default function FeatureBrowser({ ); }, [availableFeatures, search]); - const grouped = useMemo(() => moveTransportFirst(groupFeaturesByCategory(filtered)), [filtered]); + const grouped = useMemo(() => orderFilterGroups(groupFeaturesByCategory(filtered)), [filtered]); // When searching, expand all groups so results are visible const isSearching = search.length > 0; diff --git a/frontend/src/components/map/Filters.tsx b/frontend/src/components/map/Filters.tsx index 7c3a0ca..7728865 100644 --- a/frontend/src/components/map/Filters.tsx +++ b/frontend/src/components/map/Filters.tsx @@ -256,7 +256,7 @@ export default memo(function Filters({ const backendFeature = backendName ? features.find((feature) => feature.name === backendName) : undefined; - return { ...(backendFeature ?? poiFilterMetas[filterName]), name, group: 'Nearby POIs' }; + return { ...(backendFeature ?? poiFilterMetas[filterName]), name, group: 'Amenities' }; }); }, [filters, features, poiFilterMetas]); const availableFeatures = useMemo(() => { diff --git a/frontend/src/components/map/TravelTimeCard.tsx b/frontend/src/components/map/TravelTimeCard.tsx index 6a4d8a2..ad5b118 100644 --- a/frontend/src/components/map/TravelTimeCard.tsx +++ b/frontend/src/components/map/TravelTimeCard.tsx @@ -166,7 +166,7 @@ export function TravelTimeCard({ {filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)} diff --git a/frontend/src/components/map/filters/ActiveFiltersPanel.tsx b/frontend/src/components/map/filters/ActiveFiltersPanel.tsx index 8d84464..99b0b15 100644 --- a/frontend/src/components/map/filters/ActiveFiltersPanel.tsx +++ b/frontend/src/components/map/filters/ActiveFiltersPanel.tsx @@ -148,10 +148,7 @@ export function ActiveFiltersPanel({ {!collapsed && ( -
+
{filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)}
diff --git a/frontend/src/components/map/filters/EnumFeatureFilterCard.tsx b/frontend/src/components/map/filters/EnumFeatureFilterCard.tsx index 66f21ee..4654953 100644 --- a/frontend/src/components/map/filters/EnumFeatureFilterCard.tsx +++ b/frontend/src/components/map/filters/EnumFeatureFilterCard.tsx @@ -65,7 +65,7 @@ export function EnumFeatureFilterCard({ {filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)}
diff --git a/frontend/src/components/map/filters/EthnicityFilterCard.tsx b/frontend/src/components/map/filters/EthnicityFilterCard.tsx index ca25f6a..231f0ff 100644 --- a/frontend/src/components/map/filters/EthnicityFilterCard.tsx +++ b/frontend/src/components/map/filters/EthnicityFilterCard.tsx @@ -215,7 +215,7 @@ export function EthnicityFilterCard({ /> {filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)} diff --git a/frontend/src/components/map/filters/NumericFeatureFilterCard.tsx b/frontend/src/components/map/filters/NumericFeatureFilterCard.tsx index 8021685..7a31a9e 100644 --- a/frontend/src/components/map/filters/NumericFeatureFilterCard.tsx +++ b/frontend/src/components/map/filters/NumericFeatureFilterCard.tsx @@ -130,7 +130,7 @@ export function NumericFeatureFilterCard({ /> {filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)} diff --git a/frontend/src/components/map/filters/PoiDistanceFilterCard.tsx b/frontend/src/components/map/filters/PoiDistanceFilterCard.tsx index 64e0861..3fd45ef 100644 --- a/frontend/src/components/map/filters/PoiDistanceFilterCard.tsx +++ b/frontend/src/components/map/filters/PoiDistanceFilterCard.tsx @@ -198,7 +198,7 @@ export function PoiDistanceFilterCard({ /> {filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)} diff --git a/frontend/src/components/map/filters/PoiTypeDropdown.tsx b/frontend/src/components/map/filters/PoiTypeDropdown.tsx index e32b206..866a967 100644 --- a/frontend/src/components/map/filters/PoiTypeDropdown.tsx +++ b/frontend/src/components/map/filters/PoiTypeDropdown.tsx @@ -10,6 +10,9 @@ import { getGroupIcon } from '../../../lib/group-icons'; import { getPoiFeatureCategory } from '../../../lib/poi-distance-filter'; import type { FeatureMeta } from '../../../types'; +const DROPDOWN_MAX_HEIGHT = 256; +const FALLBACK_DROPDOWN_MAX_HEIGHT = 'min(16rem, 45dvh)'; + interface PoiTypeDropdownProps { options: FeatureMeta[]; value: string; @@ -108,11 +111,15 @@ export function PoiTypeDropdown({ options, value, onChange }: PoiTypeDropdownPro requestAnimationFrame(() => inputRef.current?.focus({ preventScroll: true })); }; + const dropdownStyle = pos + ? { ...dropdownPositionStyle(pos), maxHeight: Math.min(pos.maxHeight, DROPDOWN_MAX_HEIGHT) } + : { maxHeight: FALLBACK_DROPDOWN_MAX_HEIGHT }; + const dropdown = open && (
@@ -183,11 +190,8 @@ export function PoiTypeDropdown({ options, value, onChange }: PoiTypeDropdownPro onClick={() => (open ? setOpen(false) : handleOpen())} className="flex w-full items-center gap-1.5 rounded-md border border-warm-200 bg-warm-50 px-2 py-1.5 pr-8 text-left text-sm font-medium text-navy-950 shadow-inner outline-none transition-colors hover:bg-white focus:border-teal-400 focus:ring-2 focus:ring-teal-200 dark:border-warm-700 dark:bg-navy-900 dark:text-warm-100 dark:hover:bg-navy-800 dark:focus:ring-teal-900/50" > - {selected && - optionIcon(selected, 'h-4 w-4 shrink-0 text-teal-600 dark:text-teal-400')} - - {selected ? optionLabel(selected) : ''} - + {selected && optionIcon(selected, 'h-4 w-4 shrink-0 text-teal-600 dark:text-teal-400')} + {selected ? optionLabel(selected) : ''} {filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)}
diff --git a/frontend/src/components/map/filters/SpecificCrimeFilterCard.tsx b/frontend/src/components/map/filters/SpecificCrimeFilterCard.tsx index 7eaa6ab..f9cdc06 100644 --- a/frontend/src/components/map/filters/SpecificCrimeFilterCard.tsx +++ b/frontend/src/components/map/filters/SpecificCrimeFilterCard.tsx @@ -215,7 +215,7 @@ export function SpecificCrimeFilterCard({ /> {filterImpact != null && filterImpact > 0 && (

- {t('filters.withoutThisFilter', { value: formatNumber(filterImpact) })} + {t('filters.filtersOut', { value: formatNumber(filterImpact) })}

)}
diff --git a/frontend/src/components/ui/DestinationDropdown.tsx b/frontend/src/components/ui/DestinationDropdown.tsx index 962f8a4..e21cb48 100644 --- a/frontend/src/components/ui/DestinationDropdown.tsx +++ b/frontend/src/components/ui/DestinationDropdown.tsx @@ -7,6 +7,9 @@ import { MapPinIcon } from './icons/MapPinIcon'; import { ChevronIcon } from './icons/ChevronIcon'; import { CloseIcon } from './icons/CloseIcon'; +const DROPDOWN_MAX_HEIGHT = 256; +const FALLBACK_DROPDOWN_MAX_HEIGHT = 'min(16rem, 45dvh)'; + interface DestinationDropdownProps { destinations: Destination[]; loading: boolean; @@ -105,11 +108,15 @@ export function DestinationDropdown({ requestAnimationFrame(() => inputRef.current?.focus({ preventScroll: true })); }, []); + const dropdownStyle = pos + ? { ...dropdownPositionStyle(pos), maxHeight: Math.min(pos.maxHeight, DROPDOWN_MAX_HEIGHT) } + : { maxHeight: FALLBACK_DROPDOWN_MAX_HEIGHT }; + const dropdown = open && (
+ ); @@ -13,7 +22,16 @@ function Digit({ char, delay, active }: { char: string; delay: number; active: b const offset = active ? -idx * H : 0; return ( - +