Changes
This commit is contained in:
parent
3a3f899ea2
commit
128b3191e7
68 changed files with 28060 additions and 1152 deletions
123
frontend/src/components/ui/PlaceSearchInput.tsx
Normal file
123
frontend/src/components/ui/PlaceSearchInput.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import type React from 'react';
|
||||
import type { SearchResult } from '../../hooks/useLocationSearch';
|
||||
import { SearchIcon } from './icons/SearchIcon';
|
||||
import { MapPinIcon } from './icons/MapPinIcon';
|
||||
|
||||
interface SearchHook {
|
||||
query: string;
|
||||
results: SearchResult[];
|
||||
activeIndex: number;
|
||||
setActiveIndex: (idx: number) => void;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
handleInputChange: (value: string) => void;
|
||||
handleKeyDown: (
|
||||
e: React.KeyboardEvent,
|
||||
onSelect: (result: SearchResult) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface PlaceSearchInputProps {
|
||||
search: SearchHook;
|
||||
onSelect: (result: SearchResult) => void;
|
||||
loading?: boolean;
|
||||
placeholder?: string;
|
||||
size?: 'sm' | 'xs';
|
||||
inputClassName?: string;
|
||||
inputRef?: React.Ref<HTMLInputElement>;
|
||||
onInputChange?: () => void;
|
||||
}
|
||||
|
||||
export function PlaceSearchInput({
|
||||
search,
|
||||
onSelect,
|
||||
loading,
|
||||
placeholder,
|
||||
size = 'sm',
|
||||
inputClassName,
|
||||
inputRef,
|
||||
onInputChange,
|
||||
}: PlaceSearchInputProps) {
|
||||
const sm = size === 'sm';
|
||||
const iconSize = sm ? 'w-4 h-4' : 'w-3 h-3';
|
||||
const spinnerSize = sm ? 'w-4 h-4' : 'w-3 h-3';
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={search.query}
|
||||
onChange={(e) => {
|
||||
search.handleInputChange(e.target.value);
|
||||
onInputChange?.();
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (search.results.length > 0) search.setOpen(true);
|
||||
}}
|
||||
onKeyDown={(e) => search.handleKeyDown(e, onSelect)}
|
||||
placeholder={placeholder}
|
||||
className={inputClassName}
|
||||
/>
|
||||
|
||||
{loading && (
|
||||
<div
|
||||
className={`absolute right-2 top-1/2 -translate-y-1/2 ${spinnerSize} border-2 border-warm-300 dark:border-warm-600 border-t-teal-500 rounded-full animate-spin`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{search.open && search.results.length > 0 && (
|
||||
<div
|
||||
className={`absolute top-full left-0 right-0 mt-1 bg-white dark:bg-warm-800 rounded shadow-lg border border-warm-200 dark:border-warm-700 ${sm ? 'max-h-64' : 'max-h-48'} overflow-y-auto z-20`}
|
||||
>
|
||||
{search.results.map((result, idx) => (
|
||||
<button
|
||||
key={
|
||||
result.type === 'postcode'
|
||||
? `pc-${result.label}`
|
||||
: `pl-${result.name}-${result.lat}`
|
||||
}
|
||||
type="button"
|
||||
className={`w-full text-left flex items-center cursor-pointer ${
|
||||
sm ? 'px-3 py-2 gap-2 text-sm' : 'px-2 py-1.5 gap-1.5 text-xs'
|
||||
} ${
|
||||
idx === search.activeIndex
|
||||
? 'bg-teal-50 dark:bg-teal-900/30'
|
||||
: 'hover:bg-warm-50 dark:hover:bg-warm-700'
|
||||
}`}
|
||||
onMouseEnter={() => search.setActiveIndex(idx)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect(result);
|
||||
}}
|
||||
>
|
||||
{result.type === 'postcode' ? (
|
||||
<>
|
||||
<SearchIcon
|
||||
className={`${iconSize} text-warm-400 dark:text-warm-500 shrink-0`}
|
||||
/>
|
||||
<span className="text-warm-700 dark:text-warm-200">{result.label}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MapPinIcon
|
||||
className={`${iconSize} text-warm-400 dark:text-warm-500 shrink-0`}
|
||||
/>
|
||||
<span className="text-warm-700 dark:text-warm-200">
|
||||
{result.name}
|
||||
{result.city && (
|
||||
<span className="text-warm-400 dark:text-warm-500">
|
||||
{' '}
|
||||
({result.city})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue