Small fixes & fmt
This commit is contained in:
parent
6b12e21d50
commit
f32a552f46
23 changed files with 347 additions and 99 deletions
|
|
@ -140,17 +140,72 @@ function formatPropertyDetails(data: SavedPropertyData): string {
|
|||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function EditableName({ value, onSave }: { value: string; onSave: (name: string) => void }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [text, setText] = useState(value);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setText(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
const commit = () => {
|
||||
setEditing(false);
|
||||
const trimmed = text.trim();
|
||||
if (trimmed && trimmed !== value) onSave(trimmed);
|
||||
else setText(value);
|
||||
};
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') commit();
|
||||
if (e.key === 'Escape') {
|
||||
setText(value);
|
||||
setEditing(false);
|
||||
}
|
||||
}}
|
||||
onBlur={commit}
|
||||
className="w-full font-medium text-navy-950 dark:text-warm-100 bg-warm-50 dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded px-1.5 py-0.5 text-sm focus:outline-none focus:ring-1 focus:ring-teal-400"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<h3
|
||||
onClick={() => setEditing(true)}
|
||||
className="font-medium text-navy-950 dark:text-warm-100 truncate cursor-pointer hover:text-teal-600 dark:hover:text-teal-400 border-b border-dotted border-transparent hover:border-warm-400 dark:hover:border-warm-500"
|
||||
title="Click to rename"
|
||||
>
|
||||
{value}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
function SavedSearchesTab({
|
||||
searches,
|
||||
loading,
|
||||
onDelete,
|
||||
onUpdateNotes,
|
||||
onUpdateName,
|
||||
onOpen,
|
||||
}: {
|
||||
searches: SavedSearch[];
|
||||
loading: boolean;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
onUpdateNotes: (id: string, notes: string) => void;
|
||||
onUpdateName: (id: string, name: string) => void;
|
||||
onOpen: (params: string) => void;
|
||||
}) {
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||
|
|
@ -229,9 +284,12 @@ function SavedSearchesTab({
|
|||
)}
|
||||
|
||||
<div className="p-4 flex flex-col flex-1">
|
||||
<h3 className="font-medium text-navy-950 dark:text-warm-100 truncate mb-1">
|
||||
{search.name}
|
||||
</h3>
|
||||
<div className="mb-1">
|
||||
<EditableName
|
||||
value={search.name}
|
||||
onSave={(name) => onUpdateName(search.id, name)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-warm-500 dark:text-warm-400 mb-1">
|
||||
{formatRelativeTime(search.created)}
|
||||
</p>
|
||||
|
|
@ -414,6 +472,7 @@ export function SavedPage({
|
|||
searchesLoading,
|
||||
onDeleteSearch,
|
||||
onUpdateSearchNotes,
|
||||
onUpdateSearchName,
|
||||
onOpenSearch,
|
||||
savedProperties,
|
||||
propertiesLoading,
|
||||
|
|
@ -425,6 +484,7 @@ export function SavedPage({
|
|||
searchesLoading: boolean;
|
||||
onDeleteSearch: (id: string) => Promise<void>;
|
||||
onUpdateSearchNotes: (id: string, notes: string) => void;
|
||||
onUpdateSearchName: (id: string, name: string) => void;
|
||||
onOpenSearch: (params: string) => void;
|
||||
savedProperties: SavedProperty[];
|
||||
propertiesLoading: boolean;
|
||||
|
|
@ -470,6 +530,7 @@ export function SavedPage({
|
|||
loading={searchesLoading}
|
||||
onDelete={onDeleteSearch}
|
||||
onUpdateNotes={onUpdateSearchNotes}
|
||||
onUpdateName={onUpdateSearchName}
|
||||
onOpen={onOpenSearch}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -186,8 +186,8 @@ export default function HomePage({
|
|||
tabs, one postcode at a time.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'How can I check if an area is safe before I move there?',
|
||||
answer:
|
||||
"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.",
|
||||
'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.',
|
||||
},
|
||||
{
|
||||
question:
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ function EditableLabel({
|
|||
if (e.key === 'Escape') setEditing(false);
|
||||
}}
|
||||
onBlur={commit}
|
||||
className="absolute -translate-x-1/2 w-16 text-[10px] text-center rounded border border-warm-300 dark:border-warm-600 bg-white dark:bg-warm-800 text-warm-700 dark:text-warm-200 px-0.5 focus:outline-none focus:ring-1 focus:ring-teal-400"
|
||||
className="absolute w-16 text-[10px] text-center rounded border border-warm-300 dark:border-warm-600 bg-white dark:bg-warm-800 text-warm-700 dark:text-warm-200 px-0.5 focus:outline-none focus:ring-1 focus:ring-teal-400"
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
|
|
@ -81,7 +81,7 @@ function EditableLabel({
|
|||
|
||||
return (
|
||||
<span
|
||||
className={`absolute -translate-x-1/2 cursor-pointer hover:text-teal-600 dark:hover:text-teal-400 border-b border-dotted border-warm-400 dark:border-warm-500 ${className ?? ''}`}
|
||||
className={`absolute cursor-pointer hover:text-teal-600 dark:hover:text-teal-400 border-b border-dotted border-warm-400 dark:border-warm-500 ${className ?? ''}`}
|
||||
style={style}
|
||||
onClick={startEdit}
|
||||
>
|
||||
|
|
@ -119,24 +119,32 @@ function SliderLabels({
|
|||
const minLabel = isAtMin ? 'min' : formatFilterValue(labels[0], raw);
|
||||
const maxLabel = isAtMax ? 'max' : formatFilterValue(labels[1], raw);
|
||||
|
||||
// Smoothly spread labels apart as thumbs get close to prevent overlap.
|
||||
// t=1 (centered) when far apart, t=0 (split) when touching.
|
||||
const SPREAD_THRESHOLD = 20; // percentage gap below which labels start separating
|
||||
const gapPct = rightPct - leftPct;
|
||||
const t = Math.min(1, Math.max(0, gapPct / SPREAD_THRESHOLD));
|
||||
const leftTranslate = `translateX(${-100 + t * 50}%)`;
|
||||
const rightTranslate = `translateX(${-t * 50}%)`;
|
||||
|
||||
if (feature && onValueChange) {
|
||||
return (
|
||||
<div className="relative h-4 mt-2 mx-2.5 text-[10px] text-warm-500 dark:text-warm-400 leading-tight">
|
||||
<EditableLabel
|
||||
value={labels[0]}
|
||||
formatted={minLabel}
|
||||
onCommit={(v) => onValueChange([v, labels[1]])}
|
||||
onCommit={(v) => onValueChange([Math.min(v, labels[1]), labels[1]])}
|
||||
prefix={feature.prefix}
|
||||
suffix={feature.suffix}
|
||||
style={{ left: `${leftPct}%` }}
|
||||
style={{ left: `${leftPct}%`, transform: leftTranslate }}
|
||||
/>
|
||||
<EditableLabel
|
||||
value={labels[1]}
|
||||
formatted={maxLabel}
|
||||
onCommit={(v) => onValueChange([labels[0], v])}
|
||||
onCommit={(v) => onValueChange([labels[0], Math.max(v, labels[0])])}
|
||||
prefix={feature.prefix}
|
||||
suffix={feature.suffix}
|
||||
style={{ left: `${rightPct}%` }}
|
||||
style={{ left: `${rightPct}%`, transform: rightTranslate }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -144,10 +152,10 @@ function SliderLabels({
|
|||
|
||||
return (
|
||||
<div className="relative h-4 mt-2 mx-2.5 text-[10px] text-warm-500 dark:text-warm-400 leading-tight">
|
||||
<span className="absolute -translate-x-1/2" style={{ left: `${leftPct}%` }}>
|
||||
<span className="absolute" style={{ left: `${leftPct}%`, transform: leftTranslate }}>
|
||||
{minLabel}
|
||||
</span>
|
||||
<span className="absolute -translate-x-1/2" style={{ left: `${rightPct}%` }}>
|
||||
<span className="absolute" style={{ left: `${rightPct}%`, transform: rightTranslate }}>
|
||||
{maxLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -391,7 +399,9 @@ export default memo(function Filters({
|
|||
ref={containerRef}
|
||||
className="flex flex-col bg-white dark:bg-navy-950 overflow-y-auto md:overflow-hidden h-full touch-pan-y"
|
||||
>
|
||||
<div className={`shrink-0 md:shrink md:min-h-0 flex flex-col ${addFilterCollapsed ? '' : 'md:basis-[40%]'}`}>
|
||||
<div
|
||||
className={`shrink-0 md:shrink md:min-h-0 flex flex-col ${addFilterCollapsed ? '' : 'md:basis-[40%]'}`}
|
||||
>
|
||||
<div 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">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-teal-700 dark:text-teal-400">
|
||||
|
|
@ -452,10 +462,7 @@ export default memo(function Filters({
|
|||
|
||||
<div className="px-2 py-1 space-y-1">
|
||||
{travelTimeEntries.map((entry, index) => (
|
||||
<div
|
||||
key={`tt_${index}`}
|
||||
data-filter-name={`tt_${index}`}
|
||||
>
|
||||
<div key={`tt_${index}`} data-filter-name={`tt_${index}`}>
|
||||
<TravelTimeCard
|
||||
mode={entry.mode}
|
||||
slug={entry.slug}
|
||||
|
|
@ -464,9 +471,7 @@ export default memo(function Filters({
|
|||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label) =>
|
||||
onTravelTimeSetDestination(index, slug, label)
|
||||
}
|
||||
onSetDestination={(slug, label) => onTravelTimeSetDestination(index, slug, label)}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
|
|
@ -560,9 +565,7 @@ export default memo(function Filters({
|
|||
<Slider
|
||||
min={scale ? 0 : feature.min!}
|
||||
max={scale ? 100 : feature.max!}
|
||||
step={
|
||||
scale ? 1 : (feature.step ?? (feature.max! - feature.min!) / 100)
|
||||
}
|
||||
step={scale ? 1 : (feature.step ?? (feature.max! - feature.min!) / 100)}
|
||||
value={sliderValue}
|
||||
onValueChange={
|
||||
scale
|
||||
|
|
@ -570,9 +573,7 @@ export default memo(function Filters({
|
|||
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)),
|
||||
pMin <= 0 ? (hist?.min ?? feature.min!) : snap(scale.toValue(pMin)),
|
||||
pMax >= 100
|
||||
? (hist?.max ?? feature.max!)
|
||||
: snap(scale.toValue(pMax)),
|
||||
|
|
@ -606,13 +607,18 @@ export default memo(function Filters({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`shrink-0 md:shrink md:min-h-0 flex flex-col border-t border-warm-200 dark:border-warm-700 ${addFilterCollapsed ? '' : 'md:basis-[60%]'}`}>
|
||||
<div
|
||||
className={`shrink-0 md:shrink md:min-h-0 flex flex-col border-t border-warm-200 dark:border-warm-700 ${addFilterCollapsed ? '' : 'md:basis-[60%]'}`}
|
||||
>
|
||||
<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">Add Filter</span>
|
||||
<ChevronIcon direction={addFilterCollapsed ? 'down' : 'up'} className="w-4 h-4 text-warm-400 dark:text-warm-500" />
|
||||
<ChevronIcon
|
||||
direction={addFilterCollapsed ? 'down' : 'up'}
|
||||
className="w-4 h-4 text-warm-400 dark:text-warm-500"
|
||||
/>
|
||||
</button>
|
||||
{!addFilterCollapsed && (
|
||||
<div className="md:min-h-0 md:flex-1 flex flex-col">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function HistogramLegend() {
|
|||
<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">Dashed line</span>{' '}
|
||||
indicates the global average
|
||||
indicates the national average
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -264,8 +264,18 @@ export default function JourneyInstructions({
|
|||
className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
|
||||
>
|
||||
View on Google Maps
|
||||
<svg className="w-3 h-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -284,8 +294,18 @@ export default function JourneyInstructions({
|
|||
className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
|
||||
>
|
||||
View on Google Maps
|
||||
<svg className="w-3 h-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ export function FeatureActions({
|
|||
return (
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
{feature.detail && onShowInfo && (
|
||||
<IconButton onClick={() => onShowInfo(feature)} title="Feature info">
|
||||
<InfoIcon />
|
||||
<IconButton onClick={() => onShowInfo(feature)} title="Feature info" size="md">
|
||||
<InfoIcon className="w-7 h-7 md:w-3.5 md:h-3.5" />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function FeatureLabel({
|
|||
{featureIcon}
|
||||
{GroupIcon && <GroupIcon className={iconClass} />}
|
||||
<span
|
||||
className={`${textClass} text-warm-700 dark:text-warm-300 ${size === 'xs' ? 'truncate' : ''}`}
|
||||
className={`${textClass} ${size === 'sm' ? 'font-medium text-navy-950 dark:text-warm-100' : 'text-warm-700 dark:text-warm-300 truncate'}`}
|
||||
>
|
||||
{feature.name}
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue