128 lines
3.6 KiB
TypeScript
128 lines
3.6 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo, useState } from "react"
|
|
import { format, parseISO, isValid as isValidDate } from "date-fns"
|
|
import { ptBR } from "date-fns/locale"
|
|
import { Calendar as CalendarIcon, XIcon } from "lucide-react"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import { Calendar } from "@/components/ui/calendar"
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
|
import { useLocalTimeZone } from "@/hooks/use-local-time-zone"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
type DatePickerProps = {
|
|
value?: string | Date | null
|
|
onChange?: (value: string | null) => void
|
|
placeholder?: string
|
|
disabled?: boolean
|
|
minYear?: number
|
|
maxYear?: number
|
|
className?: string
|
|
align?: "start" | "center" | "end"
|
|
allowClear?: boolean
|
|
}
|
|
|
|
function normalizeDate(value?: string | Date | null) {
|
|
if (!value) return undefined
|
|
if (value instanceof Date) {
|
|
return isValidDate(value) ? value : undefined
|
|
}
|
|
const parsed = parseISO(value)
|
|
return isValidDate(parsed) ? parsed : undefined
|
|
}
|
|
|
|
export function DatePicker({
|
|
value,
|
|
onChange,
|
|
placeholder = "Selecionar data",
|
|
disabled,
|
|
minYear = 1900,
|
|
maxYear = new Date().getFullYear() + 5,
|
|
className,
|
|
align = "start",
|
|
allowClear = true,
|
|
}: DatePickerProps) {
|
|
const [open, setOpen] = useState(false)
|
|
const timeZone = useLocalTimeZone()
|
|
const selectedDate = useMemo(() => normalizeDate(value), [value])
|
|
const startMonth = useMemo(() => new Date(minYear, 0, 1), [minYear])
|
|
const endMonth = useMemo(() => new Date(maxYear, 11, 31), [maxYear])
|
|
|
|
const handleSelect = (date: Date | undefined) => {
|
|
if (!date) {
|
|
onChange?.(null)
|
|
return
|
|
}
|
|
onChange?.(format(date, "yyyy-MM-dd"))
|
|
setOpen(false)
|
|
}
|
|
|
|
const handleClear = () => {
|
|
onChange?.(null)
|
|
setOpen(false)
|
|
}
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={(next) => !disabled && setOpen(next)}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className={cn(
|
|
"w-full justify-between gap-2 text-left font-normal",
|
|
!selectedDate && "text-muted-foreground",
|
|
className
|
|
)}
|
|
disabled={disabled}
|
|
>
|
|
<span>
|
|
{selectedDate
|
|
? format(selectedDate, "dd/MM/yyyy", { locale: ptBR })
|
|
: placeholder}
|
|
</span>
|
|
{allowClear && selectedDate ? (
|
|
<XIcon
|
|
className="size-4 text-muted-foreground"
|
|
aria-label="Limpar data"
|
|
role="presentation"
|
|
onClick={(event) => {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
handleClear()
|
|
}}
|
|
/>
|
|
) : (
|
|
<CalendarIcon className="size-4 text-muted-foreground" aria-hidden="true" />
|
|
)}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0" align={align}>
|
|
<Calendar
|
|
mode="single"
|
|
selected={selectedDate}
|
|
onSelect={handleSelect}
|
|
initialFocus
|
|
captionLayout="dropdown"
|
|
startMonth={startMonth}
|
|
endMonth={endMonth}
|
|
locale={ptBR}
|
|
timeZone={timeZone}
|
|
/>
|
|
{allowClear ? (
|
|
<div className="border-t border-border/60 p-2">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-center text-xs text-muted-foreground"
|
|
onClick={handleClear}
|
|
>
|
|
Limpar data
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|