Clean up
This commit is contained in:
parent
b94cf17d75
commit
0c6d207967
41 changed files with 1809 additions and 1204 deletions
|
|
@ -601,7 +601,9 @@ function InviteTable({
|
|||
<table className="w-full table-fixed text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-warm-200 dark:border-warm-700 text-left">
|
||||
<th className="px-5 py-2 text-warm-500 dark:text-warm-400 font-medium">{t('invitesPage.link')}</th>
|
||||
<th className="px-5 py-2 text-warm-500 dark:text-warm-400 font-medium">
|
||||
{t('invitesPage.link')}
|
||||
</th>
|
||||
<th className="px-5 py-2 text-warm-500 dark:text-warm-400 font-medium w-24">
|
||||
{t('invitesPage.status')}
|
||||
</th>
|
||||
|
|
@ -754,7 +756,9 @@ export function InvitesPage({ user }: { user: AuthUser }) {
|
|||
{(user.isAdmin ? ['admin', 'referral'] : ['referral']).map((type) => (
|
||||
<div key={type} className="px-5 py-4">
|
||||
<p className="text-sm text-warm-500 dark:text-warm-400 mb-3">
|
||||
{type === 'admin' ? t('invitesPage.inviteAdminLabel') : t('invitesPage.inviteReferralLabel')}
|
||||
{type === 'admin'
|
||||
? t('invitesPage.inviteAdminLabel')
|
||||
: t('invitesPage.inviteReferralLabel')}
|
||||
</p>
|
||||
{inviteUrl[type] ? (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -783,7 +787,9 @@ export function InvitesPage({ user }: { user: AuthUser }) {
|
|||
className="px-4 py-2 rounded-lg bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium disabled:opacity-50 disabled:cursor-wait flex items-center gap-2"
|
||||
>
|
||||
{creatingInvite[type] && <SpinnerIcon className="w-4 h-4 animate-spin" />}
|
||||
{type === 'admin' ? t('invitesPage.generateFreeInvite') : t('invitesPage.generateReferralLink')}
|
||||
{type === 'admin'
|
||||
? t('invitesPage.generateFreeInvite')
|
||||
: t('invitesPage.generateReferralLink')}
|
||||
</button>
|
||||
)}
|
||||
{inviteError[type] && (
|
||||
|
|
@ -845,7 +851,9 @@ export default function AccountPage({
|
|||
{/* Email */}
|
||||
<div className="px-5 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-400">{t('accountPage.emailLabel')}</p>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-400">
|
||||
{t('accountPage.emailLabel')}
|
||||
</p>
|
||||
<p className="text-navy-950 dark:text-warm-100 font-medium">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -853,7 +861,9 @@ export default function AccountPage({
|
|||
{/* Subscription */}
|
||||
<div className="px-5 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-400">{t('accountPage.subscriptionLabel')}</p>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-400">
|
||||
{t('accountPage.subscriptionLabel')}
|
||||
</p>
|
||||
<span
|
||||
className={`inline-block text-sm font-medium px-2.5 py-0.5 rounded-full mt-1 ${badgeColor}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -68,23 +68,24 @@ export default function HomePage({
|
|||
<div className="relative z-10 max-w-4xl mx-auto px-6 md:px-10 pt-16 md:pt-24 backdrop-blur-[2px] flex-1 flex flex-col">
|
||||
<div>
|
||||
<h1 className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-[1.1] tracking-tight">
|
||||
{t('home.heroTitle1')} <span className="text-teal-400">{t('home.heroTitle2')}</span>.
|
||||
{t('home.heroTitle1')} <span className="text-teal-400">{t('home.heroTitle2')}</span>
|
||||
.
|
||||
<br />
|
||||
{t('home.heroTitle3')}
|
||||
</h1>
|
||||
<p className="text-lg text-warm-300 mb-6 leading-relaxed max-w-xl">
|
||||
<p className="text-base md:text-lg text-warm-300 mb-6 leading-relaxed max-w-xl">
|
||||
{t('home.heroSubtitle')}
|
||||
</p>
|
||||
<p className="text-lg text-warm-400 mb-8 max-w-xl">
|
||||
<p className="text-base md:text-lg text-warm-400 mb-8 max-w-xl">
|
||||
{t('home.heroDescription')}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-4 mb-10">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 mb-10">
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'explore_map' });
|
||||
onOpenDashboard();
|
||||
}}
|
||||
className="px-7 py-3.5 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-base shadow-lg shadow-coral-500/25"
|
||||
className="px-7 py-3.5 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-base shadow-lg shadow-coral-500/25 text-center"
|
||||
>
|
||||
{t('home.exploreTheMap')}
|
||||
</button>
|
||||
|
|
@ -113,12 +114,12 @@ export default function HomePage({
|
|||
};
|
||||
requestAnimationFrame(step);
|
||||
}}
|
||||
className="px-[26px] py-[12px] border-2 border-teal-400 text-teal-400 rounded-lg font-semibold hover:bg-teal-400/10 transition-colors text-base"
|
||||
className="px-[26px] py-[12px] border-2 border-teal-400 text-teal-400 rounded-lg font-semibold hover:bg-teal-400/10 transition-colors text-base text-center"
|
||||
>
|
||||
{t('home.seeTheDifference')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-12 gap-y-4 pt-3 border-t border-white/10">
|
||||
<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">
|
||||
<TickerValue text="13M" active={statsActive} />
|
||||
|
|
@ -132,7 +133,9 @@ export default function HomePage({
|
|||
<div className="text-sm text-warm-400">{t('home.statFilters')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-white">{t('home.statEvery')}</div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-white">
|
||||
{t('home.statEvery')}
|
||||
</div>
|
||||
<div className="text-sm text-warm-400">{t('home.statPostcodeInEngland')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -142,30 +145,26 @@ export default function HomePage({
|
|||
</div>
|
||||
|
||||
{/* Our philosophy */}
|
||||
<div className="px-6 md:px-12 lg:px-20 pt-20 pb-4">
|
||||
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-6">
|
||||
<div className="px-6 md:px-12 lg:px-20 pt-12 md:pt-20 pb-4">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100 mb-6">
|
||||
{t('home.ourPhilosophy')}
|
||||
</h2>
|
||||
<div className="space-y-4 text-lg md:text-xl leading-relaxed text-warm-700 dark:text-warm-300">
|
||||
<p>
|
||||
{t('home.philosophyP1')}
|
||||
</p>
|
||||
<p>
|
||||
{t('home.philosophyP2')}
|
||||
</p>
|
||||
<p>{t('home.philosophyP1')}</p>
|
||||
<p>{t('home.philosophyP2')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How to use it + Comparison table (two columns) */}
|
||||
<div id="how-it-works" className="max-w-7xl mx-auto px-6 pt-10 pb-2">
|
||||
<div ref={whyRef} className="fade-in-section">
|
||||
<div className="grid lg:grid-cols-[2fr_3fr] gap-12 items-start">
|
||||
<div className="grid lg:grid-cols-[2fr_3fr] gap-8 lg:gap-12 items-start">
|
||||
{/* Left: How to use it */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-10">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100 mb-6 md:mb-10">
|
||||
{t('home.howToUseIt')}
|
||||
</h2>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
{[
|
||||
{ title: t('home.howStep1Title'), desc: t('home.howStep1Desc') },
|
||||
{ title: t('home.howStep2Title'), desc: t('home.howStep2Desc') },
|
||||
|
|
@ -190,11 +189,11 @@ export default function HomePage({
|
|||
</div>
|
||||
{/* Right: Comparison table */}
|
||||
<div id="comparison">
|
||||
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-10">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100 mb-6 md:mb-10">
|
||||
{t('home.othersVs')}{' '}
|
||||
<span className="inline-flex items-baseline gap-3 whitespace-nowrap">
|
||||
<span className="inline-flex items-baseline gap-2 md:gap-3">
|
||||
{t('header.appName')}{' '}
|
||||
<LogoIcon className="w-8 h-8 text-teal-600 dark:text-teal-400" />
|
||||
<LogoIcon className="w-6 h-6 md:w-8 md:h-8 text-teal-600 dark:text-teal-400" />
|
||||
</span>
|
||||
</h2>
|
||||
<div className="overflow-x-auto rounded-xl border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 shadow-sm">
|
||||
|
|
@ -218,10 +217,34 @@ export default function HomePage({
|
|||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
{ feature: t('home.compSearchWithout'), subtitle: t('home.compSearchWithoutSub'), listings: false, postcode: false, guides: false },
|
||||
{ feature: t('home.compAreaData'), subtitle: t('home.compAreaDataSub'), listings: false, postcode: true, guides: true },
|
||||
{ feature: t('home.compPropertyData'), subtitle: t('home.compPropertyDataSub'), listings: true, postcode: false, guides: false },
|
||||
{ feature: t('home.compFilters'), subtitle: t('home.compFiltersSub'), listings: false, postcode: false, guides: false },
|
||||
{
|
||||
feature: t('home.compSearchWithout'),
|
||||
subtitle: t('home.compSearchWithoutSub'),
|
||||
listings: false,
|
||||
postcode: false,
|
||||
guides: false,
|
||||
},
|
||||
{
|
||||
feature: t('home.compAreaData'),
|
||||
subtitle: t('home.compAreaDataSub'),
|
||||
listings: false,
|
||||
postcode: true,
|
||||
guides: true,
|
||||
},
|
||||
{
|
||||
feature: t('home.compPropertyData'),
|
||||
subtitle: t('home.compPropertyDataSub'),
|
||||
listings: true,
|
||||
postcode: false,
|
||||
guides: false,
|
||||
},
|
||||
{
|
||||
feature: t('home.compFilters'),
|
||||
subtitle: t('home.compFiltersSub'),
|
||||
listings: false,
|
||||
postcode: false,
|
||||
guides: false,
|
||||
},
|
||||
].map((row, i, arr) => (
|
||||
<tr
|
||||
key={i}
|
||||
|
|
@ -261,7 +284,7 @@ export default function HomePage({
|
|||
</div>
|
||||
|
||||
{/* The real cost CTA */}
|
||||
<div className="max-w-4xl mx-auto px-6 pt-20 pb-12">
|
||||
<div className="max-w-4xl mx-auto px-6 pt-12 md:pt-20 pb-12">
|
||||
<div ref={ctaRef} className="fade-in-section text-center">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100 mb-4 leading-snug">
|
||||
{t('home.ctaTitle')}
|
||||
|
|
@ -274,7 +297,7 @@ export default function HomePage({
|
|||
trackEvent('CTA Click', { location: 'bottom', label: 'explore_map' });
|
||||
onOpenDashboard();
|
||||
}}
|
||||
className="px-8 py-4 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-lg shadow-lg shadow-coral-500/25"
|
||||
className="w-full sm:w-auto px-8 py-4 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-lg shadow-lg shadow-coral-500/25"
|
||||
>
|
||||
{t('home.exploreTheMap')}
|
||||
</button>
|
||||
|
|
@ -287,4 +310,3 @@ export default function HomePage({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,9 @@ export default function InvitePage({
|
|||
<span className="text-[96px] leading-none font-extrabold text-teal-600 dark:text-teal-400">
|
||||
{`\u00A3${(Math.round(pricePence * 0.7) / 100).toFixed(2)}`}
|
||||
</span>
|
||||
<span className="text-warm-500 dark:text-warm-400 ml-2 text-3xl">{t('upgrade.once')}</span>
|
||||
<span className="text-warm-500 dark:text-warm-400 ml-2 text-3xl">
|
||||
{t('upgrade.once')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-warm-600 dark:text-warm-400 text-3xl">
|
||||
|
|
|
|||
|
|
@ -14,37 +14,111 @@ interface DataSourceDef {
|
|||
}
|
||||
|
||||
const DATA_SOURCE_DEFS: DataSourceDef[] = [
|
||||
{ id: 'price-paid', url: 'https://www.gov.uk/government/statistical-data-sets/price-paid-data-downloads', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'epc', url: 'https://epc.opendatacommunities.org/downloads/domestic', license: 'Open Government Licence v3.0', optOutUrl: 'https://www.gov.uk/guidance/energy-performance-certificates-opt-out-of-public-disclosure' },
|
||||
{ id: 'nspl', url: 'https://www.arcgis.com/sharing/rest/content/items/077631e063eb4e1ab43575d01381ec33/data', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'iod', url: 'https://www.gov.uk/government/statistics/english-indices-of-deprivation-2025', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'ethnicity', url: 'https://www.ethnicity-facts-figures.service.gov.uk/uk-population-by-ethnicity/national-and-regional-populations/regional-ethnic-diversity/latest/#download-the-data', license: 'Open Government Licence v3.0' },
|
||||
{
|
||||
id: 'price-paid',
|
||||
url: 'https://www.gov.uk/government/statistical-data-sets/price-paid-data-downloads',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'epc',
|
||||
url: 'https://epc.opendatacommunities.org/downloads/domestic',
|
||||
license: 'Open Government Licence v3.0',
|
||||
optOutUrl:
|
||||
'https://www.gov.uk/guidance/energy-performance-certificates-opt-out-of-public-disclosure',
|
||||
},
|
||||
{
|
||||
id: 'nspl',
|
||||
url: 'https://www.arcgis.com/sharing/rest/content/items/077631e063eb4e1ab43575d01381ec33/data',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'iod',
|
||||
url: 'https://www.gov.uk/government/statistics/english-indices-of-deprivation-2025',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'ethnicity',
|
||||
url: 'https://www.ethnicity-facts-figures.service.gov.uk/uk-population-by-ethnicity/national-and-regional-populations/regional-ethnic-diversity/latest/#download-the-data',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{ id: 'crime', url: 'https://data.police.uk/data/', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'osm-pois', url: 'https://download.geofabrik.de/europe/great-britain-latest.osm.pbf', license: 'Open Data Commons Open Database License (ODbL)' },
|
||||
{ id: 'os-open-greenspace', url: 'https://osdatahub.os.uk/downloads/open/OpenGreenspace', 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', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'noise', url: 'https://environment.data.gov.uk/spatialdata/road-noise-all-metrics-england-round-4/wcs', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'ofsted', url: 'https://www.gov.uk/government/statistical-data-sets/monthly-management-information-ofsteds-school-inspections-outcomes', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'broadband', url: 'https://www.ofcom.org.uk/phones-and-broadband/coverage-and-speeds/connected-nations-20252/data-downloads-2025', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'council-tax', url: 'https://www.gov.uk/government/statistics/council-tax-levels-set-by-local-authorities-in-england-2025-to-2026', license: 'Open Government Licence v3.0' },
|
||||
{ id: 'ons-rental', url: 'https://www.ons.gov.uk/peoplepopulationandcommunity/housing/datasets/privaterentalmarketsummarystatisticsinengland', license: 'Open Government Licence v3.0' },
|
||||
{
|
||||
id: 'osm-pois',
|
||||
url: 'https://download.geofabrik.de/europe/great-britain-latest.osm.pbf',
|
||||
license: 'Open Data Commons Open Database License (ODbL)',
|
||||
},
|
||||
{
|
||||
id: 'os-open-greenspace',
|
||||
url: 'https://osdatahub.os.uk/downloads/open/OpenGreenspace',
|
||||
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',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'noise',
|
||||
url: 'https://environment.data.gov.uk/spatialdata/road-noise-all-metrics-england-round-4/wcs',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'ofsted',
|
||||
url: 'https://www.gov.uk/government/statistical-data-sets/monthly-management-information-ofsteds-school-inspections-outcomes',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'broadband',
|
||||
url: 'https://www.ofcom.org.uk/phones-and-broadband/coverage-and-speeds/connected-nations-20252/data-downloads-2025',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'council-tax',
|
||||
url: 'https://www.gov.uk/government/statistics/council-tax-levels-set-by-local-authorities-in-england-2025-to-2026',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'ons-rental',
|
||||
url: 'https://www.ons.gov.uk/peoplepopulationandcommunity/housing/datasets/privaterentalmarketsummarystatisticsinengland',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
];
|
||||
|
||||
// Maps data source id → [nameKey, originKey, useKey] in en.ts learnPage section
|
||||
const DS_KEYS: Record<string, [string, string, string]> = {
|
||||
'price-paid': ['learnPage.dsPricePaidName', 'learnPage.dsPricePaidOrigin', 'learnPage.dsPricePaidUse'],
|
||||
'epc': ['learnPage.dsEpcName', 'learnPage.dsEpcOrigin', 'learnPage.dsEpcUse'],
|
||||
'nspl': ['learnPage.dsNsplName', 'learnPage.dsNsplOrigin', 'learnPage.dsNsplUse'],
|
||||
'iod': ['learnPage.dsIodName', 'learnPage.dsIodOrigin', 'learnPage.dsIodUse'],
|
||||
'ethnicity': ['learnPage.dsEthnicityName', 'learnPage.dsEthnicityOrigin', 'learnPage.dsEthnicityUse'],
|
||||
'crime': ['learnPage.dsCrimeName', 'learnPage.dsCrimeOrigin', 'learnPage.dsCrimeUse'],
|
||||
'price-paid': [
|
||||
'learnPage.dsPricePaidName',
|
||||
'learnPage.dsPricePaidOrigin',
|
||||
'learnPage.dsPricePaidUse',
|
||||
],
|
||||
epc: ['learnPage.dsEpcName', 'learnPage.dsEpcOrigin', 'learnPage.dsEpcUse'],
|
||||
nspl: ['learnPage.dsNsplName', 'learnPage.dsNsplOrigin', 'learnPage.dsNsplUse'],
|
||||
iod: ['learnPage.dsIodName', 'learnPage.dsIodOrigin', 'learnPage.dsIodUse'],
|
||||
ethnicity: [
|
||||
'learnPage.dsEthnicityName',
|
||||
'learnPage.dsEthnicityOrigin',
|
||||
'learnPage.dsEthnicityUse',
|
||||
],
|
||||
crime: ['learnPage.dsCrimeName', 'learnPage.dsCrimeOrigin', 'learnPage.dsCrimeUse'],
|
||||
'osm-pois': ['learnPage.dsOsmName', 'learnPage.dsOsmOrigin', 'learnPage.dsOsmUse'],
|
||||
'os-open-greenspace': ['learnPage.dsGreenspaceName', 'learnPage.dsGreenspaceOrigin', 'learnPage.dsGreenspaceUse'],
|
||||
'naptan': ['learnPage.dsNaptanName', 'learnPage.dsNaptanOrigin', 'learnPage.dsNaptanUse'],
|
||||
'noise': ['learnPage.dsNoiseName', 'learnPage.dsNoiseOrigin', 'learnPage.dsNoiseUse'],
|
||||
'ofsted': ['learnPage.dsOfstedName', 'learnPage.dsOfstedOrigin', 'learnPage.dsOfstedUse'],
|
||||
'broadband': ['learnPage.dsBroadbandName', 'learnPage.dsBroadbandOrigin', 'learnPage.dsBroadbandUse'],
|
||||
'council-tax': ['learnPage.dsCouncilTaxName', 'learnPage.dsCouncilTaxOrigin', 'learnPage.dsCouncilTaxUse'],
|
||||
'os-open-greenspace': [
|
||||
'learnPage.dsGreenspaceName',
|
||||
'learnPage.dsGreenspaceOrigin',
|
||||
'learnPage.dsGreenspaceUse',
|
||||
],
|
||||
naptan: ['learnPage.dsNaptanName', 'learnPage.dsNaptanOrigin', 'learnPage.dsNaptanUse'],
|
||||
noise: ['learnPage.dsNoiseName', 'learnPage.dsNoiseOrigin', 'learnPage.dsNoiseUse'],
|
||||
ofsted: ['learnPage.dsOfstedName', 'learnPage.dsOfstedOrigin', 'learnPage.dsOfstedUse'],
|
||||
broadband: [
|
||||
'learnPage.dsBroadbandName',
|
||||
'learnPage.dsBroadbandOrigin',
|
||||
'learnPage.dsBroadbandUse',
|
||||
],
|
||||
'council-tax': [
|
||||
'learnPage.dsCouncilTaxName',
|
||||
'learnPage.dsCouncilTaxOrigin',
|
||||
'learnPage.dsCouncilTaxUse',
|
||||
],
|
||||
'ons-rental': ['learnPage.dsRentalName', 'learnPage.dsRentalOrigin', 'learnPage.dsRentalUse'],
|
||||
};
|
||||
|
||||
|
|
@ -207,53 +281,53 @@ export default function LearnPage() {
|
|||
const keys = DS_KEYS[source.id];
|
||||
const [nameKey, originKey, useKey] = keys;
|
||||
return (
|
||||
<div
|
||||
key={source.id}
|
||||
id={source.id}
|
||||
ref={(el) => {
|
||||
cardRefs.current[source.id] = el;
|
||||
}}
|
||||
className={`bg-white dark:bg-warm-800 rounded-lg border p-5 ${
|
||||
highlightedId === source.id
|
||||
? 'border-teal-400 ring-2 ring-teal-400'
|
||||
: 'border-warm-200 dark:border-warm-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<h2 className="text-lg font-semibold text-warm-900 dark:text-warm-100">
|
||||
{tDynamic(nameKey)}
|
||||
</h2>
|
||||
<span className="text-xs bg-warm-100 dark:bg-navy-700 text-warm-600 dark:text-warm-300 px-2 py-1 rounded text-right">
|
||||
{source.license}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-400 mb-2">
|
||||
{t('learnPage.source')} {tDynamic(originKey)}
|
||||
</p>
|
||||
<p className="text-sm text-warm-700 dark:text-warm-300 mb-3">
|
||||
{tDynamic(useKey)}
|
||||
</p>
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline break-all"
|
||||
<div
|
||||
key={source.id}
|
||||
id={source.id}
|
||||
ref={(el) => {
|
||||
cardRefs.current[source.id] = el;
|
||||
}}
|
||||
className={`bg-white dark:bg-warm-800 rounded-lg border p-5 ${
|
||||
highlightedId === source.id
|
||||
? 'border-teal-400 ring-2 ring-teal-400'
|
||||
: 'border-warm-200 dark:border-warm-700'
|
||||
}`}
|
||||
>
|
||||
{source.url}
|
||||
</a>
|
||||
{source.optOutUrl && (
|
||||
<div className="mt-2">
|
||||
<a
|
||||
href={source.optOutUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline"
|
||||
>
|
||||
{t('learnPage.optOut')}
|
||||
</a>
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<h2 className="text-lg font-semibold text-warm-900 dark:text-warm-100">
|
||||
{tDynamic(nameKey)}
|
||||
</h2>
|
||||
<span className="text-xs bg-warm-100 dark:bg-navy-700 text-warm-600 dark:text-warm-300 px-2 py-1 rounded text-right">
|
||||
{source.license}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-400 mb-2">
|
||||
{t('learnPage.source')} {tDynamic(originKey)}
|
||||
</p>
|
||||
<p className="text-sm text-warm-700 dark:text-warm-300 mb-3">
|
||||
{tDynamic(useKey)}
|
||||
</p>
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline break-all"
|
||||
>
|
||||
{source.url}
|
||||
</a>
|
||||
{source.optOutUrl && (
|
||||
<div className="mt-2">
|
||||
<a
|
||||
href={source.optOutUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline"
|
||||
>
|
||||
{t('learnPage.optOut')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -308,9 +382,7 @@ export default function LearnPage() {
|
|||
</>
|
||||
) : tab === 'faq' ? (
|
||||
<div className="max-w-3xl mx-auto px-6 py-6 w-full">
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-6">
|
||||
{t('learnPage.faqIntro')}
|
||||
</p>
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-6">{t('learnPage.faqIntro')}</p>
|
||||
<div className="space-y-8">
|
||||
{FAQ_SECTIONS.map((section) => (
|
||||
<div key={section.title}>
|
||||
|
|
@ -328,9 +400,7 @@ export default function LearnPage() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="max-w-2xl mx-auto px-6 py-6 w-full">
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-6">
|
||||
{t('learnPage.supportIntro')}
|
||||
</p>
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-6">{t('learnPage.supportIntro')}</p>
|
||||
<div className="bg-white dark:bg-warm-800 rounded-xl border border-warm-200 dark:border-warm-700 p-6 text-center">
|
||||
<p className="text-warm-600 dark:text-warm-300 mb-2">{t('accountPage.needHelp')}</p>
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -53,8 +53,19 @@ export default memo(function AiFilterInput({
|
|||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState('');
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const exampleQueries = useMemo(() => [t('aiFilter.example1'), t('aiFilter.example2'), t('aiFilter.example3')], [t]);
|
||||
const loadingMessages = useMemo(() => [t('aiFilter.analysing'), t('aiFilter.searchingDestinations'), t('aiFilter.generatingFilters'), t('aiFilter.refiningResults')], [t]);
|
||||
const exampleQueries = useMemo(
|
||||
() => [t('aiFilter.example1'), t('aiFilter.example2'), t('aiFilter.example3')],
|
||||
[t]
|
||||
);
|
||||
const loadingMessages = useMemo(
|
||||
() => [
|
||||
t('aiFilter.analysing'),
|
||||
t('aiFilter.searchingDestinations'),
|
||||
t('aiFilter.generatingFilters'),
|
||||
t('aiFilter.refiningResults'),
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const loadingMessage = useLoadingMessage(loading, loadingMessages);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
|
@ -147,7 +158,9 @@ export default memo(function AiFilterInput({
|
|||
<div ref={containerRef} className="px-3 py-2" data-tutorial="ai-filters">
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<SparklesIcon className="w-3.5 h-3.5 text-teal-500 dark:text-teal-400 shrink-0" />
|
||||
<span className="text-xs font-medium text-teal-700 dark:text-teal-300">{t('aiFilter.aiSearch')}</span>
|
||||
<span className="text-xs font-medium text-teal-700 dark:text-teal-300">
|
||||
{t('aiFilter.aiSearch')}
|
||||
</span>
|
||||
<span className="text-xs text-warm-400 dark:text-warm-500">
|
||||
{t('aiFilter.describeHint')}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,9 @@ export default function AreaPane({
|
|||
{isPostcode ? hexagonId : t('areaPane.areaStatistics')}
|
||||
</h2>
|
||||
{isPostcode && (
|
||||
<span className="text-xs text-warm-500 dark:text-warm-400">{t('common.postcode')}</span>
|
||||
<span className="text-xs text-warm-500 dark:text-warm-400">
|
||||
{t('common.postcode')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{loading && stats && (
|
||||
|
|
@ -112,7 +114,11 @@ export default function AreaPane({
|
|||
</p>
|
||||
)}
|
||||
<p className="text-xs text-warm-500 dark:text-warm-400 mt-1">
|
||||
{t('areaPane.statsFor', { type: isPostcode ? t('common.postcode').toLowerCase() : t('common.area').toLowerCase() })}
|
||||
{t('areaPane.statsFor', {
|
||||
type: isPostcode
|
||||
? t('common.postcode').toLowerCase()
|
||||
: t('common.area').toLowerCase(),
|
||||
})}
|
||||
{Object.keys(filters).length > 0 ? t('areaPane.matchingFilters') : ''}
|
||||
</p>
|
||||
{stats && stats.count > 0 && (
|
||||
|
|
@ -150,7 +156,9 @@ export default function AreaPane({
|
|||
return uniqueYears.size > 1;
|
||||
})() && (
|
||||
<div className="mx-3 mt-2 bg-warm-50 dark:bg-warm-800 rounded p-2">
|
||||
<span className="text-xs text-warm-700 dark:text-warm-300">{t('areaPane.priceHistory')}</span>
|
||||
<span className="text-xs text-warm-700 dark:text-warm-300">
|
||||
{t('areaPane.priceHistory')}
|
||||
</span>
|
||||
<PriceHistoryChart points={stats.price_history} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups';
|
||||
import { useTravelModes } from '../../hooks/useTravelModes';
|
||||
import { SearchInput } from '../ui/SearchInput';
|
||||
|
|
@ -15,9 +16,8 @@ import { IconButton } from '../ui/IconButton';
|
|||
import { TravelTimeInfoPopup } from '../ui/TravelTimeInfoPopup';
|
||||
import {
|
||||
TRANSPORT_MODES,
|
||||
MODE_LABELS,
|
||||
MODE_DESCRIPTIONS,
|
||||
MODE_ICONS,
|
||||
useTranslatedModes,
|
||||
type TransportMode,
|
||||
type TravelTimeEntry,
|
||||
} from '../../hooks/useTravelTime';
|
||||
|
|
@ -34,7 +34,6 @@ interface FeatureBrowserProps {
|
|||
travelTimeEntries: TravelTimeEntry[];
|
||||
onAddTravelTimeEntry: (mode: TransportMode) => void;
|
||||
isLicensed: boolean;
|
||||
onUpgradeClick?: () => void;
|
||||
}
|
||||
|
||||
export default function FeatureBrowser({
|
||||
|
|
@ -49,8 +48,9 @@ export default function FeatureBrowser({
|
|||
travelTimeEntries: _travelTimeEntries,
|
||||
onAddTravelTimeEntry,
|
||||
isLicensed,
|
||||
onUpgradeClick,
|
||||
}: FeatureBrowserProps) {
|
||||
const { t } = useTranslation();
|
||||
const modes = useTranslatedModes();
|
||||
const [search, setSearch] = useState('');
|
||||
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
|
||||
const [travelInfoMode, setTravelInfoMode] = useState<TransportMode | null>(null);
|
||||
|
|
@ -102,9 +102,13 @@ export default function FeatureBrowser({
|
|||
return (
|
||||
<>
|
||||
<div className="shrink-0 p-2 border-b border-warm-200 dark:border-navy-700">
|
||||
<SearchInput value={search} onChange={setSearch} placeholder="Search features..." />
|
||||
<SearchInput
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder={t('filters.searchFeatures')}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:min-h-0 md:flex-1 md:overflow-y-auto flex flex-col">
|
||||
<div>
|
||||
{mergedGrouped.map((group) => {
|
||||
const isExpanded = isSearching || isGroupExpanded(group.name);
|
||||
return (
|
||||
|
|
@ -158,24 +162,24 @@ export default function FeatureBrowser({
|
|||
<ModeIcon className="w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<span className="text-sm font-medium text-navy-950 dark:text-warm-100">
|
||||
{MODE_LABELS[mode]}
|
||||
{modes.label(mode)}
|
||||
</span>
|
||||
<span className="text-xs text-warm-400 dark:text-warm-500 block">
|
||||
{MODE_DESCRIPTIONS[mode]}
|
||||
{modes.desc(mode)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
<IconButton
|
||||
onClick={() => setTravelInfoMode(mode)}
|
||||
title="Feature info"
|
||||
title={t('filters.featureInfo')}
|
||||
size="md"
|
||||
>
|
||||
<InfoIcon className="w-5 h-5 md:w-3.5 md:h-3.5" />
|
||||
</IconButton>
|
||||
<button
|
||||
onClick={() => onAddTravelTimeEntry(mode)}
|
||||
title={`Add ${MODE_LABELS[mode]} travel time`}
|
||||
title={t('travel.addTravelTime', { mode: modes.label(mode) })}
|
||||
className="p-1 rounded-md text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/30 hover:bg-teal-100 dark:hover:bg-teal-800/40"
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 md:w-5 md:h-5" strokeWidth={2.5} />
|
||||
|
|
@ -192,45 +196,15 @@ export default function FeatureBrowser({
|
|||
{mergedGrouped.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<FilterIcon className="w-8 h-8 text-warm-300 dark:text-warm-600" />}
|
||||
title={search ? 'No matching features' : 'All features are active'}
|
||||
description={
|
||||
search ? 'Try a different search term' : 'Remove a filter to see available features'
|
||||
}
|
||||
title={search ? t('filters.noMatchingFeatures') : t('filters.allFeaturesActive')}
|
||||
description={search ? t('filters.tryDifferentSearch') : t('filters.removeFilterHint')}
|
||||
className="px-3 py-4"
|
||||
/>
|
||||
) : isLicensed ? (
|
||||
<p className="flex-1 flex items-center justify-center px-4 py-6 text-xs text-center text-warm-400 dark:text-warm-500">
|
||||
Choose the filters that matter to you. The map updates as you go.
|
||||
{t('filters.chooseFilters')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="mt-auto flex flex-col items-center px-5 pt-6 pb-0">
|
||||
<p className="text-sm text-warm-600 dark:text-warm-400 text-center leading-relaxed mb-1">
|
||||
See crime, schools, noise, broadband, and 50+ more filters across all of England.
|
||||
</p>
|
||||
<p className="text-xs text-warm-400 dark:text-warm-500 text-center mb-4">
|
||||
One-time payment, lifetime access.
|
||||
</p>
|
||||
<button
|
||||
onClick={onUpgradeClick}
|
||||
className="px-5 py-2.5 rounded-lg bg-gradient-to-r from-teal-500 to-teal-600 hover:from-teal-600 hover:to-teal-700 text-white font-medium text-sm shadow-sm hover:shadow-md"
|
||||
>
|
||||
Upgrade to full map
|
||||
</button>
|
||||
<svg
|
||||
viewBox="0 120 1600 230"
|
||||
className="w-full mt-4 block shrink-0"
|
||||
preserveAspectRatio="xMidYMax meet"
|
||||
>
|
||||
<path
|
||||
d="M0,350 C400,150 1200,150 1600,350 Z"
|
||||
className="fill-green-500 dark:fill-green-600"
|
||||
/>
|
||||
<path d="M100,350 C450,180 1150,180 1500,350 Z" fill="#000" opacity="0.08" />
|
||||
<path d="M250,350 C550,220 1050,220 1350,350 Z" fill="#000" opacity="0.06" />
|
||||
<image href="/house.png" x="735" y="110" width="130" height="120" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
{infoFeature && (
|
||||
<FeatureInfoPopup
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ import { ChevronIcon, CloseIcon, LightbulbIcon, SpinnerIcon } from '../ui/icons'
|
|||
import { PillToggle } from '../ui/PillToggle';
|
||||
import { PillGroup } from '../ui/PillGroup';
|
||||
import type { FeatureMeta, FeatureFilters } from '../../types';
|
||||
import { formatFilterValue, formatNumber, parseInputValue, buildPercentileScale } from '../../lib/format';
|
||||
import {
|
||||
formatFilterValue,
|
||||
formatNumber,
|
||||
parseInputValue,
|
||||
buildPercentileScale,
|
||||
} from '../../lib/format';
|
||||
import type { PercentileScale } from '../../lib/format';
|
||||
import InfoPopup from '../ui/InfoPopup';
|
||||
import { FeatureInfoPopup } from '../ui/FeatureInfoPopup';
|
||||
|
|
@ -186,7 +191,13 @@ interface FiltersProps {
|
|||
travelTimeEntries: TravelTimeEntry[];
|
||||
onTravelTimeAddEntry: (mode: TransportMode) => void;
|
||||
onTravelTimeRemoveEntry: (index: number) => void;
|
||||
onTravelTimeSetDestination: (index: number, slug: string, label: string, lat: number, lon: number) => void;
|
||||
onTravelTimeSetDestination: (
|
||||
index: number,
|
||||
slug: string,
|
||||
label: string,
|
||||
lat: number,
|
||||
lon: number
|
||||
) => void;
|
||||
onTravelTimeRangeChange: (index: number, range: [number, number]) => void;
|
||||
onTravelTimeDragEnd: (index: number) => void;
|
||||
onTravelTimeToggleBest: (index: number) => void;
|
||||
|
|
@ -472,10 +483,13 @@ export default memo(function Filters({
|
|||
className="relative flex flex-col bg-white dark:bg-navy-950 overflow-y-auto md:overflow-hidden h-full touch-pan-y"
|
||||
>
|
||||
<div
|
||||
className="flex flex-col min-h-0"
|
||||
style={{
|
||||
flex: activeFilterCollapsed ? '0 0 auto' : addFilterCollapsed ? '1 1 0' : '3 1 0',
|
||||
}}
|
||||
className={`flex flex-col md:min-h-0 ${
|
||||
activeFilterCollapsed
|
||||
? 'md:[flex:0_0_auto]'
|
||||
: addFilterCollapsed
|
||||
? 'md:[flex:1_1_0]'
|
||||
: 'md:[flex:3_1_0]'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveFilterCollapsed((v) => !v)}
|
||||
|
|
@ -518,291 +532,327 @@ export default memo(function Filters({
|
|||
</div>
|
||||
</button>
|
||||
|
||||
{!activeFilterCollapsed && <div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
|
||||
<AiFilterInput
|
||||
loading={aiFilterLoading}
|
||||
error={aiFilterError}
|
||||
errorType={aiFilterErrorType}
|
||||
notes={aiFilterNotes}
|
||||
summary={aiFilterSummary}
|
||||
onSubmit={onAiFilterSubmit}
|
||||
isLoggedIn={isLoggedIn}
|
||||
onLoginRequired={onLoginRequired}
|
||||
/>
|
||||
<div className="px-3 pb-2 space-y-2">
|
||||
{isAdmin && (
|
||||
<div className="flex rounded-lg bg-warm-100 dark:bg-warm-800 p-0.5">
|
||||
{(['historical', 'buy', 'rent'] as const).map((type) => {
|
||||
const labels = { historical: t('filters.historical'), buy: t('filters.buy'), rent: t('filters.rent') };
|
||||
const isActive = activeListingType === type;
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => handleListingSelect(type)}
|
||||
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md cursor-pointer ${
|
||||
isActive
|
||||
? 'bg-white dark:bg-warm-700 text-teal-600 dark:text-teal-400 ring-2 ring-teal-400 shadow-sm'
|
||||
: 'text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300'
|
||||
}`}
|
||||
>
|
||||
{labels[type]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!activeFilterCollapsed && (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="md:flex-1 md:min-h-0 md:overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<AiFilterInput
|
||||
loading={aiFilterLoading}
|
||||
error={aiFilterError}
|
||||
errorType={aiFilterErrorType}
|
||||
notes={aiFilterNotes}
|
||||
summary={aiFilterSummary}
|
||||
onSubmit={onAiFilterSubmit}
|
||||
isLoggedIn={isLoggedIn}
|
||||
onLoginRequired={onLoginRequired}
|
||||
/>
|
||||
<div className="px-3 pb-2 space-y-2">
|
||||
{isAdmin && (
|
||||
<div className="flex rounded-lg bg-warm-100 dark:bg-warm-800 p-0.5">
|
||||
{(['historical', 'buy', 'rent'] as const).map((type) => {
|
||||
const labels = {
|
||||
historical: t('filters.historical'),
|
||||
buy: t('filters.buy'),
|
||||
rent: t('filters.rent'),
|
||||
};
|
||||
const isActive = activeListingType === type;
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => handleListingSelect(type)}
|
||||
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md cursor-pointer ${
|
||||
isActive
|
||||
? 'bg-white dark:bg-warm-700 text-teal-600 dark:text-teal-400 ring-2 ring-teal-400 shadow-sm'
|
||||
: 'text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300'
|
||||
}`}
|
||||
>
|
||||
{labels[type]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowPhilosophy(true)}
|
||||
className="w-full px-3 py-1.5 rounded-lg border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 hover:bg-warm-50 dark:hover:bg-warm-700 text-teal-600 dark:text-teal-400 font-medium text-sm flex items-center justify-center gap-2"
|
||||
>
|
||||
<LightbulbIcon />
|
||||
{t('filters.findingPerfectPostcode')}
|
||||
</button>
|
||||
</div>
|
||||
{enabledFeatureList.length === 0 && activeEntryCount === 0 && (
|
||||
<p className="px-3 py-1.5 text-xs text-warm-400 dark:text-warm-500">
|
||||
{t('filters.addFiltersHint')}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowPhilosophy(true)}
|
||||
className="w-full px-3 py-1.5 rounded-lg border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 hover:bg-warm-50 dark:hover:bg-warm-700 text-teal-600 dark:text-teal-400 font-medium text-sm flex items-center justify-center gap-2"
|
||||
>
|
||||
<LightbulbIcon />
|
||||
{t('filters.findingPerfectPostcode')}
|
||||
</button>
|
||||
</div>
|
||||
{enabledFeatureList.length === 0 && activeEntryCount === 0 && (
|
||||
<p className="px-3 py-1.5 text-xs text-warm-400 dark:text-warm-500">
|
||||
{t('filters.addFiltersHint')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="px-2 py-1 space-y-1">
|
||||
{enabledFeatureList.map((feature, featureIdx) => {
|
||||
if (feature.type === 'enum') {
|
||||
const selectedValues = (filters[feature.name] as string[]) || [];
|
||||
const allValues = feature.values || [];
|
||||
<div className="px-2 py-1 space-y-1">
|
||||
{enabledFeatureList.map((feature, featureIdx) => {
|
||||
if (feature.type === 'enum') {
|
||||
const selectedValues = (filters[feature.name] as string[]) || [];
|
||||
const allValues = feature.values || [];
|
||||
return (
|
||||
<Fragment key={feature.name}>
|
||||
{featureIdx === travelInsertIdx &&
|
||||
travelTimeEntries.map((entry, index) => (
|
||||
<div key={`tt_${index}`} data-filter-name={`tt_${index}`}>
|
||||
<TravelTimeCard
|
||||
mode={entry.mode}
|
||||
slug={entry.slug}
|
||||
label={entry.label}
|
||||
timeRange={entry.timeRange}
|
||||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
isActive={activeFeature === travelFieldKey(entry)}
|
||||
dragValue={activeFeature === travelFieldKey(entry) ? dragValue : null}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label, lat, lon) =>
|
||||
onTravelTimeSetDestination(index, slug, label, lat, lon)
|
||||
}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onDragStart={() => onDragStart(travelFieldKey(entry))}
|
||||
onDragChange={onDragChange}
|
||||
onDragEnd={() => onTravelTimeDragEnd(index)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
data-filter-name={feature.name}
|
||||
className={`space-y-0.5 px-2 py-1.5 rounded ${pinnedFeature === feature.name ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<FeatureLabel feature={feature} size="sm" />
|
||||
<FeatureActions
|
||||
feature={feature}
|
||||
isPinned={pinnedFeature === feature.name}
|
||||
onTogglePin={onTogglePin}
|
||||
onShowInfo={setActiveInfoFeature}
|
||||
onRemove={onRemoveFilter}
|
||||
/>
|
||||
</div>
|
||||
<PillGroup>
|
||||
{allValues.map((val) => (
|
||||
<PillToggle
|
||||
key={val}
|
||||
label={ts(val)}
|
||||
active={selectedValues.includes(val)}
|
||||
onClick={() => {
|
||||
const next = selectedValues.includes(val)
|
||||
? selectedValues.filter((v) => v !== val)
|
||||
: [...selectedValues, val];
|
||||
onFilterChange(feature.name, next);
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
))}
|
||||
</PillGroup>
|
||||
{filterImpacts?.[feature.name] != null &&
|
||||
filterImpacts[feature.name] > 0 && (
|
||||
<p className="text-[10px] text-warm-400 dark:text-warm-500 mt-0.5">
|
||||
+{formatNumber(filterImpacts[feature.name])} without this filter
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = activeFeature === feature.name;
|
||||
const isPinned = pinnedFeature === feature.name;
|
||||
const hist = feature.histogram;
|
||||
const displayValue =
|
||||
isActive && dragValue
|
||||
? dragValue
|
||||
: (filters[feature.name] as [number, number]) || [
|
||||
hist?.min ?? feature.min!,
|
||||
hist?.max ?? feature.max!,
|
||||
];
|
||||
const scale = percentileScales.get(feature.name);
|
||||
const dataMin = hist?.min ?? feature.min!;
|
||||
const dataMax = hist?.max ?? feature.max!;
|
||||
const clampMin = displayValue[0] <= dataMin;
|
||||
const clampMax = displayValue[1] >= dataMax;
|
||||
const isAtMin = displayValue[0] === dataMin;
|
||||
const isAtMax = displayValue[1] === dataMax;
|
||||
const sliderValue: [number, number] = scale
|
||||
? [
|
||||
clampMin ? 0 : Math.round(scale.toPercentile(displayValue[0])),
|
||||
clampMax ? 100 : Math.round(scale.toPercentile(displayValue[1])),
|
||||
]
|
||||
: [
|
||||
clampMin ? feature.min! : displayValue[0],
|
||||
clampMax ? feature.max! : displayValue[1],
|
||||
];
|
||||
|
||||
const mobileIconClass = 'w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0';
|
||||
const mobileIcon =
|
||||
getFeatureIcon(feature.name, mobileIconClass) ||
|
||||
(() => {
|
||||
const G = feature.group ? getGroupIcon(feature.group) : null;
|
||||
return G ? <G className={mobileIconClass} /> : null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Fragment key={feature.name}>
|
||||
{featureIdx === travelInsertIdx && travelTimeEntries.map((entry, index) => (
|
||||
<div key={`tt_${index}`} data-filter-name={`tt_${index}`}>
|
||||
<TravelTimeCard
|
||||
mode={entry.mode}
|
||||
slug={entry.slug}
|
||||
label={entry.label}
|
||||
timeRange={entry.timeRange}
|
||||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
isActive={activeFeature === travelFieldKey(entry)}
|
||||
dragValue={activeFeature === travelFieldKey(entry) ? dragValue : null}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label, lat, lon) => onTravelTimeSetDestination(index, slug, label, lat, lon)}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onDragStart={() => onDragStart(travelFieldKey(entry))}
|
||||
onDragChange={onDragChange}
|
||||
onDragEnd={() => onTravelTimeDragEnd(index)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{featureIdx === travelInsertIdx &&
|
||||
travelTimeEntries.map((entry, index) => (
|
||||
<div key={`tt_${index}`} data-filter-name={`tt_${index}`}>
|
||||
<TravelTimeCard
|
||||
mode={entry.mode}
|
||||
slug={entry.slug}
|
||||
label={entry.label}
|
||||
timeRange={entry.timeRange}
|
||||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
isActive={activeFeature === travelFieldKey(entry)}
|
||||
dragValue={activeFeature === travelFieldKey(entry) ? dragValue : null}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label, lat, lon) =>
|
||||
onTravelTimeSetDestination(index, slug, label, lat, lon)
|
||||
}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onDragStart={() => onDragStart(travelFieldKey(entry))}
|
||||
onDragChange={onDragChange}
|
||||
onDragEnd={() => onTravelTimeDragEnd(index)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
data-filter-name={feature.name}
|
||||
className={`space-y-0.5 px-2 py-1.5 rounded ${pinnedFeature === feature.name ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
className={`space-y-0.5 px-2 py-1.5 rounded ${isActive ? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30' : isPinned ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<FeatureLabel feature={feature} size="sm" />
|
||||
<div className="relative z-10 flex items-center justify-between gap-1">
|
||||
<FeatureLabel
|
||||
feature={feature}
|
||||
size="sm"
|
||||
className="min-w-0 shrink"
|
||||
hideIconOnMobile
|
||||
/>
|
||||
<FeatureActions
|
||||
feature={feature}
|
||||
isPinned={pinnedFeature === feature.name}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
onShowInfo={setActiveInfoFeature}
|
||||
onRemove={onRemoveFilter}
|
||||
/>
|
||||
</div>
|
||||
<PillGroup>
|
||||
{allValues.map((val) => (
|
||||
<PillToggle
|
||||
key={val}
|
||||
label={ts(val)}
|
||||
active={selectedValues.includes(val)}
|
||||
onClick={() => {
|
||||
const next = selectedValues.includes(val)
|
||||
? selectedValues.filter((v) => v !== val)
|
||||
: [...selectedValues, val];
|
||||
onFilterChange(feature.name, next);
|
||||
}}
|
||||
size="xs"
|
||||
<div className="flex md:block items-start gap-1.5">
|
||||
{mobileIcon && (
|
||||
<div className="md:hidden shrink-0 pt-0.5">{mobileIcon}</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<Slider
|
||||
min={scale ? 0 : feature.min!}
|
||||
max={scale ? 100 : feature.max!}
|
||||
step={scale ? 1 : (feature.step ?? (feature.max! - feature.min!) / 100)}
|
||||
value={sliderValue}
|
||||
onValueChange={
|
||||
scale
|
||||
? ([pMin, pMax]) => {
|
||||
const step = feature.step ?? 1;
|
||||
const snap = (v: number) => Math.round(v / step) * step;
|
||||
onDragChange([
|
||||
pMin <= 0
|
||||
? (hist?.min ?? feature.min!)
|
||||
: snap(scale.toValue(pMin)),
|
||||
pMax >= 100
|
||||
? (hist?.max ?? feature.max!)
|
||||
: snap(scale.toValue(pMax)),
|
||||
]);
|
||||
}
|
||||
: ([min, max]) =>
|
||||
onDragChange([
|
||||
min <= feature.min! ? (hist?.min ?? feature.min!) : min,
|
||||
max >= feature.max! ? (hist?.max ?? feature.max!) : max,
|
||||
])
|
||||
}
|
||||
onPointerDown={() => onDragStart(feature.name)}
|
||||
onPointerUp={() => onDragEnd()}
|
||||
/>
|
||||
))}
|
||||
</PillGroup>
|
||||
{filterImpacts?.[feature.name] != null && filterImpacts[feature.name] > 0 && (
|
||||
<p className="text-[10px] text-warm-400 dark:text-warm-500 mt-0.5">
|
||||
+{formatNumber(filterImpacts[feature.name])} without this filter
|
||||
</p>
|
||||
)}
|
||||
<SliderLabels
|
||||
min={scale ? 0 : feature.min!}
|
||||
max={scale ? 100 : feature.max!}
|
||||
value={sliderValue}
|
||||
displayValues={displayValue}
|
||||
isAtMin={isAtMin}
|
||||
isAtMax={isAtMax}
|
||||
raw={feature.raw}
|
||||
feature={feature}
|
||||
onValueChange={(v) => onFilterChange(feature.name, v)}
|
||||
/>
|
||||
{filterImpacts?.[feature.name] != null &&
|
||||
filterImpacts[feature.name] > 0 && (
|
||||
<p className="text-[10px] text-warm-400 dark:text-warm-500 -mt-1 ml-2.5">
|
||||
+{formatNumber(filterImpacts[feature.name])} without this filter
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = activeFeature === feature.name;
|
||||
const isPinned = pinnedFeature === feature.name;
|
||||
const hist = feature.histogram;
|
||||
const displayValue =
|
||||
isActive && dragValue
|
||||
? dragValue
|
||||
: (filters[feature.name] as [number, number]) || [
|
||||
hist?.min ?? feature.min!,
|
||||
hist?.max ?? feature.max!,
|
||||
];
|
||||
const scale = percentileScales.get(feature.name);
|
||||
const dataMin = hist?.min ?? feature.min!;
|
||||
const dataMax = hist?.max ?? feature.max!;
|
||||
const clampMin = displayValue[0] <= dataMin;
|
||||
const clampMax = displayValue[1] >= dataMax;
|
||||
const isAtMin = displayValue[0] === dataMin;
|
||||
const isAtMax = displayValue[1] === dataMax;
|
||||
const sliderValue: [number, number] = scale
|
||||
? [
|
||||
clampMin ? 0 : Math.round(scale.toPercentile(displayValue[0])),
|
||||
clampMax ? 100 : Math.round(scale.toPercentile(displayValue[1])),
|
||||
]
|
||||
: [
|
||||
clampMin ? feature.min! : displayValue[0],
|
||||
clampMax ? feature.max! : displayValue[1],
|
||||
];
|
||||
|
||||
const mobileIconClass = 'w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0';
|
||||
const mobileIcon = getFeatureIcon(feature.name, mobileIconClass) || (() => {
|
||||
const G = feature.group ? getGroupIcon(feature.group) : null;
|
||||
return G ? <G className={mobileIconClass} /> : null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Fragment key={feature.name}>
|
||||
{featureIdx === travelInsertIdx && travelTimeEntries.map((entry, index) => (
|
||||
<div key={`tt_${index}`} data-filter-name={`tt_${index}`}>
|
||||
<TravelTimeCard
|
||||
mode={entry.mode}
|
||||
slug={entry.slug}
|
||||
label={entry.label}
|
||||
timeRange={entry.timeRange}
|
||||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
isActive={activeFeature === travelFieldKey(entry)}
|
||||
dragValue={activeFeature === travelFieldKey(entry) ? dragValue : null}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label, lat, lon) => onTravelTimeSetDestination(index, slug, label, lat, lon)}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onDragStart={() => onDragStart(travelFieldKey(entry))}
|
||||
onDragChange={onDragChange}
|
||||
onDragEnd={() => onTravelTimeDragEnd(index)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
data-filter-name={feature.name}
|
||||
className={`space-y-0.5 px-2 py-1.5 rounded ${isActive ? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30' : isPinned ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
>
|
||||
<div className="relative z-10 flex items-center justify-between gap-1">
|
||||
<FeatureLabel feature={feature} size="sm" className="min-w-0 shrink" hideIconOnMobile />
|
||||
<FeatureActions
|
||||
feature={feature}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
onShowInfo={setActiveInfoFeature}
|
||||
onRemove={onRemoveFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex md:block items-start gap-1.5">
|
||||
{mobileIcon && <div className="md:hidden shrink-0 pt-0.5">{mobileIcon}</div>}
|
||||
<div className="min-w-0 flex-1">
|
||||
<Slider
|
||||
min={scale ? 0 : feature.min!}
|
||||
max={scale ? 100 : feature.max!}
|
||||
step={scale ? 1 : (feature.step ?? (feature.max! - feature.min!) / 100)}
|
||||
value={sliderValue}
|
||||
onValueChange={
|
||||
scale
|
||||
? ([pMin, pMax]) => {
|
||||
const step = feature.step ?? 1;
|
||||
const snap = (v: number) => Math.round(v / step) * step;
|
||||
onDragChange([
|
||||
pMin <= 0 ? (hist?.min ?? feature.min!) : snap(scale.toValue(pMin)),
|
||||
pMax >= 100
|
||||
? (hist?.max ?? feature.max!)
|
||||
: snap(scale.toValue(pMax)),
|
||||
]);
|
||||
}
|
||||
: ([min, max]) =>
|
||||
onDragChange([
|
||||
min <= feature.min! ? (hist?.min ?? feature.min!) : min,
|
||||
max >= feature.max! ? (hist?.max ?? feature.max!) : max,
|
||||
])
|
||||
}
|
||||
onPointerDown={() => onDragStart(feature.name)}
|
||||
onPointerUp={() => onDragEnd()}
|
||||
/>
|
||||
<SliderLabels
|
||||
min={scale ? 0 : feature.min!}
|
||||
max={scale ? 100 : feature.max!}
|
||||
value={sliderValue}
|
||||
displayValues={displayValue}
|
||||
isAtMin={isAtMin}
|
||||
isAtMax={isAtMax}
|
||||
raw={feature.raw}
|
||||
feature={feature}
|
||||
onValueChange={(v) => onFilterChange(feature.name, v)}
|
||||
/>
|
||||
{filterImpacts?.[feature.name] != null && filterImpacts[feature.name] > 0 && (
|
||||
<p className="text-[10px] text-warm-400 dark:text-warm-500 -mt-1 ml-2.5">
|
||||
+{formatNumber(filterImpacts[feature.name])} without this filter
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
{travelInsertIdx >= enabledFeatureList.length &&
|
||||
travelTimeEntries.map((entry, index) => (
|
||||
<div key={`tt_${index}`} data-filter-name={`tt_${index}`}>
|
||||
<TravelTimeCard
|
||||
mode={entry.mode}
|
||||
slug={entry.slug}
|
||||
label={entry.label}
|
||||
timeRange={entry.timeRange}
|
||||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
isActive={activeFeature === travelFieldKey(entry)}
|
||||
dragValue={activeFeature === travelFieldKey(entry) ? dragValue : null}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label, lat, lon) =>
|
||||
onTravelTimeSetDestination(index, slug, label, lat, lon)
|
||||
}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onDragStart={() => onDragStart(travelFieldKey(entry))}
|
||||
onDragChange={onDragChange}
|
||||
onDragEnd={() => onTravelTimeDragEnd(index)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{travelInsertIdx >= enabledFeatureList.length && travelTimeEntries.map((entry, index) => (
|
||||
<div key={`tt_${index}`} data-filter-name={`tt_${index}`}>
|
||||
<TravelTimeCard
|
||||
mode={entry.mode}
|
||||
slug={entry.slug}
|
||||
label={entry.label}
|
||||
timeRange={entry.timeRange}
|
||||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
isActive={activeFeature === travelFieldKey(entry)}
|
||||
dragValue={activeFeature === travelFieldKey(entry) ? dragValue : null}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label, lat, lon) => onTravelTimeSetDestination(index, slug, label, lat, lon)}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onDragStart={() => onDragStart(travelFieldKey(entry))}
|
||||
onDragChange={onDragChange}
|
||||
onDragEnd={() => onTravelTimeDragEnd(index)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col min-h-0 border-t border-warm-200 dark:border-warm-700"
|
||||
style={{
|
||||
flex: addFilterCollapsed ? '0 0 auto' : activeFilterCollapsed ? '1 1 0' : '2 1 0',
|
||||
}}
|
||||
className={`flex flex-col md:min-h-0 border-t border-warm-200 dark:border-warm-700 ${
|
||||
addFilterCollapsed
|
||||
? 'md:[flex:0_0_auto]'
|
||||
: activeFilterCollapsed
|
||||
? 'md:[flex:1_1_0]'
|
||||
: 'md:[flex:2_1_0]'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setAddFilterCollapsed((v) => !v)}
|
||||
className="shrink-0 flex items-center justify-between px-3 py-2 border-b border-warm-200 dark:border-navy-700 bg-teal-50 dark:bg-teal-900/30 cursor-pointer hover:bg-teal-100 dark:hover:bg-teal-900/50"
|
||||
>
|
||||
<span className="text-sm font-semibold text-teal-700 dark:text-teal-400">{t('filters.addFilter')}</span>
|
||||
<span className="text-sm font-semibold text-teal-700 dark:text-teal-400">
|
||||
{t('filters.addFilter')}
|
||||
</span>
|
||||
<ChevronIcon
|
||||
direction={addFilterCollapsed ? 'down' : 'up'}
|
||||
className="w-4 h-4 text-warm-400 dark:text-warm-500"
|
||||
/>
|
||||
</button>
|
||||
{!addFilterCollapsed && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="md:flex-1 md:min-h-0 md:overflow-y-auto">
|
||||
<FeatureBrowser
|
||||
availableFeatures={availableFeatures}
|
||||
allFeatures={features}
|
||||
|
|
@ -850,11 +900,12 @@ export default memo(function Filters({
|
|||
</div>
|
||||
|
||||
{showPhilosophy && (
|
||||
<InfoPopup title={t('filters.findingPerfectPostcode')} onClose={() => setShowPhilosophy(false)}>
|
||||
<InfoPopup
|
||||
title={t('filters.findingPerfectPostcode')}
|
||||
onClose={() => setShowPhilosophy(false)}
|
||||
>
|
||||
<div className="space-y-4 text-sm">
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
{t('philosophy.intro')}
|
||||
</p>
|
||||
<p className="text-warm-600 dark:text-warm-300">{t('philosophy.intro')}</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{([1, 2, 3, 4, 5, 6] as const).map((n) => (
|
||||
|
|
@ -872,9 +923,7 @@ export default memo(function Filters({
|
|||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-warm-500 dark:text-warm-400 italic text-xs">
|
||||
{t('philosophy.tip')}
|
||||
</p>
|
||||
<p className="text-warm-500 dark:text-warm-400 italic text-xs">{t('philosophy.tip')}</p>
|
||||
|
||||
{onResetTutorial && (
|
||||
<button
|
||||
|
|
@ -900,7 +949,10 @@ export default memo(function Filters({
|
|||
)}
|
||||
|
||||
{showClearPopup && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={() => setShowClearPopup(false)}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
onClick={() => setShowClearPopup(false)}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/50 dark:bg-black/70" />
|
||||
<div
|
||||
className="relative w-full max-w-sm mx-4 bg-white dark:bg-warm-900 rounded-lg shadow-xl border border-warm-200 dark:border-warm-700"
|
||||
|
|
|
|||
|
|
@ -8,19 +8,27 @@ export default function HistogramLegend() {
|
|||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-teal-500 dark:bg-teal-400 rounded" />
|
||||
<span className="text-warm-700 dark:text-warm-300">
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">{t('histogramLegend.tealBars')}</span> {t('histogramLegend.tealBarsDesc')}
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">
|
||||
{t('histogramLegend.tealBars')}
|
||||
</span>{' '}
|
||||
{t('histogramLegend.tealBarsDesc')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-warm-300/60 dark:bg-warm-600/60 rounded" />
|
||||
<span className="text-warm-700 dark:text-warm-300">
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">{t('histogramLegend.greyBars')}</span> {t('histogramLegend.greyBarsDesc')}
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">
|
||||
{t('histogramLegend.greyBars')}
|
||||
</span>{' '}
|
||||
{t('histogramLegend.greyBarsDesc')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-px border-t border-dashed border-warm-500 dark:border-warm-400" />
|
||||
<span className="text-warm-700 dark:text-warm-300">
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">{t('histogramLegend.dashedLine')}</span>{' '}
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">
|
||||
{t('histogramLegend.dashedLine')}
|
||||
</span>{' '}
|
||||
{t('histogramLegend.dashedLineDesc')}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@ export default memo(function HoverCard({
|
|||
{/* Property count */}
|
||||
{count != null && (
|
||||
<div className="text-xs text-warm-500 dark:text-warm-300 mb-2">
|
||||
{count.toLocaleString()} {count === 1 ? t('common.property') : t('common.propertiesPlural')}
|
||||
{count.toLocaleString()}{' '}
|
||||
{count === 1 ? t('common.property') : t('common.propertiesPlural')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,8 @@ function TimelineLeg({ leg, isLast }: { leg: JourneyLeg; isLast: boolean }) {
|
|||
<BicycleIcon className="w-3 h-3 text-warm-400 dark:text-warm-500 shrink-0" />
|
||||
)}
|
||||
<span className="text-[11px] text-warm-500 dark:text-warm-400">
|
||||
{leg.mode === 'walk' ? t('areaPane.walk') : t('areaPane.cycle')} · {leg.minutes} {t('common.min')}
|
||||
{leg.mode === 'walk' ? t('areaPane.walk') : t('areaPane.cycle')} · {leg.minutes}{' '}
|
||||
{t('common.min')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -145,7 +146,9 @@ function TimelineLeg({ leg, isLast }: { leg: JourneyLeg; isLast: boolean }) {
|
|||
<div className="pb-1.5 min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<RouteBadge mode={leg.mode} />
|
||||
<span className="text-[11px] text-warm-500 dark:text-warm-400">{leg.minutes} {t('common.min')}</span>
|
||||
<span className="text-[11px] text-warm-500 dark:text-warm-400">
|
||||
{leg.minutes} {t('common.min')}
|
||||
</span>
|
||||
</div>
|
||||
{leg.from && leg.to && (
|
||||
<div className="text-[11px] text-warm-600 dark:text-warm-300 mt-0.5">
|
||||
|
|
@ -231,7 +234,9 @@ export default function JourneyInstructions({
|
|||
return (
|
||||
<div className="mx-3 mt-2 space-y-2">
|
||||
{label && (
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400">{t('areaPane.journeysFrom', { label })}</div>
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400">
|
||||
{t('areaPane.journeysFrom', { label })}
|
||||
</div>
|
||||
)}
|
||||
{journeys.map((j) => {
|
||||
const displayLegs = j.legs ? invertLegs(j.legs) : null;
|
||||
|
|
@ -253,7 +258,9 @@ export default function JourneyInstructions({
|
|||
{j.loading ? (
|
||||
<div className="flex items-center gap-2 py-1">
|
||||
<div className="w-3 h-3 border-2 border-teal-600 dark:border-teal-400 border-t-transparent rounded-full animate-spin" />
|
||||
<span className="text-xs text-warm-500 dark:text-warm-400">{t('common.loading')}</span>
|
||||
<span className="text-xs text-warm-500 dark:text-warm-400">
|
||||
{t('common.loading')}
|
||||
</span>
|
||||
</div>
|
||||
) : displayLegs && displayLegs.length > 0 ? (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -136,15 +136,26 @@ export default memo(function Map({
|
|||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let resizeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let initialized = false;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
if (width > 0 && height > 0) {
|
||||
setDimensions({ width, height });
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
setDimensions({ width, height });
|
||||
} else {
|
||||
if (resizeTimer) clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => setDimensions({ width, height }), 150);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
return () => observer.disconnect();
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
if (resizeTimer) clearTimeout(resizeTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -209,7 +220,13 @@ export default memo(function Map({
|
|||
{...viewState}
|
||||
onMove={handleMove}
|
||||
onLoad={undefined}
|
||||
onIdle={screenshotMode ? () => { window.__map_idle = true; } : undefined}
|
||||
onIdle={
|
||||
screenshotMode
|
||||
? () => {
|
||||
window.__map_idle = true;
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
mapStyle={mapStyle}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
attributionControl={false}
|
||||
|
|
@ -222,9 +239,7 @@ export default memo(function Map({
|
|||
maxBounds={MAP_BOUNDS}
|
||||
>
|
||||
<DeckOverlay layers={layers} getTooltip={null} />
|
||||
{!screenshotMode && (
|
||||
<ScaleControl position="bottom-left" maxWidth={100} unit="metric" />
|
||||
)}
|
||||
{!screenshotMode && <ScaleControl position="bottom-left" maxWidth={100} unit="metric" />}
|
||||
</MapGL>
|
||||
{screenshotMode ? (
|
||||
ogMode ? (
|
||||
|
|
@ -233,7 +248,10 @@ export default memo(function Map({
|
|||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="flex items-center gap-8 bg-navy-900/90 rounded-3xl px-14 py-10">
|
||||
<LogoIcon className="w-24 h-24 text-teal-400" />
|
||||
<span className="font-bold text-white whitespace-nowrap" style={{ fontSize: '5rem' }}>
|
||||
<span
|
||||
className="font-bold text-white whitespace-nowrap"
|
||||
style={{ fontSize: '5rem' }}
|
||||
>
|
||||
Your perfect postcode
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -280,7 +298,11 @@ export default memo(function Map({
|
|||
(viewFeature && colorRange ? (
|
||||
viewFeature.startsWith('tt_') ? (
|
||||
<MapLegend
|
||||
featureLabel={t('travel.travelTime', { mode: modes.label(viewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit') })}
|
||||
featureLabel={t('travel.travelTime', {
|
||||
mode: modes.label(
|
||||
viewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit'
|
||||
),
|
||||
})}
|
||||
range={colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={onCancelPin}
|
||||
|
|
@ -315,7 +337,8 @@ export default memo(function Map({
|
|||
: [countRange.min, countRange.max]
|
||||
}
|
||||
totalCount={
|
||||
totalCountProp ?? (usePostcodeView ? postcodeCountRange.total : countRange.total)
|
||||
totalCountProp ??
|
||||
(usePostcodeView ? postcodeCountRange.total : countRange.total)
|
||||
}
|
||||
showCancel={false}
|
||||
onCancel={onCancelPin}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from '../../hooks/useTravelTime';
|
||||
import { apiUrl, authHeaders, buildFilterString, logNonAbortError } from '../../lib/api';
|
||||
import { useFilterCounts } from '../../hooks/useFilterCounts';
|
||||
import { ts } from '../../i18n/server';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { INITIAL_VIEW_STATE } from '../../lib/consts';
|
||||
import { useLicense } from '../../hooks/useLicense';
|
||||
|
|
@ -77,6 +78,8 @@ interface MapPageProps {
|
|||
isPropertySaved?: (address?: string, postcode?: string) => boolean;
|
||||
getSavedPropertyId?: (address?: string, postcode?: string) => string | undefined;
|
||||
deferTutorial?: boolean;
|
||||
onSaveSearch?: (name: string) => Promise<void>;
|
||||
savingSearch?: boolean;
|
||||
}
|
||||
|
||||
export default function MapPage({
|
||||
|
|
@ -105,12 +108,20 @@ export default function MapPage({
|
|||
isPropertySaved,
|
||||
getSavedPropertyId,
|
||||
deferTutorial = false,
|
||||
onSaveSearch,
|
||||
savingSearch,
|
||||
}: MapPageProps) {
|
||||
const [selectedPOICategories, setSelectedPOICategories] =
|
||||
useState<Set<string>>(initialPOICategories);
|
||||
|
||||
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 0.45, 'left');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 0.45, 'right');
|
||||
const [mobileMapHeight, mobileResizeHandlers, mobileMapRef] = usePaneResize(
|
||||
Math.round(window.innerHeight * 0.4),
|
||||
120,
|
||||
0.8,
|
||||
'top'
|
||||
);
|
||||
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||
const [poiPaneOpen, setPoiPaneOpen] = useState(false);
|
||||
|
|
@ -150,7 +161,6 @@ export default function MapPage({
|
|||
handleDragEnd,
|
||||
handleDragEndNoCommit,
|
||||
handleTogglePin,
|
||||
handleSetPin,
|
||||
handleCancelPin,
|
||||
} = useFilters({
|
||||
initialFilters,
|
||||
|
|
@ -163,14 +173,6 @@ export default function MapPage({
|
|||
|
||||
const handleAiFilterSubmit = useCallback(
|
||||
async (query: string) => {
|
||||
// Derive current listing type from Listing status filter
|
||||
const listingVal = filters['Listing status'] as string[] | undefined;
|
||||
const listingType = listingVal?.includes('For sale')
|
||||
? 'buy'
|
||||
: listingVal?.includes('For rent')
|
||||
? 'rent'
|
||||
: 'historical';
|
||||
|
||||
// Build context from current filters for conversational refinement
|
||||
const context = {
|
||||
filters,
|
||||
|
|
@ -183,11 +185,7 @@ export default function MapPage({
|
|||
};
|
||||
const hasContext = Object.keys(context.filters).length > 0 || context.travelTime.length > 0;
|
||||
|
||||
const result = await aiFilters.fetchAiFilters(
|
||||
query,
|
||||
hasContext ? context : undefined,
|
||||
listingType
|
||||
);
|
||||
const result = await aiFilters.fetchAiFilters(query, hasContext ? context : undefined);
|
||||
if (!result) return;
|
||||
handleSetFilters(result.filters);
|
||||
// Always sync travel time entries — clear stale ones when AI returns none
|
||||
|
|
@ -209,6 +207,12 @@ export default function MapPage({
|
|||
]
|
||||
);
|
||||
|
||||
const handleClearAll = useCallback(() => {
|
||||
handleSetFilters({});
|
||||
handleCancelPin();
|
||||
travelTime.handleSetEntries([]);
|
||||
}, [handleSetFilters, handleCancelPin, travelTime.handleSetEntries]);
|
||||
|
||||
const handleTravelTimeRemoveEntry = useCallback(
|
||||
(index: number) => {
|
||||
const entry = travelTime.entries[index];
|
||||
|
|
@ -240,7 +244,7 @@ export default function MapPage({
|
|||
travelTimeEntries: travelTime.entries,
|
||||
});
|
||||
|
||||
const filterCounts = useFilterCounts(filters, features, mapData.bounds);
|
||||
const filterCounts = useFilterCounts(filters, features, mapData.bounds, travelTime.entries);
|
||||
|
||||
const handleTravelTimeSetDestination = useCallback(
|
||||
(index: number, slug: string, label: string, lat: number, lon: number) => {
|
||||
|
|
@ -439,10 +443,10 @@ export default function MapPage({
|
|||
|
||||
const densityLabel = useMemo(() => {
|
||||
const listingVal = filters['Listing status'] as string[] | undefined;
|
||||
if (listingVal?.includes('For sale')) return 'Properties for sale';
|
||||
if (listingVal?.includes('For rent')) return 'Properties for rent';
|
||||
return 'Historical property matches';
|
||||
}, [filters]);
|
||||
if (listingVal?.includes('For sale')) return t('mapLegend.propertiesForSale');
|
||||
if (listingVal?.includes('For rent')) return t('mapLegend.propertiesForRent');
|
||||
return t('mapLegend.historicalMatches');
|
||||
}, [filters, t]);
|
||||
|
||||
const mobileLegendMeta = useMemo(
|
||||
() => (viewFeature ? features.find((f) => f.name === viewFeature) || null : null),
|
||||
|
|
@ -634,6 +638,9 @@ export default function MapPage({
|
|||
onUpgradeClick={() => onNavigateTo('pricing')}
|
||||
onResetTutorial={tutorial.resetTutorial}
|
||||
filterImpacts={filterCounts.impacts}
|
||||
onClearAll={handleClearAll}
|
||||
onSaveSearch={onSaveSearch}
|
||||
savingSearch={savingSearch}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -651,7 +658,11 @@ export default function MapPage({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative overflow-hidden" style={{ flex: '45 0 0' }}>
|
||||
<div
|
||||
ref={mobileMapRef}
|
||||
className="relative overflow-hidden"
|
||||
style={{ height: mobileMapHeight }}
|
||||
>
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
|
|
@ -702,13 +713,27 @@ export default function MapPage({
|
|||
</div>
|
||||
|
||||
<div
|
||||
className="bg-white dark:bg-warm-900 border-t border-warm-200 dark:border-warm-700 overflow-hidden flex flex-col"
|
||||
style={{ flex: '55 0 0' }}
|
||||
className="relative z-10 py-2 -my-2 cursor-row-resize touch-none group"
|
||||
{...mobileResizeHandlers}
|
||||
>
|
||||
<div className="h-3 flex items-center justify-center bg-warm-100 dark:bg-navy-800 group-hover:bg-warm-200 dark:group-hover:bg-navy-700 border-y border-warm-200 dark:border-navy-700">
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
|
||||
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
|
||||
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 bg-white dark:bg-warm-900 overflow-hidden flex flex-col">
|
||||
{viewFeature && mapData.colorRange ? (
|
||||
viewFeature.startsWith('tt_') ? (
|
||||
<MapLegend
|
||||
featureLabel={t('travel.travelTime', { mode: modes.label(viewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit') })}
|
||||
featureLabel={t('travel.travelTime', {
|
||||
mode: modes.label(
|
||||
viewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit'
|
||||
),
|
||||
})}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
|
|
@ -721,8 +746,8 @@ export default function MapPage({
|
|||
<MapLegend
|
||||
featureLabel={
|
||||
viewSource === 'eye'
|
||||
? `Previewing \u201c${mobileLegendMeta.name}\u201d`
|
||||
: mobileLegendMeta.name
|
||||
? t('mapLegend.previewing', { name: ts(mobileLegendMeta.name) })
|
||||
: ts(mobileLegendMeta.name)
|
||||
}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
|
|
@ -848,6 +873,7 @@ export default function MapPage({
|
|||
bounds={mapData.bounds}
|
||||
travelTimeEntries={travelTime.entries}
|
||||
densityLabel={densityLabel}
|
||||
totalCount={filterCounts.total || undefined}
|
||||
/>
|
||||
{mapData.loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
|
|
|
|||
|
|
@ -37,7 +37,11 @@ export default function MobileDrawer({
|
|||
<div className="h-[90%] bg-white dark:bg-navy-950 rounded-t-2xl flex flex-col shadow-xl overflow-hidden">
|
||||
{/* Tab bar + close */}
|
||||
<div className="flex border-b border-warm-200 dark:border-navy-700 text-sm shrink-0">
|
||||
<TabButton label={t('common.area')} isActive={tab === 'area'} onClick={() => onTabChange('area')} />
|
||||
<TabButton
|
||||
label={t('common.area')}
|
||||
isActive={tab === 'area'}
|
||||
onClick={() => onTabChange('area')}
|
||||
/>
|
||||
<TabButton
|
||||
label={t('common.properties')}
|
||||
isActive={tab === 'properties'}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,9 @@ function PropertyCard({
|
|||
{askingRent !== undefined && (
|
||||
<div className="mt-2 text-lg font-bold text-teal-700 dark:text-teal-400">
|
||||
£{formatNumber(askingRent)}
|
||||
<span className="text-sm font-normal text-warm-600 dark:text-warm-400">{t('propertyCard.perMonth')}</span>
|
||||
<span className="text-sm font-normal text-warm-600 dark:text-warm-400">
|
||||
{t('propertyCard.perMonth')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -275,7 +277,8 @@ function PropertyCard({
|
|||
<div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-sm dark:text-warm-300">
|
||||
{property.property_type && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">{t('propertyCard.type')}</span> {ts(property.property_type)}
|
||||
<span className="text-warm-500 dark:text-warm-400">{t('propertyCard.type')}</span>{' '}
|
||||
{ts(property.property_type)}
|
||||
</div>
|
||||
)}
|
||||
{property.built_form && (
|
||||
|
|
@ -310,7 +313,8 @@ function PropertyCard({
|
|||
)}
|
||||
{rooms !== undefined && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">{t('propertyCard.rooms')}</span> {formatNumber(rooms)}
|
||||
<span className="text-warm-500 dark:text-warm-400">{t('propertyCard.rooms')}</span>{' '}
|
||||
{formatNumber(rooms)}
|
||||
</div>
|
||||
)}
|
||||
{age !== undefined && (
|
||||
|
|
@ -319,6 +323,14 @@ function PropertyCard({
|
|||
{formatAge(age, property.is_construction_date_approximate)}
|
||||
</div>
|
||||
)}
|
||||
{property.former_council_house === 'Yes' && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">
|
||||
{t('propertyCard.formerCouncil')}
|
||||
</span>{' '}
|
||||
{ts(property.former_council_house)}
|
||||
</div>
|
||||
)}
|
||||
{property.current_energy_rating && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">{t('propertyCard.epcRating')}</span>{' '}
|
||||
|
|
@ -327,7 +339,9 @@ function PropertyCard({
|
|||
)}
|
||||
{property.potential_energy_rating && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">{t('propertyCard.epcPotential')}</span>{' '}
|
||||
<span className="text-warm-500 dark:text-warm-400">
|
||||
{t('propertyCard.epcPotential')}
|
||||
</span>{' '}
|
||||
{ts(property.potential_energy_rating)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -341,7 +355,9 @@ function PropertyCard({
|
|||
|
||||
{property.listing_features && property.listing_features.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400 mb-1">{t('propertyCard.keyFeatures')}</div>
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400 mb-1">
|
||||
{t('propertyCard.keyFeatures')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{property.listing_features.map((feature, idx) => (
|
||||
<span
|
||||
|
|
@ -357,7 +373,9 @@ function PropertyCard({
|
|||
|
||||
{property.renovation_history && property.renovation_history.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400 mb-1">{t('propertyCard.renovations')}</div>
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400 mb-1">
|
||||
{t('propertyCard.renovations')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{property.renovation_history.map((reno, idx) => (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function TravelTimeCard({
|
|||
dragValue,
|
||||
onTogglePin,
|
||||
onSetDestination,
|
||||
onTimeRangeChange,
|
||||
onTimeRangeChange: _onTimeRangeChange,
|
||||
onDragStart,
|
||||
onDragChange,
|
||||
onDragEnd,
|
||||
|
|
@ -115,7 +115,12 @@ export function TravelTimeCard({
|
|||
{/* Best-case toggle — transit only, shown when destination is set */}
|
||||
{slug && mode === 'transit' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<PillToggle label={t('travel.bestCase')} active={useBest} onClick={onToggleBest} size="xs" />
|
||||
<PillToggle
|
||||
label={t('travel.bestCase')}
|
||||
active={useBest}
|
||||
onClick={onToggleBest}
|
||||
size="xs"
|
||||
/>
|
||||
<IconButton onClick={() => setShowBestInfo(true)} title={t('travel.bestCaseTitle')}>
|
||||
<InfoIcon className="w-3 h-3" />
|
||||
</IconButton>
|
||||
|
|
@ -149,8 +154,12 @@ export function TravelTimeCard({
|
|||
onPointerUp={() => onDragEnd()}
|
||||
/>
|
||||
<div className="relative h-4 mt-1 mx-2.5 text-[10px] text-warm-500 dark:text-warm-400 leading-tight">
|
||||
<span className="absolute left-0">{formatFilterValue(displayRange[0])} {t('common.min')}</span>
|
||||
<span className="absolute right-0">{formatFilterValue(displayRange[1])} {t('common.min')}</span>
|
||||
<span className="absolute left-0">
|
||||
{formatFilterValue(displayRange[0])} {t('common.min')}
|
||||
</span>
|
||||
<span className="absolute right-0">
|
||||
{formatFilterValue(displayRange[1])} {t('common.min')}
|
||||
</span>
|
||||
</div>
|
||||
{filterImpact != null && filterImpact > 0 && (
|
||||
<p className="text-[10px] text-warm-400 dark:text-warm-500 -mt-1 ml-2.5">
|
||||
|
|
|
|||
|
|
@ -183,18 +183,12 @@ export default function PricingPage({
|
|||
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-6 pt-16 text-center mb-6">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-white mb-3">{t('pricingPage.title')}</h1>
|
||||
<p className="text-lg text-warm-300 max-w-lg mx-auto">
|
||||
{t('pricingPage.subtitle')}
|
||||
</p>
|
||||
<p className="text-lg text-warm-300 max-w-lg mx-auto">{t('pricingPage.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-2xl mx-auto px-6 mb-12 text-center">
|
||||
<p className="text-warm-400 text-sm leading-relaxed mb-2">
|
||||
{t('pricingPage.costContext')}
|
||||
</p>
|
||||
<p className="text-warm-200 font-semibold">
|
||||
{t('pricingPage.lessThanSurvey')}
|
||||
</p>
|
||||
<p className="text-warm-400 text-sm leading-relaxed mb-2">{t('pricingPage.costContext')}</p>
|
||||
<p className="text-warm-200 font-semibold">{t('pricingPage.lessThanSurvey')}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-6 pb-16">
|
||||
|
|
@ -285,7 +279,9 @@ export default function PricingPage({
|
|||
: 'text-navy-950 dark:text-warm-100'
|
||||
}`}
|
||||
>
|
||||
{tier.price_pence === 0 ? t('upgrade.free') : formatPricePence(tier.price_pence)}
|
||||
{tier.price_pence === 0
|
||||
? t('upgrade.free')
|
||||
: formatPricePence(tier.price_pence)}
|
||||
</span>
|
||||
{tier.price_pence > 0 && (
|
||||
<span
|
||||
|
|
@ -321,7 +317,14 @@ export default function PricingPage({
|
|||
|
||||
<div className="flex-1 flex flex-col px-6 py-6 bg-white dark:bg-warm-800">
|
||||
<ul className="space-y-3 mb-6 flex-1">
|
||||
{[t('pricingPage.feat1'), t('pricingPage.feat2'), t('pricingPage.feat3'), t('pricingPage.feat4'), t('pricingPage.feat5'), t('pricingPage.feat6')].map((feat, idx) => (
|
||||
{[
|
||||
t('pricingPage.feat1'),
|
||||
t('pricingPage.feat2'),
|
||||
t('pricingPage.feat3'),
|
||||
t('pricingPage.feat4'),
|
||||
t('pricingPage.feat5'),
|
||||
t('pricingPage.feat6'),
|
||||
].map((feat, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2.5 text-sm">
|
||||
<CheckIcon className="w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0 mt-0.5" />
|
||||
<span className="text-warm-700 dark:text-warm-300">{feat}</span>
|
||||
|
|
@ -338,7 +341,9 @@ export default function PricingPage({
|
|||
</p>
|
||||
)}
|
||||
<p className="text-center text-xs text-warm-400 dark:text-warm-500 mt-2">
|
||||
{isFree ? t('pricingPage.noCreditCard') : t('pricingPage.moneyBackGuarantee')}
|
||||
{isFree
|
||||
? t('pricingPage.noCreditCard')
|
||||
: t('pricingPage.moneyBackGuarantee')}
|
||||
</p>
|
||||
</>
|
||||
) : isFilled ? (
|
||||
|
|
@ -357,9 +362,7 @@ export default function PricingPage({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-warm-400 py-16">
|
||||
{t('pricingPage.failedToLoad')}
|
||||
</p>
|
||||
<p className="text-center text-warm-400 py-16">{t('pricingPage.failedToLoad')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -192,7 +192,11 @@ export default function AuthModal({
|
|||
required
|
||||
minLength={8}
|
||||
className="w-full px-3 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 text-navy-950 dark:text-white placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-2 ring-teal-400 dark:ring-teal-500"
|
||||
placeholder={view === 'register' ? t('auth.passwordPlaceholderRegister') : t('auth.passwordPlaceholderLogin')}
|
||||
placeholder={
|
||||
view === 'register'
|
||||
? t('auth.passwordPlaceholderRegister')
|
||||
: t('auth.passwordPlaceholderLogin')
|
||||
}
|
||||
/>
|
||||
{view === 'login' && (
|
||||
<button
|
||||
|
|
@ -207,9 +211,7 @@ export default function AuthModal({
|
|||
)}
|
||||
|
||||
{view === 'forgot' && resetSent && (
|
||||
<p className="text-sm text-teal-700 dark:text-teal-400">
|
||||
{t('auth.resetSent')}
|
||||
</p>
|
||||
<p className="text-sm text-teal-700 dark:text-teal-400">{t('auth.resetSent')}</p>
|
||||
)}
|
||||
|
||||
{error && <p className="text-sm text-red-600 dark:text-red-300">{error}</p>}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import type { FeatureMeta } from '../../types';
|
||||
import { ts, tsDesc } from '../../i18n/server';
|
||||
import { ts, tsDesc, tsDetail } from '../../i18n/server';
|
||||
import InfoPopup from './InfoPopup';
|
||||
|
||||
interface FeatureInfoPopupProps {
|
||||
|
|
@ -34,7 +34,7 @@ export function FeatureInfoPopup({ feature, onClose, onNavigateToSource }: Featu
|
|||
)}
|
||||
{feature.detail && (
|
||||
<p className="text-sm text-warm-700 dark:text-warm-300 mb-4 leading-relaxed">
|
||||
{feature.detail}
|
||||
{tsDetail(feature.name, feature.detail)}
|
||||
</p>
|
||||
)}
|
||||
</InfoPopup>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ export default function LanguageDropdown() {
|
|||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const current = SUPPORTED_LANGUAGES.find((l) => l.code === i18n.language) ?? SUPPORTED_LANGUAGES[0];
|
||||
const current =
|
||||
SUPPORTED_LANGUAGES.find((l) => l.code === i18n.language) ?? SUPPORTED_LANGUAGES[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
|
@ -32,7 +33,13 @@ export default function LanguageDropdown() {
|
|||
aria-label="Language"
|
||||
>
|
||||
<span className="text-base leading-none">{current.flag}</span>
|
||||
<svg className="w-3 h-3 text-warm-400" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg
|
||||
className="w-3 h-3 text-warm-400"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M3 5l3 3 3-3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import type { Page } from './Header';
|
||||
import { PAGE_PATHS } from './Header';
|
||||
import type { AuthUser } from '../../hooks/useAuth';
|
||||
import { SUPPORTED_LANGUAGES } from '../../i18n';
|
||||
import { DownloadIcon } from './icons/DownloadIcon';
|
||||
import { BookmarkIcon } from './icons/BookmarkIcon';
|
||||
import { CheckIcon } from './icons/CheckIcon';
|
||||
|
|
@ -45,6 +47,8 @@ export default function MobileMenu({
|
|||
onShare,
|
||||
copied,
|
||||
}: MobileMenuProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const mobileNavItem = (page: Page, label: string) => (
|
||||
<a
|
||||
key={page}
|
||||
|
|
@ -72,24 +76,24 @@ export default function MobileMenu({
|
|||
{/* Menu panel */}
|
||||
<div className="fixed top-0 right-0 bottom-0 w-64 bg-navy-900 z-50 flex flex-col shadow-xl">
|
||||
<div className="flex items-center justify-between px-4 h-12 border-b border-navy-700">
|
||||
<span className="font-semibold">Menu</span>
|
||||
<span className="font-semibold">{t('mobileMenu.menu')}</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center justify-center w-10 h-10 -mr-2 rounded hover:bg-navy-800 transition-colors"
|
||||
aria-label="Close menu"
|
||||
aria-label={t('header.closeMenu')}
|
||||
>
|
||||
<CloseIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex-1 flex flex-col gap-1 p-3 overflow-y-auto">
|
||||
{mobileNavItem('home', 'Home')}
|
||||
{mobileNavItem('dashboard', 'Dashboard')}
|
||||
{mobileNavItem('learn', 'Learn')}
|
||||
{mobileNavItem('home', t('mobileMenu.home'))}
|
||||
{mobileNavItem('dashboard', t('header.dashboard'))}
|
||||
{mobileNavItem('learn', t('header.learn'))}
|
||||
{user?.subscription !== 'licensed' &&
|
||||
!user?.isAdmin &&
|
||||
mobileNavItem('pricing', 'Pricing')}
|
||||
{user && mobileNavItem('invites', 'Invite Friends')}
|
||||
{user && mobileNavItem('account', 'Account')}
|
||||
mobileNavItem('pricing', t('header.pricing'))}
|
||||
{user && mobileNavItem('invites', t('header.inviteFriends'))}
|
||||
{user && mobileNavItem('account', t('userMenu.account'))}
|
||||
|
||||
{/* Dashboard actions */}
|
||||
{activePage === 'dashboard' && (
|
||||
|
|
@ -102,7 +106,7 @@ export default function MobileMenu({
|
|||
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded"
|
||||
>
|
||||
{copied ? <CheckIcon className="w-5 h-5" /> : <ClipboardIcon className="w-5 h-5" />}
|
||||
{copied ? 'Copied!' : 'Share'}
|
||||
{copied ? t('common.copied') : t('common.share')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -113,7 +117,7 @@ export default function MobileMenu({
|
|||
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded disabled:opacity-50"
|
||||
>
|
||||
<DownloadIcon className="w-5 h-5" />
|
||||
{exporting ? 'Exporting...' : 'Export'}
|
||||
{exporting ? t('header.exporting') : t('header.exportLabel')}
|
||||
</button>
|
||||
{onSaveSearch && (
|
||||
<button
|
||||
|
|
@ -129,13 +133,13 @@ export default function MobileMenu({
|
|||
) : (
|
||||
<BookmarkIcon className="w-5 h-5" />
|
||||
)}
|
||||
Save
|
||||
{t('common.save')}
|
||||
</button>
|
||||
)}
|
||||
{user && mobileNavItem('saved', 'Saved')}
|
||||
{user && mobileNavItem('saved', t('header.saved'))}
|
||||
</div>
|
||||
)}
|
||||
{activePage !== 'dashboard' && user && mobileNavItem('saved', 'Saved')}
|
||||
{activePage !== 'dashboard' && user && mobileNavItem('saved', t('header.saved'))}
|
||||
</nav>
|
||||
|
||||
{/* Theme toggle + Auth section at bottom */}
|
||||
|
|
@ -148,9 +152,30 @@ export default function MobileMenu({
|
|||
className="w-full flex items-center gap-3 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded transition-colors"
|
||||
>
|
||||
{theme === 'light' ? <SunIcon className="w-5 h-5" /> : <MoonIcon className="w-5 h-5" />}
|
||||
<span>Theme: {theme === 'light' ? 'Light' : 'Dark'}</span>
|
||||
<span>{theme === 'light' ? t('userMenu.themeLight') : t('userMenu.themeDark')}</span>
|
||||
</button>
|
||||
|
||||
{/* Language selector */}
|
||||
<div className="flex gap-1 px-4">
|
||||
{SUPPORTED_LANGUAGES.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => {
|
||||
i18n.changeLanguage(lang.code);
|
||||
localStorage.setItem('language', lang.code);
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-2 py-2 rounded text-sm ${
|
||||
i18n.language === lang.code
|
||||
? 'bg-navy-700 text-white font-medium'
|
||||
: 'text-warm-400 hover:bg-navy-800 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base leading-none">{lang.flag}</span>
|
||||
<span className="hidden sm:inline">{lang.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Auth buttons */}
|
||||
<div>
|
||||
{user ? (
|
||||
|
|
@ -163,7 +188,7 @@ export default function MobileMenu({
|
|||
}}
|
||||
className="shrink-0 text-sm text-warm-400 hover:text-white"
|
||||
>
|
||||
Log out
|
||||
{t('userMenu.logOut')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -175,7 +200,7 @@ export default function MobileMenu({
|
|||
}}
|
||||
className="flex-1 px-3 py-2.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm text-center"
|
||||
>
|
||||
Log in
|
||||
{t('header.logIn')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -184,7 +209,7 @@ export default function MobileMenu({
|
|||
}}
|
||||
className="flex-1 px-3 py-2.5 rounded bg-teal-600 hover:bg-teal-700 transition-colors text-sm font-medium text-center"
|
||||
>
|
||||
Create account
|
||||
{t('header.createAccount')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,11 @@ export default function UpgradeModal({
|
|||
}, []);
|
||||
|
||||
const priceLabel =
|
||||
pricePence === null ? '...' : pricePence === 0 ? t('upgrade.free') : `\u00A3${pricePence / 100}`;
|
||||
pricePence === null
|
||||
? '...'
|
||||
: pricePence === 0
|
||||
? t('upgrade.free')
|
||||
: `\u00A3${pricePence / 100}`;
|
||||
const isFree = pricePence === 0;
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
|
|
@ -63,9 +67,7 @@ export default function UpgradeModal({
|
|||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-navy-950 to-teal-900 px-6 py-8 text-center">
|
||||
<h2 className="text-2xl font-bold text-white mb-2">{t('upgrade.title')}</h2>
|
||||
<p className="text-warm-300 text-sm">
|
||||
{t('upgrade.description')}
|
||||
</p>
|
||||
<p className="text-warm-300 text-sm">{t('upgrade.description')}</p>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
|
|
@ -74,12 +76,12 @@ export default function UpgradeModal({
|
|||
<span className="text-4xl font-extrabold text-navy-950 dark:text-warm-100">
|
||||
{priceLabel}
|
||||
</span>
|
||||
{!isFree && <span className="text-warm-500 dark:text-warm-400 text-lg">{t('upgrade.once')}</span>}
|
||||
{!isFree && (
|
||||
<span className="text-warm-500 dark:text-warm-400 text-lg">{t('upgrade.once')}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-center text-sm text-warm-500 dark:text-warm-400 mb-6">
|
||||
{isFree
|
||||
? t('upgrade.freeForEarly')
|
||||
: t('upgrade.oneTimePayment')}
|
||||
{isFree ? t('upgrade.freeForEarly') : t('upgrade.oneTimePayment')}
|
||||
</p>
|
||||
|
||||
{isLoggedIn ? (
|
||||
|
|
|
|||
|
|
@ -30,10 +30,7 @@ export interface AiFiltersContext {
|
|||
}
|
||||
|
||||
interface UseAiFiltersResult {
|
||||
fetchAiFilters: (
|
||||
query: string,
|
||||
context?: AiFiltersContext
|
||||
) => Promise<AiFiltersResult | null>;
|
||||
fetchAiFilters: (query: string, context?: AiFiltersContext) => Promise<AiFiltersResult | null>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
errorType: AiFilterErrorType | null;
|
||||
|
|
@ -47,7 +44,11 @@ function buildSummary(
|
|||
travelTimeFilters: AiTravelTimeFilter[],
|
||||
matchCount: number
|
||||
): string {
|
||||
const i18n = require('../i18n').default as { t: (key: string, opts?: Record<string, unknown>) => string };
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const i18n = require('../i18n').default as {
|
||||
t: (key: string, opts?: Record<string, unknown>) => string;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { ts } = require('../i18n/server') as { ts: (v: string) => string };
|
||||
const parts: string[] = [];
|
||||
|
||||
|
|
@ -83,10 +84,7 @@ export function useAiFilters(): UseAiFiltersResult {
|
|||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchAiFilters = useCallback(
|
||||
async (
|
||||
query: string,
|
||||
context?: AiFiltersContext
|
||||
): Promise<AiFiltersResult | null> => {
|
||||
async (query: string, context?: AiFiltersContext): Promise<AiFiltersResult | null> => {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ import { useState, useCallback } from 'react';
|
|||
* @param defaultCollapsed When true, groups start collapsed (tracks expanded groups).
|
||||
* When false (default), groups start expanded (tracks collapsed groups).
|
||||
*/
|
||||
export function useCollapsibleGroups(defaultCollapsed = false): [
|
||||
(name: string) => boolean,
|
||||
(name: string) => void,
|
||||
(name: string) => void,
|
||||
] {
|
||||
export function useCollapsibleGroups(
|
||||
defaultCollapsed = false
|
||||
): [(name: string) => boolean, (name: string) => void, (name: string) => void] {
|
||||
const [toggled, setToggled] = useState<Set<string>>(new Set());
|
||||
|
||||
const isExpanded = useCallback(
|
||||
|
|
|
|||
|
|
@ -134,11 +134,11 @@ export function useDeckLayers({
|
|||
? colorFeatureMeta.values.length
|
||||
: 0;
|
||||
|
||||
// --- Count ranges ---
|
||||
const countRange = useMemo(() => {
|
||||
if (data.length === 0) return { min: 0, max: 1 };
|
||||
if (data.length === 0) return { min: 0, max: 1, total: 0 };
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
let total = 0;
|
||||
for (const d of data) {
|
||||
if (viewportBounds) {
|
||||
if (
|
||||
|
|
@ -152,19 +152,21 @@ export function useDeckLayers({
|
|||
const c = d.count as number;
|
||||
if (c < min) min = c;
|
||||
if (c > max) max = c;
|
||||
total += c;
|
||||
}
|
||||
if (min === Infinity) return { min: 0, max: 1 };
|
||||
if (min === max) return { min, max: min + 1 };
|
||||
return { min, max };
|
||||
if (min === Infinity) return { min: 0, max: 1, total: 0 };
|
||||
if (min === max) return { min, max: min + 1, total };
|
||||
return { min, max, total };
|
||||
}, [data, viewportBounds]);
|
||||
|
||||
const countRangeRef = useRef(countRange);
|
||||
countRangeRef.current = countRange;
|
||||
|
||||
const postcodeCountRange = useMemo(() => {
|
||||
if (postcodeData.length === 0) return { min: 0, max: 1 };
|
||||
if (postcodeData.length === 0) return { min: 0, max: 1, total: 0 };
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
let total = 0;
|
||||
for (const d of postcodeData) {
|
||||
if (viewportBounds) {
|
||||
const [lng, lat] = d.properties.centroid as [number, number];
|
||||
|
|
@ -179,10 +181,11 @@ export function useDeckLayers({
|
|||
const c = d.properties.count;
|
||||
if (c < min) min = c;
|
||||
if (c > max) max = c;
|
||||
total += c;
|
||||
}
|
||||
if (min === Infinity) return { min: 0, max: 1 };
|
||||
if (min === max) return { min, max: min + 1 };
|
||||
return { min, max };
|
||||
if (min === Infinity) return { min: 0, max: 1, total: 0 };
|
||||
if (min === max) return { min, max: min + 1, total };
|
||||
return { min, max, total };
|
||||
}, [postcodeData, viewportBounds]);
|
||||
|
||||
const postcodeCountRangeRef = useRef(postcodeCountRange);
|
||||
|
|
|
|||
|
|
@ -291,3 +291,13 @@ export function tsDesc(featureName: string, englishFromServer: string): string {
|
|||
if (lang === 'en') return englishFromServer;
|
||||
return descriptions[lang]?.[featureName] ?? englishFromServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a feature detail (the longer explanatory paragraph in the info card).
|
||||
* Same pattern as tsDesc: English from server, other languages from this file.
|
||||
*/
|
||||
export function tsDetail(featureName: string, englishFromServer: string): string {
|
||||
const lang = i18n.language;
|
||||
if (lang === 'en') return englishFromServer;
|
||||
return details[lang]?.[featureName] ?? englishFromServer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,7 @@ const de: Translations = {
|
|||
logIn: 'Anmelden',
|
||||
createAccount: 'Konto erstellen',
|
||||
resetPassword: 'Passwort zurücksetzen',
|
||||
valueProp:
|
||||
'Speichere Suchen, merke dir Immobilien und mach dort weiter, wo du aufgehört hast.',
|
||||
valueProp: 'Speichere Suchen, merke dir Immobilien und mach dort weiter, wo du aufgehört hast.',
|
||||
continueWithGoogle: 'Weiter mit Google',
|
||||
email: 'E-Mail',
|
||||
emailPlaceholder: 'du@beispiel.de',
|
||||
|
|
@ -120,8 +119,7 @@ const de: Translations = {
|
|||
licenseSuccess: {
|
||||
title: 'Du bist dabei.',
|
||||
subtitle: 'Dein lebenslanger Zugang ist jetzt aktiv.',
|
||||
description:
|
||||
'Voller Zugang zu allen Funktionen, allen Postleitzahlen, in ganz England.',
|
||||
description: 'Voller Zugang zu allen Funktionen, allen Postleitzahlen, in ganz England.',
|
||||
startExploring: 'Jetzt entdecken',
|
||||
},
|
||||
|
||||
|
|
@ -139,8 +137,7 @@ const de: Translations = {
|
|||
'Sieh Kriminalität, Schulen, Lärm, Breitband und 50+ weitere Filter für ganz England.',
|
||||
oneTimeLifetime: 'Einmalzahlung, lebenslanger Zugang.',
|
||||
upgradeToFullMap: 'Zur Vollversion upgraden',
|
||||
chooseFilters:
|
||||
'Wähle die Filter, die dir wichtig sind. Die Karte aktualisiert sich sofort.',
|
||||
chooseFilters: 'Wähle die Filter, die dir wichtig sind. Die Karte aktualisiert sich sofort.',
|
||||
searchFeatures: 'Filter durchsuchen...',
|
||||
noMatchingFeatures: 'Keine passenden Filter',
|
||||
tryDifferentSearch: 'Versuche einen anderen Suchbegriff',
|
||||
|
|
@ -204,14 +201,11 @@ const de: Translations = {
|
|||
travelInfo: {
|
||||
transitDesc:
|
||||
' mit öffentlichen Verkehrsmitteln (Bus, Bahn, U-Bahn). Die Zeiten werden über ein typisches Werktags-Morgenfenster berechnet.',
|
||||
carDesc:
|
||||
' mit dem Auto, basierend auf typischen Straßengeschwindigkeiten und dem Straßennetz.',
|
||||
carDesc: ' mit dem Auto, basierend auf typischen Straßengeschwindigkeiten und dem Straßennetz.',
|
||||
bicycleDesc: ' mit dem Fahrrad, auf fahrradfreundlichen Strecken.',
|
||||
walkingDesc: ' zu Fuß, über Fußwege und Bürgersteige.',
|
||||
mainDesc:
|
||||
'Zeigt, wie lange es dauert, das ausgewählte Ziel von jedem Gebiet aus zu erreichen',
|
||||
sliderHint:
|
||||
'Verwende den Schieberegler, um deine maximale Pendelzeit festzulegen.',
|
||||
mainDesc: 'Zeigt, wie lange es dauert, das ausgewählte Ziel von jedem Gebiet aus zu erreichen',
|
||||
sliderHint: 'Verwende den Schieberegler, um deine maximale Pendelzeit festzulegen.',
|
||||
},
|
||||
|
||||
// ── AI Filter ──────────────────────────────────────
|
||||
|
|
@ -256,6 +250,7 @@ const de: Translations = {
|
|||
bathrooms: 'Badezimmer:',
|
||||
rooms: 'Zimmer:',
|
||||
built: 'Baujahr:',
|
||||
formerCouncil: 'Ehem. Sozialbau:',
|
||||
epcRating: 'EPC-Bewertung:',
|
||||
epcPotential: 'EPC-Potenzial:',
|
||||
listed: 'Inseriert:',
|
||||
|
|
@ -358,8 +353,7 @@ const de: Translations = {
|
|||
howStep2Title: 'Entdecke Gebiete und versteckte Perlen',
|
||||
howStep2Desc: 'Zoom rein, schau dir Details und Kann-Kriterien an.',
|
||||
howStep3Title: 'Einzelne Postleitzahlen erkunden',
|
||||
howStep3Desc:
|
||||
'Sieh einzelne Immobilien, Verkaufspreise, Wohnflächen und vergleiche.',
|
||||
howStep3Desc: 'Sieh einzelne Immobilien, Verkaufspreise, Wohnflächen und vergleiche.',
|
||||
howStep4Title: 'Engere Auswahl mit Zuversicht',
|
||||
howStep4Desc:
|
||||
'Jedes Gebiet auf deiner Liste erfüllt deine tatsächlichen Kriterien — nicht nur, was diese Woche inseriert war.',
|
||||
|
|
@ -375,17 +369,14 @@ const de: Translations = {
|
|||
compPropertyDataSub: '(Preis, EPC, Wohnfläche)',
|
||||
compFilters: '56 kombinierbare Filter an einem Ort',
|
||||
compFiltersSub: '(alle Einblicke, eine interaktive Karte)',
|
||||
ctaTitle:
|
||||
'Mach aus deiner größten Investition deine klügste Entscheidung.',
|
||||
ctaDescription:
|
||||
'Das verdient die richtigen Werkzeuge — überlass es nicht dem Zufall.',
|
||||
ctaTitle: 'Mach aus deiner größten Investition deine klügste Entscheidung.',
|
||||
ctaDescription: 'Das verdient die richtigen Werkzeuge — überlass es nicht dem Zufall.',
|
||||
},
|
||||
|
||||
// ── Pricing Page ───────────────────────────────────
|
||||
pricingPage: {
|
||||
title: 'Frühzugangspreis',
|
||||
subtitle:
|
||||
'Einmal zahlen, für immer nutzen. Je früher du dabei bist, desto weniger zahlst du.',
|
||||
subtitle: 'Einmal zahlen, für immer nutzen. Je früher du dabei bist, desto weniger zahlst du.',
|
||||
costContext:
|
||||
'Ein Hauskauf kostet £10.000+ an Grunderwerbsteuer, £1.500 an Anwaltsgebühren, £500 für ein Gutachten. Wählst du das falsche Gebiet, steckst du mit einem langen Pendelweg, schlechten Schulen oder einer Straße fest, von der du nichts wusstest.',
|
||||
lessThanSurvey: 'Weniger als ein Hausgutachten. Deutlich nützlicher.',
|
||||
|
|
@ -404,8 +395,7 @@ const de: Translations = {
|
|||
moneyBackGuarantee: '30 Tage Geld-zurück-Garantie',
|
||||
soldOut: 'Ausverkauft',
|
||||
upcoming: 'Demnächst',
|
||||
failedToLoad:
|
||||
'Preise konnten nicht geladen werden. Bitte später erneut versuchen.',
|
||||
failedToLoad: 'Preise konnten nicht geladen werden. Bitte später erneut versuchen.',
|
||||
feat1: '56 Datenebenen für ganz England',
|
||||
feat2: 'Jede Postleitzahl bewertet und filterbar',
|
||||
feat3: 'Unbegrenztes Erkunden der Karte und Exporte',
|
||||
|
|
@ -419,13 +409,17 @@ const de: Translations = {
|
|||
faq: 'Häufige Fragen',
|
||||
dataSources: 'Datenquellen',
|
||||
support: 'Support',
|
||||
dataSourcesIntro: 'Diese Anwendung kombiniert {{count}} offene Datensätze zu Immobilienpreisen, Energieeffizienz, Verkehr, Demografie, Kriminalität, Umwelt und mehr.',
|
||||
faqIntro: 'Ob Sie kaufen, mieten oder einfach nur stöbern – so hilft Ihnen Perfect Postcode, das richtige Gebiet zu finden.',
|
||||
supportIntro: 'Haben Sie eine Frage? Schauen Sie in unsere FAQ oder kontaktieren Sie uns direkt.',
|
||||
dataSourcesIntro:
|
||||
'Diese Anwendung kombiniert {{count}} offene Datensätze zu Immobilienpreisen, Energieeffizienz, Verkehr, Demografie, Kriminalität, Umwelt und mehr.',
|
||||
faqIntro:
|
||||
'Ob Sie kaufen, mieten oder einfach nur stöbern – so hilft Ihnen Perfect Postcode, das richtige Gebiet zu finden.',
|
||||
supportIntro:
|
||||
'Haben Sie eine Frage? Schauen Sie in unsere FAQ oder kontaktieren Sie uns direkt.',
|
||||
source: 'Quelle:',
|
||||
optOut: 'Widerspruch gegen öffentliche Offenlegung',
|
||||
attribution: 'Quellenangaben',
|
||||
attrLandRegistry: 'Enthält Daten des HM Land Registry © Crown copyright and database right 2025.',
|
||||
attrLandRegistry:
|
||||
'Enthält Daten des HM Land Registry © Crown copyright and database right 2025.',
|
||||
attrOgl: 'Enthält öffentliche Informationen lizenziert unter der',
|
||||
attrOglLink: 'Open Government Licence v3.0',
|
||||
attrOs: 'Enthält OS-Daten © Crown copyright and database rights 2025.',
|
||||
|
|
@ -440,43 +434,56 @@ const de: Translations = {
|
|||
dsPricePaidUse: 'Vollständige historische Immobilien-Verkaufspreise für England.',
|
||||
dsEpcName: 'Energy Performance Certificates (EPC)',
|
||||
dsEpcOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsEpcUse: 'Energieausweise für Wohngebäude mit Angaben zu Wohnfläche, Zimmeranzahl, Baujahr, Energiebewertungen, Immobilientyp und Bauform. Über Adresse innerhalb jeder Postleitzahl mit Price-Paid-Daten verknüpft. Eigentümer können der öffentlichen Offenlegung widersprechen.',
|
||||
dsEpcUse:
|
||||
'Energieausweise für Wohngebäude mit Angaben zu Wohnfläche, Zimmeranzahl, Baujahr, Energiebewertungen, Immobilientyp und Bauform. Über Adresse innerhalb jeder Postleitzahl mit Price-Paid-Daten verknüpft. Eigentümer können der öffentlichen Offenlegung widersprechen.',
|
||||
dsNsplName: 'National Statistics Postcode Lookup (NSPL)',
|
||||
dsNsplOrigin: 'ONS / ArcGIS',
|
||||
dsNsplUse: 'Ordnet Postleitzahlen Koordinaten und statistischen Gebietscodes zu, um alle gebietsbezogenen Datensätze mit einzelnen Immobilien zu verknüpfen.',
|
||||
dsNsplUse:
|
||||
'Ordnet Postleitzahlen Koordinaten und statistischen Gebietscodes zu, um alle gebietsbezogenen Datensätze mit einzelnen Immobilien zu verknüpfen.',
|
||||
dsIodName: 'English Indices of Deprivation 2025',
|
||||
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsIodUse: 'Relative Benachteiligungswerte für Einkommen, Beschäftigung, Bildung, Gesundheit, Kriminalität und Wohnumfeld für jedes Viertel in England.',
|
||||
dsIodUse:
|
||||
'Relative Benachteiligungswerte für Einkommen, Beschäftigung, Bildung, Gesundheit, Kriminalität und Wohnumfeld für jedes Viertel in England.',
|
||||
dsEthnicityName: 'Bevölkerung nach Ethnie (Zensus 2021)',
|
||||
dsEthnicityOrigin: 'ONS',
|
||||
dsEthnicityUse: 'Bevölkerungsanteile nach ethnischer Gruppe (südasiatisch, ostasiatisch, schwarz, gemischt, weiß, andere) pro Bezirk.',
|
||||
dsEthnicityUse:
|
||||
'Bevölkerungsanteile nach ethnischer Gruppe (südasiatisch, ostasiatisch, schwarz, gemischt, weiß, andere) pro Bezirk.',
|
||||
dsCrimeName: 'Street-level Crime Data',
|
||||
dsCrimeOrigin: 'data.police.uk',
|
||||
dsCrimeUse: 'Kriminalitätsdaten auf Straßenebene von 2023 bis 2025, aggregiert als Jahresdurchschnitte nach LSOA und Deliktsart (Gewalt, Einbruch, antisoziales Verhalten, Drogen, Fahrzeugkriminalität usw.).',
|
||||
dsCrimeUse:
|
||||
'Kriminalitätsdaten auf Straßenebene von 2023 bis 2025, aggregiert als Jahresdurchschnitte nach LSOA und Deliktsart (Gewalt, Einbruch, antisoziales Verhalten, Drogen, Fahrzeugkriminalität usw.).',
|
||||
dsOsmName: 'OpenStreetMap POIs',
|
||||
dsOsmOrigin: 'OpenStreetMap contributors / Geofabrik',
|
||||
dsOsmUse: 'Sehenswürdigkeiten und Einrichtungen wie Geschäfte, Restaurants, Gesundheitseinrichtungen, Freizeit, Tourismus und mehr in ganz Großbritannien.',
|
||||
dsOsmUse:
|
||||
'Sehenswürdigkeiten und Einrichtungen wie Geschäfte, Restaurants, Gesundheitseinrichtungen, Freizeit, Tourismus und mehr in ganz Großbritannien.',
|
||||
dsGreenspaceName: 'OS Open Greenspace',
|
||||
dsGreenspaceOrigin: 'Ordnance Survey',
|
||||
dsGreenspaceUse: 'Offizielle Grünflächengrenzen für Großbritannien, einschließlich öffentlicher Parks, Gärten, Sportplätze und Spielplätze. Polygon-Schwerpunkte werden für die Parknähezählung und Entfernungsberechnung zum nächsten Park verwendet.',
|
||||
dsGreenspaceUse:
|
||||
'Offizielle Grünflächengrenzen für Großbritannien, einschließlich öffentlicher Parks, Gärten, Sportplätze und Spielplätze. Polygon-Schwerpunkte werden für die Parknähezählung und Entfernungsberechnung zum nächsten Park verwendet.',
|
||||
dsNaptanName: 'NaPTAN (Public Transport Stops)',
|
||||
dsNaptanOrigin: 'Department for Transport',
|
||||
dsNaptanUse: 'Standorte von Bahnhöfen und Haltestellen für Bahn, Bus, U-Bahn/Straßenbahn, Fähre und Flughäfen in ganz England.',
|
||||
dsNaptanUse:
|
||||
'Standorte von Bahnhöfen und Haltestellen für Bahn, Bus, U-Bahn/Straßenbahn, Fähre und Flughäfen in ganz England.',
|
||||
dsNoiseName: 'Defra Noise Mapping',
|
||||
dsNoiseOrigin: 'Defra / Environment Agency',
|
||||
dsNoiseUse: 'Straßenlärmpegel (24-Stunden-gewichteter Durchschnitt) aus der strategischen Lärmkartierung 2022, hochauflösend modelliert und an jeder Postleitzahl abgetastet.',
|
||||
dsNoiseUse:
|
||||
'Straßenlärmpegel (24-Stunden-gewichteter Durchschnitt) aus der strategischen Lärmkartierung 2022, hochauflösend modelliert und an jeder Postleitzahl abgetastet.',
|
||||
dsOfstedName: 'Ofsted School Inspections',
|
||||
dsOfstedOrigin: 'Ofsted',
|
||||
dsOfstedUse: 'Neueste Inspektionsergebnisse für staatlich finanzierte Schulen (Stand April 2025). Pro Postleitzahl gemittelt für einen lokalen Schulqualitätswert (1=Hervorragend bis 4=Unzureichend).',
|
||||
dsOfstedUse:
|
||||
'Neueste Inspektionsergebnisse für staatlich finanzierte Schulen (Stand April 2025). Pro Postleitzahl gemittelt für einen lokalen Schulqualitätswert (1=Hervorragend bis 4=Unzureichend).',
|
||||
dsBroadbandName: 'Ofcom Broadband Performance',
|
||||
dsBroadbandOrigin: 'Ofcom',
|
||||
dsBroadbandUse: 'Festnetz-Breitbandabdeckung und maximale Download-Geschwindigkeiten nach Gebiet aus Ofcom Connected Nations 2025.',
|
||||
dsBroadbandUse:
|
||||
'Festnetz-Breitbandabdeckung und maximale Download-Geschwindigkeiten nach Gebiet aus Ofcom Connected Nations 2025.',
|
||||
dsCouncilTaxName: 'Council Tax Levels 2025-26',
|
||||
dsCouncilTaxOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsCouncilTaxUse: 'Jährliche Council-Tax-Sätze für die Stufen A bis H für alle 296 Abrechnungsbehörden in England, für eine von zwei Erwachsenen bewohnte Immobilie. Über den Bezirkscode aus dem NSPL-Postleitzahlenverzeichnis mit Immobilien verknüpft.',
|
||||
dsCouncilTaxUse:
|
||||
'Jährliche Council-Tax-Sätze für die Stufen A bis H für alle 296 Abrechnungsbehörden in England, für eine von zwei Erwachsenen bewohnte Immobilie. Über den Bezirkscode aus dem NSPL-Postleitzahlenverzeichnis mit Immobilien verknüpft.',
|
||||
dsRentalName: 'Private Rental Market Statistics',
|
||||
dsRentalOrigin: 'ONS / Valuation Office Agency',
|
||||
dsRentalUse: 'Monatliche Medianmieten des privaten Mietmarkts nach Bezirk und Schlafzimmerkategorie (Okt. 2022 - Sept. 2023). Über Bezirkscode und geschätzte Schlafzimmeranzahl mit Immobilien verknüpft.',
|
||||
dsRentalUse:
|
||||
'Monatliche Medianmieten des privaten Mietmarkts nach Bezirk und Schlafzimmerkategorie (Okt. 2022 - Sept. 2023). Über Bezirkscode und geschätzte Schlafzimmeranzahl mit Immobilien verknüpft.',
|
||||
// FAQ section titles
|
||||
faqFindingTitle: 'Ihr Gebiet finden',
|
||||
faqCommuteTitle: 'Pendelweg und Reisezeit',
|
||||
|
|
@ -488,62 +495,92 @@ const de: Translations = {
|
|||
faqPricingTitle: 'Preise und Zugang',
|
||||
faqTipsTitle: 'Tipps und Tricks',
|
||||
// FAQ items — Finding Your Area
|
||||
faqFinding1Q: 'Ich weiß nicht einmal, welche Gebiete ich mir ansehen soll. Kann mir das helfen?',
|
||||
faqFinding1A: 'Genau dafür ist es da. Legen Sie Ihre Filter fest (Budget, Pendelzeit, geringe Kriminalität, gute Schulen) und die Karte leuchtet auf, um Ihnen jedes Gebiet zu zeigen, das alle Kriterien erfüllt. Kein nächtliches Googeln nach „beste Wohngegenden bei Manchester“ mehr.',
|
||||
faqFinding1Q:
|
||||
'Ich weiß nicht einmal, welche Gebiete ich mir ansehen soll. Kann mir das helfen?',
|
||||
faqFinding1A:
|
||||
'Genau dafür ist es da. Legen Sie Ihre Filter fest (Budget, Pendelzeit, geringe Kriminalität, gute Schulen) und die Karte leuchtet auf, um Ihnen jedes Gebiet zu zeigen, das alle Kriterien erfüllt. Kein nächtliches Googeln nach „beste Wohngegenden bei Manchester“ mehr.',
|
||||
faqFinding2Q: 'Ich ziehe irgendwohin, wo ich noch nie war. Wie fange ich überhaupt an?',
|
||||
faqFinding2A: 'Stellen Sie Ihre Filter für das ein, was Ihnen wichtig ist, und die Karte hebt sofort die passenden Gebiete hervor. Sie gehen von „Ich kenne keine einzige Straße“ zu einer Auswahlliste in wenigen Minuten.',
|
||||
faqFinding2A:
|
||||
'Stellen Sie Ihre Filter für das ein, was Ihnen wichtig ist, und die Karte hebt sofort die passenden Gebiete hervor. Sie gehen von „Ich kenne keine einzige Straße“ zu einer Auswahlliste in wenigen Minuten.',
|
||||
faqFinding3Q: 'Wie finde ich Gebiete, die alle meine Kriterien gleichzeitig erfüllen?',
|
||||
faqFinding3A: 'Kombinieren Sie mehrere Filter (Kriminalität unter dem Durchschnitt, gute Schulen, Pendelweg unter 40 Minuten) und färben Sie die Karte nach Preis, um die Gebiete mit dem besten Preis-Leistungs-Verhältnis zu finden. Die Karte aktualisiert sich in Echtzeit, wenn Sie die Regler bewegen.',
|
||||
faqFinding3A:
|
||||
'Kombinieren Sie mehrere Filter (Kriminalität unter dem Durchschnitt, gute Schulen, Pendelweg unter 40 Minuten) und färben Sie die Karte nach Preis, um die Gebiete mit dem besten Preis-Leistungs-Verhältnis zu finden. Die Karte aktualisiert sich in Echtzeit, wenn Sie die Regler bewegen.',
|
||||
// FAQ items — Commute and Travel
|
||||
faqCommute1Q: 'Kann ich sehen, wie lange mein Pendelweg aus verschiedenen Gebieten tatsächlich dauern würde?',
|
||||
faqCommute1A: 'Legen Sie Ihren Arbeitsplatz als Ziel fest und wir färben jede Postleitzahl nach Fahrzeit – ob mit Auto, Fahrrad oder öffentlichen Verkehrsmitteln. Filtern Sie nach Ihrer maximalen Pendelzeit und der Rest verschwindet.',
|
||||
faqCommute1Q:
|
||||
'Kann ich sehen, wie lange mein Pendelweg aus verschiedenen Gebieten tatsächlich dauern würde?',
|
||||
faqCommute1A:
|
||||
'Legen Sie Ihren Arbeitsplatz als Ziel fest und wir färben jede Postleitzahl nach Fahrzeit – ob mit Auto, Fahrrad oder öffentlichen Verkehrsmitteln. Filtern Sie nach Ihrer maximalen Pendelzeit und der Rest verschwindet.',
|
||||
faqCommute2Q: 'Wie ist das besser als Google Maps?',
|
||||
faqCommute2A: 'Google Maps zeigt Ihnen eine Fahrt auf einmal. Wir färben jede Postleitzahl in England nach Pendelzeit in einem Blick, sodass Sie Hunderte von Gebieten nebeneinander vergleichen können, anstatt sie einzeln zu suchen.',
|
||||
faqCommute2A:
|
||||
'Google Maps zeigt Ihnen eine Fahrt auf einmal. Wir färben jede Postleitzahl in England nach Pendelzeit in einem Blick, sodass Sie Hunderte von Gebieten nebeneinander vergleichen können, anstatt sie einzeln zu suchen.',
|
||||
// FAQ items — Budget and Value
|
||||
faqBudget1Q: 'Wie finde ich Gebiete, in denen ich am meisten Wohnfläche für mein Geld bekomme?',
|
||||
faqBudget1A: 'Filtern Sie nach Preis pro m² und Sie sehen sofort, welche Postleitzahlen am meisten Fläche pro Pfund bieten. Kombinieren Sie es mit dem Energiebewertungsfilter, um Immobilien mit hohen Heizkosten zu vermeiden.',
|
||||
faqBudget2Q: 'Wie stelle ich sicher, dass ein günstiges Gebiet nicht aus gutem Grund günstig ist?',
|
||||
faqBudget2A: 'Legen Sie Benachteiligungswerte, Kriminalitätsstatistiken, Schulbewertungen und Breitbandgeschwindigkeiten neben den Preis. Wenn eine Postleitzahl erschwinglich ist und bei allem, was zählt, gut abschneidet, haben Sie echten Wert gefunden – nicht nur einen niedrigen Preis mit Kompromissen, die Sie noch nicht bemerkt haben.',
|
||||
faqBudget1A:
|
||||
'Filtern Sie nach Preis pro m² und Sie sehen sofort, welche Postleitzahlen am meisten Fläche pro Pfund bieten. Kombinieren Sie es mit dem Energiebewertungsfilter, um Immobilien mit hohen Heizkosten zu vermeiden.',
|
||||
faqBudget2Q:
|
||||
'Wie stelle ich sicher, dass ein günstiges Gebiet nicht aus gutem Grund günstig ist?',
|
||||
faqBudget2A:
|
||||
'Legen Sie Benachteiligungswerte, Kriminalitätsstatistiken, Schulbewertungen und Breitbandgeschwindigkeiten neben den Preis. Wenn eine Postleitzahl erschwinglich ist und bei allem, was zählt, gut abschneidet, haben Sie echten Wert gefunden – nicht nur einen niedrigen Preis mit Kompromissen, die Sie noch nicht bemerkt haben.',
|
||||
// FAQ items — Safety and Neighbourhood
|
||||
faqSafety1Q: 'Wie kann ich prüfen, ob ein Gebiet sicher ist, bevor ich dorthin ziehe?',
|
||||
faqSafety1A: 'Wir überlagern echte polizeilich erfasste Kriminalitätsdaten, aufgeschlüsselt nach Art, über jedes Viertel in England. Filtern Sie nach Gewaltkriminalität, Einbruch oder antisozialem Verhalten und sehen Sie sofort, welche Postleitzahlen die niedrigsten Zahlen haben.',
|
||||
faqSafety2Q: 'Ich finde ständig Wohnungen, die online toll aussehen, aber dann stellt sich die Gegend als schwierig heraus.',
|
||||
faqSafety2A: 'Genau dafür gibt es dieses Tool. Kombinieren Sie Kriminalitätsraten, Lärmpegel, Benachteiligungswerte, Pubs und Parks in der Nähe sowie Breitbandgeschwindigkeiten auf einer Karte, damit Sie wissen, wie ein Viertel wirklich ist, bevor Sie eine Besichtigung buchen.',
|
||||
faqSafety1A:
|
||||
'Wir überlagern echte polizeilich erfasste Kriminalitätsdaten, aufgeschlüsselt nach Art, über jedes Viertel in England. Filtern Sie nach Gewaltkriminalität, Einbruch oder antisozialem Verhalten und sehen Sie sofort, welche Postleitzahlen die niedrigsten Zahlen haben.',
|
||||
faqSafety2Q:
|
||||
'Ich finde ständig Wohnungen, die online toll aussehen, aber dann stellt sich die Gegend als schwierig heraus.',
|
||||
faqSafety2A:
|
||||
'Genau dafür gibt es dieses Tool. Kombinieren Sie Kriminalitätsraten, Lärmpegel, Benachteiligungswerte, Pubs und Parks in der Nähe sowie Breitbandgeschwindigkeiten auf einer Karte, damit Sie wissen, wie ein Viertel wirklich ist, bevor Sie eine Besichtigung buchen.',
|
||||
// FAQ items — Families and Schools
|
||||
faqFamilies1Q: 'Kann ich Gebiete mit guten Schulen UND geringer Kriminalität in einer Suche finden?',
|
||||
faqFamilies1A: 'Ja. Kombinieren Sie Filter für Ofsted-Bewertungen, Kriminalitätsraten, Parks und alles andere, was für Ihre Familie wichtig ist, und die Karte hebt nur die Gebiete hervor, die alles erfüllen. Kein Abgleich über fünf verschiedene Websites mehr.',
|
||||
faqFamilies1Q:
|
||||
'Kann ich Gebiete mit guten Schulen UND geringer Kriminalität in einer Suche finden?',
|
||||
faqFamilies1A:
|
||||
'Ja. Kombinieren Sie Filter für Ofsted-Bewertungen, Kriminalitätsraten, Parks und alles andere, was für Ihre Familie wichtig ist, und die Karte hebt nur die Gebiete hervor, die alles erfüllen. Kein Abgleich über fünf verschiedene Websites mehr.',
|
||||
faqFamilies2Q: 'Woher weiß ich, ob ein Viertel Parks und Spielplätze in der Nähe hat?',
|
||||
faqFamilies2A: 'Schalten Sie die POI-Ebene für Parks und Grünflächen ein, um sie direkt auf der Karte zu sehen. Sie können auch nach der Anzahl der fußläufig erreichbaren Parks pro Postleitzahl filtern.',
|
||||
faqFamilies2A:
|
||||
'Schalten Sie die POI-Ebene für Parks und Grünflächen ein, um sie direkt auf der Karte zu sehen. Sie können auch nach der Anzahl der fußläufig erreichbaren Parks pro Postleitzahl filtern.',
|
||||
// FAQ items — Environment and Quality of Life
|
||||
faqEnv1Q: 'Kann ich energieeffiziente Wohnungen finden, die nicht an einer lauten Straße liegen?',
|
||||
faqEnv1A: 'Filtern Sie nach EPC-Bewertung (A bis C), dann überlagern Sie die Straßenlärmdaten, um alles über Ihrem Schwellenwert auszuschließen. Färben Sie nach einem der beiden Kriterien, um ruhige, effiziente Straßen auf einen Blick zu erkennen.',
|
||||
faqEnv1Q:
|
||||
'Kann ich energieeffiziente Wohnungen finden, die nicht an einer lauten Straße liegen?',
|
||||
faqEnv1A:
|
||||
'Filtern Sie nach EPC-Bewertung (A bis C), dann überlagern Sie die Straßenlärmdaten, um alles über Ihrem Schwellenwert auszuschließen. Färben Sie nach einem der beiden Kriterien, um ruhige, effiziente Straßen auf einen Blick zu erkennen.',
|
||||
faqEnv2Q: 'Zeigt es Hochwasser- oder Senkungsrisiken?',
|
||||
faqEnv2A: 'Wir integrieren Bodenstabilitätsdaten, damit Sie vor dem Kauf auf Senkungen, Schrumpf-Quell-Tone und andere geologische Risiken prüfen können. Schließen Sie Risikogebiete frühzeitig aus.',
|
||||
faqEnv2A:
|
||||
'Wir integrieren Bodenstabilitätsdaten, damit Sie vor dem Kauf auf Senkungen, Schrumpf-Quell-Tone und andere geologische Risiken prüfen können. Schließen Sie Risikogebiete frühzeitig aus.',
|
||||
faqEnv3Q: 'Kann ich Gebiete mit schnellem Breitband finden, die wirklich ruhig sind?',
|
||||
faqEnv3A: 'Überlagern Sie den Breitbandfilter mit den Straßenlärmdaten, um Straßen mit guter Anbindung und wenig Verkehrslärm zu finden. Färben Sie nach einem der beiden Kriterien, um Gebiete auf einen Blick zu vergleichen.',
|
||||
faqEnv3A:
|
||||
'Überlagern Sie den Breitbandfilter mit den Straßenlärmdaten, um Straßen mit guter Anbindung und wenig Verkehrslärm zu finden. Färben Sie nach einem der beiden Kriterien, um Gebiete auf einen Blick zu vergleichen.',
|
||||
// FAQ items — Why Perfect Postcode
|
||||
faqWhy1Q: 'Ich benutze bereits Rightmove. Was bringt mir das zusätzlich?',
|
||||
faqWhy1A: 'Rightmove zeigt Ihnen Häuser. Wir zeigen Ihnen Gebiete. Kriminalitätsraten, Schulbewertungen, Breitbandgeschwindigkeiten, Lärmpegel, Benachteiligungswerte und mehr – alles filterbar auf einer Karte. Sie können ein Viertel beurteilen, bevor Sie sich die Angebote ansehen.',
|
||||
faqWhy1A:
|
||||
'Rightmove zeigt Ihnen Häuser. Wir zeigen Ihnen Gebiete. Kriminalitätsraten, Schulbewertungen, Breitbandgeschwindigkeiten, Lärmpegel, Benachteiligungswerte und mehr – alles filterbar auf einer Karte. Sie können ein Viertel beurteilen, bevor Sie sich die Angebote ansehen.',
|
||||
faqWhy2Q: 'Kann ich das nicht alles kostenlos selbst recherchieren?',
|
||||
faqWhy2A: 'Sie könnten Polizeidaten, Ofsted-Berichte, EPC-Register, Land-Registry-Einträge und ONS-Statistiken eine Postleitzahl nach der anderen abgleichen. Oder Sie haben alles filterbar und farbkodiert auf einer Karte in Sekunden.',
|
||||
faqWhy2A:
|
||||
'Sie könnten Polizeidaten, Ofsted-Berichte, EPC-Register, Land-Registry-Einträge und ONS-Statistiken eine Postleitzahl nach der anderen abgleichen. Oder Sie haben alles filterbar und farbkodiert auf einer Karte in Sekunden.',
|
||||
faqWhy3Q: 'Woher stammen die Daten tatsächlich?',
|
||||
faqWhy3A: 'Jeder Datensatz stammt aus offiziellen britischen Regierungsquellen: Land Registry, EPC-Register, ONS, Ofsted, Ofcom, data.police.uk und Defra. Wir scrapen keine Makler und erfinden nichts. Sie können jeden Eintrag anhand der Originalquelle überprüfen.',
|
||||
faqWhy3A:
|
||||
'Jeder Datensatz stammt aus offiziellen britischen Regierungsquellen: Land Registry, EPC-Register, ONS, Ofsted, Ofcom, data.police.uk und Defra. Wir scrapen keine Makler und erfinden nichts. Sie können jeden Eintrag anhand der Originalquelle überprüfen.',
|
||||
// FAQ items — Pricing and Access
|
||||
faqPricing1Q: 'Lohnt es sich wirklich, für ein Immobilien-Suchtool zu bezahlen?',
|
||||
faqPricing1A: 'Ein Hauskauf ist wahrscheinlich die größte Anschaffung Ihres Lebens. Ein einziges Warnsignal zu erkennen (eine laute Straße, schlechtes Breitband, steigende Kriminalität) bevor Sie sich festlegen, könnte Ihnen Jahre des Bedauerns ersparen. Das kostet weniger als eine Tankfüllung.',
|
||||
faqPricing1A:
|
||||
'Ein Hauskauf ist wahrscheinlich die größte Anschaffung Ihres Lebens. Ein einziges Warnsignal zu erkennen (eine laute Straße, schlechtes Breitband, steigende Kriminalität) bevor Sie sich festlegen, könnte Ihnen Jahre des Bedauerns ersparen. Das kostet weniger als eine Tankfüllung.',
|
||||
faqPricing2Q: 'Ist das ein Abonnement?',
|
||||
faqPricing2A: 'Nein. Einmalzahlung, Ihres für immer. Nutzen Sie es intensiv während Ihrer Suche, kommen Sie zurück, wenn Sie neugierig auf ein neues Gebiet sind, und es ist immer noch da, falls Sie erneut umziehen.',
|
||||
faqPricing2A:
|
||||
'Nein. Einmalzahlung, Ihres für immer. Nutzen Sie es intensiv während Ihrer Suche, kommen Sie zurück, wenn Sie neugierig auf ein neues Gebiet sind, und es ist immer noch da, falls Sie erneut umziehen.',
|
||||
faqPricing3Q: 'Was kann ich mit der kostenlosen Version nutzen?',
|
||||
faqPricing3A: 'Kostenlose Nutzer können alle Funktionen im Demogebiet erkunden (Innenstadt London, ungefähr Zonen 1 bis 2). Für den Zugang zu Daten für den Rest Englands benötigen Sie den lebenslangen Zugang.',
|
||||
faqPricing3A:
|
||||
'Kostenlose Nutzer können alle Funktionen im Demogebiet erkunden (Innenstadt London, ungefähr Zonen 1 bis 2). Für den Zugang zu Daten für den Rest Englands benötigen Sie den lebenslangen Zugang.',
|
||||
faqPricing4Q: 'Kann ich eine Rückerstattung erhalten?',
|
||||
faqPricing4A: 'Selbstverständlich. Wir bieten eine 30-Tage-Geld-zurück-Garantie. Wenn Sie nicht zufrieden sind, schreiben Sie innerhalb von 30 Tagen an support@perfect-postcode.co.uk für eine vollständige Rückerstattung.',
|
||||
faqPricing4A:
|
||||
'Selbstverständlich. Wir bieten eine 30-Tage-Geld-zurück-Garantie. Wenn Sie nicht zufrieden sind, schreiben Sie innerhalb von 30 Tagen an support@perfect-postcode.co.uk für eine vollständige Rückerstattung.',
|
||||
// FAQ items — Tips and Tricks
|
||||
faqTips1Q: 'Wie nutze ich den KI-Filter, anstatt Filter einzeln hinzuzufügen?',
|
||||
faqTips1A: 'Beschreiben Sie, was Sie suchen, z. B. „ruhige Gegend nahe guten Schulen mit schnellem Breitband unter £400k“, und die KI richtet alle relevanten Filter auf einmal ein. Passen Sie danach manuell an.',
|
||||
faqTips1A:
|
||||
'Beschreiben Sie, was Sie suchen, z. B. „ruhige Gegend nahe guten Schulen mit schnellem Breitband unter £400k“, und die KI richtet alle relevanten Filter auf einmal ein. Passen Sie danach manuell an.',
|
||||
faqTips2Q: 'Kann ich eine Suche speichern und später darauf zurückkommen?',
|
||||
faqTips2A: 'Klicken Sie auf Speichern und alles wird erfasst: Ihre Filter, die Zoomstufe und die angezeigte Datenebene. Machen Sie genau dort weiter, wo Sie aufgehört haben, oder teilen Sie den Link mit Ihrem Partner.',
|
||||
faqTips2A:
|
||||
'Klicken Sie auf Speichern und alles wird erfasst: Ihre Filter, die Zoomstufe und die angezeigte Datenebene. Machen Sie genau dort weiter, wo Sie aufgehört haben, oder teilen Sie den Link mit Ihrem Partner.',
|
||||
faqTips3Q: 'Kann ich die angezeigten Daten exportieren?',
|
||||
faqTips3A: 'Nutzen Sie den Export-Button, um die aktuell gefilterten Immobilien als Tabelle herunterzuladen. Der Export berücksichtigt alle aktiven Filter, sodass Sie genau die gewünschten Daten erhalten.',
|
||||
faqTips3A:
|
||||
'Nutzen Sie den Export-Button, um die aktuell gefilterten Immobilien als Tabelle herunterzuladen. Der Export berücksichtigt alle aktiven Filter, sodass Sie genau die gewünschten Daten erhalten.',
|
||||
},
|
||||
|
||||
// ── Account Page ───────────────────────────────────
|
||||
|
|
@ -582,8 +619,7 @@ const de: Translations = {
|
|||
|
||||
// ── Invites Page ───────────────────────────────────
|
||||
invitesPage: {
|
||||
inviteLinksLicensed:
|
||||
'Einladungslinks sind für lizenzierte Nutzer verfügbar.',
|
||||
inviteLinksLicensed: 'Einladungslinks sind für lizenzierte Nutzer verfügbar.',
|
||||
inviteAdminLabel: 'Freunde einladen (100% Rabatt)',
|
||||
inviteReferralLabel: 'Freunde einladen (30% Rabatt)',
|
||||
generateFreeInvite: 'Kostenlosen Einladungslink erstellen',
|
||||
|
|
@ -604,27 +640,20 @@ const de: Translations = {
|
|||
invitePage: {
|
||||
youreInvited: 'Du bist eingeladen!',
|
||||
specialOffer: 'Sonderangebot!',
|
||||
invitedByFree:
|
||||
'{{name}} hat dich eingeladen, kostenlosen lebenslangen Zugang zu erhalten.',
|
||||
invitedByDiscount:
|
||||
'{{name}} hat 30% Rabatt auf lebenslangen Zugang mit dir geteilt.',
|
||||
genericFreeInvite:
|
||||
'Du wurdest eingeladen, kostenlosen lebenslangen Zugang zu erhalten.',
|
||||
genericDiscount:
|
||||
'Ein Freund hat 30% Rabatt auf lebenslangen Zugang mit dir geteilt.',
|
||||
invitedByFree: '{{name}} hat dich eingeladen, kostenlosen lebenslangen Zugang zu erhalten.',
|
||||
invitedByDiscount: '{{name}} hat 30% Rabatt auf lebenslangen Zugang mit dir geteilt.',
|
||||
genericFreeInvite: 'Du wurdest eingeladen, kostenlosen lebenslangen Zugang zu erhalten.',
|
||||
genericDiscount: 'Ein Freund hat 30% Rabatt auf lebenslangen Zugang mit dir geteilt.',
|
||||
exploreEvery: 'Entdecke jedes Viertel in England',
|
||||
propertyInfo:
|
||||
'Immobilienpreise, Energiebewertungen, Kriminalitätsstatistiken, Schulbewertungen und mehr',
|
||||
invalidInvite: 'Ungültige Einladung',
|
||||
inviteAlreadyUsed: 'Einladung bereits verwendet',
|
||||
inviteAlreadyUsedDesc:
|
||||
'Dieser Einladungslink wurde bereits eingelöst.',
|
||||
inviteAlreadyUsedDesc: 'Dieser Einladungslink wurde bereits eingelöst.',
|
||||
invalidInviteLink: 'Ungültiger Einladungslink',
|
||||
invalidInviteLinkDesc:
|
||||
'Dieser Einladungslink ist ungültig oder abgelaufen.',
|
||||
invalidInviteLinkDesc: 'Dieser Einladungslink ist ungültig oder abgelaufen.',
|
||||
licenseActivated: 'Lizenz aktiviert!',
|
||||
fullAccessGranted:
|
||||
'Du hast jetzt vollen Zugang zu Perfect Postcode.',
|
||||
fullAccessGranted: 'Du hast jetzt vollen Zugang zu Perfect Postcode.',
|
||||
activating: 'Wird aktiviert...',
|
||||
activateLicense: 'Lizenz aktivieren',
|
||||
claimDiscount: 'Rabatt einlösen',
|
||||
|
|
@ -663,17 +692,23 @@ const de: Translations = {
|
|||
// ── Tutorial ──────────────────────────────────────
|
||||
tutorial: {
|
||||
step1Title: 'Sagen Sie der Karte, was zählt',
|
||||
step1Content: 'Legen Sie Ihr Budget, maximale Pendelzeit, Schulqualität und Kriminalitätsschwelle fest. Was Ihnen wichtig ist. Nur qualifizierende Gebiete bleiben hervorgehoben. Nutzen Sie das Augensymbol, um nach beliebigem Merkmal einzufärben.',
|
||||
step1Content:
|
||||
'Legen Sie Ihr Budget, maximale Pendelzeit, Schulqualität und Kriminalitätsschwelle fest. Was Ihnen wichtig ist. Nur qualifizierende Gebiete bleiben hervorgehoben. Nutzen Sie das Augensymbol, um nach beliebigem Merkmal einzufärben.',
|
||||
step2Title: 'Oder einfach beschreiben',
|
||||
step2Content: 'Tippen Sie auf Deutsch ein, was Sie suchen, z. B. „ruhige Gegend nahe guter Schulen unter £400k“, und wir richten die Filter für Sie ein.',
|
||||
step2Content:
|
||||
'Tippen Sie auf Deutsch ein, was Sie suchen, z. B. „ruhige Gegend nahe guter Schulen unter £400k“, und wir richten die Filter für Sie ein.',
|
||||
step3Title: 'Erkunden Sie, was es gibt',
|
||||
step3Content: 'Schwenken und zoomen Sie durch England. Klicken Sie auf ein beliebiges farbiges Gebiet, um Kriminalität, Schulen, Preise, Breitband, Lärm und mehr zu sehen.',
|
||||
step3Content:
|
||||
'Schwenken und zoomen Sie durch England. Klicken Sie auf ein beliebiges farbiges Gebiet, um Kriminalität, Schulen, Preise, Breitband, Lärm und mehr zu sehen.',
|
||||
step4Title: 'Direkt zu einem Ort springen',
|
||||
step4Content: 'Suchen Sie nach einem Ort oder einer Postleitzahl, um sofort dorthin zu gelangen.',
|
||||
step4Content:
|
||||
'Suchen Sie nach einem Ort oder einer Postleitzahl, um sofort dorthin zu gelangen.',
|
||||
step5Title: 'Ins Detail gehen',
|
||||
step5Content: 'Sehen Sie Gebietsstatistiken, Histogramme und einzelne Immobiliendaten: Preise, Wohnfläche, Energiebewertungen und mehr.',
|
||||
step5Content:
|
||||
'Sehen Sie Gebietsstatistiken, Histogramme und einzelne Immobiliendaten: Preise, Wohnfläche, Energiebewertungen und mehr.',
|
||||
step6Title: 'Was ist in der Nähe?',
|
||||
step6Content: 'Blenden Sie Schulen, Geschäfte, Bahnhöfe, Parks und Restaurants auf der Karte ein, um zu sehen, was erreichbar ist.',
|
||||
step6Content:
|
||||
'Blenden Sie Schulen, Geschäfte, Bahnhöfe, Parks und Restaurants auf der Karte ein, um zu sehen, was erreichbar ist.',
|
||||
},
|
||||
|
||||
// ── Server-derived values ──────────────────────────
|
||||
|
|
@ -681,13 +716,13 @@ const de: Translations = {
|
|||
// The English keys MUST match exactly what the API returns.
|
||||
server: {
|
||||
// ─ Feature group names ─
|
||||
'Properties': 'Immobilien',
|
||||
'Transport': 'Verkehr',
|
||||
'Education': 'Bildung',
|
||||
'Deprivation': 'Benachteiligung',
|
||||
'Crime': 'Kriminalität',
|
||||
'Demographics': 'Demografie',
|
||||
'Amenities': 'Infrastruktur',
|
||||
Properties: 'Immobilien',
|
||||
Transport: 'Verkehr',
|
||||
Education: 'Bildung',
|
||||
Deprivation: 'Benachteiligung',
|
||||
Crime: 'Kriminalität',
|
||||
Demographics: 'Demografie',
|
||||
Amenities: 'Infrastruktur',
|
||||
|
||||
// ─ Feature names (Properties) ─
|
||||
'Listing status': 'Inseratsstatus',
|
||||
|
|
@ -703,8 +738,8 @@ const de: Translations = {
|
|||
'Asking rent (monthly)': 'Angebotsmiete (monatlich)',
|
||||
'Total floor area (sqm)': 'Gesamtwohnfläche (m²)',
|
||||
'Number of bedrooms & living rooms': 'Anzahl Schlaf- & Wohnzimmer',
|
||||
'Bedrooms': 'Schlafzimmer',
|
||||
'Bathrooms': 'Badezimmer',
|
||||
Bedrooms: 'Schlafzimmer',
|
||||
Bathrooms: 'Badezimmer',
|
||||
'Construction year': 'Baujahr',
|
||||
'Date of last transaction': 'Datum der letzten Transaktion',
|
||||
'Listing date': 'Inseratsdatum',
|
||||
|
|
@ -714,7 +749,8 @@ const de: Translations = {
|
|||
'Interior height (m)': 'Raumhöhe (m)',
|
||||
|
||||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)': 'Entfernung zum nächsten Bahn- oder U-Bahnhof (km)',
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Entfernung zum nächsten Bahn- oder U-Bahnhof (km)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Gute+ Grundschulen im Umkreis von 2 km',
|
||||
|
|
@ -732,8 +768,10 @@ const de: Translations = {
|
|||
'Outdoors Sub-domain Score': 'Score der Umgebungsqualität (außen)',
|
||||
|
||||
// ─ Feature names (Crime) ─
|
||||
'Serious crime per 1k residents (avg/yr)': 'Schwere Straftaten pro 1k Einwohner (Durchschn./Jahr)',
|
||||
'Minor crime per 1k residents (avg/yr)': 'Leichte Straftaten pro 1k Einwohner (Durchschn./Jahr)',
|
||||
'Serious crime per 1k residents (avg/yr)':
|
||||
'Schwere Straftaten pro 1k Einwohner (Durchschn./Jahr)',
|
||||
'Minor crime per 1k residents (avg/yr)':
|
||||
'Leichte Straftaten pro 1k Einwohner (Durchschn./Jahr)',
|
||||
'Serious crime (avg/yr)': 'Schwere Straftaten (Durchschn./Jahr)',
|
||||
'Minor crime (avg/yr)': 'Leichte Straftaten (Durchschn./Jahr)',
|
||||
'Violence and sexual offences (avg/yr)': 'Gewalt- und Sexualdelikte (Durchschn./Jahr)',
|
||||
|
|
@ -764,24 +802,24 @@ const de: Translations = {
|
|||
'Distance to nearest park (km)': 'Entfernung zum nächsten Park (km)',
|
||||
'Number of parks within 2km': 'Anzahl Parks im Umkreis von 2 km',
|
||||
'Number of restaurants within 2km': 'Anzahl Restaurants im Umkreis von 2 km',
|
||||
'Number of grocery shops and supermarkets within 2km': 'Anzahl Lebensmittelgeschäfte und Supermärkte im Umkreis von 2 km',
|
||||
'Number of grocery shops and supermarkets within 2km':
|
||||
'Anzahl Lebensmittelgeschäfte und Supermärkte im Umkreis von 2 km',
|
||||
'Noise (dB)': 'Lärm (dB)',
|
||||
'Max available download speed (Mbps)': 'Max. verfügbare Downloadgeschwindigkeit (Mbps)',
|
||||
|
||||
|
||||
// ─ Enum values ─
|
||||
'Historical sale': 'Historischer Verkauf',
|
||||
'For sale': 'Zum Verkauf',
|
||||
'For rent': 'Zur Miete',
|
||||
'Detached': 'Freistehend',
|
||||
Detached: 'Freistehend',
|
||||
'Semi-Detached': 'Doppelhaushälfte',
|
||||
'Terraced': 'Reihenhaus',
|
||||
Terraced: 'Reihenhaus',
|
||||
'Flats/Maisonettes': 'Wohnungen/Maisonetten',
|
||||
'Other': 'Sonstige',
|
||||
'Freehold': 'Volleigentum',
|
||||
'Leasehold': 'Erbbaurecht',
|
||||
'Yes': 'Ja',
|
||||
'No': 'Nein',
|
||||
Other: 'Sonstige',
|
||||
Freehold: 'Volleigentum',
|
||||
Leasehold: 'Erbbaurecht',
|
||||
Yes: 'Ja',
|
||||
No: 'Nein',
|
||||
|
||||
// ─ Stacked chart labels ─
|
||||
'Serious crime': 'Schwere Straftaten',
|
||||
|
|
@ -790,52 +828,52 @@ const de: Translations = {
|
|||
|
||||
// ─ POI group names ─
|
||||
'Public Transport': 'Öffentlicher Nahverkehr',
|
||||
'Leisure': 'Freizeit',
|
||||
'Health': 'Gesundheit',
|
||||
Leisure: 'Freizeit',
|
||||
Health: 'Gesundheit',
|
||||
'Emergency Services': 'Rettungsdienste',
|
||||
'Groceries': 'Lebensmittel',
|
||||
Groceries: 'Lebensmittel',
|
||||
'Local Businesses': 'Lokale Geschäfte',
|
||||
'Culture': 'Kultur',
|
||||
'Services': 'Dienstleistungen',
|
||||
'Shops': 'Geschäfte',
|
||||
Culture: 'Kultur',
|
||||
Services: 'Dienstleistungen',
|
||||
Shops: 'Geschäfte',
|
||||
|
||||
// ─ POI categories ─
|
||||
'Airport': 'Flughafen',
|
||||
'Ferry': 'Fähre',
|
||||
Airport: 'Flughafen',
|
||||
Ferry: 'Fähre',
|
||||
'Rail station': 'Bahnhof',
|
||||
'Bus stop': 'Bushaltestelle',
|
||||
'Bus station': 'Busbahnhof',
|
||||
'Taxi rank': 'Taxistand',
|
||||
'Metro or Tram stop': 'U-Bahn- oder Straßenbahnhaltestelle',
|
||||
'Café': 'Café',
|
||||
'Restaurant': 'Restaurant',
|
||||
'Pub': 'Pub',
|
||||
'Bar': 'Bar',
|
||||
Café: 'Café',
|
||||
Restaurant: 'Restaurant',
|
||||
Pub: 'Pub',
|
||||
Bar: 'Bar',
|
||||
'Fast Food': 'Fast Food',
|
||||
'Nightclub': 'Nachtclub',
|
||||
'Cinema': 'Kino',
|
||||
'Theatre': 'Theater',
|
||||
Nightclub: 'Nachtclub',
|
||||
Cinema: 'Kino',
|
||||
Theatre: 'Theater',
|
||||
'Live Music & Events': 'Live-Musik & Veranstaltungen',
|
||||
'Park': 'Park',
|
||||
'Playground': 'Spielplatz',
|
||||
Park: 'Park',
|
||||
Playground: 'Spielplatz',
|
||||
'Sports Centre': 'Sportzentrum',
|
||||
'Entertainment': 'Unterhaltung',
|
||||
'Supermarket': 'Supermarkt',
|
||||
Entertainment: 'Unterhaltung',
|
||||
Supermarket: 'Supermarkt',
|
||||
'Convenience Store': 'Spätkauf',
|
||||
'Bakery': 'Bäckerei',
|
||||
Bakery: 'Bäckerei',
|
||||
'Butcher & Fishmonger': 'Metzgerei & Fischhändler',
|
||||
'Greengrocer': 'Gemüsehändler',
|
||||
Greengrocer: 'Gemüsehändler',
|
||||
'Off-Licence': 'Getränkeladen',
|
||||
'Deli & Specialty': 'Feinkost & Spezialitäten',
|
||||
'Fashion & Clothing': 'Mode & Bekleidung',
|
||||
'Electronics': 'Elektronik',
|
||||
Electronics: 'Elektronik',
|
||||
'Charity Shop': 'Secondhand-Laden',
|
||||
'DIY & Hardware': 'Baumarkt & Eisenwaren',
|
||||
'Home & Garden': 'Haus & Garten',
|
||||
'Bookshop': 'Buchhandlung',
|
||||
Bookshop: 'Buchhandlung',
|
||||
'Pet Shop': 'Tierhandlung',
|
||||
'Sports & Outdoor': 'Sport & Outdoor',
|
||||
'Newsagent': 'Zeitungshändler',
|
||||
Newsagent: 'Zeitungshändler',
|
||||
'Department Store': 'Kaufhaus',
|
||||
'Gift & Hobby': 'Geschenke & Hobby',
|
||||
'Specialist Shop': 'Fachgeschäft',
|
||||
|
|
@ -845,31 +883,31 @@ const de: Translations = {
|
|||
'Car Services': 'Autoservice',
|
||||
'Post Office': 'Postamt',
|
||||
'Vet & Pet Care': 'Tierarzt & Tierpflege',
|
||||
'Bank': 'Bank',
|
||||
Bank: 'Bank',
|
||||
'Travel Agent': 'Reisebüro',
|
||||
'Police': 'Polizei',
|
||||
Police: 'Polizei',
|
||||
'Fire Station': 'Feuerwache',
|
||||
'Ambulance Station': 'Rettungswache',
|
||||
'GP Surgery': 'Hausarztpraxis',
|
||||
'Dentist': 'Zahnarzt',
|
||||
'Pharmacy': 'Apotheke',
|
||||
Dentist: 'Zahnarzt',
|
||||
Pharmacy: 'Apotheke',
|
||||
'Hospital & Clinic': 'Krankenhaus & Klinik',
|
||||
'Optician': 'Optiker',
|
||||
'Physiotherapy': 'Physiotherapie',
|
||||
Optician: 'Optiker',
|
||||
Physiotherapy: 'Physiotherapie',
|
||||
'Counselling & Therapy': 'Beratung & Therapie',
|
||||
'Care Home': 'Pflegeheim',
|
||||
'Medical & Mobility': 'Medizintechnik & Mobilität',
|
||||
'Museum': 'Museum',
|
||||
'Gallery': 'Galerie',
|
||||
'Library': 'Bibliothek',
|
||||
Museum: 'Museum',
|
||||
Gallery: 'Galerie',
|
||||
Library: 'Bibliothek',
|
||||
'Place of Worship': 'Gebetsstätte',
|
||||
'Arts Centre': 'Kunstzentrum',
|
||||
'Zoo': 'Zoo',
|
||||
Zoo: 'Zoo',
|
||||
'Tourist Attraction': 'Touristenattraktion',
|
||||
'School': 'Schule',
|
||||
'Hotel': 'Hotel',
|
||||
School: 'Schule',
|
||||
Hotel: 'Hotel',
|
||||
'Local Business': 'Lokales Geschäft',
|
||||
'Offices': 'Büros',
|
||||
Offices: 'Büros',
|
||||
'EV Charging': 'E-Ladestation',
|
||||
'Fuel Station': 'Tankstelle',
|
||||
'Community Centre': 'Gemeindezentrum',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ const en = {
|
|||
properties: 'Properties',
|
||||
postcode: 'Postcode',
|
||||
noAreaSelected: 'No area selected',
|
||||
noAreaSelectedDesc: 'Click any coloured area on the map to see crime, schools, prices, and more',
|
||||
noAreaSelectedDesc:
|
||||
'Click any coloured area on the map to see crime, schools, prices, and more',
|
||||
clickForDetails: 'Click for details',
|
||||
property: 'property',
|
||||
propertiesPlural: 'properties',
|
||||
|
|
@ -86,7 +87,8 @@ const en = {
|
|||
// ── Upgrade Modal ──────────────────────────────────
|
||||
upgrade: {
|
||||
title: 'See all of England',
|
||||
description: "You're currently exploring the demo area. Get lifetime access to every postcode, every filter, every neighbourhood. One payment, forever.",
|
||||
description:
|
||||
"You're currently exploring the demo area. Get lifetime access to every postcode, every filter, every neighbourhood. One payment, forever.",
|
||||
free: 'Free',
|
||||
once: '/once',
|
||||
freeForEarly: 'Free for early adopters. No credit card required.',
|
||||
|
|
@ -128,7 +130,8 @@ const en = {
|
|||
rent: 'Rent',
|
||||
findingPerfectPostcode: 'Finding the Perfect Postcode',
|
||||
addFiltersHint: 'Add filters below to narrow the map to areas that match your criteria',
|
||||
upgradePrompt: 'See crime, schools, noise, broadband, and 50+ more filters across all of England.',
|
||||
upgradePrompt:
|
||||
'See crime, schools, noise, broadband, and 50+ more filters across all of England.',
|
||||
oneTimeLifetime: 'One-time payment, lifetime access.',
|
||||
upgradeToFullMap: 'Upgrade to full map',
|
||||
chooseFilters: 'Choose the filters that matter to you. The map updates as you go.',
|
||||
|
|
@ -148,7 +151,8 @@ const en = {
|
|||
|
||||
// ── Philosophy Popup ───────────────────────────────
|
||||
philosophy: {
|
||||
intro: 'Start with your must-haves, then layer on nice-to-haves. The map narrows as you add filters. The areas left are your best matches.',
|
||||
intro:
|
||||
'Start with your must-haves, then layer on nice-to-haves. The map narrows as you add filters. The areas left are your best matches.',
|
||||
step1Title: 'Budget and basics',
|
||||
step1Desc: '(price range, floor area, property type)',
|
||||
step2Title: 'Commute',
|
||||
|
|
@ -161,7 +165,7 @@ const en = {
|
|||
step5Desc: '(restaurants, parks, broadband speed)',
|
||||
step6Title: 'Energy',
|
||||
step6Desc: '(EPC ratings, insulation, heating costs)',
|
||||
tip: "Tip: if nothing matches, relax one constraint at a time to see which trade-off opens up the most options.",
|
||||
tip: 'Tip: if nothing matches, relax one constraint at a time to see which trade-off opens up the most options.',
|
||||
},
|
||||
|
||||
// ── Travel Time ────────────────────────────────────
|
||||
|
|
@ -171,7 +175,8 @@ const en = {
|
|||
selectDestination: 'Select destination...',
|
||||
bestCase: 'Best case',
|
||||
bestCaseTitle: 'Best case travel time',
|
||||
bestCaseDesc: 'Uses the fastest realistic journey time (if you time your departure well and catch good connections). The default uses the <strong>median</strong>, representing a typical journey regardless of when you leave.',
|
||||
bestCaseDesc:
|
||||
'Uses the fastest realistic journey time (if you time your departure well and catch good connections). The default uses the <strong>median</strong>, representing a typical journey regardless of when you leave.',
|
||||
previewOnMap: 'Preview on map',
|
||||
stopPreviewing: 'Stop previewing',
|
||||
removeTravelTime: 'Remove travel time',
|
||||
|
|
@ -191,7 +196,8 @@ const en = {
|
|||
|
||||
// ── Travel Time Info Popup ─────────────────────────
|
||||
travelInfo: {
|
||||
transitDesc: ' by public transport (bus, rail, tube). Times are computed across a typical weekday morning window.',
|
||||
transitDesc:
|
||||
' by public transport (bus, rail, tube). Times are computed across a typical weekday morning window.',
|
||||
carDesc: ' by car, based on typical road speeds and the road network.',
|
||||
bicycleDesc: ' by bicycle, using cycle-friendly routes.',
|
||||
walkingDesc: ' on foot, using pedestrian paths and pavements.',
|
||||
|
|
@ -212,7 +218,8 @@ const en = {
|
|||
searchingDestinations: 'Searching for destinations...',
|
||||
generatingFilters: 'Generating filters...',
|
||||
refiningResults: 'Refining results...',
|
||||
weeklyLimitReached: "You've reached the weekly AI usage limit. It will reset automatically next week.",
|
||||
weeklyLimitReached:
|
||||
"You've reached the weekly AI usage limit. It will reset automatically next week.",
|
||||
},
|
||||
|
||||
// ── Map Legend ─────────────────────────────────────
|
||||
|
|
@ -240,6 +247,7 @@ const en = {
|
|||
bathrooms: 'Bathrooms:',
|
||||
rooms: 'Rooms:',
|
||||
built: 'Built:',
|
||||
formerCouncil: 'Ex-council:',
|
||||
epcRating: 'EPC rating:',
|
||||
epcPotential: 'EPC potential:',
|
||||
listed: 'Listed:',
|
||||
|
|
@ -250,7 +258,8 @@ const en = {
|
|||
perSqm: '/m²',
|
||||
searchPlaceholder: 'Search by address or postcode...',
|
||||
propertyData: 'Property Data',
|
||||
propertyDataDesc: 'Prices come from HM Land Registry (what buyers actually paid). Floor area, energy ratings, construction year, and tenure come from official EPC surveys. Both sources are matched by address within each postcode.',
|
||||
propertyDataDesc:
|
||||
'Prices come from HM Land Registry (what buyers actually paid). Floor area, energy ratings, construction year, and tenure come from official EPC surveys. Both sources are matched by address within each postcode.',
|
||||
},
|
||||
|
||||
// ── Area Pane ──────────────────────────────────────
|
||||
|
|
@ -287,7 +296,8 @@ const en = {
|
|||
poiPane: {
|
||||
pois: 'POIs',
|
||||
pointsOfInterest: 'Points of Interest',
|
||||
poiDescription: 'Sourced from OpenStreetMap. Covers public transport stops, shops, restaurants, healthcare, leisure, and more. Updated regularly with complete category coverage.',
|
||||
poiDescription:
|
||||
'Sourced from OpenStreetMap. Covers public transport stops, shops, restaurants, healthcare, leisure, and more. Updated regularly with complete category coverage.',
|
||||
searchCategories: 'Search categories...',
|
||||
dataSourceInfo: 'Data source info',
|
||||
},
|
||||
|
|
@ -320,7 +330,8 @@ const en = {
|
|||
heroTitle2: 'Value',
|
||||
heroTitle3: 'Minimum Compromise.',
|
||||
heroSubtitle: 'House hunting? Make your biggest investment your smartest move.',
|
||||
heroDescription: 'So many options - choosing the right one can feel overwhelming. Our interactive map makes it simple: select your must-haves and instantly see the areas that fit.',
|
||||
heroDescription:
|
||||
'So many options - choosing the right one can feel overwhelming. Our interactive map makes it simple: select your must-haves and instantly see the areas that fit.',
|
||||
exploreTheMap: 'Explore the map',
|
||||
seeTheDifference: 'See the difference',
|
||||
statProperties: 'properties',
|
||||
|
|
@ -328,8 +339,10 @@ const en = {
|
|||
statEvery: 'Every',
|
||||
statPostcodeInEngland: 'postcode in England',
|
||||
ourPhilosophy: 'Our philosophy',
|
||||
philosophyP1: "On Rightmove, you pick an area first, then hope it's good. You end up cross-referencing crime stats, school reports, and broadband checkers across a dozen tabs, one postcode at a time.",
|
||||
philosophyP2: 'We flip that. Tell us what you need (budget, commute, schools, safety) and we show you every area in England that qualifies. No guesswork. No wasted viewings.',
|
||||
philosophyP1:
|
||||
"On Rightmove, you pick an area first, then hope it's good. You end up cross-referencing crime stats, school reports, and broadband checkers across a dozen tabs, one postcode at a time.",
|
||||
philosophyP2:
|
||||
'We flip that. Tell us what you need (budget, commute, schools, safety) and we show you every area in England that qualifies. No guesswork. No wasted viewings.',
|
||||
howToUseIt: 'How to use it',
|
||||
howStep1Title: 'Set your must-haves',
|
||||
howStep1Desc: 'Budget, commute, schools — the map shows only what qualifies.',
|
||||
|
|
@ -338,7 +351,8 @@ const en = {
|
|||
howStep3Title: 'Drill into postcodes',
|
||||
howStep3Desc: 'See individual properties, sale prices, floor area, and compare.',
|
||||
howStep4Title: 'Shortlist with confidence',
|
||||
howStep4Desc: 'Every area on your list meets your actual criteria — not just what was listed that week.',
|
||||
howStep4Desc:
|
||||
'Every area on your list meets your actual criteria — not just what was listed that week.',
|
||||
othersVs: 'Others vs',
|
||||
listingPortals: 'Listing portals',
|
||||
checkMyPostcode: '“Check my postcode”',
|
||||
|
|
@ -359,7 +373,8 @@ const en = {
|
|||
pricingPage: {
|
||||
title: 'Early access pricing',
|
||||
subtitle: 'Pay once, access forever. The earlier you join, the less you pay.',
|
||||
costContext: "Buying a home costs £10k+ in stamp duty, £1,500 in solicitor fees, £500 for a survey. Get the wrong area and you're stuck with a long commute, bad schools, or a road you didn't know about.",
|
||||
costContext:
|
||||
"Buying a home costs £10k+ in stamp duty, £1,500 in solicitor fees, £500 for a survey. Get the wrong area and you're stuck with a long commute, bad schools, or a road you didn't know about.",
|
||||
lessThanSurvey: 'Less than a home survey. Far more useful.',
|
||||
currentTier: 'Current tier',
|
||||
firstNUsers: 'First {{count}} users',
|
||||
|
|
@ -390,8 +405,10 @@ const en = {
|
|||
faq: 'FAQ',
|
||||
dataSources: 'Data Sources',
|
||||
support: 'Support',
|
||||
dataSourcesIntro: 'This application combines {{count}} open datasets covering property prices, energy performance, transport, demographics, crime, environment, and more.',
|
||||
faqIntro: "Whether you're buying, renting, or just exploring, here's how Perfect Postcode helps you find the right area.",
|
||||
dataSourcesIntro:
|
||||
'This application combines {{count}} open datasets covering property prices, energy performance, transport, demographics, crime, environment, and more.',
|
||||
faqIntro:
|
||||
"Whether you're buying, renting, or just exploring, here's how Perfect Postcode helps you find the right area.",
|
||||
supportIntro: 'Have a question? Check our FAQ or reach out to us directly.',
|
||||
source: 'Source:',
|
||||
optOut: 'Opt out of public disclosure',
|
||||
|
|
@ -411,43 +428,56 @@ const en = {
|
|||
dsPricePaidUse: 'Complete historical property sale prices for England.',
|
||||
dsEpcName: 'Energy Performance Certificates (EPC)',
|
||||
dsEpcOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsEpcUse: 'Domestic Energy Performance Certificates providing floor area, number of rooms, construction year, energy ratings, property type, and built form. Matched with Price Paid records by address within each postcode. Property owners can opt out of public disclosure.',
|
||||
dsEpcUse:
|
||||
'Domestic Energy Performance Certificates providing floor area, number of rooms, construction year, energy ratings, property type, and built form. Matched with Price Paid records by address within each postcode. Property owners can opt out of public disclosure.',
|
||||
dsNsplName: 'National Statistics Postcode Lookup (NSPL)',
|
||||
dsNsplOrigin: 'ONS / ArcGIS',
|
||||
dsNsplUse: 'Maps postcodes to coordinates and statistical area codes, used to link all area-level datasets to individual properties.',
|
||||
dsNsplUse:
|
||||
'Maps postcodes to coordinates and statistical area codes, used to link all area-level datasets to individual properties.',
|
||||
dsIodName: 'English Indices of Deprivation 2025',
|
||||
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsIodUse: 'Relative deprivation scores across income, employment, education, health, crime, and living environment for every neighbourhood in England.',
|
||||
dsIodUse:
|
||||
'Relative deprivation scores across income, employment, education, health, crime, and living environment for every neighbourhood in England.',
|
||||
dsEthnicityName: 'Population by Ethnicity (2021 Census)',
|
||||
dsEthnicityOrigin: 'ONS',
|
||||
dsEthnicityUse: 'Population percentages by ethnic group (South Asian, East Asian, Black, Mixed, White, Other) per local authority.',
|
||||
dsEthnicityUse:
|
||||
'Population percentages by ethnic group (South Asian, East Asian, Black, Mixed, White, Other) per local authority.',
|
||||
dsCrimeName: 'Street-level Crime Data',
|
||||
dsCrimeOrigin: 'data.police.uk',
|
||||
dsCrimeUse: 'Street-level crime data from 2023 to 2025, aggregated into yearly averages by LSOA and crime type (violence, burglary, anti-social behaviour, drugs, vehicle crime, etc.).',
|
||||
dsCrimeUse:
|
||||
'Street-level crime data from 2023 to 2025, aggregated into yearly averages by LSOA and crime type (violence, burglary, anti-social behaviour, drugs, vehicle crime, etc.).',
|
||||
dsOsmName: 'OpenStreetMap POIs',
|
||||
dsOsmOrigin: 'OpenStreetMap contributors / Geofabrik',
|
||||
dsOsmUse: 'Points of interest covering shops, restaurants, healthcare, leisure, tourism, and more across Great Britain.',
|
||||
dsOsmUse:
|
||||
'Points of interest covering shops, restaurants, healthcare, leisure, tourism, and more across Great Britain.',
|
||||
dsGreenspaceName: 'OS Open Greenspace',
|
||||
dsGreenspaceOrigin: 'Ordnance Survey',
|
||||
dsGreenspaceUse: 'Authoritative green space boundaries for Great Britain, including public parks, gardens, playing fields, and play spaces. Polygon centroids are used for park proximity counts and distance-to-nearest-park calculations.',
|
||||
dsGreenspaceUse:
|
||||
'Authoritative green space boundaries for Great Britain, including public parks, gardens, playing fields, and play spaces. Polygon centroids are used for park proximity counts and distance-to-nearest-park calculations.',
|
||||
dsNaptanName: 'NaPTAN (Public Transport Stops)',
|
||||
dsNaptanOrigin: 'Department for Transport',
|
||||
dsNaptanUse: 'Station and stop locations for rail, bus, metro/tram, ferry, and airports across England.',
|
||||
dsNaptanUse:
|
||||
'Station and stop locations for rail, bus, metro/tram, ferry, and airports across England.',
|
||||
dsNoiseName: 'Defra Noise Mapping',
|
||||
dsNoiseOrigin: 'Defra / Environment Agency',
|
||||
dsNoiseUse: 'Road noise levels (24-hour weighted average) from the 2022 strategic noise mapping, modelled at high resolution and sampled at each postcode.',
|
||||
dsNoiseUse:
|
||||
'Road noise levels (24-hour weighted average) from the 2022 strategic noise mapping, modelled at high resolution and sampled at each postcode.',
|
||||
dsOfstedName: 'Ofsted School Inspections',
|
||||
dsOfstedOrigin: 'Ofsted',
|
||||
dsOfstedUse: 'Latest inspection outcomes for state-funded schools (as at April 2025). Averaged per postcode to give a local school quality score (1=Outstanding to 4=Inadequate).',
|
||||
dsOfstedUse:
|
||||
'Latest inspection outcomes for state-funded schools (as at April 2025). Averaged per postcode to give a local school quality score (1=Outstanding to 4=Inadequate).',
|
||||
dsBroadbandName: 'Ofcom Broadband Performance',
|
||||
dsBroadbandOrigin: 'Ofcom',
|
||||
dsBroadbandUse: 'Fixed broadband coverage and maximum download speeds by area from Ofcom Connected Nations 2025.',
|
||||
dsBroadbandUse:
|
||||
'Fixed broadband coverage and maximum download speeds by area from Ofcom Connected Nations 2025.',
|
||||
dsCouncilTaxName: 'Council Tax Levels 2025-26',
|
||||
dsCouncilTaxOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsCouncilTaxUse: 'Annual council tax rates for Bands A-H for all 296 billing authorities in England, for a dwelling occupied by two adults. Joined to properties via local authority district code from the NSPL postcode lookup.',
|
||||
dsCouncilTaxUse:
|
||||
'Annual council tax rates for Bands A-H for all 296 billing authorities in England, for a dwelling occupied by two adults. Joined to properties via local authority district code from the NSPL postcode lookup.',
|
||||
dsRentalName: 'Private Rental Market Statistics',
|
||||
dsRentalOrigin: 'ONS / Valuation Office Agency',
|
||||
dsRentalUse: 'Median monthly private rental prices by local authority and bedroom category (Oct 2022 - Sep 2023). Joined to properties via local authority district code and estimated bedroom count.',
|
||||
dsRentalUse:
|
||||
'Median monthly private rental prices by local authority and bedroom category (Oct 2022 - Sep 2023). Joined to properties via local authority district code and estimated bedroom count.',
|
||||
// FAQ section titles
|
||||
faqFindingTitle: 'Finding Your Area',
|
||||
faqCommuteTitle: 'Commute and Travel',
|
||||
|
|
@ -460,61 +490,86 @@ const en = {
|
|||
faqTipsTitle: 'Tips and Tricks',
|
||||
// FAQ items — Finding Your Area
|
||||
faqFinding1Q: "I don't even know which areas to look at. Can this help?",
|
||||
faqFinding1A: "That's exactly what it's for. Set your filters (budget, commute time, low crime, good schools) and the map lights up to show you every area that ticks every box. No more Googling \"best areas to live near Manchester\" at midnight.",
|
||||
faqFinding1A:
|
||||
'That\'s exactly what it\'s for. Set your filters (budget, commute time, low crime, good schools) and the map lights up to show you every area that ticks every box. No more Googling "best areas to live near Manchester" at midnight.',
|
||||
faqFinding2Q: "I'm moving somewhere I've never been. How do I even start?",
|
||||
faqFinding2A: "Set your filters for what matters and the map instantly highlights the areas that qualify. You go from \"I don't know a single street\" to a shortlist in minutes.",
|
||||
faqFinding2A:
|
||||
'Set your filters for what matters and the map instantly highlights the areas that qualify. You go from "I don\'t know a single street" to a shortlist in minutes.',
|
||||
faqFinding3Q: 'How do I find areas that tick all my boxes at once?',
|
||||
faqFinding3A: 'Stack multiple filters (crime below average, good schools, commute under 40 minutes) then colour the map by price to spot the best value areas. The map updates live as you drag sliders, so you can see results change in real time.',
|
||||
faqFinding3A:
|
||||
'Stack multiple filters (crime below average, good schools, commute under 40 minutes) then colour the map by price to spot the best value areas. The map updates live as you drag sliders, so you can see results change in real time.',
|
||||
// FAQ items — Commute and Travel
|
||||
faqCommute1Q: 'Can I see how long my commute would actually be from different areas?',
|
||||
faqCommute1A: "Set your workplace as a destination and we'll colour every postcode by journey time, whether that's by car, bike, or public transport. Filter to your max commute and the rest disappears.",
|
||||
faqCommute1A:
|
||||
"Set your workplace as a destination and we'll colour every postcode by journey time, whether that's by car, bike, or public transport. Filter to your max commute and the rest disappears.",
|
||||
faqCommute2Q: 'How is that better than checking Google Maps?',
|
||||
faqCommute2A: 'Google Maps shows you one journey at a time. We colour every postcode in England by commute time in one go, so you can compare hundreds of areas side by side instead of searching them one by one.',
|
||||
faqCommute2A:
|
||||
'Google Maps shows you one journey at a time. We colour every postcode in England by commute time in one go, so you can compare hundreds of areas side by side instead of searching them one by one.',
|
||||
// FAQ items — Budget and Value
|
||||
faqBudget1Q: 'How do I find areas where I get the most space for my money?',
|
||||
faqBudget1A: "Filter by price per sqm and you'll instantly see which postcodes give you the most space per pound. Pair it with the energy rating filter to avoid properties with high heating costs.",
|
||||
faqBudget1A:
|
||||
"Filter by price per sqm and you'll instantly see which postcodes give you the most space per pound. Pair it with the energy rating filter to avoid properties with high heating costs.",
|
||||
faqBudget2Q: "How do I make sure a cheap area isn't cheap for a reason?",
|
||||
faqBudget2A: "Layer deprivation scores, crime stats, school ratings, and broadband speeds alongside price. If a postcode is affordable and scores well on everything that matters, you've found genuine value, not just a low price with trade-offs you haven't spotted yet.",
|
||||
faqBudget2A:
|
||||
"Layer deprivation scores, crime stats, school ratings, and broadband speeds alongside price. If a postcode is affordable and scores well on everything that matters, you've found genuine value, not just a low price with trade-offs you haven't spotted yet.",
|
||||
// FAQ items — Safety and Neighbourhood
|
||||
faqSafety1Q: 'How can I check if an area is safe before I move there?',
|
||||
faqSafety1A: 'We overlay real police-recorded crime data, broken down by type, onto every neighbourhood in England. Filter by violent crime, burglary, or antisocial behaviour and instantly see which postcodes have the lowest numbers.',
|
||||
faqSafety2Q: 'I keep finding flats that look great online, then the area turns out to be rough.',
|
||||
faqSafety2A: "That's exactly why this exists. Stack crime rates, noise levels, deprivation scores, nearby pubs and parks, and broadband speeds all on one map so you know what a neighbourhood is actually like before you book a viewing.",
|
||||
faqSafety1A:
|
||||
'We overlay real police-recorded crime data, broken down by type, onto every neighbourhood in England. Filter by violent crime, burglary, or antisocial behaviour and instantly see which postcodes have the lowest numbers.',
|
||||
faqSafety2Q:
|
||||
'I keep finding flats that look great online, then the area turns out to be rough.',
|
||||
faqSafety2A:
|
||||
"That's exactly why this exists. Stack crime rates, noise levels, deprivation scores, nearby pubs and parks, and broadband speeds all on one map so you know what a neighbourhood is actually like before you book a viewing.",
|
||||
// FAQ items — Families and Schools
|
||||
faqFamilies1Q: 'Can I find areas with good schools AND low crime in one search?',
|
||||
faqFamilies1A: "Yes. Stack filters for Ofsted ratings, crime rates, parks, and whatever else matters to your family, and the map highlights only the areas that tick every box. No more cross-referencing five different websites.",
|
||||
faqFamilies1A:
|
||||
'Yes. Stack filters for Ofsted ratings, crime rates, parks, and whatever else matters to your family, and the map highlights only the areas that tick every box. No more cross-referencing five different websites.',
|
||||
faqFamilies2Q: 'How do I know if a neighbourhood has parks and playgrounds nearby?',
|
||||
faqFamilies2A: 'Toggle on the parks and green spaces POI layer to see them right on the map. You can also filter by how many are within walking distance of each postcode.',
|
||||
faqFamilies2A:
|
||||
'Toggle on the parks and green spaces POI layer to see them right on the map. You can also filter by how many are within walking distance of each postcode.',
|
||||
// FAQ items — Environment and Quality of Life
|
||||
faqEnv1Q: "Can I find energy-efficient homes that aren't on a noisy road?",
|
||||
faqEnv1A: 'Filter by EPC rating (A to C), then layer on road noise data to rule out anything above your threshold. Colour-code by either feature to spot quiet, efficient streets at a glance.',
|
||||
faqEnv1A:
|
||||
'Filter by EPC rating (A to C), then layer on road noise data to rule out anything above your threshold. Colour-code by either feature to spot quiet, efficient streets at a glance.',
|
||||
faqEnv2Q: 'Does it show flood or subsidence risk?',
|
||||
faqEnv2A: "We include ground stability data so you can check for subsidence, shrink-swell clay, and other geological hazards before committing to a property. Filter out risky areas early.",
|
||||
faqEnv2A:
|
||||
'We include ground stability data so you can check for subsidence, shrink-swell clay, and other geological hazards before committing to a property. Filter out risky areas early.',
|
||||
faqEnv3Q: 'Can I find areas with fast broadband that are actually quiet?',
|
||||
faqEnv3A: 'Layer the broadband speed filter with road noise data to find streets with great connectivity and low traffic noise. Colour-code by either metric to compare areas at a glance.',
|
||||
faqEnv3A:
|
||||
'Layer the broadband speed filter with road noise data to find streets with great connectivity and low traffic noise. Colour-code by either metric to compare areas at a glance.',
|
||||
// FAQ items — Why Perfect Postcode
|
||||
faqWhy1Q: 'I already use Rightmove. What does this add?',
|
||||
faqWhy1A: "Rightmove shows you houses. We show you areas. Crime rates, school ratings, broadband speeds, noise levels, deprivation scores, and more, all filterable on one map. You can judge a neighbourhood before you even look at listings.",
|
||||
faqWhy1A:
|
||||
'Rightmove shows you houses. We show you areas. Crime rates, school ratings, broadband speeds, noise levels, deprivation scores, and more, all filterable on one map. You can judge a neighbourhood before you even look at listings.',
|
||||
faqWhy2Q: "Can't I just research all this myself for free?",
|
||||
faqWhy2A: 'You could cross-reference police data, Ofsted reports, EPC registers, Land Registry records, and ONS statistics one postcode at a time. Or you could have it all filterable and colour-coded on one map in seconds.',
|
||||
faqWhy2A:
|
||||
'You could cross-reference police data, Ofsted reports, EPC registers, Land Registry records, and ONS statistics one postcode at a time. Or you could have it all filterable and colour-coded on one map in seconds.',
|
||||
faqWhy3Q: 'Where does the data actually come from?',
|
||||
faqWhy3A: "Every dataset comes from official UK government sources: Land Registry, the EPC register, ONS, Ofsted, Ofcom, data.police.uk, and Defra. We don't scrape estate agents or make anything up. You can verify any record against the original source.",
|
||||
faqWhy3A:
|
||||
"Every dataset comes from official UK government sources: Land Registry, the EPC register, ONS, Ofsted, Ofcom, data.police.uk, and Defra. We don't scrape estate agents or make anything up. You can verify any record against the original source.",
|
||||
// FAQ items — Pricing and Access
|
||||
faqPricing1Q: 'Is it really worth paying for a property search tool?',
|
||||
faqPricing1A: "Buying a home is likely the biggest purchase you'll make. Spotting one red flag (a noisy road, poor broadband, rising crime) before committing could save you years of regret. This costs less than a tank of petrol.",
|
||||
faqPricing1A:
|
||||
"Buying a home is likely the biggest purchase you'll make. Spotting one red flag (a noisy road, poor broadband, rising crime) before committing could save you years of regret. This costs less than a tank of petrol.",
|
||||
faqPricing2Q: 'Is this a subscription?',
|
||||
faqPricing2A: "No. One-time payment, yours forever. Use it intensively during your search, come back whenever you're curious about a new area, and it's still there if you ever move again.",
|
||||
faqPricing2A:
|
||||
"No. One-time payment, yours forever. Use it intensively during your search, come back whenever you're curious about a new area, and it's still there if you ever move again.",
|
||||
faqPricing3Q: 'What can I access on the free tier?',
|
||||
faqPricing3A: 'Free users can explore all features within the demo area (inner London, roughly zones 1 to 2). To access data for the rest of England, you need lifetime access.',
|
||||
faqPricing3A:
|
||||
'Free users can explore all features within the demo area (inner London, roughly zones 1 to 2). To access data for the rest of England, you need lifetime access.',
|
||||
faqPricing4Q: 'Can I get a refund?',
|
||||
faqPricing4A: 'Absolutely. We offer a 30-day money-back guarantee. If you’re not satisfied, email support@perfect-postcode.co.uk within 30 days for a full refund.',
|
||||
faqPricing4A:
|
||||
'Absolutely. We offer a 30-day money-back guarantee. If you’re not satisfied, email support@perfect-postcode.co.uk within 30 days for a full refund.',
|
||||
// FAQ items — Tips and Tricks
|
||||
faqTips1Q: 'How do I use the AI filter instead of adding filters one by one?',
|
||||
faqTips1A: 'Type what you want in plain English, something like "quiet area near good schools with fast broadband under £400k", and it\'ll set up all the relevant filters in one go. Tweak any of them manually afterwards.',
|
||||
faqTips1A:
|
||||
'Type what you want in plain English, something like "quiet area near good schools with fast broadband under £400k", and it\'ll set up all the relevant filters in one go. Tweak any of them manually afterwards.',
|
||||
faqTips2Q: 'Can I save a search and come back to it later?',
|
||||
faqTips2A: 'Hit the save button and everything is captured: your filters, zoom level, and which data layer you’re colouring by. Pick up exactly where you left off or share the link with your partner.',
|
||||
faqTips2A:
|
||||
'Hit the save button and everything is captured: your filters, zoom level, and which data layer you’re colouring by. Pick up exactly where you left off or share the link with your partner.',
|
||||
faqTips3Q: "Can I export the data I'm looking at?",
|
||||
faqTips3A: 'Use the export button to download the currently filtered properties as a spreadsheet. The export respects all your active filters, so you get exactly the data you want.',
|
||||
faqTips3A:
|
||||
'Use the export button to download the currently filtered properties as a spreadsheet. The export respects all your active filters, so you get exactly the data you want.',
|
||||
},
|
||||
|
||||
// ── Account Page ───────────────────────────────────
|
||||
|
|
@ -532,17 +587,21 @@ const en = {
|
|||
savedPage: {
|
||||
searches: 'Searches',
|
||||
noSavedSearches: 'No saved searches yet',
|
||||
noSavedSearchesDesc: 'Save your filters and map view so you can pick up exactly where you left off.',
|
||||
noSavedSearchesDesc:
|
||||
'Save your filters and map view so you can pick up exactly where you left off.',
|
||||
noSavedProperties: 'No saved properties yet',
|
||||
noSavedPropertiesDesc: 'Bookmark properties as you explore and build your shortlist without losing track.',
|
||||
noSavedPropertiesDesc:
|
||||
'Bookmark properties as you explore and build your shortlist without losing track.',
|
||||
openPostcode: 'Open postcode',
|
||||
viewListing: 'View listing',
|
||||
clickToRename: 'Click to rename',
|
||||
notesPlaceholder: 'Jot down your thoughts...',
|
||||
deleteSearch: 'Delete search',
|
||||
deleteSearchConfirm: 'Are you sure you want to delete this saved search? This cannot be undone.',
|
||||
deleteSearchConfirm:
|
||||
'Are you sure you want to delete this saved search? This cannot be undone.',
|
||||
deleteProperty: 'Delete property',
|
||||
deletePropertyConfirm: 'Are you sure you want to delete this saved property? This cannot be undone.',
|
||||
deletePropertyConfirm:
|
||||
'Are you sure you want to delete this saved property? This cannot be undone.',
|
||||
bed: 'bed',
|
||||
epc: 'EPC',
|
||||
},
|
||||
|
|
@ -621,17 +680,22 @@ const en = {
|
|||
// ── Tutorial ──────────────────────────────────────
|
||||
tutorial: {
|
||||
step1Title: 'Tell the map what matters',
|
||||
step1Content: '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.',
|
||||
step1Content:
|
||||
'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.',
|
||||
step2Title: 'Or just describe it',
|
||||
step2Content: 'Type what you want in plain English, like "quiet area near good schools under £400k", and we’ll set up the filters for you.',
|
||||
step2Content:
|
||||
'Type what you want in plain English, like "quiet area near good schools under £400k", and we’ll set up the filters for you.',
|
||||
step3Title: 'Explore what’s out there',
|
||||
step3Content: 'Pan and zoom across England. Click any coloured area to see crime, schools, prices, broadband, noise, and more about that neighbourhood.',
|
||||
step3Content:
|
||||
'Pan and zoom across England. Click any coloured area to see crime, schools, prices, broadband, noise, and more about that neighbourhood.',
|
||||
step4Title: 'Jump to a location',
|
||||
step4Content: 'Search for any place or postcode to fly straight there.',
|
||||
step5Title: 'Dig into the details',
|
||||
step5Content: 'See area statistics, histograms, and individual property records: prices, floor area, energy ratings, and more.',
|
||||
step5Content:
|
||||
'See area statistics, histograms, and individual property records: prices, floor area, energy ratings, and more.',
|
||||
step6Title: 'What’s nearby?',
|
||||
step6Content: 'Toggle schools, shops, stations, parks, and restaurants on the map to see what’s within reach.',
|
||||
step6Content:
|
||||
'Toggle schools, shops, stations, parks, and restaurants on the map to see what’s within reach.',
|
||||
},
|
||||
|
||||
// ── Server-derived values ──────────────────────────
|
||||
|
|
@ -639,13 +703,13 @@ const en = {
|
|||
// The English keys MUST match exactly what the API returns.
|
||||
server: {
|
||||
// ─ Feature group names ─
|
||||
'Properties': 'Properties',
|
||||
'Transport': 'Transport',
|
||||
'Education': 'Education',
|
||||
'Deprivation': 'Deprivation',
|
||||
'Crime': 'Crime',
|
||||
'Demographics': 'Demographics',
|
||||
'Amenities': 'Amenities',
|
||||
Properties: 'Properties',
|
||||
Transport: 'Transport',
|
||||
Education: 'Education',
|
||||
Deprivation: 'Deprivation',
|
||||
Crime: 'Crime',
|
||||
Demographics: 'Demographics',
|
||||
Amenities: 'Amenities',
|
||||
|
||||
// ─ Feature names (Properties) ─
|
||||
'Listing status': 'Listing status',
|
||||
|
|
@ -661,8 +725,8 @@ const en = {
|
|||
'Asking rent (monthly)': 'Asking rent (monthly)',
|
||||
'Total floor area (sqm)': 'Total floor area (sqm)',
|
||||
'Number of bedrooms & living rooms': 'Number of bedrooms & living rooms',
|
||||
'Bedrooms': 'Bedrooms',
|
||||
'Bathrooms': 'Bathrooms',
|
||||
Bedrooms: 'Bedrooms',
|
||||
Bathrooms: 'Bathrooms',
|
||||
'Construction year': 'Construction year',
|
||||
'Date of last transaction': 'Date of last transaction',
|
||||
'Listing date': 'Listing date',
|
||||
|
|
@ -672,7 +736,8 @@ const en = {
|
|||
'Interior height (m)': 'Interior height (m)',
|
||||
|
||||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)': 'Distance to nearest train or tube station (km)',
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Distance to nearest train or tube station (km)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Good+ primary schools within 2km',
|
||||
|
|
@ -722,24 +787,24 @@ const en = {
|
|||
'Distance to nearest park (km)': 'Distance to nearest park (km)',
|
||||
'Number of parks within 2km': 'Number of parks within 2km',
|
||||
'Number of restaurants within 2km': 'Number of restaurants within 2km',
|
||||
'Number of grocery shops and supermarkets within 2km': 'Number of grocery shops and supermarkets within 2km',
|
||||
'Number of grocery shops and supermarkets within 2km':
|
||||
'Number of grocery shops and supermarkets within 2km',
|
||||
'Noise (dB)': 'Noise (dB)',
|
||||
'Max available download speed (Mbps)': 'Max available download speed (Mbps)',
|
||||
|
||||
|
||||
// ─ Enum values ─
|
||||
'Historical sale': 'Historical sale',
|
||||
'For sale': 'For sale',
|
||||
'For rent': 'For rent',
|
||||
'Detached': 'Detached',
|
||||
Detached: 'Detached',
|
||||
'Semi-Detached': 'Semi-Detached',
|
||||
'Terraced': 'Terraced',
|
||||
Terraced: 'Terraced',
|
||||
'Flats/Maisonettes': 'Flats/Maisonettes',
|
||||
'Other': 'Other',
|
||||
'Freehold': 'Freehold',
|
||||
'Leasehold': 'Leasehold',
|
||||
'Yes': 'Yes',
|
||||
'No': 'No',
|
||||
Other: 'Other',
|
||||
Freehold: 'Freehold',
|
||||
Leasehold: 'Leasehold',
|
||||
Yes: 'Yes',
|
||||
No: 'No',
|
||||
|
||||
// ─ Stacked chart labels ─
|
||||
'Serious crime': 'Serious crime',
|
||||
|
|
@ -748,52 +813,52 @@ const en = {
|
|||
|
||||
// ─ POI group names ─
|
||||
'Public Transport': 'Public Transport',
|
||||
'Leisure': 'Leisure',
|
||||
'Health': 'Health',
|
||||
Leisure: 'Leisure',
|
||||
Health: 'Health',
|
||||
'Emergency Services': 'Emergency Services',
|
||||
'Groceries': 'Groceries',
|
||||
Groceries: 'Groceries',
|
||||
'Local Businesses': 'Local Businesses',
|
||||
'Culture': 'Culture',
|
||||
'Services': 'Services',
|
||||
'Shops': 'Shops',
|
||||
Culture: 'Culture',
|
||||
Services: 'Services',
|
||||
Shops: 'Shops',
|
||||
|
||||
// ─ POI categories ─
|
||||
'Airport': 'Airport',
|
||||
'Ferry': 'Ferry',
|
||||
Airport: 'Airport',
|
||||
Ferry: 'Ferry',
|
||||
'Rail station': 'Rail station',
|
||||
'Bus stop': 'Bus stop',
|
||||
'Bus station': 'Bus station',
|
||||
'Taxi rank': 'Taxi rank',
|
||||
'Metro or Tram stop': 'Metro or Tram stop',
|
||||
'Café': 'Café',
|
||||
'Restaurant': 'Restaurant',
|
||||
'Pub': 'Pub',
|
||||
'Bar': 'Bar',
|
||||
Café: 'Café',
|
||||
Restaurant: 'Restaurant',
|
||||
Pub: 'Pub',
|
||||
Bar: 'Bar',
|
||||
'Fast Food': 'Fast Food',
|
||||
'Nightclub': 'Nightclub',
|
||||
'Cinema': 'Cinema',
|
||||
'Theatre': 'Theatre',
|
||||
Nightclub: 'Nightclub',
|
||||
Cinema: 'Cinema',
|
||||
Theatre: 'Theatre',
|
||||
'Live Music & Events': 'Live Music & Events',
|
||||
'Park': 'Park',
|
||||
'Playground': 'Playground',
|
||||
Park: 'Park',
|
||||
Playground: 'Playground',
|
||||
'Sports Centre': 'Sports Centre',
|
||||
'Entertainment': 'Entertainment',
|
||||
'Supermarket': 'Supermarket',
|
||||
Entertainment: 'Entertainment',
|
||||
Supermarket: 'Supermarket',
|
||||
'Convenience Store': 'Convenience Store',
|
||||
'Bakery': 'Bakery',
|
||||
Bakery: 'Bakery',
|
||||
'Butcher & Fishmonger': 'Butcher & Fishmonger',
|
||||
'Greengrocer': 'Greengrocer',
|
||||
Greengrocer: 'Greengrocer',
|
||||
'Off-Licence': 'Off-Licence',
|
||||
'Deli & Specialty': 'Deli & Specialty',
|
||||
'Fashion & Clothing': 'Fashion & Clothing',
|
||||
'Electronics': 'Electronics',
|
||||
Electronics: 'Electronics',
|
||||
'Charity Shop': 'Charity Shop',
|
||||
'DIY & Hardware': 'DIY & Hardware',
|
||||
'Home & Garden': 'Home & Garden',
|
||||
'Bookshop': 'Bookshop',
|
||||
Bookshop: 'Bookshop',
|
||||
'Pet Shop': 'Pet Shop',
|
||||
'Sports & Outdoor': 'Sports & Outdoor',
|
||||
'Newsagent': 'Newsagent',
|
||||
Newsagent: 'Newsagent',
|
||||
'Department Store': 'Department Store',
|
||||
'Gift & Hobby': 'Gift & Hobby',
|
||||
'Specialist Shop': 'Specialist Shop',
|
||||
|
|
@ -803,31 +868,31 @@ const en = {
|
|||
'Car Services': 'Car Services',
|
||||
'Post Office': 'Post Office',
|
||||
'Vet & Pet Care': 'Vet & Pet Care',
|
||||
'Bank': 'Bank',
|
||||
Bank: 'Bank',
|
||||
'Travel Agent': 'Travel Agent',
|
||||
'Police': 'Police',
|
||||
Police: 'Police',
|
||||
'Fire Station': 'Fire Station',
|
||||
'Ambulance Station': 'Ambulance Station',
|
||||
'GP Surgery': 'GP Surgery',
|
||||
'Dentist': 'Dentist',
|
||||
'Pharmacy': 'Pharmacy',
|
||||
Dentist: 'Dentist',
|
||||
Pharmacy: 'Pharmacy',
|
||||
'Hospital & Clinic': 'Hospital & Clinic',
|
||||
'Optician': 'Optician',
|
||||
'Physiotherapy': 'Physiotherapy',
|
||||
Optician: 'Optician',
|
||||
Physiotherapy: 'Physiotherapy',
|
||||
'Counselling & Therapy': 'Counselling & Therapy',
|
||||
'Care Home': 'Care Home',
|
||||
'Medical & Mobility': 'Medical & Mobility',
|
||||
'Museum': 'Museum',
|
||||
'Gallery': 'Gallery',
|
||||
'Library': 'Library',
|
||||
Museum: 'Museum',
|
||||
Gallery: 'Gallery',
|
||||
Library: 'Library',
|
||||
'Place of Worship': 'Place of Worship',
|
||||
'Arts Centre': 'Arts Centre',
|
||||
'Zoo': 'Zoo',
|
||||
Zoo: 'Zoo',
|
||||
'Tourist Attraction': 'Tourist Attraction',
|
||||
'School': 'School',
|
||||
'Hotel': 'Hotel',
|
||||
School: 'School',
|
||||
Hotel: 'Hotel',
|
||||
'Local Business': 'Local Business',
|
||||
'Offices': 'Offices',
|
||||
Offices: 'Offices',
|
||||
'EV Charging': 'EV Charging',
|
||||
'Fuel Station': 'Fuel Station',
|
||||
'Community Centre': 'Community Centre',
|
||||
|
|
|
|||
|
|
@ -91,12 +91,11 @@ const fr: Translations = {
|
|||
upgrade: {
|
||||
title: "Découvrez toute l'Angleterre",
|
||||
description:
|
||||
"Vous explorez actuellement la zone de démonstration. Obtenez un accès à vie à chaque code postal, chaque filtre, chaque quartier. Un seul paiement, pour toujours.",
|
||||
'Vous explorez actuellement la zone de démonstration. Obtenez un accès à vie à chaque code postal, chaque filtre, chaque quartier. Un seul paiement, pour toujours.',
|
||||
free: 'Gratuit',
|
||||
once: '/unique',
|
||||
freeForEarly: 'Gratuit pour les premiers utilisateurs. Aucune carte bancaire requise.',
|
||||
oneTimePayment:
|
||||
'Paiement unique. Accès à vie. Garantie satisfait ou remboursé sous 30 jours.',
|
||||
oneTimePayment: 'Paiement unique. Accès à vie. Garantie satisfait ou remboursé sous 30 jours.',
|
||||
redirecting: 'Redirection...',
|
||||
claimFreeAccess: "Réclamer l'accès gratuit",
|
||||
upgradeFor: 'Passer à la version complète pour {{price}}',
|
||||
|
|
@ -159,7 +158,7 @@ const fr: Translations = {
|
|||
// ── Philosophy Popup ───────────────────────────────
|
||||
philosophy: {
|
||||
intro:
|
||||
"Commencez par vos critères indispensables, puis ajoutez les critères souhaités. La carte se réduit au fur et à mesure que vous ajoutez des filtres. Les zones restantes sont vos meilleures correspondances.",
|
||||
'Commencez par vos critères indispensables, puis ajoutez les critères souhaités. La carte se réduit au fur et à mesure que vous ajoutez des filtres. Les zones restantes sont vos meilleures correspondances.',
|
||||
step1Title: 'Budget et fondamentaux',
|
||||
step1Desc: '(fourchette de prix, surface, type de bien)',
|
||||
step2Title: 'Trajet',
|
||||
|
|
@ -205,14 +204,12 @@ const fr: Translations = {
|
|||
travelInfo: {
|
||||
transitDesc:
|
||||
' en transports en commun (bus, train, métro). Les temps sont calculés sur une fenêtre typique d’un matin de semaine.',
|
||||
carDesc:
|
||||
' en voiture, basé sur les vitesses de circulation habituelles et le réseau routier.',
|
||||
carDesc: ' en voiture, basé sur les vitesses de circulation habituelles et le réseau routier.',
|
||||
bicycleDesc: ' à vélo, via des itinéraires adaptés aux cyclistes.',
|
||||
walkingDesc: ' à pied, via les chemins piétons et trottoirs.',
|
||||
mainDesc:
|
||||
'Affiche le temps nécessaire pour atteindre la destination sélectionnée depuis chaque zone',
|
||||
sliderHint:
|
||||
'Utilisez le curseur pour définir votre temps de trajet maximum.',
|
||||
sliderHint: 'Utilisez le curseur pour définir votre temps de trajet maximum.',
|
||||
},
|
||||
|
||||
// ── AI Filter ──────────────────────────────────────
|
||||
|
|
@ -220,8 +217,7 @@ const fr: Translations = {
|
|||
describeIdealArea: 'Décrivez votre zone idéale avec l’IA',
|
||||
aiSearch: 'Recherche IA',
|
||||
describeHint: 'décrivez ce que vous recherchez',
|
||||
placeholder:
|
||||
'ex. quartier calme, moins de £400k, près de bonnes écoles...',
|
||||
placeholder: 'ex. quartier calme, moins de £400k, près de bonnes écoles...',
|
||||
example1: 'Quartier sûr près de bonnes écoles',
|
||||
example2: '30 min de trajet jusqu’à Kings Cross, moins de £500k',
|
||||
example3: 'Village tranquille, 3 chambres, débit internet rapide',
|
||||
|
|
@ -258,6 +254,7 @@ const fr: Translations = {
|
|||
bathrooms: 'Salles de bain :',
|
||||
rooms: 'Pièces :',
|
||||
built: 'Construction :',
|
||||
formerCouncil: 'Ancien logement social :',
|
||||
epcRating: 'Classement DPE :',
|
||||
epcPotential: 'Potentiel DPE :',
|
||||
listed: 'Mise en vente :',
|
||||
|
|
@ -325,7 +322,7 @@ const fr: Translations = {
|
|||
lookupFailed: 'Échec de la recherche',
|
||||
searchLabel: 'Rechercher des lieux ou codes postaux',
|
||||
locateMe: 'Aller à ma position',
|
||||
geolocationUnsupported: 'La géolocalisation n\'est pas prise en charge par votre navigateur',
|
||||
geolocationUnsupported: "La géolocalisation n'est pas prise en charge par votre navigateur",
|
||||
geolocationFailed: 'Impossible de déterminer votre position',
|
||||
},
|
||||
|
||||
|
|
@ -356,8 +353,7 @@ const fr: Translations = {
|
|||
"Nous inversons la logique. Dites-nous ce qu'il vous faut (budget, trajet, écoles, sécurité) et nous vous montrons chaque zone d'Angleterre qui correspond. Plus de devinettes. Plus de visites inutiles.",
|
||||
howToUseIt: 'Comment l’utiliser',
|
||||
howStep1Title: 'Définissez vos indispensables',
|
||||
howStep1Desc:
|
||||
'Budget, trajet, écoles — la carte n’affiche que ce qui correspond.',
|
||||
howStep1Desc: 'Budget, trajet, écoles — la carte n’affiche que ce qui correspond.',
|
||||
howStep2Title: 'Explorez les zones et découvrez des pépites cachées',
|
||||
howStep2Desc: 'Zoomez, examinez les détails et les critères secondaires.',
|
||||
howStep3Title: 'Plongez dans les codes postaux',
|
||||
|
|
@ -378,17 +374,14 @@ const fr: Translations = {
|
|||
compPropertyDataSub: '(prix, DPE, surface)',
|
||||
compFilters: '56 filtres combinables en un seul endroit',
|
||||
compFiltersSub: '(toutes les informations, une seule carte interactive)',
|
||||
ctaTitle:
|
||||
'Faites de votre plus gros investissement votre meilleure décision.',
|
||||
ctaDescription:
|
||||
'Un tel enjeu mérite de vrais outils, ne laissez pas la chance décider.',
|
||||
ctaTitle: 'Faites de votre plus gros investissement votre meilleure décision.',
|
||||
ctaDescription: 'Un tel enjeu mérite de vrais outils, ne laissez pas la chance décider.',
|
||||
},
|
||||
|
||||
// ── Pricing Page ───────────────────────────────────
|
||||
pricingPage: {
|
||||
title: 'Tarifs early access',
|
||||
subtitle:
|
||||
"Payez une fois, accédez pour toujours. Plus vous rejoignez tôt, moins vous payez.",
|
||||
subtitle: 'Payez une fois, accédez pour toujours. Plus vous rejoignez tôt, moins vous payez.',
|
||||
costContext:
|
||||
"L'achat d'un bien coûte plus de £10 000 en droits de mutation, £1 500 en frais de notaire, £500 pour une expertise. Choisissez le mauvais quartier et vous vous retrouvez avec un long trajet, de mauvaises écoles ou une route dont vous ignoriez l'existence.",
|
||||
lessThanSurvey: "Moins cher qu'une expertise immobilière. Bien plus utile.",
|
||||
|
|
@ -407,8 +400,7 @@ const fr: Translations = {
|
|||
moneyBackGuarantee: 'Garantie satisfait ou remboursé sous 30 jours',
|
||||
soldOut: 'Épuisé',
|
||||
upcoming: 'À venir',
|
||||
failedToLoad:
|
||||
'Échec du chargement des tarifs. Veuillez réessayer plus tard.',
|
||||
failedToLoad: 'Échec du chargement des tarifs. Veuillez réessayer plus tard.',
|
||||
feat1: "56 couches de données à travers l'Angleterre",
|
||||
feat2: 'Chaque code postal noté et filtrable',
|
||||
feat3: 'Exploration de la carte et exportations illimitées',
|
||||
|
|
@ -422,13 +414,16 @@ const fr: Translations = {
|
|||
faq: 'FAQ',
|
||||
dataSources: 'Sources de données',
|
||||
support: 'Assistance',
|
||||
dataSourcesIntro: 'Cette application combine {{count}} jeux de données ouverts couvrant les prix immobiliers, la performance énergétique, les transports, la démographie, la criminalité, l’environnement et plus encore.',
|
||||
faqIntro: 'Que vous achetiez, louiez ou exploriez simplement, voici comment Perfect Postcode vous aide à trouver le bon quartier.',
|
||||
dataSourcesIntro:
|
||||
'Cette application combine {{count}} jeux de données ouverts couvrant les prix immobiliers, la performance énergétique, les transports, la démographie, la criminalité, l’environnement et plus encore.',
|
||||
faqIntro:
|
||||
'Que vous achetiez, louiez ou exploriez simplement, voici comment Perfect Postcode vous aide à trouver le bon quartier.',
|
||||
supportIntro: 'Vous avez une question ? Consultez notre FAQ ou contactez-nous directement.',
|
||||
source: 'Source :',
|
||||
optOut: 'Retrait de la divulgation publique',
|
||||
attribution: 'Attribution',
|
||||
attrLandRegistry: 'Contient des données du HM Land Registry © Crown copyright and database right 2025.',
|
||||
attrLandRegistry:
|
||||
'Contient des données du HM Land Registry © Crown copyright and database right 2025.',
|
||||
attrOgl: 'Contient des informations du secteur public sous licence',
|
||||
attrOglLink: 'Open Government Licence v3.0',
|
||||
attrOs: 'Contient des données OS © Crown copyright and database rights 2025.',
|
||||
|
|
@ -443,43 +438,56 @@ const fr: Translations = {
|
|||
dsPricePaidUse: 'Historique complet des prix de vente immobiliers en Angleterre.',
|
||||
dsEpcName: 'Energy Performance Certificates (EPC)',
|
||||
dsEpcOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsEpcUse: 'Certificats de performance énergétique domestiques fournissant la surface, le nombre de pièces, l’année de construction, les classements énergétiques, le type de bien et la forme du bâti. Associés aux données Price Paid par adresse au sein de chaque code postal. Les propriétaires peuvent demander le retrait de la divulgation publique.',
|
||||
dsEpcUse:
|
||||
'Certificats de performance énergétique domestiques fournissant la surface, le nombre de pièces, l’année de construction, les classements énergétiques, le type de bien et la forme du bâti. Associés aux données Price Paid par adresse au sein de chaque code postal. Les propriétaires peuvent demander le retrait de la divulgation publique.',
|
||||
dsNsplName: 'National Statistics Postcode Lookup (NSPL)',
|
||||
dsNsplOrigin: 'ONS / ArcGIS',
|
||||
dsNsplUse: 'Associe les codes postaux aux coordonnées et aux codes de zones statistiques, utilisé pour relier tous les jeux de données au niveau de la zone aux propriétés individuelles.',
|
||||
dsNsplUse:
|
||||
'Associe les codes postaux aux coordonnées et aux codes de zones statistiques, utilisé pour relier tous les jeux de données au niveau de la zone aux propriétés individuelles.',
|
||||
dsIodName: 'English Indices of Deprivation 2025',
|
||||
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsIodUse: 'Scores de défaveur relative couvrant le revenu, l’emploi, l’éducation, la santé, la criminalité et le cadre de vie pour chaque quartier d’Angleterre.',
|
||||
dsIodUse:
|
||||
'Scores de défaveur relative couvrant le revenu, l’emploi, l’éducation, la santé, la criminalité et le cadre de vie pour chaque quartier d’Angleterre.',
|
||||
dsEthnicityName: 'Population par ethnie (recensement 2021)',
|
||||
dsEthnicityOrigin: 'ONS',
|
||||
dsEthnicityUse: 'Pourcentages de population par groupe ethnique (sud-asiatique, est-asiatique, noir, mixte, blanc, autre) par autorité locale.',
|
||||
dsEthnicityUse:
|
||||
'Pourcentages de population par groupe ethnique (sud-asiatique, est-asiatique, noir, mixte, blanc, autre) par autorité locale.',
|
||||
dsCrimeName: 'Street-level Crime Data',
|
||||
dsCrimeOrigin: 'data.police.uk',
|
||||
dsCrimeUse: 'Données de criminalité de proximité de 2023 à 2025, agrégées en moyennes annuelles par LSOA et type d’infraction (violences, cambriolages, troubles à l’ordre public, stupéfiants, vols de véhicules, etc.).',
|
||||
dsCrimeUse:
|
||||
'Données de criminalité de proximité de 2023 à 2025, agrégées en moyennes annuelles par LSOA et type d’infraction (violences, cambriolages, troubles à l’ordre public, stupéfiants, vols de véhicules, etc.).',
|
||||
dsOsmName: 'OpenStreetMap POIs',
|
||||
dsOsmOrigin: 'OpenStreetMap contributors / Geofabrik',
|
||||
dsOsmUse: 'Points d’intérêt couvrant commerces, restaurants, santé, loisirs, tourisme et plus à travers la Grande-Bretagne.',
|
||||
dsOsmUse:
|
||||
'Points d’intérêt couvrant commerces, restaurants, santé, loisirs, tourisme et plus à travers la Grande-Bretagne.',
|
||||
dsGreenspaceName: 'OS Open Greenspace',
|
||||
dsGreenspaceOrigin: 'Ordnance Survey',
|
||||
dsGreenspaceUse: 'Limites officielles des espaces verts de Grande-Bretagne, incluant parcs publics, jardins, terrains de sport et aires de jeux. Les centroïdes des polygones sont utilisés pour le comptage de proximité des parcs et le calcul de la distance au parc le plus proche.',
|
||||
dsGreenspaceUse:
|
||||
'Limites officielles des espaces verts de Grande-Bretagne, incluant parcs publics, jardins, terrains de sport et aires de jeux. Les centroïdes des polygones sont utilisés pour le comptage de proximité des parcs et le calcul de la distance au parc le plus proche.',
|
||||
dsNaptanName: 'NaPTAN (Public Transport Stops)',
|
||||
dsNaptanOrigin: 'Department for Transport',
|
||||
dsNaptanUse: 'Emplacements des gares et arrêts pour le rail, le bus, le métro/tramway, le ferry et les aéroports à travers l’Angleterre.',
|
||||
dsNaptanUse:
|
||||
'Emplacements des gares et arrêts pour le rail, le bus, le métro/tramway, le ferry et les aéroports à travers l’Angleterre.',
|
||||
dsNoiseName: 'Defra Noise Mapping',
|
||||
dsNoiseOrigin: 'Defra / Environment Agency',
|
||||
dsNoiseUse: 'Niveaux de bruit routier (moyenne pondérée sur 24 heures) issus de la cartographie stratégique du bruit de 2022, modélisés à haute résolution et échantillonnés à chaque code postal.',
|
||||
dsNoiseUse:
|
||||
'Niveaux de bruit routier (moyenne pondérée sur 24 heures) issus de la cartographie stratégique du bruit de 2022, modélisés à haute résolution et échantillonnés à chaque code postal.',
|
||||
dsOfstedName: 'Ofsted School Inspections',
|
||||
dsOfstedOrigin: 'Ofsted',
|
||||
dsOfstedUse: 'Derniers résultats d’inspection des écoles publiques (avril 2025). Moyennés par code postal pour donner un score de qualité scolaire local (1=Excellent à 4=Insuffisant).',
|
||||
dsOfstedUse:
|
||||
'Derniers résultats d’inspection des écoles publiques (avril 2025). Moyennés par code postal pour donner un score de qualité scolaire local (1=Excellent à 4=Insuffisant).',
|
||||
dsBroadbandName: 'Ofcom Broadband Performance',
|
||||
dsBroadbandOrigin: 'Ofcom',
|
||||
dsBroadbandUse: 'Couverture haut débit fixe et débits de téléchargement maximum par zone, issus de Ofcom Connected Nations 2025.',
|
||||
dsBroadbandUse:
|
||||
'Couverture haut débit fixe et débits de téléchargement maximum par zone, issus de Ofcom Connected Nations 2025.',
|
||||
dsCouncilTaxName: 'Council Tax Levels 2025-26',
|
||||
dsCouncilTaxOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsCouncilTaxUse: 'Taux annuels de taxe d’habitation pour les tranches A à H pour les 296 autorités de facturation d’Angleterre, pour un logement occupé par deux adultes. Reliés aux propriétés via le code d’autorité locale du répertoire de codes postaux NSPL.',
|
||||
dsCouncilTaxUse:
|
||||
'Taux annuels de taxe d’habitation pour les tranches A à H pour les 296 autorités de facturation d’Angleterre, pour un logement occupé par deux adultes. Reliés aux propriétés via le code d’autorité locale du répertoire de codes postaux NSPL.',
|
||||
dsRentalName: 'Private Rental Market Statistics',
|
||||
dsRentalOrigin: 'ONS / Valuation Office Agency',
|
||||
dsRentalUse: 'Loyers mensuels médians du marché locatif privé par autorité locale et catégorie de chambres (oct. 2022 - sept. 2023). Reliés aux propriétés via le code d’autorité locale et le nombre estimé de chambres.',
|
||||
dsRentalUse:
|
||||
'Loyers mensuels médians du marché locatif privé par autorité locale et catégorie de chambres (oct. 2022 - sept. 2023). Reliés aux propriétés via le code d’autorité locale et le nombre estimé de chambres.',
|
||||
// FAQ section titles
|
||||
faqFindingTitle: 'Trouver votre quartier',
|
||||
faqCommuteTitle: 'Trajet et déplacements',
|
||||
|
|
@ -492,61 +500,90 @@ const fr: Translations = {
|
|||
faqTipsTitle: 'Astuces',
|
||||
// FAQ items — Finding Your Area
|
||||
faqFinding1Q: 'Je ne sais même pas quelles zones regarder. Est-ce que ça peut m’aider ?',
|
||||
faqFinding1A: 'C’est exactement à ça que ça sert. Définissez vos filtres (budget, temps de trajet, faible criminalité, bonnes écoles) et la carte s’illumine pour montrer chaque zone qui coche toutes les cases. Fini de chercher « meilleures zones pour vivre près de Manchester » à minuit.',
|
||||
faqFinding1A:
|
||||
'C’est exactement à ça que ça sert. Définissez vos filtres (budget, temps de trajet, faible criminalité, bonnes écoles) et la carte s’illumine pour montrer chaque zone qui coche toutes les cases. Fini de chercher « meilleures zones pour vivre près de Manchester » à minuit.',
|
||||
faqFinding2Q: 'Je déménage dans un endroit que je ne connais pas du tout. Par où commencer ?',
|
||||
faqFinding2A: 'Définissez vos filtres pour ce qui compte et la carte met instantanément en évidence les zones qui correspondent. Vous passez de « je ne connais pas une seule rue » à une sélection en quelques minutes.',
|
||||
faqFinding2A:
|
||||
'Définissez vos filtres pour ce qui compte et la carte met instantanément en évidence les zones qui correspondent. Vous passez de « je ne connais pas une seule rue » à une sélection en quelques minutes.',
|
||||
faqFinding3Q: 'Comment trouver des zones qui cochent toutes mes cases en une seule fois ?',
|
||||
faqFinding3A: 'Empilez plusieurs filtres (criminalité sous la moyenne, bonnes écoles, trajet de moins de 40 minutes) puis colorez la carte par prix pour repérer les zones au meilleur rapport qualité-prix. La carte se met à jour en temps réel quand vous bougez les curseurs.',
|
||||
faqFinding3A:
|
||||
'Empilez plusieurs filtres (criminalité sous la moyenne, bonnes écoles, trajet de moins de 40 minutes) puis colorez la carte par prix pour repérer les zones au meilleur rapport qualité-prix. La carte se met à jour en temps réel quand vous bougez les curseurs.',
|
||||
// FAQ items — Commute and Travel
|
||||
faqCommute1Q: 'Puis-je voir combien de temps durerait mon trajet depuis différentes zones ?',
|
||||
faqCommute1A: 'Définissez votre lieu de travail comme destination et nous colorons chaque code postal par temps de trajet, que ce soit en voiture, à vélo ou en transports en commun. Filtrez par votre trajet maximum et le reste disparaît.',
|
||||
faqCommute1A:
|
||||
'Définissez votre lieu de travail comme destination et nous colorons chaque code postal par temps de trajet, que ce soit en voiture, à vélo ou en transports en commun. Filtrez par votre trajet maximum et le reste disparaît.',
|
||||
faqCommute2Q: 'En quoi c’est mieux que Google Maps ?',
|
||||
faqCommute2A: 'Google Maps vous montre un trajet à la fois. Nous colorons chaque code postal d’Angleterre par temps de trajet en une seule vue, pour que vous puissiez comparer des centaines de zones côte à côte au lieu de les chercher une par une.',
|
||||
faqCommute2A:
|
||||
'Google Maps vous montre un trajet à la fois. Nous colorons chaque code postal d’Angleterre par temps de trajet en une seule vue, pour que vous puissiez comparer des centaines de zones côte à côte au lieu de les chercher une par une.',
|
||||
// FAQ items — Budget and Value
|
||||
faqBudget1Q: 'Comment trouver les zones où j’ai le plus d’espace pour mon argent ?',
|
||||
faqBudget1A: 'Filtrez par prix au m² et vous verrez instantanément quels codes postaux offrent le plus d’espace par livre. Combinez avec le filtre de classement énergétique pour éviter les biens aux coûts de chauffage élevés.',
|
||||
faqBudget2Q: 'Comment m’assurer qu’une zone bon marché ne l’est pas pour de mauvaises raisons ?',
|
||||
faqBudget2A: 'Superposez les scores de défaveur, les statistiques de criminalité, les notes des écoles et les débits internet à côté du prix. Si un code postal est abordable et obtient de bons scores sur tout ce qui compte, vous avez trouvé une vraie bonne affaire, pas juste un prix bas avec des compromis que vous n’avez pas encore repérés.',
|
||||
faqBudget1A:
|
||||
'Filtrez par prix au m² et vous verrez instantanément quels codes postaux offrent le plus d’espace par livre. Combinez avec le filtre de classement énergétique pour éviter les biens aux coûts de chauffage élevés.',
|
||||
faqBudget2Q:
|
||||
'Comment m’assurer qu’une zone bon marché ne l’est pas pour de mauvaises raisons ?',
|
||||
faqBudget2A:
|
||||
'Superposez les scores de défaveur, les statistiques de criminalité, les notes des écoles et les débits internet à côté du prix. Si un code postal est abordable et obtient de bons scores sur tout ce qui compte, vous avez trouvé une vraie bonne affaire, pas juste un prix bas avec des compromis que vous n’avez pas encore repérés.',
|
||||
// FAQ items — Safety and Neighbourhood
|
||||
faqSafety1Q: 'Comment vérifier si une zone est sûre avant d’y déménager ?',
|
||||
faqSafety1A: 'Nous superposons les données réelles de criminalité enregistrées par la police, ventilées par type, sur chaque quartier d’Angleterre. Filtrez par criminalité violente, cambriolages ou troubles à l’ordre public et voyez instantanément quels codes postaux ont les chiffres les plus bas.',
|
||||
faqSafety2Q: 'Je trouve sans cesse des appartements superbes en ligne, puis le quartier s’avère difficile.',
|
||||
faqSafety2A: 'C’est exactement pour ça que cet outil existe. Empilez taux de criminalité, niveaux de bruit, scores de défaveur, pubs et parcs à proximité, et débits internet, le tout sur une seule carte, pour savoir à quoi ressemble vraiment un quartier avant de réserver une visite.',
|
||||
faqSafety1A:
|
||||
'Nous superposons les données réelles de criminalité enregistrées par la police, ventilées par type, sur chaque quartier d’Angleterre. Filtrez par criminalité violente, cambriolages ou troubles à l’ordre public et voyez instantanément quels codes postaux ont les chiffres les plus bas.',
|
||||
faqSafety2Q:
|
||||
'Je trouve sans cesse des appartements superbes en ligne, puis le quartier s’avère difficile.',
|
||||
faqSafety2A:
|
||||
'C’est exactement pour ça que cet outil existe. Empilez taux de criminalité, niveaux de bruit, scores de défaveur, pubs et parcs à proximité, et débits internet, le tout sur une seule carte, pour savoir à quoi ressemble vraiment un quartier avant de réserver une visite.',
|
||||
// FAQ items — Families and Schools
|
||||
faqFamilies1Q: 'Puis-je trouver des zones avec de bonnes écoles ET peu de criminalité en une seule recherche ?',
|
||||
faqFamilies1A: 'Oui. Empilez les filtres pour les notes Ofsted, les taux de criminalité, les parcs et tout ce qui compte pour votre famille, et la carte ne met en évidence que les zones qui cochent toutes les cases. Fini de croiser cinq sites différents.',
|
||||
faqFamilies1Q:
|
||||
'Puis-je trouver des zones avec de bonnes écoles ET peu de criminalité en une seule recherche ?',
|
||||
faqFamilies1A:
|
||||
'Oui. Empilez les filtres pour les notes Ofsted, les taux de criminalité, les parcs et tout ce qui compte pour votre famille, et la carte ne met en évidence que les zones qui cochent toutes les cases. Fini de croiser cinq sites différents.',
|
||||
faqFamilies2Q: 'Comment savoir si un quartier a des parcs et des aires de jeux à proximité ?',
|
||||
faqFamilies2A: 'Activez la couche de POI parcs et espaces verts pour les voir directement sur la carte. Vous pouvez aussi filtrer par le nombre de parcs accessibles à pied depuis chaque code postal.',
|
||||
faqFamilies2A:
|
||||
'Activez la couche de POI parcs et espaces verts pour les voir directement sur la carte. Vous pouvez aussi filtrer par le nombre de parcs accessibles à pied depuis chaque code postal.',
|
||||
// FAQ items — Environment and Quality of Life
|
||||
faqEnv1Q: 'Puis-je trouver des logements économes en énergie qui ne sont pas sur une route bruyante ?',
|
||||
faqEnv1A: 'Filtrez par classement EPC (A à C), puis superposez les données de bruit routier pour exclure tout ce qui dépasse votre seuil. Colorez par l’un ou l’autre critère pour repérer les rues calmes et économes d’un coup d’œil.',
|
||||
faqEnv1Q:
|
||||
'Puis-je trouver des logements économes en énergie qui ne sont pas sur une route bruyante ?',
|
||||
faqEnv1A:
|
||||
'Filtrez par classement EPC (A à C), puis superposez les données de bruit routier pour exclure tout ce qui dépasse votre seuil. Colorez par l’un ou l’autre critère pour repérer les rues calmes et économes d’un coup d’œil.',
|
||||
faqEnv2Q: 'Est-ce que ça montre le risque d’inondation ou d’affaissement ?',
|
||||
faqEnv2A: 'Nous incluons des données de stabilité du sol pour que vous puissiez vérifier les risques d’affaissement, de retrait-gonflement des argiles et d’autres aléas géologiques avant de vous engager. Excluez les zones à risque dès le départ.',
|
||||
faqEnv2A:
|
||||
'Nous incluons des données de stabilité du sol pour que vous puissiez vérifier les risques d’affaissement, de retrait-gonflement des argiles et d’autres aléas géologiques avant de vous engager. Excluez les zones à risque dès le départ.',
|
||||
faqEnv3Q: 'Puis-je trouver des zones avec un bon débit internet qui soient aussi calmes ?',
|
||||
faqEnv3A: 'Superposez le filtre de débit internet avec les données de bruit routier pour trouver des rues avec une bonne connectivité et peu de bruit. Colorez par l’un ou l’autre critère pour comparer les zones d’un coup d’œil.',
|
||||
faqEnv3A:
|
||||
'Superposez le filtre de débit internet avec les données de bruit routier pour trouver des rues avec une bonne connectivité et peu de bruit. Colorez par l’un ou l’autre critère pour comparer les zones d’un coup d’œil.',
|
||||
// FAQ items — Why Perfect Postcode
|
||||
faqWhy1Q: 'J’utilise déjà Rightmove. Qu’est-ce que ça apporte de plus ?',
|
||||
faqWhy1A: 'Rightmove vous montre des maisons. Nous vous montrons des quartiers. Taux de criminalité, notes des écoles, débits internet, niveaux de bruit, scores de défaveur et plus, tout filtrable sur une seule carte. Vous pouvez juger un quartier avant même de regarder les annonces.',
|
||||
faqWhy1A:
|
||||
'Rightmove vous montre des maisons. Nous vous montrons des quartiers. Taux de criminalité, notes des écoles, débits internet, niveaux de bruit, scores de défaveur et plus, tout filtrable sur une seule carte. Vous pouvez juger un quartier avant même de regarder les annonces.',
|
||||
faqWhy2Q: 'Je ne peux pas simplement faire ces recherches gratuitement moi-même ?',
|
||||
faqWhy2A: 'Vous pourriez croiser les données policières, les rapports Ofsted, les registres EPC, les archives du Land Registry et les statistiques ONS un code postal à la fois. Ou vous pouvez avoir le tout filtrable et coloré sur une seule carte en quelques secondes.',
|
||||
faqWhy2A:
|
||||
'Vous pourriez croiser les données policières, les rapports Ofsted, les registres EPC, les archives du Land Registry et les statistiques ONS un code postal à la fois. Ou vous pouvez avoir le tout filtrable et coloré sur une seule carte en quelques secondes.',
|
||||
faqWhy3Q: 'D’où viennent réellement les données ?',
|
||||
faqWhy3A: 'Chaque jeu de données provient de sources officielles du gouvernement britannique : Land Registry, le registre EPC, ONS, Ofsted, Ofcom, data.police.uk et Defra. Nous ne scrapons pas les agents immobiliers et n’inventons rien. Vous pouvez vérifier chaque donnée auprès de la source originale.',
|
||||
faqWhy3A:
|
||||
'Chaque jeu de données provient de sources officielles du gouvernement britannique : Land Registry, le registre EPC, ONS, Ofsted, Ofcom, data.police.uk et Defra. Nous ne scrapons pas les agents immobiliers et n’inventons rien. Vous pouvez vérifier chaque donnée auprès de la source originale.',
|
||||
// FAQ items — Pricing and Access
|
||||
faqPricing1Q: 'Est-ce que ça vaut vraiment le coup de payer pour un outil de recherche immobilière ?',
|
||||
faqPricing1A: 'L’achat d’un logement est probablement le plus gros achat de votre vie. Repérer un seul signal d’alerte (une route bruyante, un mauvais débit, une criminalité en hausse) avant de vous engager pourrait vous épargner des années de regrets. Ça coûte moins qu’un plein d’essence.',
|
||||
faqPricing1Q:
|
||||
'Est-ce que ça vaut vraiment le coup de payer pour un outil de recherche immobilière ?',
|
||||
faqPricing1A:
|
||||
'L’achat d’un logement est probablement le plus gros achat de votre vie. Repérer un seul signal d’alerte (une route bruyante, un mauvais débit, une criminalité en hausse) avant de vous engager pourrait vous épargner des années de regrets. Ça coûte moins qu’un plein d’essence.',
|
||||
faqPricing2Q: 'Est-ce un abonnement ?',
|
||||
faqPricing2A: 'Non. Paiement unique, à vous pour toujours. Utilisez-le intensivement pendant votre recherche, revenez quand vous êtes curieux d’une nouvelle zone, et c’est toujours là si vous déménagez à nouveau.',
|
||||
faqPricing2A:
|
||||
'Non. Paiement unique, à vous pour toujours. Utilisez-le intensivement pendant votre recherche, revenez quand vous êtes curieux d’une nouvelle zone, et c’est toujours là si vous déménagez à nouveau.',
|
||||
faqPricing3Q: 'Que puis-je faire avec la version gratuite ?',
|
||||
faqPricing3A: 'Les utilisateurs gratuits peuvent explorer toutes les fonctionnalités dans la zone de démonstration (centre de Londres, approximativement zones 1 à 2). Pour accéder aux données du reste de l’Angleterre, il faut l’accès à vie.',
|
||||
faqPricing3A:
|
||||
'Les utilisateurs gratuits peuvent explorer toutes les fonctionnalités dans la zone de démonstration (centre de Londres, approximativement zones 1 à 2). Pour accéder aux données du reste de l’Angleterre, il faut l’accès à vie.',
|
||||
faqPricing4Q: 'Puis-je obtenir un remboursement ?',
|
||||
faqPricing4A: 'Absolument. Nous offrons une garantie satisfait ou remboursé sous 30 jours. Si vous n’êtes pas satisfait, envoyez un e-mail à support@perfect-postcode.co.uk dans les 30 jours pour un remboursement intégral.',
|
||||
faqPricing4A:
|
||||
'Absolument. Nous offrons une garantie satisfait ou remboursé sous 30 jours. Si vous n’êtes pas satisfait, envoyez un e-mail à support@perfect-postcode.co.uk dans les 30 jours pour un remboursement intégral.',
|
||||
// FAQ items — Tips and Tricks
|
||||
faqTips1Q: 'Comment utiliser le filtre IA au lieu d’ajouter les filtres un par un ?',
|
||||
faqTips1A: 'Tapez ce que vous voulez en langage courant, par exemple « quartier calme près de bonnes écoles avec bon débit internet à moins de £400k », et il configurera tous les filtres pertinents d’un coup. Ajustez ensuite manuellement si nécessaire.',
|
||||
faqTips1A:
|
||||
'Tapez ce que vous voulez en langage courant, par exemple « quartier calme près de bonnes écoles avec bon débit internet à moins de £400k », et il configurera tous les filtres pertinents d’un coup. Ajustez ensuite manuellement si nécessaire.',
|
||||
faqTips2Q: 'Puis-je enregistrer une recherche et y revenir plus tard ?',
|
||||
faqTips2A: 'Cliquez sur le bouton d’enregistrement et tout est capturé : vos filtres, le niveau de zoom et la couche de données affichée. Reprenez exactement où vous en étiez ou partagez le lien avec votre conjoint.',
|
||||
faqTips2A:
|
||||
'Cliquez sur le bouton d’enregistrement et tout est capturé : vos filtres, le niveau de zoom et la couche de données affichée. Reprenez exactement où vous en étiez ou partagez le lien avec votre conjoint.',
|
||||
faqTips3Q: 'Puis-je exporter les données que je consulte ?',
|
||||
faqTips3A: 'Utilisez le bouton d’exportation pour télécharger les propriétés filtrées sous forme de tableur. L’export respecte tous vos filtres actifs, vous obtenez donc exactement les données souhaitées.',
|
||||
faqTips3A:
|
||||
'Utilisez le bouton d’exportation pour télécharger les propriétés filtrées sous forme de tableur. L’export respecte tous vos filtres actifs, vous obtenez donc exactement les données souhaitées.',
|
||||
},
|
||||
|
||||
// ── Account Page ───────────────────────────────────
|
||||
|
|
@ -585,8 +622,7 @@ const fr: Translations = {
|
|||
|
||||
// ── Invites Page ───────────────────────────────────
|
||||
invitesPage: {
|
||||
inviteLinksLicensed:
|
||||
"Les liens d'invitation sont disponibles pour les utilisateurs licenciés.",
|
||||
inviteLinksLicensed: "Les liens d'invitation sont disponibles pour les utilisateurs licenciés.",
|
||||
inviteAdminLabel: 'Inviter des amis (100% de réduction)',
|
||||
inviteReferralLabel: 'Inviter des amis (30% de réduction)',
|
||||
generateFreeInvite: "Générer un lien d'invitation gratuit",
|
||||
|
|
@ -607,26 +643,20 @@ const fr: Translations = {
|
|||
invitePage: {
|
||||
youreInvited: 'Vous êtes invité !',
|
||||
specialOffer: 'Offre spéciale !',
|
||||
invitedByFree:
|
||||
'{{name}} vous invite à obtenir un accès à vie gratuit.',
|
||||
invitedByDiscount:
|
||||
"{{name}} vous fait bénéficier d'une réduction de 30% sur l'accès à vie.",
|
||||
genericFreeInvite:
|
||||
'Vous avez été invité à obtenir un accès à vie gratuit.',
|
||||
genericDiscount:
|
||||
"Un ami vous fait bénéficier d'une réduction de 30% sur l'accès à vie.",
|
||||
invitedByFree: '{{name}} vous invite à obtenir un accès à vie gratuit.',
|
||||
invitedByDiscount: "{{name}} vous fait bénéficier d'une réduction de 30% sur l'accès à vie.",
|
||||
genericFreeInvite: 'Vous avez été invité à obtenir un accès à vie gratuit.',
|
||||
genericDiscount: "Un ami vous fait bénéficier d'une réduction de 30% sur l'accès à vie.",
|
||||
exploreEvery: "Explorez chaque quartier d'Angleterre",
|
||||
propertyInfo:
|
||||
"Prix immobiliers, classements énergétiques, statistiques de criminalité, notes des écoles et plus encore",
|
||||
'Prix immobiliers, classements énergétiques, statistiques de criminalité, notes des écoles et plus encore',
|
||||
invalidInvite: 'Invitation invalide',
|
||||
inviteAlreadyUsed: 'Invitation déjà utilisée',
|
||||
inviteAlreadyUsedDesc: "Ce lien d'invitation a déjà été utilisé.",
|
||||
invalidInviteLink: "Lien d'invitation invalide",
|
||||
invalidInviteLinkDesc:
|
||||
"Ce lien d'invitation est invalide ou a expiré.",
|
||||
invalidInviteLinkDesc: "Ce lien d'invitation est invalide ou a expiré.",
|
||||
licenseActivated: 'Licence activée !',
|
||||
fullAccessGranted:
|
||||
'Vous avez désormais un accès complet à Perfect Postcode.',
|
||||
fullAccessGranted: 'Vous avez désormais un accès complet à Perfect Postcode.',
|
||||
activating: 'Activation...',
|
||||
activateLicense: 'Activer la licence',
|
||||
claimDiscount: 'Réclamer la réduction',
|
||||
|
|
@ -665,17 +695,23 @@ const fr: Translations = {
|
|||
// ── Tutorial ──────────────────────────────────────
|
||||
tutorial: {
|
||||
step1Title: 'Dites à la carte ce qui compte',
|
||||
step1Content: 'Définissez votre budget, temps de trajet maximum, qualité des écoles, seuil de criminalité. Ce qui compte pour vous. Seules les zones qui correspondent restent éclairées. Utilisez l’icône œil pour colorier par n’importe quel critère.',
|
||||
step1Content:
|
||||
'Définissez votre budget, temps de trajet maximum, qualité des écoles, seuil de criminalité. Ce qui compte pour vous. Seules les zones qui correspondent restent éclairées. Utilisez l’icône œil pour colorier par n’importe quel critère.',
|
||||
step2Title: 'Ou décrivez simplement',
|
||||
step2Content: 'Tapez ce que vous voulez en français, par exemple « quartier calme près de bonnes écoles sous £400k », et nous configurerons les filtres pour vous.',
|
||||
step2Content:
|
||||
'Tapez ce que vous voulez en français, par exemple « quartier calme près de bonnes écoles sous £400k », et nous configurerons les filtres pour vous.',
|
||||
step3Title: 'Explorez ce qui existe',
|
||||
step3Content: 'Naviguez et zoomez à travers l’Angleterre. Cliquez sur n’importe quelle zone colorée pour voir la criminalité, les écoles, les prix, le haut débit, le bruit et plus encore.',
|
||||
step3Content:
|
||||
'Naviguez et zoomez à travers l’Angleterre. Cliquez sur n’importe quelle zone colorée pour voir la criminalité, les écoles, les prix, le haut débit, le bruit et plus encore.',
|
||||
step4Title: 'Allez directement à un lieu',
|
||||
step4Content: 'Recherchez n’importe quel lieu ou code postal pour vous y rendre instantanément.',
|
||||
step4Content:
|
||||
'Recherchez n’importe quel lieu ou code postal pour vous y rendre instantanément.',
|
||||
step5Title: 'Examinez les détails',
|
||||
step5Content: 'Consultez les statistiques de zone, histogrammes et fiches individuelles : prix, surface, performances énergétiques et plus.',
|
||||
step5Content:
|
||||
'Consultez les statistiques de zone, histogrammes et fiches individuelles : prix, surface, performances énergétiques et plus.',
|
||||
step6Title: 'Qu’y a-t-il à proximité ?',
|
||||
step6Content: 'Activez les écoles, commerces, gares, parcs et restaurants sur la carte pour voir ce qui est à portée.',
|
||||
step6Content:
|
||||
'Activez les écoles, commerces, gares, parcs et restaurants sur la carte pour voir ce qui est à portée.',
|
||||
},
|
||||
|
||||
// ── Server-derived values ──────────────────────────
|
||||
|
|
@ -683,13 +719,13 @@ const fr: Translations = {
|
|||
// The English keys MUST match exactly what the API returns.
|
||||
server: {
|
||||
// ─ Feature group names ─
|
||||
'Properties': 'Propriétés',
|
||||
'Transport': 'Transports',
|
||||
'Education': 'Éducation',
|
||||
'Deprivation': 'Précarité',
|
||||
'Crime': 'Criminalité',
|
||||
'Demographics': 'Démographie',
|
||||
'Amenities': 'Commodités',
|
||||
Properties: 'Propriétés',
|
||||
Transport: 'Transports',
|
||||
Education: 'Éducation',
|
||||
Deprivation: 'Précarité',
|
||||
Crime: 'Criminalité',
|
||||
Demographics: 'Démographie',
|
||||
Amenities: 'Commodités',
|
||||
|
||||
// ─ Feature names (Properties) ─
|
||||
'Listing status': 'Statut de l’annonce',
|
||||
|
|
@ -705,8 +741,8 @@ const fr: Translations = {
|
|||
'Asking rent (monthly)': 'Loyer demandé (mensuel)',
|
||||
'Total floor area (sqm)': 'Surface totale (m²)',
|
||||
'Number of bedrooms & living rooms': 'Nombre de chambres et séjours',
|
||||
'Bedrooms': 'Chambres',
|
||||
'Bathrooms': 'Salles de bain',
|
||||
Bedrooms: 'Chambres',
|
||||
Bathrooms: 'Salles de bain',
|
||||
'Construction year': 'Année de construction',
|
||||
'Date of last transaction': 'Date de la dernière transaction',
|
||||
'Listing date': 'Date de mise en ligne',
|
||||
|
|
@ -716,7 +752,8 @@ const fr: Translations = {
|
|||
'Interior height (m)': 'Hauteur intérieure (m)',
|
||||
|
||||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)': 'Distance à la gare ou station de métro la plus proche (km)',
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Distance à la gare ou station de métro la plus proche (km)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Écoles primaires Bien+ dans un rayon de 2 km',
|
||||
|
|
@ -766,24 +803,24 @@ const fr: Translations = {
|
|||
'Distance to nearest park (km)': 'Distance au parc le plus proche (km)',
|
||||
'Number of parks within 2km': 'Nombre de parcs à moins de 2 km',
|
||||
'Number of restaurants within 2km': 'Nombre de restaurants à moins de 2 km',
|
||||
'Number of grocery shops and supermarkets within 2km': 'Nombre d’épiceries et supermarchés à moins de 2 km',
|
||||
'Number of grocery shops and supermarkets within 2km':
|
||||
'Nombre d’épiceries et supermarchés à moins de 2 km',
|
||||
'Noise (dB)': 'Bruit (dB)',
|
||||
'Max available download speed (Mbps)': 'Débit descendant max. disponible (Mbps)',
|
||||
|
||||
|
||||
// ─ Enum values ─
|
||||
'Historical sale': 'Vente historique',
|
||||
'For sale': 'En vente',
|
||||
'For rent': 'En location',
|
||||
'Detached': 'Individuelle',
|
||||
Detached: 'Individuelle',
|
||||
'Semi-Detached': 'Jumelée',
|
||||
'Terraced': 'Mitoyenne',
|
||||
Terraced: 'Mitoyenne',
|
||||
'Flats/Maisonettes': 'Appartements/Duplex',
|
||||
'Other': 'Autre',
|
||||
'Freehold': 'Pleine propriété',
|
||||
'Leasehold': 'Bail emphytéotique',
|
||||
'Yes': 'Oui',
|
||||
'No': 'Non',
|
||||
Other: 'Autre',
|
||||
Freehold: 'Pleine propriété',
|
||||
Leasehold: 'Bail emphytéotique',
|
||||
Yes: 'Oui',
|
||||
No: 'Non',
|
||||
|
||||
// ─ Stacked chart labels ─
|
||||
'Serious crime': 'Crimes graves',
|
||||
|
|
@ -792,52 +829,52 @@ const fr: Translations = {
|
|||
|
||||
// ─ POI group names ─
|
||||
'Public Transport': 'Transports en commun',
|
||||
'Leisure': 'Loisirs',
|
||||
'Health': 'Santé',
|
||||
Leisure: 'Loisirs',
|
||||
Health: 'Santé',
|
||||
'Emergency Services': 'Services d’urgence',
|
||||
'Groceries': 'Alimentation',
|
||||
Groceries: 'Alimentation',
|
||||
'Local Businesses': 'Commerces de proximité',
|
||||
'Culture': 'Culture',
|
||||
'Services': 'Services',
|
||||
'Shops': 'Boutiques',
|
||||
Culture: 'Culture',
|
||||
Services: 'Services',
|
||||
Shops: 'Boutiques',
|
||||
|
||||
// ─ POI categories ─
|
||||
'Airport': 'Aéroport',
|
||||
'Ferry': 'Ferry',
|
||||
Airport: 'Aéroport',
|
||||
Ferry: 'Ferry',
|
||||
'Rail station': 'Gare',
|
||||
'Bus stop': 'Arrêt de bus',
|
||||
'Bus station': 'Gare routière',
|
||||
'Taxi rank': 'Station de taxi',
|
||||
'Metro or Tram stop': 'Station de métro ou tramway',
|
||||
'Café': 'Café',
|
||||
'Restaurant': 'Restaurant',
|
||||
'Pub': 'Pub',
|
||||
'Bar': 'Bar',
|
||||
Café: 'Café',
|
||||
Restaurant: 'Restaurant',
|
||||
Pub: 'Pub',
|
||||
Bar: 'Bar',
|
||||
'Fast Food': 'Restauration rapide',
|
||||
'Nightclub': 'Boîte de nuit',
|
||||
'Cinema': 'Cinéma',
|
||||
'Theatre': 'Théâtre',
|
||||
Nightclub: 'Boîte de nuit',
|
||||
Cinema: 'Cinéma',
|
||||
Theatre: 'Théâtre',
|
||||
'Live Music & Events': 'Musique live et événements',
|
||||
'Park': 'Parc',
|
||||
'Playground': 'Aire de jeux',
|
||||
Park: 'Parc',
|
||||
Playground: 'Aire de jeux',
|
||||
'Sports Centre': 'Centre sportif',
|
||||
'Entertainment': 'Divertissement',
|
||||
'Supermarket': 'Supermarché',
|
||||
Entertainment: 'Divertissement',
|
||||
Supermarket: 'Supermarché',
|
||||
'Convenience Store': 'Supérette',
|
||||
'Bakery': 'Boulangerie',
|
||||
Bakery: 'Boulangerie',
|
||||
'Butcher & Fishmonger': 'Boucherie et poissonnerie',
|
||||
'Greengrocer': 'Primeur',
|
||||
Greengrocer: 'Primeur',
|
||||
'Off-Licence': 'Caviste',
|
||||
'Deli & Specialty': 'Traiteur et épicerie fine',
|
||||
'Fashion & Clothing': 'Mode et vêtements',
|
||||
'Electronics': 'Électronique',
|
||||
Electronics: 'Électronique',
|
||||
'Charity Shop': 'Boutique caritative',
|
||||
'DIY & Hardware': 'Bricolage et quincaillerie',
|
||||
'Home & Garden': 'Maison et jardin',
|
||||
'Bookshop': 'Librairie',
|
||||
Bookshop: 'Librairie',
|
||||
'Pet Shop': 'Animalerie',
|
||||
'Sports & Outdoor': 'Sports et plein air',
|
||||
'Newsagent': 'Marchand de journaux',
|
||||
Newsagent: 'Marchand de journaux',
|
||||
'Department Store': 'Grand magasin',
|
||||
'Gift & Hobby': 'Cadeaux et loisirs créatifs',
|
||||
'Specialist Shop': 'Boutique spécialisée',
|
||||
|
|
@ -847,31 +884,31 @@ const fr: Translations = {
|
|||
'Car Services': 'Services automobiles',
|
||||
'Post Office': 'Bureau de poste',
|
||||
'Vet & Pet Care': 'Vétérinaire et soins animaliers',
|
||||
'Bank': 'Banque',
|
||||
Bank: 'Banque',
|
||||
'Travel Agent': 'Agence de voyage',
|
||||
'Police': 'Police',
|
||||
Police: 'Police',
|
||||
'Fire Station': 'Caserne de pompiers',
|
||||
'Ambulance Station': 'Centre ambulancier',
|
||||
'GP Surgery': 'Cabinet médical',
|
||||
'Dentist': 'Dentiste',
|
||||
'Pharmacy': 'Pharmacie',
|
||||
Dentist: 'Dentiste',
|
||||
Pharmacy: 'Pharmacie',
|
||||
'Hospital & Clinic': 'Hôpital et clinique',
|
||||
'Optician': 'Opticien',
|
||||
'Physiotherapy': 'Kinésithérapie',
|
||||
Optician: 'Opticien',
|
||||
Physiotherapy: 'Kinésithérapie',
|
||||
'Counselling & Therapy': 'Conseil et thérapie',
|
||||
'Care Home': 'Maison de retraite',
|
||||
'Medical & Mobility': 'Matériel médical et mobilité',
|
||||
'Museum': 'Musée',
|
||||
'Gallery': 'Galerie',
|
||||
'Library': 'Bibliothèque',
|
||||
Museum: 'Musée',
|
||||
Gallery: 'Galerie',
|
||||
Library: 'Bibliothèque',
|
||||
'Place of Worship': 'Lieu de culte',
|
||||
'Arts Centre': 'Centre artistique',
|
||||
'Zoo': 'Zoo',
|
||||
Zoo: 'Zoo',
|
||||
'Tourist Attraction': 'Attraction touristique',
|
||||
'School': 'École',
|
||||
'Hotel': 'Hôtel',
|
||||
School: 'École',
|
||||
Hotel: 'Hôtel',
|
||||
'Local Business': 'Commerce local',
|
||||
'Offices': 'Bureaux',
|
||||
Offices: 'Bureaux',
|
||||
'EV Charging': 'Borne de recharge',
|
||||
'Fuel Station': 'Station-service',
|
||||
'Community Centre': 'Centre communautaire',
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ const hu: Translations = {
|
|||
logIn: 'Bejelentkezés',
|
||||
createAccount: 'Regisztráció',
|
||||
resetPassword: 'Jelszó visszaállítása',
|
||||
valueProp: 'Mentsd el a kereséseidet, jelöld meg az ingatlanokat, és folytasd ott, ahol abbahagytad.',
|
||||
valueProp:
|
||||
'Mentsd el a kereséseidet, jelöld meg az ingatlanokat, és folytasd ott, ahol abbahagytad.',
|
||||
continueWithGoogle: 'Folytatás Google-lel',
|
||||
email: 'E-mail',
|
||||
emailPlaceholder: 'te@pelda.hu',
|
||||
|
|
@ -89,11 +90,13 @@ const hu: Translations = {
|
|||
// ── Upgrade Modal ──────────────────────────────────
|
||||
upgrade: {
|
||||
title: 'Fedezd fel egész Angliát',
|
||||
description: 'Jelenleg a demó területet felfedezed. Szerezz élethosszig tartó hozzáférést minden irányítószámhoz, szűrőhöz és környékhez. Egyetlen fizetés, örökre.',
|
||||
description:
|
||||
'Jelenleg a demó területet felfedezed. Szerezz élethosszig tartó hozzáférést minden irányítószámhoz, szűrőhöz és környékhez. Egyetlen fizetés, örökre.',
|
||||
free: 'Ingyenes',
|
||||
once: '/egyszeri',
|
||||
freeForEarly: 'Ingyenes a korai felhasználóknak. Nem szükséges bankkartya.',
|
||||
oneTimePayment: 'Egyszeri fizetés. Élethosszig tartó hozzáférés. 30 napos pénzvisszatérítési garancia.',
|
||||
oneTimePayment:
|
||||
'Egyszeri fizetés. Élethosszig tartó hozzáférés. 30 napos pénzvisszatérítési garancia.',
|
||||
redirecting: 'Átirányítás...',
|
||||
claimFreeAccess: 'Ingyenes hozzáférés igénylése',
|
||||
upgradeFor: 'Frissítés {{price}} áron',
|
||||
|
|
@ -151,7 +154,8 @@ const hu: Translations = {
|
|||
|
||||
// ── Philosophy Popup ───────────────────────────────
|
||||
philosophy: {
|
||||
intro: 'Kezdd a feltétlenül szükséges feltételekkel, majd add hozzá a kívánalmakat. A térkép szűkül, ahogy szűrőket adsz hozzá. A megmaradó területek a legjobb találatok.',
|
||||
intro:
|
||||
'Kezdd a feltétlenül szükséges feltételekkel, majd add hozzá a kívánalmakat. A térkép szűkül, ahogy szűrőket adsz hozzá. A megmaradó területek a legjobb találatok.',
|
||||
step1Title: 'Költségvetés és alapok',
|
||||
step1Desc: '(ártartomány, alapterület, ingatlantípus)',
|
||||
step2Title: 'Ingazás',
|
||||
|
|
@ -174,7 +178,8 @@ const hu: Translations = {
|
|||
selectDestination: 'Úticél kiválasztása...',
|
||||
bestCase: 'Legjobb eset',
|
||||
bestCaseTitle: 'Legjobb utazási idő',
|
||||
bestCaseDesc: 'A leggyorsabb reális utazási időt használja (ha jól időzíted az indulást és jó csatlakozásokat érsz el). Az alapértelmezett a <strong>mediánt</strong> használja, ami egy átlagos utazást képvisel, függetlenül az indulás idejétől.',
|
||||
bestCaseDesc:
|
||||
'A leggyorsabb reális utazási időt használja (ha jól időzíted az indulást és jó csatlakozásokat érsz el). Az alapértelmezett a <strong>mediánt</strong> használja, ami egy átlagos utazást képvisel, függetlenül az indulás idejétől.',
|
||||
previewOnMap: 'Előnézet a térképen',
|
||||
stopPreviewing: 'Előnézet leállítása',
|
||||
removeTravelTime: 'Utazási idő eltávolítása',
|
||||
|
|
@ -194,7 +199,8 @@ const hu: Translations = {
|
|||
|
||||
// ── Travel Time Info Popup ─────────────────────────
|
||||
travelInfo: {
|
||||
transitDesc: ' tömegközlekedéssel (busz, vonat, metró). Az időket egy átlagos hétköznap délelőtti időablakra számítjuk.',
|
||||
transitDesc:
|
||||
' tömegközlekedéssel (busz, vonat, metró). Az időket egy átlagos hétköznap délelőtti időablakra számítjuk.',
|
||||
carDesc: ' autóval, a típikus sebességek és az úthálózat alapján.',
|
||||
bicycleDesc: ' kerékpárral, kerékpárbarát útvonalakon.',
|
||||
walkingDesc: ' gyalog, sétálóutakon és járdákon.',
|
||||
|
|
@ -243,6 +249,7 @@ const hu: Translations = {
|
|||
bathrooms: 'Fürdőszobák:',
|
||||
rooms: 'Szobák:',
|
||||
built: 'Építve:',
|
||||
formerCouncil: 'Volt önk. lakás:',
|
||||
epcRating: 'EPC minősítés:',
|
||||
epcPotential: 'EPC potenciál:',
|
||||
listed: 'Hirdetve:',
|
||||
|
|
@ -253,7 +260,8 @@ const hu: Translations = {
|
|||
perSqm: '/m²',
|
||||
searchPlaceholder: 'Keresés cím vagy irányítószám alapján...',
|
||||
propertyData: 'Ingatlanadatok',
|
||||
propertyDataDesc: 'Az árak a HM Land Registry-ből származnak (a vevők által ténylegesen fizetett összeg). Az alapterület, energetikai minősítések, építési év és tulajdonforma a hivatalos EPC felmérésekből származnak. Mindkét forrás cím alapján van összepárosítva az egyes irányítószámokon belül.',
|
||||
propertyDataDesc:
|
||||
'Az árak a HM Land Registry-ből származnak (a vevők által ténylegesen fizetett összeg). Az alapterület, energetikai minősítések, építési év és tulajdonforma a hivatalos EPC felmérésekből származnak. Mindkét forrás cím alapján van összepárosítva az egyes irányítószámokon belül.',
|
||||
},
|
||||
|
||||
// ── Area Pane ──────────────────────────────────────
|
||||
|
|
@ -290,7 +298,8 @@ const hu: Translations = {
|
|||
poiPane: {
|
||||
pois: 'POI-k',
|
||||
pointsOfInterest: 'Érdekes pontok',
|
||||
poiDescription: 'Forrás: OpenStreetMap. Tartalmazza a tömegközlekedési megállókat, üzleteket, éttermeket, egészségügyi intézményeket, szabadidős létesítményeket és még sok mást. Rendszeresen frissítve, teljes kategórialefedettséggel.',
|
||||
poiDescription:
|
||||
'Forrás: OpenStreetMap. Tartalmazza a tömegközlekedési megállókat, üzleteket, éttermeket, egészségügyi intézményeket, szabadidős létesítményeket és még sok mást. Rendszeresen frissítve, teljes kategórialefedettséggel.',
|
||||
searchCategories: 'Kategóriák keresése...',
|
||||
dataSourceInfo: 'Adatforrás információ',
|
||||
},
|
||||
|
|
@ -323,7 +332,8 @@ const hu: Translations = {
|
|||
heroTitle2: 'Érték',
|
||||
heroTitle3: 'Minimális kompromisszum.',
|
||||
heroSubtitle: 'Ingatlant keresel? Legyen a legnagyobb befektetésed a legokosabb döntésed.',
|
||||
heroDescription: 'Annyi lehetőség – a megfelelő kiválasztása nehéz lehet. Interaktív térképünk egyszerűvé teszi: válaszd ki a feltételeidet, és azonnal lásd a megfelelő területeket.',
|
||||
heroDescription:
|
||||
'Annyi lehetőség – a megfelelő kiválasztása nehéz lehet. Interaktív térképünk egyszerűvé teszi: válaszd ki a feltételeidet, és azonnal lásd a megfelelő területeket.',
|
||||
exploreTheMap: 'Térkép felfedezése',
|
||||
seeTheDifference: 'Nézd meg a különbséget',
|
||||
statProperties: 'ingatlan',
|
||||
|
|
@ -331,17 +341,21 @@ const hu: Translations = {
|
|||
statEvery: 'Minden',
|
||||
statPostcodeInEngland: 'irányítószám Angliában',
|
||||
ourPhilosophy: 'Filozófiánk',
|
||||
philosophyP1: 'A Rightmove-on először területet választasz, és reméled, hogy jó. Végül bűnözési statisztikákat, iskolai jelentéseket és szélessáv-ellenőrzőket böngészel tucat füleken, egyszerre egy irányítószámmal.',
|
||||
philosophyP2: 'Mi megfordítjuk. Mondd el, mire van szükséged (költségvetés, ingazás, iskolák, biztonság), és megmutatjuk Anglia összes megfelelő területét. Nincs találgatás. Nincs felesleges megtekintés.',
|
||||
philosophyP1:
|
||||
'A Rightmove-on először területet választasz, és reméled, hogy jó. Végül bűnözési statisztikákat, iskolai jelentéseket és szélessáv-ellenőrzőket böngészel tucat füleken, egyszerre egy irányítószámmal.',
|
||||
philosophyP2:
|
||||
'Mi megfordítjuk. Mondd el, mire van szükséged (költségvetés, ingazás, iskolák, biztonság), és megmutatjuk Anglia összes megfelelő területét. Nincs találgatás. Nincs felesleges megtekintés.',
|
||||
howToUseIt: 'Hogyan használd',
|
||||
howStep1Title: 'Állítsd be a feltételeidet',
|
||||
howStep1Desc: 'Költségvetés, ingazás, iskolák — a térkép csak a megfelelőket mutatja.',
|
||||
howStep2Title: 'Fedezz fel területeket és rejtett kincseket',
|
||||
howStep2Desc: 'Nagyíts rá, mélyedj el a részletekben és a pluszokban.',
|
||||
howStep3Title: 'Vizsgáld meg az irányítószámokat',
|
||||
howStep3Desc: 'Nézd meg az egyes ingatlanokat, eladási árakat, alapterületet, és hasonlítsd össze.',
|
||||
howStep3Desc:
|
||||
'Nézd meg az egyes ingatlanokat, eladási árakat, alapterületet, és hasonlítsd össze.',
|
||||
howStep4Title: 'Válassz magabiztosan',
|
||||
howStep4Desc: 'A listádon minden terület megfelel a valós feltételeidnek — nem csak annak, amit azon a héten hirdettek.',
|
||||
howStep4Desc:
|
||||
'A listádon minden terület megfelel a valós feltételeidnek — nem csak annak, amit azon a héten hirdettek.',
|
||||
othersVs: 'Mások vs.',
|
||||
listingPortals: 'Hirdetési portálok',
|
||||
checkMyPostcode: '“Irányítószám ellenőrzése”',
|
||||
|
|
@ -361,8 +375,10 @@ const hu: Translations = {
|
|||
// ── Pricing Page ───────────────────────────────────
|
||||
pricingPage: {
|
||||
title: 'Korai hozzáférés árak',
|
||||
subtitle: 'Fizess egyszer, használd örökre. Minél korábban csatlakozol, annál kevesebbet fizetsz.',
|
||||
costContext: 'Egy lakásvásárlás £10 000+ illetékbe, £1 500 ügyvédi díjba, £500 szakértői vizsgálatba kerül. Ha rossz területet választasz, ráragadsz egy hosszú ingazásra, rossz iskolákra, vagy egy útra, amelyről nem tudtál.',
|
||||
subtitle:
|
||||
'Fizess egyszer, használd örökre. Minél korábban csatlakozol, annál kevesebbet fizetsz.',
|
||||
costContext:
|
||||
'Egy lakásvásárlás £10 000+ illetékbe, £1 500 ügyvédi díjba, £500 szakértői vizsgálatba kerül. Ha rossz területet választasz, ráragadsz egy hosszú ingazásra, rossz iskolákra, vagy egy útra, amelyről nem tudtál.',
|
||||
lessThanSurvey: 'Kevesebbe kerül, mint egy épületszakértői vizsgálat. Sokkal hasznosabb.',
|
||||
currentTier: 'Jelenlegi szint',
|
||||
firstNUsers: 'Első {{count}} felhasználó',
|
||||
|
|
@ -393,13 +409,16 @@ const hu: Translations = {
|
|||
faq: 'GYIK',
|
||||
dataSources: 'Adatforrások',
|
||||
support: 'Támogatás',
|
||||
dataSourcesIntro: 'Ez az alkalmazás {{count}} nyilvános adatkészletet kombinál, amelyek ingatllanárakat, energetikai teljesítményt, közlekedést, demográfiát, bűnözést, környezetet és még sok mást fednek le.',
|
||||
faqIntro: 'Akár vásárolsz, akár bérelsz, akár csak felfedezed, így segít a Perfect Postcode megtalálni a megfelelő területet.',
|
||||
dataSourcesIntro:
|
||||
'Ez az alkalmazás {{count}} nyilvános adatkészletet kombinál, amelyek ingatllanárakat, energetikai teljesítményt, közlekedést, demográfiát, bűnözést, környezetet és még sok mást fednek le.',
|
||||
faqIntro:
|
||||
'Akár vásárolsz, akár bérelsz, akár csak felfedezed, így segít a Perfect Postcode megtalálni a megfelelő területet.',
|
||||
supportIntro: 'Kérdésed van? Nézd meg a GYIK-et, vagy írj nekünk közvetlenül.',
|
||||
source: 'Forrás:',
|
||||
optOut: 'Nyilvános közzététel visszautasítása',
|
||||
attribution: 'Forrásmegnevezés',
|
||||
attrLandRegistry: 'HM Land Registry adatokat tartalmaz © Crown copyright and database right 2025.',
|
||||
attrLandRegistry:
|
||||
'HM Land Registry adatokat tartalmaz © Crown copyright and database right 2025.',
|
||||
attrOgl: 'Közszektorbeli információt tartalmaz a következő licenc alatt:',
|
||||
attrOglLink: 'Open Government Licence v3.0',
|
||||
attrOs: 'OS adatokat tartalmaz © Crown copyright and database rights 2025.',
|
||||
|
|
@ -414,43 +433,56 @@ const hu: Translations = {
|
|||
dsPricePaidUse: 'Teljes történelmi ingatlanaladási árak Angliában.',
|
||||
dsEpcName: 'Energetikai tanúsítványok (EPC)',
|
||||
dsEpcOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsEpcUse: 'Lakóingatlan energetikai tanúsítványok, amelyek tartalmazzpák az alapterületet, szobaszámot, építési évet, energetikai minősítéseket, ingatlantípust és épületformát. Az Árfizetett nyilvántartásokkal cím alapján párosítva az egyes irányítószámokon belül. Az ingatlantulajdonosok visszautasíthatják a nyilvános közzétételt.',
|
||||
dsEpcUse:
|
||||
'Lakóingatlan energetikai tanúsítványok, amelyek tartalmazzpák az alapterületet, szobaszámot, építési évet, energetikai minősítéseket, ingatlantípust és épületformát. Az Árfizetett nyilvántartásokkal cím alapján párosítva az egyes irányítószámokon belül. Az ingatlantulajdonosok visszautasíthatják a nyilvános közzétételt.',
|
||||
dsNsplName: 'Nemzeti Statisztikai Irányítószám Kereső (NSPL)',
|
||||
dsNsplOrigin: 'ONS / ArcGIS',
|
||||
dsNsplUse: 'Irányítószámokat koordinátákhoz és statisztikai területkódokhoz rendeli, amelyekkel az összes területi szintű adatkészletet az egyes ingatlanokhoz kapcsoljuk.',
|
||||
dsNsplUse:
|
||||
'Irányítószámokat koordinátákhoz és statisztikai területkódokhoz rendeli, amelyekkel az összes területi szintű adatkészletet az egyes ingatlanokhoz kapcsoljuk.',
|
||||
dsIodName: 'Angol Deprivációs Mutatók 2025',
|
||||
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsIodUse: 'Relatív deprivációs pontok jövedelem, foglalkoztatottság, oktatás, egészség, bűnözés és lakókörnyezet területén Anglia minden szomszédságára.',
|
||||
dsIodUse:
|
||||
'Relatív deprivációs pontok jövedelem, foglalkoztatottság, oktatás, egészség, bűnözés és lakókörnyezet területén Anglia minden szomszédságára.',
|
||||
dsEthnicityName: 'Népesség etnikai megoszlás szerint (2021-es népszámlálás)',
|
||||
dsEthnicityOrigin: 'ONS',
|
||||
dsEthnicityUse: 'Népesség százalékos megoszlása etnikai csoportonként (dél-ázsiai, kelet-ázsiai, fekete, vegyes, fehér, egyéb) helyi önkormányzatonként.',
|
||||
dsEthnicityUse:
|
||||
'Népesség százalékos megoszlása etnikai csoportonként (dél-ázsiai, kelet-ázsiai, fekete, vegyes, fehér, egyéb) helyi önkormányzatonként.',
|
||||
dsCrimeName: 'Utcaszintű bűnözési adatok',
|
||||
dsCrimeOrigin: 'data.police.uk',
|
||||
dsCrimeUse: 'Utcaszintű bűnözési adatok 2023-tól 2025-ig, éves átlagokba összegézve LSOA-nként és bűncselekménytípusonként (erőszak, betörés, közérdekű rendsértség, kábítószer, járműbűnözés stb.).',
|
||||
dsCrimeUse:
|
||||
'Utcaszintű bűnözési adatok 2023-tól 2025-ig, éves átlagokba összegézve LSOA-nként és bűncselekménytípusonként (erőszak, betörés, közérdekű rendsértség, kábítószer, járműbűnözés stb.).',
|
||||
dsOsmName: 'OpenStreetMap POI-k',
|
||||
dsOsmOrigin: 'OpenStreetMap contributors / Geofabrik',
|
||||
dsOsmUse: 'Érdekes pontok, beleértve üzleteket, éttermeket, egészségügyet, szabadidőt, turizmust és még sok mást Nagy-Britanniában.',
|
||||
dsOsmUse:
|
||||
'Érdekes pontok, beleértve üzleteket, éttermeket, egészségügyet, szabadidőt, turizmust és még sok mást Nagy-Britanniában.',
|
||||
dsGreenspaceName: 'OS Open Greenspace',
|
||||
dsGreenspaceOrigin: 'Ordnance Survey',
|
||||
dsGreenspaceUse: 'Hivatalos zöldterületi határok Nagy-Britanniában, beleértve a közparkokat, kerteket, sportterületeket és játszótereket. A poligon középpontjait használjuk a park közelségi számláláshoz és a legközelebbi park távolságának számításához.',
|
||||
dsGreenspaceUse:
|
||||
'Hivatalos zöldterületi határok Nagy-Britanniában, beleértve a közparkokat, kerteket, sportterületeket és játszótereket. A poligon középpontjait használjuk a park közelségi számláláshoz és a legközelebbi park távolságának számításához.',
|
||||
dsNaptanName: 'NaPTAN (Tömegközlekedési megállók)',
|
||||
dsNaptanOrigin: 'Department for Transport',
|
||||
dsNaptanUse: 'Állomás- és megállóhelyek vasút, busz, metró/villamos, komp és repülőtér számára Angliában.',
|
||||
dsNaptanUse:
|
||||
'Állomás- és megállóhelyek vasút, busz, metró/villamos, komp és repülőtér számára Angliában.',
|
||||
dsNoiseName: 'Defra zajtérképezés',
|
||||
dsNoiseOrigin: 'Defra / Environment Agency',
|
||||
dsNoiseUse: 'Közúti zajszintek (24 órás súlyozott átlag) a 2022-es stratégiai zajtérképezésből, nagy felbontásban modellezve és minden irányítószámnál mintavételezve.',
|
||||
dsNoiseUse:
|
||||
'Közúti zajszintek (24 órás súlyozott átlag) a 2022-es stratégiai zajtérképezésből, nagy felbontásban modellezve és minden irányítószámnál mintavételezve.',
|
||||
dsOfstedName: 'Ofsted iskolai vizsgálatok',
|
||||
dsOfstedOrigin: 'Ofsted',
|
||||
dsOfstedUse: 'Legfrissebb vizsgálati eredmények az állami fenntartású iskolákról (2025 áprilisáig). Irányítószámonként átlagolva a helyi iskolai minőség pontozásához (1=Kiváló-tól 4=Elégtelenig).',
|
||||
dsOfstedUse:
|
||||
'Legfrissebb vizsgálati eredmények az állami fenntartású iskolákról (2025 áprilisáig). Irányítószámonként átlagolva a helyi iskolai minőség pontozásához (1=Kiváló-tól 4=Elégtelenig).',
|
||||
dsBroadbandName: 'Ofcom szélessávú teljesítmény',
|
||||
dsBroadbandOrigin: 'Ofcom',
|
||||
dsBroadbandUse: 'Vezetékes szélessávú lefedettség és maximális letöltési sebességek terültenként az Ofcom Connected Nations 2025 jelentésből.',
|
||||
dsBroadbandUse:
|
||||
'Vezetékes szélessávú lefedettség és maximális letöltési sebességek terültenként az Ofcom Connected Nations 2025 jelentésből.',
|
||||
dsCouncilTaxName: 'Helyi adószintek 2025-26',
|
||||
dsCouncilTaxOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsCouncilTaxUse: 'Éves helyi adó díjszabások A-H sávokra Anglia mind a 296 számlázó hatóságánál, két felnőtt által lakott ingatlanra. Az ingatlanokhoz a helyi önkormányzati kerületi kódon keresztül csatolva az NSPL irányítószám keresőből.',
|
||||
dsCouncilTaxUse:
|
||||
'Éves helyi adó díjszabások A-H sávokra Anglia mind a 296 számlázó hatóságánál, két felnőtt által lakott ingatlanra. Az ingatlanokhoz a helyi önkormányzati kerületi kódon keresztül csatolva az NSPL irányítószám keresőből.',
|
||||
dsRentalName: 'Magánbérleti piaci statisztikák',
|
||||
dsRentalOrigin: 'ONS / Valuation Office Agency',
|
||||
dsRentalUse: 'Medián havi magánbérleti díjak helyi önkormányzatonként és hálószoba-kategóriánként (2022. okt. – 2023. szept.). Az ingatlanokhoz a helyi önkormányzati kerületi kódon és becsült hálószobaszámon keresztül csatolva.',
|
||||
dsRentalUse:
|
||||
'Medián havi magánbérleti díjak helyi önkormányzatonként és hálószoba-kategóriánként (2022. okt. – 2023. szept.). Az ingatlanokhoz a helyi önkormányzati kerületi kódon és becsült hálószobaszámon keresztül csatolva.',
|
||||
// FAQ section titles
|
||||
faqFindingTitle: 'Területed megtalálása',
|
||||
faqCommuteTitle: 'Ingazás és utazás',
|
||||
|
|
@ -463,61 +495,87 @@ const hu: Translations = {
|
|||
faqTipsTitle: 'Tippek és trükkök',
|
||||
// FAQ items — Finding Your Area
|
||||
faqFinding1Q: 'Fogalmam sincs, hol keressek. Segít ebben?',
|
||||
faqFinding1A: 'Pont erre való. Állítsd be a szűrőket (költségvetés, ingazási idő, alacsony bűnözés, jó iskolák), és a térkép kivilgítja minden területet, ami megfelel. Nem kell többé éjfélkor guglizni, hogy “hol a legjobb lakni Manchester közelében”.',
|
||||
faqFinding1A:
|
||||
'Pont erre való. Állítsd be a szűrőket (költségvetés, ingazási idő, alacsony bűnözés, jó iskolák), és a térkép kivilgítja minden területet, ami megfelel. Nem kell többé éjfélkor guglizni, hogy “hol a legjobb lakni Manchester közelében”.',
|
||||
faqFinding2Q: 'Olyan helyre költözöm, ahol még soha nem voltam. Hogyan kezdjem?',
|
||||
faqFinding2A: 'Állítsd be a szűrőket arra, ami fontos, és a térkép azonnal kiemeli a megfelelő területeket. Az “egyetlen utcát sem ismerek”-ből percek alatt rövid listához jutsz.',
|
||||
faqFinding3Q: 'Hogyan találom meg azokat a területeket, amelyek minden feltételemnek megfelelnek?',
|
||||
faqFinding3A: 'Kombinálj több szűrőt (bűnözés átlag alatt, jó iskolák, ingazás 40 perc alatt), majd színezd a térképet ár szerint a legjobb értékű területek megtalálásához. A térkép élőben frissül, ahogy a csúszákat húzod.',
|
||||
faqFinding2A:
|
||||
'Állítsd be a szűrőket arra, ami fontos, és a térkép azonnal kiemeli a megfelelő területeket. Az “egyetlen utcát sem ismerek”-ből percek alatt rövid listához jutsz.',
|
||||
faqFinding3Q:
|
||||
'Hogyan találom meg azokat a területeket, amelyek minden feltételemnek megfelelnek?',
|
||||
faqFinding3A:
|
||||
'Kombinálj több szűrőt (bűnözés átlag alatt, jó iskolák, ingazás 40 perc alatt), majd színezd a térképet ár szerint a legjobb értékű területek megtalálásához. A térkép élőben frissül, ahogy a csúszákat húzod.',
|
||||
// FAQ items — Commute and Travel
|
||||
faqCommute1Q: 'Láthatom, mennyi lenne az ingazásom különböző területekről?',
|
||||
faqCommute1A: 'Állítsd be a munkahelyed úticélként, és minden irányítószámot kiszínezünk utazási idő szerint, legyen az autó, kerékpár vagy tömegközlekedés. Szűrj a maximális ingazási időre, és a többi eltűnik.',
|
||||
faqCommute1A:
|
||||
'Állítsd be a munkahelyed úticélként, és minden irányítószámot kiszínezünk utazási idő szerint, legyen az autó, kerékpár vagy tömegközlekedés. Szűrj a maximális ingazási időre, és a többi eltűnik.',
|
||||
faqCommute2Q: 'Miért jobb ez, mint a Google Maps?',
|
||||
faqCommute2A: 'A Google Maps egyszerre egy utazást mutat. Mi Anglia összes irányítószámát kiszínezzük ingazási idő szerint egyszerre, így száznál több területet hasonlíthatsz össze egyetlen pillantással, ahelyett hogy egyenként keres-gétnéd őket.',
|
||||
faqCommute2A:
|
||||
'A Google Maps egyszerre egy utazást mutat. Mi Anglia összes irányítószámát kiszínezzük ingazási idő szerint egyszerre, így száznál több területet hasonlíthatsz össze egyetlen pillantással, ahelyett hogy egyenként keres-gétnéd őket.',
|
||||
// FAQ items — Budget and Value
|
||||
faqBudget1Q: 'Hogyan találom meg, hol kapom a legtöbb helyet a pénzememért?',
|
||||
faqBudget1A: 'Szűrj négyzetméterár szerint, és azonnal látod, mely irányítószámok adják a legtöbb helyet fontonként. Párosítsd az energetikai minősítés szűrővel, hogy elkerüld a magas fűtési költségű ingatlanokat.',
|
||||
faqBudget1A:
|
||||
'Szűrj négyzetméterár szerint, és azonnal látod, mely irányítószámok adják a legtöbb helyet fontonként. Párosítsd az energetikai minősítés szűrővel, hogy elkerüld a magas fűtési költségű ingatlanokat.',
|
||||
faqBudget2Q: 'Hogyan bizonyosodjak meg, hogy egy olcsó terület nem ok nélkül olcsó?',
|
||||
faqBudget2A: 'Rétegezd rá a deprivációs pontokat, bűnözési statisztikákat, iskolai minősítéseket és szélessáv-sebességeket az ár mellé. Ha egy irányítószám megfizethető és minden fontos szempont szerint jól teljesít, valódi értéket találtál, nem csak alacsony árat észrevétlen kompromisszumokkal.',
|
||||
faqBudget2A:
|
||||
'Rétegezd rá a deprivációs pontokat, bűnözési statisztikákat, iskolai minősítéseket és szélessáv-sebességeket az ár mellé. Ha egy irányítószám megfizethető és minden fontos szempont szerint jól teljesít, valódi értéket találtál, nem csak alacsony árat észrevétlen kompromisszumokkal.',
|
||||
// FAQ items — Safety and Neighbourhood
|
||||
faqSafety1Q: 'Hogyan ellenőrizhetem, biztonságos-e egy terület, mielőtt odaköltözöm?',
|
||||
faqSafety1A: 'Valós rendőrségi bűnözési adatokat vetitünk Anglia minden szomszédságára, típusonként lebontva. Szűrj erőszakos bűncselekményre, betörésre vagy közérdekű rendsértségre, és azonnal lásd, mely irányítószámok a legbiztosabbak.',
|
||||
faqSafety1A:
|
||||
'Valós rendőrségi bűnözési adatokat vetitünk Anglia minden szomszédságára, típusonként lebontva. Szűrj erőszakos bűncselekményre, betörésre vagy közérdekű rendsértségre, és azonnal lásd, mely irányítószámok a legbiztosabbak.',
|
||||
faqSafety2Q: 'Folyamatosan találok reméknek tűnő lakásokat online, de a környezet rossz.',
|
||||
faqSafety2A: 'Pont ezért készült ez. Rétegezd a bűnözési arányokat, zajszinteket, deprivációs pontokat, közeli kocsmkat és parkokat, valamint a szélessáv-sebességeket egyetlen térképre, így tudhatod, milyen valójában egy szomszédság, mielőtt megtekintést foglalsz.',
|
||||
faqSafety2A:
|
||||
'Pont ezért készült ez. Rétegezd a bűnözési arányokat, zajszinteket, deprivációs pontokat, közeli kocsmkat és parkokat, valamint a szélessáv-sebességeket egyetlen térképre, így tudhatod, milyen valójában egy szomszédság, mielőtt megtekintést foglalsz.',
|
||||
// FAQ items — Families and Schools
|
||||
faqFamilies1Q: 'Találhatok területeket jó iskolákkal ÉS alacsony bűnözéssel egyetlen kereséssel?',
|
||||
faqFamilies1A: 'Igen. Kombináld az Ofsted minősítések, bűnözési arányok, parkok és bármi más, a családod számára fontos szempont szűrőit, és a térkép csak a minden feltételnek megfelelő területeket emeli ki. Nem kell többé öt különböző weboldalt összevetni.',
|
||||
faqFamilies1Q:
|
||||
'Találhatok területeket jó iskolákkal ÉS alacsony bűnözéssel egyetlen kereséssel?',
|
||||
faqFamilies1A:
|
||||
'Igen. Kombináld az Ofsted minősítések, bűnözési arányok, parkok és bármi más, a családod számára fontos szempont szűrőit, és a térkép csak a minden feltételnek megfelelő területeket emeli ki. Nem kell többé öt különböző weboldalt összevetni.',
|
||||
faqFamilies2Q: 'Hogyan tudhatom meg, van-e park és játszótér a közelben?',
|
||||
faqFamilies2A: 'Kapcsold be a parkok és zöldterületek POI réteget, hogy közvetlenül a térképen lásd őket. Szűrhetsz aszerint is, hány van sétatávolságon belül az egyes irányítószámoktól.',
|
||||
faqFamilies2A:
|
||||
'Kapcsold be a parkok és zöldterületek POI réteget, hogy közvetlenül a térképen lásd őket. Szűrhetsz aszerint is, hány van sétatávolságon belül az egyes irányítószámoktól.',
|
||||
// FAQ items — Environment and Quality of Life
|
||||
faqEnv1Q: 'Találhatok energiahatékony otthonokat, amelyek nincsenek zajos úton?',
|
||||
faqEnv1A: 'Szűrj EPC minősítés szerint (A-C), majd rétegezd rá a közúti zajadatokat, hogy kiszűrd a küszöbértéked feletti területeket. Színezd bármelyik jellemző szerint, hogy egy pillantással észrevedd a csendes, hatékony utcákat.',
|
||||
faqEnv1A:
|
||||
'Szűrj EPC minősítés szerint (A-C), majd rétegezd rá a közúti zajadatokat, hogy kiszűrd a küszöbértéked feletti területeket. Színezd bármelyik jellemző szerint, hogy egy pillantással észrevedd a csendes, hatékony utcákat.',
|
||||
faqEnv2Q: 'Mutatja az árvíz- vagy süllyedeskockázatot?',
|
||||
faqEnv2A: 'Tartalmazunk talajstabilitási adatokat, így ellenőrizheted a süllyeedést, agyagtalan zsugorodás-duzzadást és egyéb geológiai veszélyeket, mielőtt elköteleznéd magad egy ingatlan mellett. Szűrd ki a kockázatos területeket korán.',
|
||||
faqEnv2A:
|
||||
'Tartalmazunk talajstabilitási adatokat, így ellenőrizheted a süllyeedést, agyagtalan zsugorodás-duzzadást és egyéb geológiai veszélyeket, mielőtt elköteleznéd magad egy ingatlan mellett. Szűrd ki a kockázatos területeket korán.',
|
||||
faqEnv3Q: 'Találhatok területeket gyors internettel, amelyek tényleg csendesek?',
|
||||
faqEnv3A: 'Rétegezd a szélessáv-sebesség szűrőt a közúti zajadatokkal, hogy megtaláld a kitűnő kapcsolattal és alacsony forgalmi zajjal rendelkező utcákat. Színezd bármelyik mérőszám szerint a területek összehasonlításához.',
|
||||
faqEnv3A:
|
||||
'Rétegezd a szélessáv-sebesség szűrőt a közúti zajadatokkal, hogy megtaláld a kitűnő kapcsolattal és alacsony forgalmi zajjal rendelkező utcákat. Színezd bármelyik mérőszám szerint a területek összehasonlításához.',
|
||||
// FAQ items — Why Perfect Postcode
|
||||
faqWhy1Q: 'Már használom a Rightmove-ot. Mit ad ez hozzá?',
|
||||
faqWhy1A: 'A Rightmove házakat mutat. Mi területeket. Bűnözési arányok, iskolai minősítések, szélessáv-sebességek, zajszintek, deprivációs pontok és még sok más, minden szűrhető egyetlen térképen. Még azelőtt megítélheted a szomszédságot, hogy akad hirdetésekre néznél.',
|
||||
faqWhy1A:
|
||||
'A Rightmove házakat mutat. Mi területeket. Bűnözési arányok, iskolai minősítések, szélessáv-sebességek, zajszintek, deprivációs pontok és még sok más, minden szűrhető egyetlen térképen. Még azelőtt megítélheted a szomszédságot, hogy akad hirdetésekre néznél.',
|
||||
faqWhy2Q: 'Nem tudom mindezt ingyen is utánanézni?',
|
||||
faqWhy2A: 'Összevethatnéd a rendőrségi adatokat, Ofsted jelentéseket, EPC nyilvántartást, Land Registry adatokat és ONS statisztikákat egyenként, irányítószámonként. Vagy mindezt szűrhetően és színkódoltan egyetlen térképen, másodpercek alatt.',
|
||||
faqWhy2A:
|
||||
'Összevethatnéd a rendőrségi adatokat, Ofsted jelentéseket, EPC nyilvántartást, Land Registry adatokat és ONS statisztikákat egyenként, irányítószámonként. Vagy mindezt szűrhetően és színkódoltan egyetlen térképen, másodpercek alatt.',
|
||||
faqWhy3Q: 'Honnan származnak az adatok?',
|
||||
faqWhy3A: 'Minden adatkészlet hivatalos brit kormányzati forrásokból származik: Land Registry, EPC nyilvántartás, ONS, Ofsted, Ofcom, data.police.uk és Defra. Nem scrapelünk ingatlanirrodákat és nem találunk ki semmit. Bármely rekordot ellenőrizheted az eredeti forrásban.',
|
||||
faqWhy3A:
|
||||
'Minden adatkészlet hivatalos brit kormányzati forrásokból származik: Land Registry, EPC nyilvántartás, ONS, Ofsted, Ofcom, data.police.uk és Defra. Nem scrapelünk ingatlanirrodákat és nem találunk ki semmit. Bármely rekordot ellenőrizheted az eredeti forrásban.',
|
||||
// FAQ items — Pricing and Access
|
||||
faqPricing1Q: 'Tényleg megéri fizetni egy ingatlan-kereső eszközért?',
|
||||
faqPricing1A: 'Egy lakásvásárlás valószínűleg a legnagyobb vásárlásod lesz. Egyetlen figyelmeztető jel felismerése (zajos út, gyenge internet, növekvő bűnözés) elköteleződés előtt éveknűi megbánást takaríthat meg. Ez kevesebbe kerül, mint egy tank benzin.',
|
||||
faqPricing1A:
|
||||
'Egy lakásvásárlás valószínűleg a legnagyobb vásárlásod lesz. Egyetlen figyelmeztető jel felismerése (zajos út, gyenge internet, növekvő bűnözés) elköteleződés előtt éveknűi megbánást takaríthat meg. Ez kevesebbe kerül, mint egy tank benzin.',
|
||||
faqPricing2Q: 'Ez előfizetés?',
|
||||
faqPricing2A: 'Nem. Egyszeri fizetés, örökre a tied. Használd intenzíven a keresés során, gyere vissza bármikor, ha kíváncsi vagy egy új területre, és még mindig ott van, ha újra költözöl.',
|
||||
faqPricing2A:
|
||||
'Nem. Egyszeri fizetés, örökre a tied. Használd intenzíven a keresés során, gyere vissza bármikor, ha kíváncsi vagy egy új területre, és még mindig ott van, ha újra költözöl.',
|
||||
faqPricing3Q: 'Mit érhetek el az ingyenes szinten?',
|
||||
faqPricing3A: 'Az ingyenes felhasználók a demó területen (Belső-London, megközelítőleg az 1-2. zóna) fedezhetik fel az összes funkciót. Anglia többi részének adataihoz élethosszig tartó hozzáférés szükséges.',
|
||||
faqPricing3A:
|
||||
'Az ingyenes felhasználók a demó területen (Belső-London, megközelítőleg az 1-2. zóna) fedezhetik fel az összes funkciót. Anglia többi részének adataihoz élethosszig tartó hozzáférés szükséges.',
|
||||
faqPricing4Q: 'Kérhetek visszatérítést?',
|
||||
faqPricing4A: 'Természetesen. 30 napos pénzvisszatérítési garanciát kínálunk. Ha nem vagy elégedett, írj a support@perfect-postcode.co.uk címre 30 napon belül a teljes visszatérítésért.',
|
||||
faqPricing4A:
|
||||
'Természetesen. 30 napos pénzvisszatérítési garanciát kínálunk. Ha nem vagy elégedett, írj a support@perfect-postcode.co.uk címre 30 napon belül a teljes visszatérítésért.',
|
||||
// FAQ items — Tips and Tricks
|
||||
faqTips1Q: 'Hogyan használjam az AI szűrőt a szűrők egyenkénti hozzáadása helyett?',
|
||||
faqTips1A: 'Írd le egyszerű angolul, mit szeretnél, például “csendes terület jó iskolák közelében, gyors internettel, £400e alatt”, és az összes megfelelő szűrőt egyszerre beállítja. Utána bármelyiket kézzel finomhangolhatod.',
|
||||
faqTips1A:
|
||||
'Írd le egyszerű angolul, mit szeretnél, például “csendes terület jó iskolák közelében, gyors internettel, £400e alatt”, és az összes megfelelő szűrőt egyszerre beállítja. Utána bármelyiket kézzel finomhangolhatod.',
|
||||
faqTips2Q: 'Elmenthetem a keresést, és később visszatérhetek hozzá?',
|
||||
faqTips2A: 'Nyomd meg a mentés gombot, és mindent rögzítünk: szűrőid, a nagyítási szint, és melyik adatréteg szerint színezel. Folytasd pontosan ott, ahol abbahagytad, vagy oszd meg a linket a pároddal.',
|
||||
faqTips2A:
|
||||
'Nyomd meg a mentés gombot, és mindent rögzítünk: szűrőid, a nagyítási szint, és melyik adatréteg szerint színezel. Folytasd pontosan ott, ahol abbahagytad, vagy oszd meg a linket a pároddal.',
|
||||
faqTips3Q: 'Exportálhatom az adatokat, amiket látok?',
|
||||
faqTips3A: 'Az exportálás gombbal letöltheted a jelenlegi szűrőknek megfelelő ingatlanokat táblázatként. Az export figyelembe veszi az összes aktív szűrőt, így pontosan azokat az adatokat kapod, amiket szeretnél.',
|
||||
faqTips3A:
|
||||
'Az exportálás gombbal letöltheted a jelenlegi szűrőknek megfelelő ingatlanokat táblázatként. Az export figyelembe veszi az összes aktív szűrőt, így pontosan azokat az adatokat kapod, amiket szeretnél.',
|
||||
},
|
||||
|
||||
// ── Account Page ───────────────────────────────────
|
||||
|
|
@ -535,17 +593,21 @@ const hu: Translations = {
|
|||
savedPage: {
|
||||
searches: 'Keresések',
|
||||
noSavedSearches: 'Még nincsenek mentett keresések',
|
||||
noSavedSearchesDesc: 'Mentsd el a szűrőket és a térképnézetet, hogy pontosan ott folytasd, ahol abbahagytad.',
|
||||
noSavedSearchesDesc:
|
||||
'Mentsd el a szűrőket és a térképnézetet, hogy pontosan ott folytasd, ahol abbahagytad.',
|
||||
noSavedProperties: 'Még nincsenek mentett ingatlanok',
|
||||
noSavedPropertiesDesc: 'Jelöld meg az ingatlanokat felfedezés közben, és építsd a rövid listádat elvesztés nélkül.',
|
||||
noSavedPropertiesDesc:
|
||||
'Jelöld meg az ingatlanokat felfedezés közben, és építsd a rövid listádat elvesztés nélkül.',
|
||||
openPostcode: 'Irányítószám megnyitása',
|
||||
viewListing: 'Hirdetés megtekintése',
|
||||
clickToRename: 'Kattints az átnevezéshez',
|
||||
notesPlaceholder: 'Írd le a gondolataidat...',
|
||||
deleteSearch: 'Keresés törlése',
|
||||
deleteSearchConfirm: 'Biztosan törölni szeretnéd ezt a mentett keresést? Ez nem vonható vissza.',
|
||||
deleteSearchConfirm:
|
||||
'Biztosan törölni szeretnéd ezt a mentett keresést? Ez nem vonható vissza.',
|
||||
deleteProperty: 'Ingatlan törlése',
|
||||
deletePropertyConfirm: 'Biztosan törölni szeretnéd ezt a mentett ingatlant? Ez nem vonható vissza.',
|
||||
deletePropertyConfirm:
|
||||
'Biztosan törölni szeretnéd ezt a mentett ingatlant? Ez nem vonható vissza.',
|
||||
bed: 'háló',
|
||||
epc: 'EPC',
|
||||
},
|
||||
|
|
@ -574,11 +636,14 @@ const hu: Translations = {
|
|||
youreInvited: 'Meghívást kaptál!',
|
||||
specialOffer: 'Különleges ajánlat!',
|
||||
invitedByFree: '{{name}} meghívott, hogy ingyenes élethosszig tartó hozzáférést kapj.',
|
||||
invitedByDiscount: '{{name}} megoszt veled egy 30%-os kedvezményt az élethosszig tartó hozzáférésre.',
|
||||
invitedByDiscount:
|
||||
'{{name}} megoszt veled egy 30%-os kedvezményt az élethosszig tartó hozzáférésre.',
|
||||
genericFreeInvite: 'Meghívást kaptál ingyenes élethosszig tartó hozzáférésre.',
|
||||
genericDiscount: 'Egy barát megoszt veled egy 30%-os kedvezményt az élethosszig tartó hozzáférésre.',
|
||||
genericDiscount:
|
||||
'Egy barát megoszt veled egy 30%-os kedvezményt az élethosszig tartó hozzáférésre.',
|
||||
exploreEvery: 'Fedezd fel Anglia minden szomszédságát',
|
||||
propertyInfo: 'Ingatlanárak, energetikai minősítések, bűnözési adatok, iskolai minősítések és még sok más',
|
||||
propertyInfo:
|
||||
'Ingatlanárak, energetikai minősítések, bűnözési adatok, iskolai minősítések és még sok más',
|
||||
invalidInvite: 'Érvénytelen meghívó',
|
||||
inviteAlreadyUsed: 'A meghívó már felhasználva',
|
||||
inviteAlreadyUsedDesc: 'Ez a meghívó link már be lett váltva.',
|
||||
|
|
@ -624,17 +689,22 @@ const hu: Translations = {
|
|||
// ── Tutorial ──────────────────────────────────────
|
||||
tutorial: {
|
||||
step1Title: 'Mondja el a térképnek, mi fontos',
|
||||
step1Content: 'Állítsa be a költségvetést, maximalis ingazási időt, iskola minőséget és bűnözési kúszöböt. Ami Önnek fontos. Csak a megfelelő területek maradnak kiemelve. Használja a szem ikont bármely jellemző szerinti színezéshez.',
|
||||
step1Content:
|
||||
'Állítsa be a költségvetést, maximalis ingazási időt, iskola minőséget és bűnözési kúszöböt. Ami Önnek fontos. Csak a megfelelő területek maradnak kiemelve. Használja a szem ikont bármely jellemző szerinti színezéshez.',
|
||||
step2Title: 'Vagy egyszerűen írja le',
|
||||
step2Content: 'Írja le magyarul, mit keres, például „csendes terület jó iskolák közelében £400k alatt”, és beállítjuk a szűrőket Önnek.',
|
||||
step2Content:
|
||||
'Írja le magyarul, mit keres, például „csendes terület jó iskolák közelében £400k alatt”, és beállítjuk a szűrőket Önnek.',
|
||||
step3Title: 'Fedezze fel, mi van odakint',
|
||||
step3Content: 'Görgessen és nagyítson Anglia-szerte. Kattintson bármely színes területre a bűnözés, iskolák, árak, szélessáv, zaj és egyéb adatok megtekintéséhez.',
|
||||
step3Content:
|
||||
'Görgessen és nagyítson Anglia-szerte. Kattintson bármely színes területre a bűnözés, iskolák, árak, szélessáv, zaj és egyéb adatok megtekintéséhez.',
|
||||
step4Title: 'Ugrás egy helyre',
|
||||
step4Content: 'Keressen rá bármely helyre vagy irányítószámra, hogy azonnal odajusson.',
|
||||
step5Title: 'Merüljön el a részletekben',
|
||||
step5Content: 'Tekintse meg a területi statisztikákat, hisztogramokat és az egyes ingatlanadatokat: árak, alapterület, energetikai besorolás és több.',
|
||||
step5Content:
|
||||
'Tekintse meg a területi statisztikákat, hisztogramokat és az egyes ingatlanadatokat: árak, alapterület, energetikai besorolás és több.',
|
||||
step6Title: 'Mi van a közelben?',
|
||||
step6Content: 'Kapcsolja be az iskolákat, üzleteket, állomásokat, parkokat és éttermeket a térképen, hogy lássa, mi érhető el.',
|
||||
step6Content:
|
||||
'Kapcsolja be az iskolákat, üzleteket, állomásokat, parkokat és éttermeket a térképen, hogy lássa, mi érhető el.',
|
||||
},
|
||||
|
||||
// ── Server-derived values ──────────────────────────
|
||||
|
|
@ -642,13 +712,13 @@ const hu: Translations = {
|
|||
// The English keys MUST match exactly what the API returns.
|
||||
server: {
|
||||
// ─ Feature group names ─
|
||||
'Properties': 'Ingatlanok',
|
||||
'Transport': 'Közlekedés',
|
||||
'Education': 'Oktatás',
|
||||
'Deprivation': 'Depriváció',
|
||||
'Crime': 'Bűnözés',
|
||||
'Demographics': 'Demográfia',
|
||||
'Amenities': 'Szolgáltatások',
|
||||
Properties: 'Ingatlanok',
|
||||
Transport: 'Közlekedés',
|
||||
Education: 'Oktatás',
|
||||
Deprivation: 'Depriváció',
|
||||
Crime: 'Bűnözés',
|
||||
Demographics: 'Demográfia',
|
||||
Amenities: 'Szolgáltatások',
|
||||
|
||||
// ─ Feature names (Properties) ─
|
||||
'Listing status': 'Hirdetés állapota',
|
||||
|
|
@ -664,8 +734,8 @@ const hu: Translations = {
|
|||
'Asking rent (monthly)': 'Kért bérleti díj (havi)',
|
||||
'Total floor area (sqm)': 'Teljes alapterület (nm)',
|
||||
'Number of bedrooms & living rooms': 'Háló- és nappalik száma',
|
||||
'Bedrooms': 'Hálószobák',
|
||||
'Bathrooms': 'Fürdőszobák',
|
||||
Bedrooms: 'Hálószobák',
|
||||
Bathrooms: 'Fürdőszobák',
|
||||
'Construction year': 'Építési év',
|
||||
'Date of last transaction': 'Utolsó tranzakció dátuma',
|
||||
'Listing date': 'Hirdetés dátuma',
|
||||
|
|
@ -675,7 +745,8 @@ const hu: Translations = {
|
|||
'Interior height (m)': 'Belmagasság (m)',
|
||||
|
||||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)': 'Távolság a legközelebbi vonat- vagy metróállomástól (km)',
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Távolság a legközelebbi vonat- vagy metróállomástól (km)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Jó+ általános iskolák 2 km-en belül',
|
||||
|
|
@ -725,7 +796,8 @@ const hu: Translations = {
|
|||
'Distance to nearest park (km)': 'Távolság a legközelebbi parktól (km)',
|
||||
'Number of parks within 2km': 'Parkok száma 2 km-en belül',
|
||||
'Number of restaurants within 2km': 'Éttermek száma 2 km-en belül',
|
||||
'Number of grocery shops and supermarkets within 2km': 'Élelmiszerboltok és szupermarketek száma 2 km-en belül',
|
||||
'Number of grocery shops and supermarkets within 2km':
|
||||
'Élelmiszerboltok és szupermarketek száma 2 km-en belül',
|
||||
'Noise (dB)': 'Zaj (dB)',
|
||||
'Max available download speed (Mbps)': 'Max elérhető letöltési sebesség (Mbps)',
|
||||
|
||||
|
|
@ -733,15 +805,15 @@ const hu: Translations = {
|
|||
'Historical sale': 'Történelmi eladás',
|
||||
'For sale': 'Eladó',
|
||||
'For rent': 'Kiadó',
|
||||
'Detached': 'Különálló',
|
||||
Detached: 'Különálló',
|
||||
'Semi-Detached': 'Ikerház',
|
||||
'Terraced': 'Sorház',
|
||||
Terraced: 'Sorház',
|
||||
'Flats/Maisonettes': 'Lakások/Maisonette-ek',
|
||||
'Other': 'Egyéb',
|
||||
'Freehold': 'Tulajdonjog',
|
||||
'Leasehold': 'Bérleti jog',
|
||||
'Yes': 'Igen',
|
||||
'No': 'Nem',
|
||||
Other: 'Egyéb',
|
||||
Freehold: 'Tulajdonjog',
|
||||
Leasehold: 'Bérleti jog',
|
||||
Yes: 'Igen',
|
||||
No: 'Nem',
|
||||
|
||||
// ─ Stacked chart labels ─
|
||||
'Serious crime': 'Súlyos bűncselekmény',
|
||||
|
|
@ -750,52 +822,52 @@ const hu: Translations = {
|
|||
|
||||
// ─ POI group names ─
|
||||
'Public Transport': 'Tömegközlekedés',
|
||||
'Leisure': 'Szabadidő',
|
||||
'Health': 'Egészségügy',
|
||||
Leisure: 'Szabadidő',
|
||||
Health: 'Egészségügy',
|
||||
'Emergency Services': 'Sürgősségi szolgálatok',
|
||||
'Groceries': 'Élelmiszer',
|
||||
Groceries: 'Élelmiszer',
|
||||
'Local Businesses': 'Helyi vállalkozások',
|
||||
'Culture': 'Kultúra',
|
||||
'Services': 'Szolgáltatások',
|
||||
'Shops': 'Üzletek',
|
||||
Culture: 'Kultúra',
|
||||
Services: 'Szolgáltatások',
|
||||
Shops: 'Üzletek',
|
||||
|
||||
// ─ POI categories ─
|
||||
'Airport': 'Repülőtér',
|
||||
'Ferry': 'Komp',
|
||||
Airport: 'Repülőtér',
|
||||
Ferry: 'Komp',
|
||||
'Rail station': 'Vasútállomás',
|
||||
'Bus stop': 'Buszmegálló',
|
||||
'Bus station': 'Buszpályaudvar',
|
||||
'Taxi rank': 'Taxiállomás',
|
||||
'Metro or Tram stop': 'Metró- vagy villamosmegálló',
|
||||
'Café': 'Kávézó',
|
||||
'Restaurant': 'Étterem',
|
||||
'Pub': 'Kocsma',
|
||||
'Bar': 'Bár',
|
||||
Café: 'Kávézó',
|
||||
Restaurant: 'Étterem',
|
||||
Pub: 'Kocsma',
|
||||
Bar: 'Bár',
|
||||
'Fast Food': 'Gyorsétterem',
|
||||
'Nightclub': 'Éjszakai klub',
|
||||
'Cinema': 'Mozi',
|
||||
'Theatre': 'Színház',
|
||||
Nightclub: 'Éjszakai klub',
|
||||
Cinema: 'Mozi',
|
||||
Theatre: 'Színház',
|
||||
'Live Music & Events': 'Élőzene és rendezvények',
|
||||
'Park': 'Park',
|
||||
'Playground': 'Játszótér',
|
||||
Park: 'Park',
|
||||
Playground: 'Játszótér',
|
||||
'Sports Centre': 'Sportközpont',
|
||||
'Entertainment': 'Szórakoztatás',
|
||||
'Supermarket': 'Szupermarket',
|
||||
Entertainment: 'Szórakoztatás',
|
||||
Supermarket: 'Szupermarket',
|
||||
'Convenience Store': 'Kísbolt',
|
||||
'Bakery': 'Pékség',
|
||||
Bakery: 'Pékség',
|
||||
'Butcher & Fishmonger': 'Hentes és halas',
|
||||
'Greengrocer': 'Zöldséges',
|
||||
Greengrocer: 'Zöldséges',
|
||||
'Off-Licence': 'Italozó',
|
||||
'Deli & Specialty': 'Csemege és különleges',
|
||||
'Fashion & Clothing': 'Divat és ruházat',
|
||||
'Electronics': 'Elektronika',
|
||||
Electronics: 'Elektronika',
|
||||
'Charity Shop': 'Jótékonysági bolt',
|
||||
'DIY & Hardware': 'Barkacs és vas',
|
||||
'Home & Garden': 'Otthon és kert',
|
||||
'Bookshop': 'Könyvesbolt',
|
||||
Bookshop: 'Könyvesbolt',
|
||||
'Pet Shop': 'Állatkereskedés',
|
||||
'Sports & Outdoor': 'Sport és szabadtér',
|
||||
'Newsagent': 'Újságárus',
|
||||
Newsagent: 'Újságárus',
|
||||
'Department Store': 'Áruház',
|
||||
'Gift & Hobby': 'Ajándék és hobbi',
|
||||
'Specialist Shop': 'Szaküzlet',
|
||||
|
|
@ -805,31 +877,31 @@ const hu: Translations = {
|
|||
'Car Services': 'Autós szolgáltatások',
|
||||
'Post Office': 'Posta',
|
||||
'Vet & Pet Care': 'Állatorvos és állatgondozás',
|
||||
'Bank': 'Bank',
|
||||
Bank: 'Bank',
|
||||
'Travel Agent': 'Utazási iroda',
|
||||
'Police': 'Rendőrség',
|
||||
Police: 'Rendőrség',
|
||||
'Fire Station': 'Tűzoltóság',
|
||||
'Ambulance Station': 'Mentőállomás',
|
||||
'GP Surgery': 'Háziorvosi rendelő',
|
||||
'Dentist': 'Fogorvos',
|
||||
'Pharmacy': 'Gyógyszertár',
|
||||
Dentist: 'Fogorvos',
|
||||
Pharmacy: 'Gyógyszertár',
|
||||
'Hospital & Clinic': 'Kórház és klinika',
|
||||
'Optician': 'Optikus',
|
||||
'Physiotherapy': 'Fizioterápia',
|
||||
Optician: 'Optikus',
|
||||
Physiotherapy: 'Fizioterápia',
|
||||
'Counselling & Therapy': 'Tanácsadás és terápia',
|
||||
'Care Home': 'Gondozóház',
|
||||
'Medical & Mobility': 'Egészségügyi és mobilitási eszközök',
|
||||
'Museum': 'Múzeum',
|
||||
'Gallery': 'Galéria',
|
||||
'Library': 'Könyvtár',
|
||||
Museum: 'Múzeum',
|
||||
Gallery: 'Galéria',
|
||||
Library: 'Könyvtár',
|
||||
'Place of Worship': 'Istentiszteleti hely',
|
||||
'Arts Centre': 'Művészeti központ',
|
||||
'Zoo': 'Állatkert',
|
||||
Zoo: 'Állatkert',
|
||||
'Tourist Attraction': 'Turisztikai látványosság',
|
||||
'School': 'Iskola',
|
||||
'Hotel': 'Szálloda',
|
||||
School: 'Iskola',
|
||||
Hotel: 'Szálloda',
|
||||
'Local Business': 'Helyi vállalkozás',
|
||||
'Offices': 'Irodák',
|
||||
Offices: 'Irodák',
|
||||
'EV Charging': 'Elektromos töltőállomás',
|
||||
'Fuel Station': 'Benzinkút',
|
||||
'Community Centre': 'Közösségi központ',
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ const zh: Translations = {
|
|||
// ── Upgrade Modal ──────────────────────────────────
|
||||
upgrade: {
|
||||
title: '查看整个英格兰',
|
||||
description: '您目前正在浏览演示区域。获取终身访问权限,覆盖每个邮编、每项筛选条件、每个社区。一次付款,永久使用。',
|
||||
description:
|
||||
'您目前正在浏览演示区域。获取终身访问权限,覆盖每个邮编、每项筛选条件、每个社区。一次付款,永久使用。',
|
||||
free: '免费',
|
||||
once: '/一次性',
|
||||
freeForEarly: '早期用户免费。无需信用卡。',
|
||||
|
|
@ -150,7 +151,8 @@ const zh: Translations = {
|
|||
|
||||
// ── Philosophy Popup ───────────────────────────────
|
||||
philosophy: {
|
||||
intro: '从必须满足的条件开始,再逐步添加加分项。每添加一个筛选条件,地图范围就会缩小。剩下的区域就是最适合您的。',
|
||||
intro:
|
||||
'从必须满足的条件开始,再逐步添加加分项。每添加一个筛选条件,地图范围就会缩小。剩下的区域就是最适合您的。',
|
||||
step1Title: '预算和基本条件',
|
||||
step1Desc: '(价格范围、建筑面积、房产类型)',
|
||||
step2Title: '通勤',
|
||||
|
|
@ -173,7 +175,8 @@ const zh: Translations = {
|
|||
selectDestination: '选择目的地...',
|
||||
bestCase: '最佳情况',
|
||||
bestCaseTitle: '最佳通勤时间',
|
||||
bestCaseDesc: '使用最快的实际出行时间(如果您把握好出发时间并赶上良好的换乘)。默认使用<strong>中位数</strong>,代表无论何时出发的典型出行时间。',
|
||||
bestCaseDesc:
|
||||
'使用最快的实际出行时间(如果您把握好出发时间并赶上良好的换乘)。默认使用<strong>中位数</strong>,代表无论何时出发的典型出行时间。',
|
||||
previewOnMap: '在地图上预览',
|
||||
stopPreviewing: '停止预览',
|
||||
removeTravelTime: '移除通勤时间',
|
||||
|
|
@ -242,6 +245,7 @@ const zh: Translations = {
|
|||
bathrooms: '浴室:',
|
||||
rooms: '房间:',
|
||||
built: '建造年份:',
|
||||
formerCouncil: '原公房:',
|
||||
epcRating: '能源评级:',
|
||||
epcPotential: '潜在能源评级:',
|
||||
listed: '上市日期:',
|
||||
|
|
@ -252,7 +256,8 @@ const zh: Translations = {
|
|||
perSqm: '/m²',
|
||||
searchPlaceholder: '按地址或邮编搜索...',
|
||||
propertyData: '房产数据',
|
||||
propertyDataDesc: '价格来自英国土地注册局(买家实际支付的金额)。建筑面积、能源评级、建造年份和产权来自官方能源性能证书调查。两个数据源通过每个邮编内的地址进行匹配。',
|
||||
propertyDataDesc:
|
||||
'价格来自英国土地注册局(买家实际支付的金额)。建筑面积、能源评级、建造年份和产权来自官方能源性能证书调查。两个数据源通过每个邮编内的地址进行匹配。',
|
||||
},
|
||||
|
||||
// ── Area Pane ──────────────────────────────────────
|
||||
|
|
@ -289,7 +294,8 @@ const zh: Translations = {
|
|||
poiPane: {
|
||||
pois: '兴趣点',
|
||||
pointsOfInterest: '兴趣点',
|
||||
poiDescription: '数据来自 OpenStreetMap。涵盖公共交通站点、商店、餐厅、医疗机构、休闲场所等。定期更新,类别覆盖完整。',
|
||||
poiDescription:
|
||||
'数据来自 OpenStreetMap。涵盖公共交通站点、商店、餐厅、医疗机构、休闲场所等。定期更新,类别覆盖完整。',
|
||||
searchCategories: '搜索类别...',
|
||||
dataSourceInfo: '数据来源信息',
|
||||
},
|
||||
|
|
@ -322,7 +328,8 @@ const zh: Translations = {
|
|||
heroTitle2: '价值',
|
||||
heroTitle3: '最小妥协。',
|
||||
heroSubtitle: '正在找房?让您最大的投资成为最明智的决定。',
|
||||
heroDescription: '选择太多,找到合适的可能让人不知所措。我们的交互式地图让一切变得简单:选择您的必要条件,立即看到符合的区域。',
|
||||
heroDescription:
|
||||
'选择太多,找到合适的可能让人不知所措。我们的交互式地图让一切变得简单:选择您的必要条件,立即看到符合的区域。',
|
||||
exploreTheMap: '探索地图',
|
||||
seeTheDifference: '看看有何不同',
|
||||
statProperties: '处房产',
|
||||
|
|
@ -330,8 +337,10 @@ const zh: Translations = {
|
|||
statEvery: '覆盖',
|
||||
statPostcodeInEngland: '英格兰每个邮编',
|
||||
ourPhilosophy: '我们的理念',
|
||||
philosophyP1: '在 Rightmove 上,您需要先选一个区域,然后期望它足够好。最终您不得不在十几个标签页中交叉对比犯罪数据、学校报告和宽带速度,一个邮编一个邮编地查。',
|
||||
philosophyP2: '我们反其道而行。告诉我们您的需求(预算、通勤、学校、安全),我们为您展示英格兰所有符合条件的区域。不用猜测,不浪费看房时间。',
|
||||
philosophyP1:
|
||||
'在 Rightmove 上,您需要先选一个区域,然后期望它足够好。最终您不得不在十几个标签页中交叉对比犯罪数据、学校报告和宽带速度,一个邮编一个邮编地查。',
|
||||
philosophyP2:
|
||||
'我们反其道而行。告诉我们您的需求(预算、通勤、学校、安全),我们为您展示英格兰所有符合条件的区域。不用猜测,不浪费看房时间。',
|
||||
howToUseIt: '使用方法',
|
||||
howStep1Title: '设定必要条件',
|
||||
howStep1Desc: '预算、通勤、学校——地图只显示符合条件的区域。',
|
||||
|
|
@ -361,7 +370,8 @@ const zh: Translations = {
|
|||
pricingPage: {
|
||||
title: '早期访问价格',
|
||||
subtitle: '一次付款,永久访问。越早加入,价格越优惠。',
|
||||
costContext: '买房需要支付超过 £10,000 的印花税、£1,500 的律师费、£500 的房屋评估费。选错区域,您可能要忍受漫长的通勤、差劲的学校,或一条您事先不知道的嘈杂马路。',
|
||||
costContext:
|
||||
'买房需要支付超过 £10,000 的印花税、£1,500 的律师费、£500 的房屋评估费。选错区域,您可能要忍受漫长的通勤、差劲的学校,或一条您事先不知道的嘈杂马路。',
|
||||
lessThanSurvey: '不到一次房屋评估的费用,却有用得多。',
|
||||
currentTier: '当前档位',
|
||||
firstNUsers: '前 {{count}} 名用户',
|
||||
|
|
@ -392,7 +402,8 @@ const zh: Translations = {
|
|||
faq: '常见问题',
|
||||
dataSources: '数据来源',
|
||||
support: '支持',
|
||||
dataSourcesIntro: '本应用整合了 {{count}} 个开放数据集,涵盖房产价格、能源性能、交通、人口统计、犯罪、环境等领域。',
|
||||
dataSourcesIntro:
|
||||
'本应用整合了 {{count}} 个开放数据集,涵盖房产价格、能源性能、交通、人口统计、犯罪、环境等领域。',
|
||||
faqIntro: '无论您是购房、租房还是单纯浏览,以下是 Perfect Postcode 如何帮助您找到理想区域。',
|
||||
supportIntro: '有问题?请查看我们的常见问题或直接联系我们。',
|
||||
source: '来源:',
|
||||
|
|
@ -413,7 +424,8 @@ const zh: Translations = {
|
|||
dsPricePaidUse: '英格兰完整的历史房产成交价格数据。',
|
||||
dsEpcName: 'Energy Performance Certificates (EPC)',
|
||||
dsEpcOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsEpcUse: '住宅能源性能证书,提供建筑面积、房间数量、建造年份、能源评级、房产类型和建筑形式等信息。通过每个邮编内的地址与成交价格数据进行匹配。业主可以退出公开披露。',
|
||||
dsEpcUse:
|
||||
'住宅能源性能证书,提供建筑面积、房间数量、建造年份、能源评级、房产类型和建筑形式等信息。通过每个邮编内的地址与成交价格数据进行匹配。业主可以退出公开披露。',
|
||||
dsNsplName: 'National Statistics Postcode Lookup (NSPL)',
|
||||
dsNsplOrigin: 'ONS / ArcGIS',
|
||||
dsNsplUse: '将邮编映射到坐标和统计区域代码,用于将所有区域级数据集关联到各个房产。',
|
||||
|
|
@ -422,34 +434,41 @@ const zh: Translations = {
|
|||
dsIodUse: '英格兰每个社区在收入、就业、教育、健康、犯罪和居住环境方面的相对贫困指数。',
|
||||
dsEthnicityName: '按族裔划分的人口(2021 年人口普查)',
|
||||
dsEthnicityOrigin: 'ONS',
|
||||
dsEthnicityUse: '按族裔群体(南亚裔、东亚裔、黑人、混血、白人、其他)划分的各地方政府辖区人口百分比。',
|
||||
dsEthnicityUse:
|
||||
'按族裔群体(南亚裔、东亚裔、黑人、混血、白人、其他)划分的各地方政府辖区人口百分比。',
|
||||
dsCrimeName: 'Street-level Crime Data',
|
||||
dsCrimeOrigin: 'data.police.uk',
|
||||
dsCrimeUse: '2023 年至 2025 年的街道级犯罪数据,按 LSOA 和犯罪类型(暴力犯罪、入室盗窃、反社会行为、毒品、车辆犯罪等)汇总为年均值。',
|
||||
dsCrimeUse:
|
||||
'2023 年至 2025 年的街道级犯罪数据,按 LSOA 和犯罪类型(暴力犯罪、入室盗窃、反社会行为、毒品、车辆犯罪等)汇总为年均值。',
|
||||
dsOsmName: 'OpenStreetMap POIs',
|
||||
dsOsmOrigin: 'OpenStreetMap contributors / Geofabrik',
|
||||
dsOsmUse: '涵盖大不列颠地区的商店、餐厅、医疗、休闲、旅游等兴趣点。',
|
||||
dsGreenspaceName: 'OS Open Greenspace',
|
||||
dsGreenspaceOrigin: 'Ordnance Survey',
|
||||
dsGreenspaceUse: '大不列颠地区权威的绿地边界数据,包括公共公园、花园、运动场和游乐场。多边形质心用于公园邻近度计数和最近公园距离计算。',
|
||||
dsGreenspaceUse:
|
||||
'大不列颠地区权威的绿地边界数据,包括公共公园、花园、运动场和游乐场。多边形质心用于公园邻近度计数和最近公园距离计算。',
|
||||
dsNaptanName: 'NaPTAN (Public Transport Stops)',
|
||||
dsNaptanOrigin: 'Department for Transport',
|
||||
dsNaptanUse: '英格兰各地铁路、公交、地铁/有轨电车、渡轮和机场的站点位置。',
|
||||
dsNoiseName: 'Defra Noise Mapping',
|
||||
dsNoiseOrigin: 'Defra / Environment Agency',
|
||||
dsNoiseUse: '来自 2022 年战略噪音测绘的道路噪音水平(24 小时加权平均值),经高分辨率建模并在每个邮编处采样。',
|
||||
dsNoiseUse:
|
||||
'来自 2022 年战略噪音测绘的道路噪音水平(24 小时加权平均值),经高分辨率建模并在每个邮编处采样。',
|
||||
dsOfstedName: 'Ofsted School Inspections',
|
||||
dsOfstedOrigin: 'Ofsted',
|
||||
dsOfstedUse: '公立学校最新督察结果(截至 2025 年 4 月)。按邮编取平均值,得出当地学校质量评分(1=优秀至4=不合格)。',
|
||||
dsOfstedUse:
|
||||
'公立学校最新督察结果(截至 2025 年 4 月)。按邮编取平均值,得出当地学校质量评分(1=优秀至4=不合格)。',
|
||||
dsBroadbandName: 'Ofcom Broadband Performance',
|
||||
dsBroadbandOrigin: 'Ofcom',
|
||||
dsBroadbandUse: '来自 Ofcom Connected Nations 2025 的各区域固定宽带覆盖率和最大下载速度。',
|
||||
dsCouncilTaxName: 'Council Tax Levels 2025-26',
|
||||
dsCouncilTaxOrigin: 'Ministry of Housing, Communities & Local Government',
|
||||
dsCouncilTaxUse: '英格兰所有 296 个计费机构的 A 至 H 等级年度市政税税率,适用于两名成年人居住的住宅。通过 NSPL 邮编查询中的地方政府区域代码关联到房产。',
|
||||
dsCouncilTaxUse:
|
||||
'英格兰所有 296 个计费机构的 A 至 H 等级年度市政税税率,适用于两名成年人居住的住宅。通过 NSPL 邮编查询中的地方政府区域代码关联到房产。',
|
||||
dsRentalName: 'Private Rental Market Statistics',
|
||||
dsRentalOrigin: 'ONS / Valuation Office Agency',
|
||||
dsRentalUse: '按地方政府辖区和卧室类别划分的月度私人租金中位数(2022 年 10 月至 2023 年 9 月)。通过地方政府区域代码和估算卧室数量关联到房产。',
|
||||
dsRentalUse:
|
||||
'按地方政府辖区和卧室类别划分的月度私人租金中位数(2022 年 10 月至 2023 年 9 月)。通过地方政府区域代码和估算卧室数量关联到房产。',
|
||||
// FAQ section titles
|
||||
faqFindingTitle: '寻找理想区域',
|
||||
faqCommuteTitle: '通勤与出行',
|
||||
|
|
@ -462,61 +481,85 @@ const zh: Translations = {
|
|||
faqTipsTitle: '使用技巧',
|
||||
// FAQ items — Finding Your Area
|
||||
faqFinding1Q: '我完全不知道该看哪些区域,这个工具能帮到我吗?',
|
||||
faqFinding1A: '这正是它的用途。设置您的筛选条件(预算、通勤时间、低犯罪率、好学校),地图就会亮起来,显示所有符合条件的区域。不用再半夜搜索"曼彻斯特附近最好的居住区"了。',
|
||||
faqFinding1A:
|
||||
'这正是它的用途。设置您的筛选条件(预算、通勤时间、低犯罪率、好学校),地图就会亮起来,显示所有符合条件的区域。不用再半夜搜索"曼彻斯特附近最好的居住区"了。',
|
||||
faqFinding2Q: '我要搬到一个从未去过的地方,该从何开始?',
|
||||
faqFinding2A: '设置您关心的筛选条件,地图会立即高亮显示符合条件的区域。从"我一条街都不认识"到得出候选名单,只需几分钟。',
|
||||
faqFinding2A:
|
||||
'设置您关心的筛选条件,地图会立即高亮显示符合条件的区域。从"我一条街都不认识"到得出候选名单,只需几分钟。',
|
||||
faqFinding3Q: '如何找到同时满足我所有要求的区域?',
|
||||
faqFinding3A: '叠加多个筛选条件(犯罪率低于平均水平、好学校、通勤时间少于 40 分钟),然后按价格为地图着色,找出性价比最高的区域。拖动滑块时地图会实时更新,让您即时看到变化。',
|
||||
faqFinding3A:
|
||||
'叠加多个筛选条件(犯罪率低于平均水平、好学校、通勤时间少于 40 分钟),然后按价格为地图着色,找出性价比最高的区域。拖动滑块时地图会实时更新,让您即时看到变化。',
|
||||
// FAQ items — Commute and Travel
|
||||
faqCommute1Q: '我能看到从不同区域到公司的实际通勤时间吗?',
|
||||
faqCommute1A: '设置您的工作地点作为目的地,我们会按通勤时间为每个邮编着色——无论是开车、骑车还是公共交通。筛选出您的最大通勤时间,其余区域就会消失。',
|
||||
faqCommute1A:
|
||||
'设置您的工作地点作为目的地,我们会按通勤时间为每个邮编着色——无论是开车、骑车还是公共交通。筛选出您的最大通勤时间,其余区域就会消失。',
|
||||
faqCommute2Q: '这比查 Google Maps 好在哪里?',
|
||||
faqCommute2A: 'Google Maps 一次只能查看一条路线。我们一次性将英格兰每个邮编按通勤时间着色,让您可以同时比较数百个区域,而不是逐个搜索。',
|
||||
faqCommute2A:
|
||||
'Google Maps 一次只能查看一条路线。我们一次性将英格兰每个邮编按通勤时间着色,让您可以同时比较数百个区域,而不是逐个搜索。',
|
||||
// FAQ items — Budget and Value
|
||||
faqBudget1Q: '如何找到单位面积性价比最高的区域?',
|
||||
faqBudget1A: '按每平方米价格筛选,您会立即看到哪些邮编的单位面积价格最低。搭配能源评级筛选,避免取暖费用过高的房产。',
|
||||
faqBudget1A:
|
||||
'按每平方米价格筛选,您会立即看到哪些邮编的单位面积价格最低。搭配能源评级筛选,避免取暖费用过高的房产。',
|
||||
faqBudget2Q: '怎么确定一个便宜的区域不是因为有问题才便宜?',
|
||||
faqBudget2A: '将贫困指数、犯罪统计、学校评级和宽带速度叠加在价格旁边查看。如果一个邮编价格实惠且在各项重要指标上表现良好,那您就找到了真正的高性价比——而不是隐藏着您还没发现的问题的低价。',
|
||||
faqBudget2A:
|
||||
'将贫困指数、犯罪统计、学校评级和宽带速度叠加在价格旁边查看。如果一个邮编价格实惠且在各项重要指标上表现良好,那您就找到了真正的高性价比——而不是隐藏着您还没发现的问题的低价。',
|
||||
// FAQ items — Safety and Neighbourhood
|
||||
faqSafety1Q: '搬家前如何查看一个区域是否安全?',
|
||||
faqSafety1A: '我们将真实的警方犯罪记录数据按类型细分,叠加到英格兰每个社区上。按暴力犯罪、入室盗窃或反社会行为筛选,立即看到哪些邮编的犯罪数据最低。',
|
||||
faqSafety1A:
|
||||
'我们将真实的警方犯罪记录数据按类型细分,叠加到英格兰每个社区上。按暴力犯罪、入室盗窃或反社会行为筛选,立即看到哪些邮编的犯罪数据最低。',
|
||||
faqSafety2Q: '我总是找到网上看起来很好的房子,到了才发现周边环境很差。',
|
||||
faqSafety2A: '这正是这个工具存在的意义。在一张地图上叠加犯罪率、噪音水平、贫困指数、附近的酒吧和公园以及宽带速度,这样您在预约看房之前就能了解一个社区的真实面貌。',
|
||||
faqSafety2A:
|
||||
'这正是这个工具存在的意义。在一张地图上叠加犯罪率、噪音水平、贫困指数、附近的酒吧和公园以及宽带速度,这样您在预约看房之前就能了解一个社区的真实面貌。',
|
||||
// FAQ items — Families and Schools
|
||||
faqFamilies1Q: '我能在一次搜索中找到学校好又犯罪率低的区域吗?',
|
||||
faqFamilies1A: '可以。叠加 Ofsted 评级、犯罪率、公园等对您家庭重要的筛选条件,地图只会高亮显示符合所有条件的区域。不用再在五个不同网站之间交叉比对了。',
|
||||
faqFamilies1A:
|
||||
'可以。叠加 Ofsted 评级、犯罪率、公园等对您家庭重要的筛选条件,地图只会高亮显示符合所有条件的区域。不用再在五个不同网站之间交叉比对了。',
|
||||
faqFamilies2Q: '如何知道一个社区附近是否有公园和游乐场?',
|
||||
faqFamilies2A: '打开公园和绿地 POI 图层,直接在地图上查看。您还可以按每个邮编步行范围内的公园数量进行筛选。',
|
||||
faqFamilies2A:
|
||||
'打开公园和绿地 POI 图层,直接在地图上查看。您还可以按每个邮编步行范围内的公园数量进行筛选。',
|
||||
// FAQ items — Environment and Quality of Life
|
||||
faqEnv1Q: '能找到不在嘈杂马路旁的节能住宅吗?',
|
||||
faqEnv1A: '按 EPC 评级(A 至 C)筛选,然后叠加道路噪音数据,排除超过您阈值的区域。按任一指标为地图着色,一目了然地找到安静且节能的街道。',
|
||||
faqEnv1A:
|
||||
'按 EPC 评级(A 至 C)筛选,然后叠加道路噪音数据,排除超过您阈值的区域。按任一指标为地图着色,一目了然地找到安静且节能的街道。',
|
||||
faqEnv2Q: '有洪水或地基沉降风险数据吗?',
|
||||
faqEnv2A: '我们包含地基稳定性数据,让您在购房前检查沉降、膨胀收缩黏土和其他地质风险。尽早排除高风险区域。',
|
||||
faqEnv2A:
|
||||
'我们包含地基稳定性数据,让您在购房前检查沉降、膨胀收缩黏土和其他地质风险。尽早排除高风险区域。',
|
||||
faqEnv3Q: '能找到宽带速度快又安静的区域吗?',
|
||||
faqEnv3A: '将宽带速度筛选与道路噪音数据叠加,找到连接速度快且交通噪音低的街道。按任一指标着色,一目了然地比较各区域。',
|
||||
faqEnv3A:
|
||||
'将宽带速度筛选与道路噪音数据叠加,找到连接速度快且交通噪音低的街道。按任一指标着色,一目了然地比较各区域。',
|
||||
// FAQ items — Why Perfect Postcode
|
||||
faqWhy1Q: '我已经在用 Rightmove 了,这个工具有什么额外价值?',
|
||||
faqWhy1A: 'Rightmove 展示房源,我们展示区域。犯罪率、学校评级、宽带速度、噪音水平、贫困指数等等——全部可在一张地图上筛选。您可以在查看房源之前先了解一个社区。',
|
||||
faqWhy1A:
|
||||
'Rightmove 展示房源,我们展示区域。犯罪率、学校评级、宽带速度、噪音水平、贫困指数等等——全部可在一张地图上筛选。您可以在查看房源之前先了解一个社区。',
|
||||
faqWhy2Q: '我不能自己免费查到这些信息吗?',
|
||||
faqWhy2A: '您当然可以逐个邮编地交叉比对警方数据、Ofsted 报告、EPC 登记、Land Registry 记录和 ONS 统计数据。或者,您可以在几秒钟内在一张地图上筛选和查看所有信息。',
|
||||
faqWhy2A:
|
||||
'您当然可以逐个邮编地交叉比对警方数据、Ofsted 报告、EPC 登记、Land Registry 记录和 ONS 统计数据。或者,您可以在几秒钟内在一张地图上筛选和查看所有信息。',
|
||||
faqWhy3Q: '数据到底来自哪里?',
|
||||
faqWhy3A: '每个数据集都来自英国官方政府来源:Land Registry、EPC 登记、ONS、Ofsted、Ofcom、data.police.uk 和 Defra。我们不抓取房产中介数据,也不编造任何信息。您可以对照原始来源验证任何记录。',
|
||||
faqWhy3A:
|
||||
'每个数据集都来自英国官方政府来源:Land Registry、EPC 登记、ONS、Ofsted、Ofcom、data.police.uk 和 Defra。我们不抓取房产中介数据,也不编造任何信息。您可以对照原始来源验证任何记录。',
|
||||
// FAQ items — Pricing and Access
|
||||
faqPricing1Q: '花钱买一个找房工具真的值得吗?',
|
||||
faqPricing1A: '买房可能是您一生中最大的一笔支出。在做决定之前发现一个问题(嘈杂的马路、差劲的宽带、上升的犯罪率)就可能让您避免多年的后悔。而这个工具的费用还不到一箱油钱。',
|
||||
faqPricing1A:
|
||||
'买房可能是您一生中最大的一笔支出。在做决定之前发现一个问题(嘈杂的马路、差劲的宽带、上升的犯罪率)就可能让您避免多年的后悔。而这个工具的费用还不到一箱油钱。',
|
||||
faqPricing2Q: '这是订阅制吗?',
|
||||
faqPricing2A: '不是。一次性付款,永久使用。在找房期间密集使用,对新区域好奇时随时回来看,将来再搬家时它依然在。',
|
||||
faqPricing2A:
|
||||
'不是。一次性付款,永久使用。在找房期间密集使用,对新区域好奇时随时回来看,将来再搬家时它依然在。',
|
||||
faqPricing3Q: '免费版能用哪些功能?',
|
||||
faqPricing3A: '免费用户可以在演示区域(伦敦市中心,大约 1 至 2 区)内探索所有功能。要访问英格兰其他地区的数据,需要获取终身访问权限。',
|
||||
faqPricing3A:
|
||||
'免费用户可以在演示区域(伦敦市中心,大约 1 至 2 区)内探索所有功能。要访问英格兰其他地区的数据,需要获取终身访问权限。',
|
||||
faqPricing4Q: '可以退款吗?',
|
||||
faqPricing4A: '当然可以。我们提供 30 天退款保证。如果您不满意,请在 30 天内发送邮件至 support@perfect-postcode.co.uk 申请全额退款。',
|
||||
faqPricing4A:
|
||||
'当然可以。我们提供 30 天退款保证。如果您不满意,请在 30 天内发送邮件至 support@perfect-postcode.co.uk 申请全额退款。',
|
||||
// FAQ items — Tips and Tricks
|
||||
faqTips1Q: '如何使用 AI 筛选功能,而不是逐个添加筛选条件?',
|
||||
faqTips1A: '用自然语言描述您的需求,例如"安静的区域、好学校附近、宽带速度快、40 万英镑以下",系统会一次性设置所有相关筛选条件。之后您可以手动微调。',
|
||||
faqTips1A:
|
||||
'用自然语言描述您的需求,例如"安静的区域、好学校附近、宽带速度快、40 万英镑以下",系统会一次性设置所有相关筛选条件。之后您可以手动微调。',
|
||||
faqTips2Q: '我能保存搜索条件以后再用吗?',
|
||||
faqTips2A: '点击保存按钮,所有内容都会被记录:您的筛选条件、缩放级别以及当前着色的数据图层。下次从上次离开的地方继续,或将链接分享给您的伴侣。',
|
||||
faqTips2A:
|
||||
'点击保存按钮,所有内容都会被记录:您的筛选条件、缩放级别以及当前着色的数据图层。下次从上次离开的地方继续,或将链接分享给您的伴侣。',
|
||||
faqTips3Q: '我能导出正在查看的数据吗?',
|
||||
faqTips3A: '使用导出按钮将当前筛选后的房产下载为电子表格。导出结果会遵循您所有的活动筛选条件,确保您获得的正是所需的数据。',
|
||||
faqTips3A:
|
||||
'使用导出按钮将当前筛选后的房产下载为电子表格。导出结果会遵循您所有的活动筛选条件,确保您获得的正是所需的数据。',
|
||||
},
|
||||
|
||||
// ── Account Page ───────────────────────────────────
|
||||
|
|
@ -623,11 +666,14 @@ const zh: Translations = {
|
|||
// ── Tutorial ──────────────────────────────────────
|
||||
tutorial: {
|
||||
step1Title: '告诉地图什么重要',
|
||||
step1Content: '设置预算、通勤上限、学校质量、犯罪门槛。您关心的一切。只有符合条件的区域会保持高亮。使用眼睛图标按任意特征着色。',
|
||||
step1Content:
|
||||
'设置预算、通勤上限、学校质量、犯罪门槛。您关心的一切。只有符合条件的区域会保持高亮。使用眼睛图标按任意特征着色。',
|
||||
step2Title: '或者直接描述',
|
||||
step2Content: '用中文输入您的需求,例如“安静的地区,靠近好学校,£400k 以下”,我们会为您设置筛选。',
|
||||
step2Content:
|
||||
'用中文输入您的需求,例如“安静的地区,靠近好学校,£400k 以下”,我们会为您设置筛选。',
|
||||
step3Title: '探索现有住宅',
|
||||
step3Content: '在英格兰各地平移和缩放。点击任何彩色区域查看犯罪、学校、价格、宽带、噪音等信息。',
|
||||
step3Content:
|
||||
'在英格兰各地平移和缩放。点击任何彩色区域查看犯罪、学校、价格、宽带、噪音等信息。',
|
||||
step4Title: '跳转到某个位置',
|
||||
step4Content: '搜索任何地点或邮编,即可直接跳转。',
|
||||
step5Title: '深入了解细节',
|
||||
|
|
@ -641,13 +687,13 @@ const zh: Translations = {
|
|||
// The English keys MUST match exactly what the API returns.
|
||||
server: {
|
||||
// ─ Feature group names ─
|
||||
'Properties': '房产',
|
||||
'Transport': '交通',
|
||||
'Education': '教育',
|
||||
'Deprivation': '贫困指数',
|
||||
'Crime': '犯罪',
|
||||
'Demographics': '人口统计',
|
||||
'Amenities': '配套设施',
|
||||
Properties: '房产',
|
||||
Transport: '交通',
|
||||
Education: '教育',
|
||||
Deprivation: '贫困指数',
|
||||
Crime: '犯罪',
|
||||
Demographics: '人口统计',
|
||||
Amenities: '配套设施',
|
||||
|
||||
// ─ Feature names (Properties) ─
|
||||
'Listing status': '房源状态',
|
||||
|
|
@ -663,8 +709,8 @@ const zh: Translations = {
|
|||
'Asking rent (monthly)': '月租',
|
||||
'Total floor area (sqm)': '总建筑面积(平方米)',
|
||||
'Number of bedrooms & living rooms': '卧室和客厅数量',
|
||||
'Bedrooms': '卧室',
|
||||
'Bathrooms': '浴室',
|
||||
Bedrooms: '卧室',
|
||||
Bathrooms: '浴室',
|
||||
'Construction year': '建造年份',
|
||||
'Date of last transaction': '上次交易日期',
|
||||
'Listing date': '上市日期',
|
||||
|
|
@ -728,20 +774,19 @@ const zh: Translations = {
|
|||
'Noise (dB)': '噪音(分贝)',
|
||||
'Max available download speed (Mbps)': '最大可用下载速度(Mbps)',
|
||||
|
||||
|
||||
// ─ Enum values ─
|
||||
'Historical sale': '历史交易',
|
||||
'For sale': '在售',
|
||||
'For rent': '出租',
|
||||
'Detached': '独立式住宅',
|
||||
Detached: '独立式住宅',
|
||||
'Semi-Detached': '半独立式住宅',
|
||||
'Terraced': '联排住宅',
|
||||
Terraced: '联排住宅',
|
||||
'Flats/Maisonettes': '公寓/复式公寓',
|
||||
'Other': '其他',
|
||||
'Freehold': '永久产权',
|
||||
'Leasehold': '租赁产权',
|
||||
'Yes': '是',
|
||||
'No': '否',
|
||||
Other: '其他',
|
||||
Freehold: '永久产权',
|
||||
Leasehold: '租赁产权',
|
||||
Yes: '是',
|
||||
No: '否',
|
||||
|
||||
// ─ Stacked chart labels ─
|
||||
'Serious crime': '严重犯罪',
|
||||
|
|
@ -750,52 +795,52 @@ const zh: Translations = {
|
|||
|
||||
// ─ POI group names ─
|
||||
'Public Transport': '公共交通',
|
||||
'Leisure': '休闲',
|
||||
'Health': '健康',
|
||||
Leisure: '休闲',
|
||||
Health: '健康',
|
||||
'Emergency Services': '紧急服务',
|
||||
'Groceries': '食品杂货',
|
||||
Groceries: '食品杂货',
|
||||
'Local Businesses': '本地商业',
|
||||
'Culture': '文化',
|
||||
'Services': '服务',
|
||||
'Shops': '商店',
|
||||
Culture: '文化',
|
||||
Services: '服务',
|
||||
Shops: '商店',
|
||||
|
||||
// ─ POI categories ─
|
||||
'Airport': '机场',
|
||||
'Ferry': '渡轮',
|
||||
Airport: '机场',
|
||||
Ferry: '渡轮',
|
||||
'Rail station': '火车站',
|
||||
'Bus stop': '公交站',
|
||||
'Bus station': '公交枢纽',
|
||||
'Taxi rank': '出租车站',
|
||||
'Metro or Tram stop': '地铁或有轨电车站',
|
||||
'Café': '咖啡馆',
|
||||
'Restaurant': '餐厅',
|
||||
'Pub': '酒吧',
|
||||
'Bar': '酒吧',
|
||||
Café: '咖啡馆',
|
||||
Restaurant: '餐厅',
|
||||
Pub: '酒吧',
|
||||
Bar: '酒吧',
|
||||
'Fast Food': '快餐',
|
||||
'Nightclub': '夜店',
|
||||
'Cinema': '电影院',
|
||||
'Theatre': '剧院',
|
||||
Nightclub: '夜店',
|
||||
Cinema: '电影院',
|
||||
Theatre: '剧院',
|
||||
'Live Music & Events': '现场音乐与活动',
|
||||
'Park': '公园',
|
||||
'Playground': '游乐场',
|
||||
Park: '公园',
|
||||
Playground: '游乐场',
|
||||
'Sports Centre': '体育中心',
|
||||
'Entertainment': '娱乐',
|
||||
'Supermarket': '超市',
|
||||
Entertainment: '娱乐',
|
||||
Supermarket: '超市',
|
||||
'Convenience Store': '便利店',
|
||||
'Bakery': '面包戺',
|
||||
Bakery: '面包戺',
|
||||
'Butcher & Fishmonger': '肉铺与鱼铺',
|
||||
'Greengrocer': '果蔬店',
|
||||
Greengrocer: '果蔬店',
|
||||
'Off-Licence': '酒类商店',
|
||||
'Deli & Specialty': '熟食与特产店',
|
||||
'Fashion & Clothing': '时装服饰',
|
||||
'Electronics': '电子产品',
|
||||
Electronics: '电子产品',
|
||||
'Charity Shop': '慈善商店',
|
||||
'DIY & Hardware': '建材五金',
|
||||
'Home & Garden': '家居与园艺',
|
||||
'Bookshop': '书店',
|
||||
Bookshop: '书店',
|
||||
'Pet Shop': '宠物店',
|
||||
'Sports & Outdoor': '体育与户外',
|
||||
'Newsagent': '报刊亭',
|
||||
Newsagent: '报刊亭',
|
||||
'Department Store': '百货商店',
|
||||
'Gift & Hobby': '礼品与爱好',
|
||||
'Specialist Shop': '专业商店',
|
||||
|
|
@ -805,31 +850,31 @@ const zh: Translations = {
|
|||
'Car Services': '汽车服务',
|
||||
'Post Office': '邮局',
|
||||
'Vet & Pet Care': '宠物医院与护理',
|
||||
'Bank': '银行',
|
||||
Bank: '银行',
|
||||
'Travel Agent': '旅行社',
|
||||
'Police': '警察',
|
||||
Police: '警察',
|
||||
'Fire Station': '消防站',
|
||||
'Ambulance Station': '急救站',
|
||||
'GP Surgery': '全科诊所',
|
||||
'Dentist': '牙科',
|
||||
'Pharmacy': '药房',
|
||||
Dentist: '牙科',
|
||||
Pharmacy: '药房',
|
||||
'Hospital & Clinic': '医院与诊所',
|
||||
'Optician': '眼镜店',
|
||||
'Physiotherapy': '理疗',
|
||||
Optician: '眼镜店',
|
||||
Physiotherapy: '理疗',
|
||||
'Counselling & Therapy': '心理咨询与治疗',
|
||||
'Care Home': '养老院',
|
||||
'Medical & Mobility': '医疗器械与辅助设备',
|
||||
'Museum': '博物馆',
|
||||
'Gallery': '美术馆',
|
||||
'Library': '图书馆',
|
||||
Museum: '博物馆',
|
||||
Gallery: '美术馆',
|
||||
Library: '图书馆',
|
||||
'Place of Worship': '宗教场所',
|
||||
'Arts Centre': '艺术中心',
|
||||
'Zoo': '动物园',
|
||||
Zoo: '动物园',
|
||||
'Tourist Attraction': '旅游景点',
|
||||
'School': '学校',
|
||||
'Hotel': '酒店',
|
||||
School: '学校',
|
||||
Hotel: '酒店',
|
||||
'Local Business': '本地商业',
|
||||
'Offices': '写字楼',
|
||||
Offices: '写字楼',
|
||||
'EV Charging': '电动车充电站',
|
||||
'Fuel Station': '加油站',
|
||||
'Community Centre': '社区中心',
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ export interface Property {
|
|||
listing_url?: string;
|
||||
property_sub_type?: string;
|
||||
price_qualifier?: string;
|
||||
former_council_house?: string;
|
||||
|
||||
// Numeric fields
|
||||
lat: number;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue