Working
This commit is contained in:
parent
14a3555cf1
commit
7e92bf112e
34 changed files with 1214437 additions and 224 deletions
|
|
@ -31,8 +31,8 @@ export default memo(function HoverCard({ x, y, id, isPostcode, data, filters, fe
|
|||
|
||||
const results: { name: string; value: string }[] = [];
|
||||
|
||||
// Show stats for active filters (up to 4)
|
||||
for (const name of activeFilterNames.slice(0, 4)) {
|
||||
// Show stats for active filters (up to 4), excluding Listing status
|
||||
for (const name of activeFilterNames.filter((n) => n !== 'Listing status').slice(0, 4)) {
|
||||
const val = data[`avg_${name}`] ?? data[`min_${name}`];
|
||||
if (val == null || typeof val !== 'number') continue;
|
||||
const meta = featureMap.get(name);
|
||||
|
|
@ -50,14 +50,31 @@ export default memo(function HoverCard({ x, y, id, isPostcode, data, filters, fe
|
|||
const displayStats = getDisplayStats();
|
||||
const count = data?.count;
|
||||
|
||||
const cardStyle = {
|
||||
left: x,
|
||||
top: y - 12,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
};
|
||||
|
||||
// Loading state: show skeleton when data hasn't arrived yet
|
||||
if (!data) {
|
||||
return (
|
||||
<div
|
||||
className="absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg p-3 text-sm pointer-events-none z-50 min-w-[140px]"
|
||||
style={cardStyle}
|
||||
>
|
||||
<div className="animate-pulse space-y-2">
|
||||
<div className="h-3.5 w-20 bg-warm-200 dark:bg-warm-600 rounded" />
|
||||
<div className="h-2.5 w-14 bg-warm-100 dark:bg-warm-700 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg p-3 text-sm dark:text-white pointer-events-none z-50 min-w-[180px] max-w-[260px]"
|
||||
style={{
|
||||
left: x,
|
||||
top: y - 12,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
}}
|
||||
style={cardStyle}
|
||||
>
|
||||
<div className="relative">
|
||||
{/* Header */}
|
||||
|
|
@ -89,11 +106,9 @@ export default memo(function HoverCard({ x, y, id, isPostcode, data, filters, fe
|
|||
)}
|
||||
|
||||
{/* Hint */}
|
||||
{data && (
|
||||
<div className="text-[10px] text-warm-400 dark:text-warm-400 mt-2 text-center">
|
||||
Click for details
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-warm-400 dark:text-warm-400 mt-2 text-center">
|
||||
Click for details
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,47 @@
|
|||
import { formatValue } from '../../lib/format';
|
||||
import { FEATURE_GRADIENT, DENSITY_GRADIENT, DENSITY_GRADIENT_DARK } from '../../lib/consts';
|
||||
import { FEATURE_GRADIENT, DENSITY_GRADIENT, DENSITY_GRADIENT_DARK, ENUM_PALETTE } from '../../lib/consts';
|
||||
import { gradientToCss } from '../../lib/utils';
|
||||
import { CloseIcon } from '../ui/icons/CloseIcon';
|
||||
import { TickerValue } from '../ui/TickerValue';
|
||||
|
||||
function EnumSwatches({ values }: { values: string[] }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{values.map((label, i) => {
|
||||
const color = ENUM_PALETTE[i % ENUM_PALETTE.length];
|
||||
return (
|
||||
<div key={label} className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="w-3 h-3 rounded-sm shrink-0"
|
||||
style={{ backgroundColor: `rgb(${color[0]},${color[1]},${color[2]})` }}
|
||||
/>
|
||||
<span className="text-warm-600 dark:text-warm-300 truncate">{label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineEnumSwatches({ values }: { values: string[] }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-1 min-w-[40%] flex-wrap">
|
||||
{values.map((label, i) => {
|
||||
const color = ENUM_PALETTE[i % ENUM_PALETTE.length];
|
||||
return (
|
||||
<div key={label} className="flex items-center gap-1">
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded-sm shrink-0"
|
||||
style={{ backgroundColor: `rgb(${color[0]},${color[1]},${color[2]})` }}
|
||||
/>
|
||||
<span className="text-warm-500 dark:text-warm-400 whitespace-nowrap text-[11px]">{label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MapLegend({
|
||||
featureLabel,
|
||||
range,
|
||||
|
|
@ -27,6 +65,7 @@ export default function MapLegend({
|
|||
suffix?: string;
|
||||
raw?: boolean;
|
||||
}) {
|
||||
const isEnum = enumValues && enumValues.length > 0;
|
||||
const densityGradient = theme === 'dark' ? DENSITY_GRADIENT_DARK : DENSITY_GRADIENT;
|
||||
const gradientStyle =
|
||||
mode === 'density' ? gradientToCss(densityGradient) : gradientToCss(FEATURE_GRADIENT);
|
||||
|
|
@ -36,18 +75,14 @@ export default function MapLegend({
|
|||
const rangeMin =
|
||||
mode === 'density' ? (
|
||||
<TickerValue text={formatValue(range[0])} />
|
||||
) : enumValues && enumValues.length > 0 ? (
|
||||
<span>{enumValues[0]}</span>
|
||||
) : (
|
||||
) : isEnum ? null : (
|
||||
<TickerValue text={formatValue(range[0], fmt) + (suffix || '')} />
|
||||
);
|
||||
|
||||
const rangeMax =
|
||||
mode === 'density' ? (
|
||||
<TickerValue text={formatValue(range[1])} />
|
||||
) : enumValues && enumValues.length > 0 ? (
|
||||
<span>{enumValues[enumValues.length - 1]}</span>
|
||||
) : (
|
||||
) : isEnum ? null : (
|
||||
<TickerValue text={formatValue(range[1], fmt) + (suffix || '')} />
|
||||
);
|
||||
|
||||
|
|
@ -66,11 +101,15 @@ export default function MapLegend({
|
|||
<CloseIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-[40%] text-warm-500 dark:text-warm-400">
|
||||
{rangeMin}
|
||||
<div className="h-2.5 rounded flex-1 min-w-[40px]" style={{ background: gradientStyle }} />
|
||||
{rangeMax}
|
||||
</div>
|
||||
{isEnum ? (
|
||||
<InlineEnumSwatches values={enumValues} />
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-[40%] text-warm-500 dark:text-warm-400">
|
||||
{rangeMin}
|
||||
<div className="h-2.5 rounded flex-1 min-w-[40px]" style={{ background: gradientStyle }} />
|
||||
{rangeMax}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -89,11 +128,17 @@ export default function MapLegend({
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-3 rounded" style={{ background: gradientStyle }} />
|
||||
<div className="flex justify-between mt-1 text-warm-600 dark:text-warm-200">
|
||||
{rangeMin}
|
||||
{rangeMax}
|
||||
</div>
|
||||
{isEnum ? (
|
||||
<EnumSwatches values={enumValues} />
|
||||
) : (
|
||||
<>
|
||||
<div className="h-3 rounded" style={{ background: gradientStyle }} />
|
||||
<div className="flex justify-between mt-1 text-warm-600 dark:text-warm-200">
|
||||
{rangeMin}
|
||||
{rangeMax}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export default function MapPage({
|
|||
useState<Set<string>>(initialPOICategories);
|
||||
|
||||
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 600, 'left');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(288, 200, 500, 'right');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 500, 'right');
|
||||
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||
const [poiPaneOpen, setPoiPaneOpen] = useState(false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { FeatureMeta } from '../../types';
|
||||
import { InfoIcon } from './icons';
|
||||
import { getFeatureIcon } from '../../lib/feature-icons';
|
||||
import { getGroupIcon } from '../../lib/group-icons';
|
||||
|
||||
const MODE_LABELS: Record<string, string> = {
|
||||
|
|
@ -22,7 +23,9 @@ export function FeatureLabel({
|
|||
size = 'xs',
|
||||
}: FeatureLabelProps) {
|
||||
const textClass = size === 'sm' ? 'text-sm' : 'text-xs';
|
||||
const GroupIcon = feature.group ? getGroupIcon(feature.group) : null;
|
||||
const iconClass = 'w-3.5 h-3.5 text-teal-600 dark:text-teal-400 shrink-0';
|
||||
const featureIcon = getFeatureIcon(feature.name, iconClass);
|
||||
const GroupIcon = !featureIcon && feature.group ? getGroupIcon(feature.group) : null;
|
||||
const modeTag =
|
||||
feature.modes && feature.modes.length > 0
|
||||
? feature.modes.map((m) => MODE_LABELS[m] || m).join(' · ')
|
||||
|
|
@ -32,9 +35,8 @@ export function FeatureLabel({
|
|||
<div
|
||||
className={`flex ${size === 'xs' ? 'items-center' : 'items-start'} gap-1 min-w-0 ${className}`}
|
||||
>
|
||||
{GroupIcon && (
|
||||
<GroupIcon className="w-3.5 h-3.5 text-teal-600 dark:text-teal-400 shrink-0" />
|
||||
)}
|
||||
{featureIcon}
|
||||
{GroupIcon && <GroupIcon className={iconClass} />}
|
||||
<span
|
||||
className={`${textClass} text-warm-700 dark:text-warm-300 ${size === 'xs' ? 'truncate' : ''}`}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue