This commit is contained in:
2025-12-23 13:42:50 -03:00
parent 22a2046fb1
commit 7ab0eab6bb
4 changed files with 150 additions and 117 deletions

View File

@@ -1,4 +1,10 @@
import { use, useState, useMemo } from 'react'
import {
use,
useState,
useMemo,
forwardRef,
type InputHTMLAttributes
} from 'react'
import { useToggle } from 'ahooks'
import Fuse from 'fuse.js'
import {
@@ -32,124 +38,129 @@ import {
import { type Course } from './data'
interface CoursePickerProps {
interface CoursePickerProps extends Omit<
InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange'
> {
value?: Course
options: Promise<{ hits: any[] }>
onChange?: (value: any) => void
error?: any
}
export function CoursePicker({
value,
onChange,
options,
error
}: CoursePickerProps) {
const { hits } = use(options)
const [search, setSearch] = useState<string>('')
const [open, { set }] = useToggle()
const [sort, { toggle }] = useToggle('a-z', 'z-a')
const fuse = useMemo(() => {
return new Fuse(hits, {
keys: ['name'],
threshold: 0.3,
includeMatches: true
})
}, [hits])
export const CoursePicker = forwardRef<HTMLInputElement, CoursePickerProps>(
({ value, onChange, options, error, ...props }, ref) => {
const { hits } = use(options)
const [search, setSearch] = useState<string>('')
const [open, { set }] = useToggle()
const [sort, { toggle }] = useToggle('a-z', 'z-a')
const fuse = useMemo(() => {
return new Fuse(hits, {
keys: ['name'],
threshold: 0.3,
includeMatches: true
})
}, [hits])
const filtered = useMemo(() => {
const results = !search ? hits : fuse.search(search).map(({ item }) => item)
const filtered = useMemo(() => {
const results = !search
? hits
: fuse.search(search).map(({ item }) => item)
return results.sort((a, b) => {
const comparison = a.name.localeCompare(b.name)
return sort === 'a-z' ? comparison : -comparison
})
}, [search, fuse, hits, sort])
return results.sort((a, b) => {
const comparison = a.name.localeCompare(b.name)
return sort === 'a-z' ? comparison : -comparison
})
}, [search, fuse, hits, sort])
return (
<Popover open={open} onOpenChange={set}>
<PopoverTrigger asChild>
<InputGroup>
<InputGroupInput
readOnly
placeholder="Curso"
value={value?.name || ''}
aria-invalid={!!error}
/>
<InputGroupAddon>
<BookIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<ChevronsUpDownIcon />
</InputGroupAddon>
</InputGroup>
</PopoverTrigger>
return (
<Popover open={open} onOpenChange={set}>
<PopoverTrigger asChild>
<InputGroup>
<InputGroupInput
ref={ref}
placeholder="Curso"
value={value?.name || ''}
aria-invalid={!!error}
{...props}
/>
<PopoverContent className="lg:w-84 p-0" align="start">
<Command shouldFilter={false}>
<div className="flex">
<div className="flex-1">
<CommandInput
placeholder="Curso"
autoComplete="off"
onValueChange={setSearch}
/>
<InputGroupAddon>
<BookIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<ChevronsUpDownIcon />
</InputGroupAddon>
</InputGroup>
</PopoverTrigger>
<PopoverContent className="lg:w-84 p-0" align="start">
<Command shouldFilter={false}>
<div className="flex">
<div className="flex-1">
<CommandInput
placeholder="Curso"
autoComplete="off"
onValueChange={setSearch}
/>
</div>
<div className="border-b flex items-center justify-end">
<Button
variant="link"
size="icon-sm"
tabIndex={-1}
className="cursor-pointer text-muted-foreground"
onClick={toggle}
>
{sort == 'a-z' ? <ArrowDownAZIcon /> : <ArrowUpAZIcon />}
</Button>
</div>
</div>
<div className="border-b flex items-center justify-end">
<Button
variant="link"
size="icon-sm"
tabIndex={-1}
className="cursor-pointer text-muted-foreground"
onClick={toggle}
>
{sort == 'a-z' ? <ArrowDownAZIcon /> : <ArrowUpAZIcon />}
</Button>
</div>
</div>
{/* Force rerender to reset the scroll position */}
<CommandList key={sort}>
<CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
<CommandGroup>
{filtered
.filter(
({ metadata__unit_price = 0 }) => metadata__unit_price > 0
)
.map(
({
id,
name,
access_period,
metadata__unit_price: unit_price
}) => (
<CommandItem
key={id}
value={id}
className="cursor-pointer"
onSelect={() => {
onChange?.({
id,
name,
access_period: Number(access_period),
unit_price: Number(unit_price)
})
set(false)
}}
>
{name}
<CheckIcon
className={cn(
'ml-auto',
value?.id === id ? 'opacity-100' : 'opacity-0'
)}
/>
</CommandItem>
{/* Force rerender to reset the scroll position */}
<CommandList key={sort}>
<CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
<CommandGroup>
{filtered
.filter(
({ metadata__unit_price = 0 }) => metadata__unit_price > 0
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
.map(
({
id,
name,
access_period,
metadata__unit_price: unit_price
}) => (
<CommandItem
key={id}
value={id}
className="cursor-pointer"
onSelect={() => {
onChange?.({
id,
name,
access_period: Number(access_period),
unit_price: Number(unit_price)
})
set(false)
}}
>
{name}
<CheckIcon
className={cn(
'ml-auto',
value?.id === id ? 'opacity-100' : 'opacity-0'
)}
/>
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
)