This commit is contained in:
Andras Schmelczer 2026-05-14 22:07:14 +01:00
parent 084117cea8
commit a8de0a614d
36 changed files with 1329 additions and 522 deletions

View file

@ -1,8 +1,13 @@
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ts } from '../../i18n/server';
import type { FeatureFilters, FeatureMeta, HexagonStatsResponse } from '../../types';
import type { TravelTimeEntry } from '../../hooks/useTravelTime';
import type {
FeatureFilters,
FeatureMeta,
FilterExclusion,
HexagonStatsResponse,
} from '../../types';
import { travelFieldKey, type TravelTimeEntry } from '../../hooks/useTravelTime';
import type { HexagonLocation } from '../../lib/external-search';
import {
formatValue,
@ -61,6 +66,21 @@ function normalizePercentageSegments<T extends { value: number }>(segments: T[])
return segments.map((segment, index) => ({ ...segment, value: normalizedValues[index] }));
}
function filterValueFormat(feature?: FeatureMeta) {
if (!feature) return undefined;
return {
prefix: feature.prefix,
suffix: feature.suffix,
raw: feature.raw,
};
}
function formatExclusionPercent(value: number): string {
const percent = value * 100;
if (percent < 10) return `${percent.toFixed(1)}%`;
return `${Math.round(percent)}%`;
}
export default function AreaPane({
stats,
globalFeatures,
@ -103,6 +123,36 @@ export default function AreaPane({
() => new Map(globalFeatures.map((f) => [f.name, f])),
[globalFeatures]
);
const travelEntryByField = useMemo(() => {
const map = new Map<string, TravelTimeEntry>();
for (const entry of travelTimeEntries ?? []) {
map.set(travelFieldKey(entry), entry);
}
return map;
}, [travelTimeEntries]);
const filterExclusions = stats?.filter_exclusions ?? [];
const getExclusionLabel = (exclusion: FilterExclusion) => {
const travelEntry = travelEntryByField.get(exclusion.name);
if (travelEntry) return t('areaPane.travelTo', { destination: travelEntry.label });
return ts(exclusion.name);
};
const formatExclusionValue = (exclusion: FilterExclusion, value: number) => {
if (exclusion.kind === 'travel') return `${Math.round(value)} ${t('common.min')}`;
return formatFilterValue(value, filterValueFormat(globalFeatureByName.get(exclusion.name)));
};
const getExclusionAdjustment = (exclusion: FilterExclusion) => {
if (exclusion.direction === 'allow_value') {
return t('areaPane.allowCategory', { value: ts(exclusion.category ?? '') });
}
if (exclusion.value == null) return '';
const value = formatExclusionValue(exclusion, exclusion.value);
return exclusion.direction === 'lower_min'
? t('areaPane.lowerMinTo', { value })
: t('areaPane.raiseMaxTo', { value });
};
if (!hexagonId) {
return (
@ -205,6 +255,31 @@ export default function AreaPane({
>
{t('areaPane.showAllStats')}
</button>
{filterExclusions.length > 0 && (
<div className="mt-2 border-t border-amber-200 pt-2 dark:border-amber-800/70">
<p className="font-semibold">{t('areaPane.closestBlockingFilters')}</p>
<ol className="mt-1.5 space-y-1.5">
{filterExclusions.map((exclusion) => (
<li
key={`${exclusion.kind}:${exclusion.name}:${exclusion.direction}:${exclusion.category ?? ''}`}
className="rounded bg-white/70 px-2 py-1.5 dark:bg-navy-950/40"
>
<div className="flex items-baseline justify-between gap-2">
<span className="min-w-0 truncate font-medium">
{getExclusionLabel(exclusion)}
</span>
<span className="shrink-0 tabular-nums text-amber-700 dark:text-amber-200">
{formatExclusionPercent(exclusion.relative_difference)}
</span>
</div>
<p className="mt-0.5 text-amber-800/80 dark:text-amber-100/80">
{getExclusionAdjustment(exclusion)}
</p>
</li>
))}
</ol>
</div>
)}
</div>
)}
{canViewProperties && (