Translate pages

This commit is contained in:
Andras Schmelczer 2026-04-04 09:47:18 +01:00
parent a7aaf5effa
commit 96402228e3
49 changed files with 1458 additions and 926 deletions

View file

@ -50,27 +50,31 @@ function buildSummary(
travelTimeFilters: AiTravelTimeFilter[],
matchCount: number
): string {
const i18n = require('../i18n').default as { t: (key: string, opts?: Record<string, unknown>) => string };
const { ts } = require('../i18n/server') as { ts: (v: string) => string };
const parts: string[] = [];
for (const [name, value] of Object.entries(filters)) {
// Skip Listing status — shown via the mode selector UI
if (name === 'Listing status') continue;
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'number') {
parts.push(name);
parts.push(ts(name));
} else if (Array.isArray(value)) {
parts.push(`${name}: ${(value as string[]).join(', ')}`);
parts.push(`${ts(name)}: ${(value as string[]).map((v) => ts(v)).join(', ')}`);
}
}
for (const tt of travelTimeFilters) {
const bounds =
tt.max !== undefined ? `< ${tt.max} min` : tt.min !== undefined ? `> ${tt.min} min` : '';
parts.push(`${tt.mode} to ${tt.label} ${bounds}`.trim());
tt.max !== undefined
? i18n.t('format.lessThanMin', { max: tt.max })
: tt.min !== undefined
? i18n.t('format.moreThanMin', { min: tt.min })
: '';
parts.push(i18n.t('format.toDestination', { mode: tt.mode, label: tt.label, bounds }).trim());
}
if (parts.length === 0) return 'No filters set';
const countStr = matchCount.toLocaleString();
return `${countStr} properties match · Set ${parts.length} filter${parts.length > 1 ? 's' : ''}: ${parts.join(', ')}`;
if (parts.length === 0) return i18n.t('format.noFiltersSet');
return `${i18n.t('format.propertiesMatch', { count: matchCount.toLocaleString() })} \u00B7 ${i18n.t('format.setFilters', { count: parts.length, list: parts.join(', ') })}`;
}
export function useAiFilters(): UseAiFiltersResult {

View file

@ -1,5 +1,6 @@
import { useState, useCallback, useMemo } from 'react';
import type { ComponentType } from 'react';
import { useTranslation } from 'react-i18next';
import { CarIcon, BicycleIcon, WalkingIcon, TransitIcon } from '../components/ui/icons';
export type TransportMode = 'car' | 'bicycle' | 'walking' | 'transit';
@ -27,6 +28,24 @@ export const MODE_ICONS: Record<TransportMode, ComponentType<{ className?: strin
transit: TransitIcon,
};
/**
* Hook returning translated mode labels and descriptions.
*/
export function useTranslatedModes() {
const { t } = useTranslation();
const label = useCallback(
(mode: TransportMode): string =>
({ car: t('travel.modeCar'), bicycle: t('travel.modeBicycle'), walking: t('travel.modeWalking'), transit: t('travel.modeTransit') })[mode],
[t]
);
const desc = useCallback(
(mode: TransportMode): string =>
({ car: t('travel.modeCarDesc'), bicycle: t('travel.modeBicycleDesc'), walking: t('travel.modeWalkingDesc'), transit: t('travel.modeTransitDesc') })[mode],
[t]
);
return { label, desc };
}
export interface TravelTimeEntry {
mode: TransportMode;
slug: string;

View file

@ -1,65 +1,22 @@
import { useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { Step, CallBackProps } from 'react-joyride';
import { ACTIONS, EVENTS, STATUS } from 'react-joyride';
const STORAGE_KEY = 'tutorial_completed';
const STEPS: Step[] = [
{
target: '[data-tutorial="filters"]',
title: 'Tell the map what matters',
content:
'Set your budget, commute limit, school quality, crime threshold. Whatever matters to you. Only areas that qualify stay lit. Use the eye icon to colour by any feature.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-tutorial="ai-filters"]',
title: 'Or just describe it',
content:
'Type what you want in plain English, like "quiet area near good schools under \u00A3400k", and we\u2019ll set up the filters for you.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-tutorial="map"]',
title: 'Explore what\u2019s out there',
content:
'Pan and zoom across England. Click any coloured area to see crime, schools, prices, broadband, noise, and more about that neighbourhood.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tutorial="search"]',
title: 'Jump to a location',
content: 'Search for any place or postcode to fly straight there.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tutorial="right-pane"]',
title: 'Dig into the details',
content:
'See area statistics, histograms, and individual property records: prices, floor area, energy ratings, and more.',
placement: 'left',
disableBeacon: true,
},
{
target: '[data-tutorial="poi-button"]',
title: 'What\u2019s nearby?',
content:
'Toggle schools, shops, stations, parks, and restaurants on the map to see what\u2019s within reach.',
placement: 'left',
disableBeacon: true,
styles: {
tooltip: {
transform: 'translateY(-50px)',
},
},
},
];
export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked = false) {
const { t } = useTranslation();
const steps: Step[] = useMemo(() => [
{ target: '[data-tutorial="filters"]', title: t('tutorial.step1Title'), content: t('tutorial.step1Content'), placement: 'right' as const, disableBeacon: true },
{ target: '[data-tutorial="ai-filters"]', title: t('tutorial.step2Title'), content: t('tutorial.step2Content'), placement: 'right' as const, disableBeacon: true },
{ target: '[data-tutorial="map"]', title: t('tutorial.step3Title'), content: t('tutorial.step3Content'), placement: 'bottom' as const, disableBeacon: true },
{ target: '[data-tutorial="search"]', title: t('tutorial.step4Title'), content: t('tutorial.step4Content'), placement: 'bottom' as const, disableBeacon: true },
{ target: '[data-tutorial="right-pane"]', title: t('tutorial.step5Title'), content: t('tutorial.step5Content'), placement: 'left' as const, disableBeacon: true },
{ target: '[data-tutorial="poi-button"]', title: t('tutorial.step6Title'), content: t('tutorial.step6Content'), placement: 'left' as const, disableBeacon: true, styles: { tooltip: { transform: 'translateY(-50px)' } } },
], [t]);
const [run, setRun] = useState(() => {
if (isMobile) return false;
return !localStorage.getItem(STORAGE_KEY);
@ -88,7 +45,7 @@ export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked
return useMemo(
() => ({
steps: STEPS,
steps,
run: shouldRun,
handleCallback,
resetTutorial,