The DataTable's pagination carries the only interpolated strings in this area
('Showing page {{current}} of {{total}}', '{{size}} / page'). No plural forms:
nothing here is a count-varying noun, so inventing one would only add a key
nobody uses.
DataTable.test.tsx now renders inside an I18nProvider with the real en-GB
catalog, so it still asserts the English a user sees rather than a dot-path.
Left/right utilities became logical (ms-/me-/ps-/pe-/start-) per D3.
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Search as SearchIcon, X as ClearIcon } from 'lucide-react';
|
|
import { Input } from '@/components/ui/input';
|
|
import { cn } from 'helpers/cn';
|
|
import { useTranslation } from '@/components/locales';
|
|
|
|
type SearchInputProps = {
|
|
className?: string;
|
|
handleSearch: (value: string) => void;
|
|
value?: string;
|
|
placeholder?: string;
|
|
};
|
|
|
|
export function SearchInput(props: SearchInputProps) {
|
|
const { t } = useTranslation();
|
|
const { className, handleSearch, value = '', placeholder = t('search.placeholder') } = props;
|
|
const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);
|
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
return (
|
|
<div className={cn(isMobileSearchOpen && 'h-24 md:h-auto')}>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
aria-label={isMobileSearchOpen ? t('search.close') : t('search.open')}
|
|
onClick={() => {
|
|
setIsMobileSearchOpen((v) => !v);
|
|
if (!isMobileSearchOpen) {
|
|
setTimeout(() => inputRef.current?.focus(), 160);
|
|
}
|
|
}}
|
|
className="ms-auto shrink-0 md:hidden"
|
|
>
|
|
{isMobileSearchOpen ? <ClearIcon className="h-5 w-5" /> : <SearchIcon className="h-5 w-5" />}
|
|
</Button>
|
|
<div
|
|
className={cn(
|
|
'relative w-fit max-w-lg',
|
|
isMobileSearchOpen
|
|
? 'absolute start-0 mt-4 block w-full md:relative md:mt-0 md:w-fit'
|
|
: 'hidden w-auto md:block',
|
|
className,
|
|
)}
|
|
>
|
|
{!!value ? (
|
|
<button
|
|
type="button"
|
|
aria-label={t('search.clear')}
|
|
onClick={() => handleSearch('')}
|
|
className="absolute start-3 top-1/2 -translate-y-1/2 z-10 rounded p-1 hover:bg-muted"
|
|
>
|
|
<ClearIcon className="h-4 w-4 text-muted-foreground" />
|
|
</button>
|
|
) : (
|
|
<span className="pointer-events-none absolute start-3 top-1/2 z-10 -translate-y-1/2">
|
|
<SearchIcon className="text-muted-foreground h-4 w-4" />
|
|
</span>
|
|
)}
|
|
|
|
<Input
|
|
ref={inputRef}
|
|
placeholder={placeholder}
|
|
value={value}
|
|
onChange={(ev) => handleSearch(ev.target.value)}
|
|
className="ps-10 pe-4 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|