feat: searchable select dropdowns + updated levels
- Replace native select with custom searchable dropdown - Search matches word starts (type 'а' finds 'Анна' or 'Мария Андреева') - Search input shows for lists with 4+ options - Updated levels: Начинающий/Без опыта, Продвинутый Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useEffect } from "react";
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface InputFieldProps {
|
interface InputFieldProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -94,25 +94,87 @@ export function SelectField({
|
|||||||
options,
|
options,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: SelectFieldProps) {
|
}: SelectFieldProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const selectedLabel = options.find((o) => o.value === value)?.label || "";
|
||||||
|
const filtered = search
|
||||||
|
? options.filter((o) => {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
// Match any word that starts with the search query
|
||||||
|
return o.label.toLowerCase().split(/\s+/).some((word) => word.startsWith(q));
|
||||||
|
})
|
||||||
|
: options;
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
function handle(e: MouseEvent) {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false);
|
||||||
|
setSearch("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handle);
|
||||||
|
return () => document.removeEventListener("mousedown", handle);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div ref={containerRef} className="relative">
|
||||||
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
|
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
|
||||||
<select
|
<button
|
||||||
value={value}
|
type="button"
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onClick={() => {
|
||||||
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-white outline-none focus:border-gold transition-colors"
|
setOpen(!open);
|
||||||
|
setSearch("");
|
||||||
|
setTimeout(() => inputRef.current?.focus(), 0);
|
||||||
|
}}
|
||||||
|
className={`w-full rounded-lg border bg-neutral-800 px-4 py-2.5 text-left outline-none transition-colors ${
|
||||||
|
open ? "border-gold" : "border-white/10"
|
||||||
|
} ${value ? "text-white" : "text-neutral-500"}`}
|
||||||
>
|
>
|
||||||
{placeholder && (
|
{selectedLabel || placeholder || "Выберите..."}
|
||||||
<option value="" disabled>
|
</button>
|
||||||
{placeholder}
|
|
||||||
</option>
|
{open && (
|
||||||
)}
|
<div className="absolute z-50 mt-1 w-full rounded-lg border border-white/10 bg-neutral-800 shadow-xl overflow-hidden">
|
||||||
{options.map((opt) => (
|
{options.length > 3 && (
|
||||||
<option key={opt.value} value={opt.value}>
|
<div className="p-1.5">
|
||||||
{opt.label}
|
<input
|
||||||
</option>
|
ref={inputRef}
|
||||||
))}
|
type="text"
|
||||||
</select>
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Поиск..."
|
||||||
|
className="w-full rounded-md border border-white/10 bg-neutral-900 px-3 py-1.5 text-sm text-white outline-none focus:border-gold/50 placeholder:text-neutral-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="max-h-48 overflow-y-auto">
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<div className="px-4 py-2 text-sm text-neutral-500">Ничего не найдено</div>
|
||||||
|
)}
|
||||||
|
{filtered.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onChange(opt.value);
|
||||||
|
setOpen(false);
|
||||||
|
setSearch("");
|
||||||
|
}}
|
||||||
|
className={`w-full px-4 py-2 text-left text-sm transition-colors hover:bg-white/5 ${
|
||||||
|
opt.value === value ? "text-gold bg-gold/5" : "text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const DAY_ORDER: Record<string, number> = Object.fromEntries(
|
|||||||
|
|
||||||
const LEVELS = [
|
const LEVELS = [
|
||||||
{ value: "", label: "Без уровня" },
|
{ value: "", label: "Без уровня" },
|
||||||
{ value: "Начинающий", label: "Начинающий" },
|
{ value: "Начинающий/Без опыта", label: "Начинающий/Без опыта" },
|
||||||
{ value: "Продвинутый", label: "Продвинутый" },
|
{ value: "Продвинутый", label: "Продвинутый" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user