Added house function: add, update, view (admin function)
update npm package
This commit is contained in:
184
src/components/ui/select-user.tsx
Normal file
184
src/components/ui/select-user.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@lib/utils';
|
||||
import { CaretDownIcon, MagnifyingGlassIcon } from '@phosphor-icons/react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type SelectUserItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const userLabel = (u: { name: string; email: string }) =>
|
||||
`${u.name} - ${u.email}`;
|
||||
|
||||
export type SelectUserProps = {
|
||||
value: string;
|
||||
onValueChange: (userId: string) => void;
|
||||
values: SelectUserItem[];
|
||||
placeholder?: string;
|
||||
/** Khi truyền cùng onKeywordChange: tìm kiếm theo API (keyword gửi lên server) */
|
||||
keyword?: string;
|
||||
onKeywordChange?: (value: string) => void;
|
||||
searchPlaceholder?: string;
|
||||
name?: string;
|
||||
id?: string;
|
||||
'aria-invalid'?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function SelectUser({
|
||||
value,
|
||||
onValueChange,
|
||||
values,
|
||||
placeholder,
|
||||
keyword,
|
||||
onKeywordChange,
|
||||
searchPlaceholder = 'Tìm theo tên hoặc email...',
|
||||
name,
|
||||
id,
|
||||
'aria-invalid': ariaInvalid,
|
||||
disabled = false,
|
||||
className,
|
||||
}: SelectUserProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [localQuery, setLocalQuery] = useState('');
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const useServerSearch = keyword !== undefined && onKeywordChange != null;
|
||||
const searchValue = useServerSearch ? keyword : localQuery;
|
||||
const setSearchValue = useServerSearch ? onKeywordChange! : setLocalQuery;
|
||||
|
||||
const selectedUser =
|
||||
value != null && value !== '' ? values.find((u) => u.id === value) : null;
|
||||
const displayValue = selectedUser ? userLabel(selectedUser) : '';
|
||||
|
||||
const filtered = useServerSearch
|
||||
? values
|
||||
: (() => {
|
||||
const q = localQuery.trim().toLowerCase();
|
||||
return q === ''
|
||||
? values
|
||||
: values.filter(
|
||||
(u) =>
|
||||
u.name.toLowerCase().includes(q) ||
|
||||
u.email.toLowerCase().includes(q),
|
||||
);
|
||||
})();
|
||||
|
||||
const close = useCallback(() => {
|
||||
setOpen(false);
|
||||
if (!useServerSearch) setLocalQuery('');
|
||||
}, [useServerSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
searchInputRef.current?.focus();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (
|
||||
wrapperRef.current &&
|
||||
!wrapperRef.current.contains(e.target as Node)
|
||||
) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', onMouseDown);
|
||||
return () => document.removeEventListener('mousedown', onMouseDown);
|
||||
}, [open, close]);
|
||||
|
||||
const handleSelect = (userId: string) => {
|
||||
onValueChange(userId);
|
||||
close();
|
||||
};
|
||||
|
||||
const controlId = id ?? name;
|
||||
const listboxId = controlId ? `${controlId}-listbox` : undefined;
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={cn('relative', className)}>
|
||||
{name != null && (
|
||||
<input type="hidden" name={name} value={value} readOnly />
|
||||
)}
|
||||
<div
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
aria-invalid={ariaInvalid}
|
||||
aria-controls={open ? listboxId : undefined}
|
||||
aria-disabled={disabled}
|
||||
id={controlId}
|
||||
className={cn(
|
||||
'border-input bg-input/20 dark:bg-input/30 flex h-7 w-full cursor-pointer items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs/relaxed outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
)}
|
||||
onClick={() => !disabled && setOpen((o) => !o)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate text-left">
|
||||
{displayValue || (
|
||||
<span className="text-muted-foreground">{placeholder}</span>
|
||||
)}
|
||||
</span>
|
||||
<CaretDownIcon
|
||||
className={cn(
|
||||
'size-3.5 shrink-0 text-muted-foreground transition-transform',
|
||||
open && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{open && (
|
||||
<div
|
||||
id={listboxId}
|
||||
role="listbox"
|
||||
className="border-input bg-popover text-popover-foreground absolute top-full z-50 mt-1 max-h-60 w-full min-w-[var(--radix-popper-anchor-width)] overflow-hidden rounded-lg border shadow-md"
|
||||
>
|
||||
<div className="border-input flex items-center gap-1 border-b px-2 py-1">
|
||||
<MagnifyingGlassIcon className="text-muted-foreground size-3.5 shrink-0" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="min-w-0 flex-1 bg-transparent py-1 text-xs outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-52 overflow-y-auto p-1">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-muted-foreground py-4 text-center text-xs">
|
||||
Không có kết quả
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={value === u.id}
|
||||
className={cn(
|
||||
'hover:bg-accent hover:text-accent-foreground flex w-full cursor-pointer items-center rounded-md px-2 py-1.5 text-left text-xs/relaxed outline-none',
|
||||
value === u.id && 'bg-accent text-accent-foreground',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSelect(u.id);
|
||||
}}
|
||||
>
|
||||
{userLabel(u)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user