Files
aggios.app/front-end-agency/components/ui/DatePicker.tsx
2025-12-29 17:23:59 -03:00

243 lines
12 KiB
TypeScript

"use client";
import { useState, Fragment } from "react";
import { Popover, PopoverButton, PopoverPanel, Transition } from "@headlessui/react";
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon } from "@heroicons/react/24/outline";
import {
format,
addMonths,
subMonths,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
isSameMonth,
isSameDay,
addDays,
eachDayOfInterval,
isWithinInterval,
isBefore,
subDays
} from "date-fns";
import { ptBR } from "date-fns/locale";
interface DatePickerProps {
value?: { start: Date | null; end: Date | null } | Date | null;
onChange: (val: any) => void;
placeholder?: string;
className?: string;
buttonClassName?: string;
mode?: 'single' | 'range';
label?: string;
}
export default function DatePicker({
value,
onChange,
placeholder,
className = "",
buttonClassName = "",
mode = 'range',
label
}: DatePickerProps) {
const [currentMonth, setCurrentMonth] = useState(new Date());
// Helper to normalize value
const getRange = () => {
if (mode === 'single') {
const date = value instanceof Date ? value : (value as any)?.start || null;
return { start: date, end: date };
}
const range = (value as { start: Date | null; end: Date | null }) || { start: null, end: null };
return range;
};
const range = getRange();
const quickRanges = [
{ label: 'Hoje', getValue: () => ({ start: new Date(), end: new Date() }) },
{ label: 'Últimos 7 dias', getValue: () => ({ start: subDays(new Date(), 7), end: new Date() }) },
{ label: 'Últimos 14 dias', getValue: () => ({ start: subDays(new Date(), 14), end: new Date() }) },
{ label: 'Últimos 30 dias', getValue: () => ({ start: subDays(new Date(), 30), end: new Date() }) },
{ label: 'Este Mês', getValue: () => ({ start: startOfMonth(new Date()), end: endOfMonth(new Date()) }) },
];
const handleDateClick = (day: Date) => {
if (mode === 'single') {
onChange(day);
return;
}
if (!range.start || (range.start && range.end)) {
onChange({ start: day, end: null });
} else {
if (isBefore(day, range.start)) {
onChange({ start: day, end: range.start });
} else {
onChange({ start: range.start, end: day });
}
}
};
const isInRange = (day: Date) => {
if (range.start && range.end) {
return isWithinInterval(day, { start: range.start, end: range.end });
}
return false;
};
const displayValue = () => {
if (!range.start) return placeholder || (mode === 'single' ? "Selecionar data" : "Selecionar período");
if (mode === 'single') return format(range.start, "dd/MM/yyyy", { locale: ptBR });
if (!range.end || isSameDay(range.start, range.end)) return format(range.start, "dd MMM yyyy", { locale: ptBR });
return `${format(range.start, "dd MMM", { locale: ptBR })} - ${format(range.end, "dd MMM yyyy", { locale: ptBR })}`;
};
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
onChange(mode === 'single' ? null : { start: null, end: null });
};
return (
<Popover className={`relative ${className}`}>
{label && (
<label className="block text-sm font-bold text-zinc-700 dark:text-zinc-300 mb-2">
{label}
</label>
)}
<PopoverButton
className={`
w-full flex items-center gap-3 px-4 py-2.5 text-sm font-bold transition-all outline-none border rounded-xl group
${buttonClassName || 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-700 dark:text-zinc-300 hover:border-zinc-400'}
focus:border-zinc-400 dark:focus:border-zinc-500
`}
>
<CalendarIcon className="w-4.5 h-4.5 text-zinc-400 group-hover:text-brand-500 transition-colors shrink-0" />
<span className="flex-1 text-left truncate">
{displayValue()}
</span>
{range.start && (
<button
type="button"
onClick={handleClear}
className="p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg text-zinc-400 hover:text-rose-500 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.6} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</PopoverButton>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel
anchor="bottom end"
className="flex bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl shadow-xl overflow-hidden outline-none [--anchor-gap:8px] min-w-[480px]"
>
{/* Filtros Rápidos (Apenas para Range) */}
{mode === 'range' && (
<div className="w-48 bg-zinc-50/80 dark:bg-zinc-900/80 border-r border-zinc-100 dark:border-zinc-800 p-4 space-y-1.5">
<div className="flex items-center gap-2 px-1 py-1 mb-2">
<ClockIcon className="w-3.5 h-3.5 text-zinc-400" />
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-[0.1em]">Atalhos</p>
</div>
{quickRanges.map((r) => (
<button
key={r.label}
type="button"
onClick={() => onChange(r.getValue())}
className="w-full px-3 py-2 text-left text-[11px] font-bold text-zinc-600 dark:text-zinc-400 hover:bg-white dark:hover:bg-zinc-800 hover:text-brand-600 dark:hover:text-brand-400 rounded-xl transition-all border border-transparent hover:border-zinc-100 dark:hover:border-zinc-700 hover:shadow-sm"
>
{r.label}
</button>
))}
<div className="pt-4 mt-4 border-t border-zinc-100 dark:border-zinc-800">
<button
type="button"
onClick={() => onChange({ start: null, end: null })}
className="w-full px-3 py-2 text-left text-[11px] font-bold text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-all"
>
Limpar Filtro
</button>
</div>
</div>
)}
{/* Calendário */}
<div className="flex-1 p-4">
<div className="flex items-center justify-between mb-4 px-1">
<span className="text-sm font-bold text-zinc-900 dark:text-white capitalize">
{format(currentMonth, "MMMM yyyy", { locale: ptBR })}
</span>
<div className="flex gap-1">
<button
type="button"
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
className="p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors border border-zinc-100 dark:border-zinc-800"
>
<ChevronLeftIcon className="w-4 h-4 text-zinc-500" />
</button>
<button
type="button"
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
className="p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors border border-zinc-100 dark:border-zinc-800"
>
<ChevronRightIcon className="w-4 h-4 text-zinc-500" />
</button>
</div>
</div>
<div className="grid grid-cols-7 mb-2">
{["D", "S", "T", "Q", "Q", "S", "S"].map((day, idx) => (
<div key={idx} className="text-center text-[10px] font-bold text-zinc-400 py-1">
{day}
</div>
))}
</div>
<div className="grid grid-cols-7 gap-1">
{eachDayOfInterval({
start: startOfWeek(startOfMonth(currentMonth)),
end: endOfWeek(endOfMonth(currentMonth))
}).map((day, idx) => {
const isStart = range.start && isSameDay(day, range.start);
const isEnd = range.end && isSameDay(day, range.end);
const isSelected = isStart || isEnd;
const isRange = isInRange(day);
const isCurrentMonth = isSameMonth(day, startOfMonth(currentMonth));
const isTodayDate = isSameDay(day, new Date());
return (
<button
key={idx}
type="button"
onClick={() => handleDateClick(day)}
className={`
relative py-2.5 text-[11px] transition-all outline-none flex items-center justify-center font-bold h-10 w-10
${!isCurrentMonth ? 'text-zinc-300 dark:text-zinc-600' : 'text-zinc-900 dark:text-zinc-100'}
${isSelected ? 'bg-brand-500 text-white rounded-xl z-10 scale-105 shadow-lg shadow-brand-500/20' : ''}
${isRange && !isSelected && mode === 'range' ? 'bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400' : ''}
${!isSelected ? 'hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:rounded-xl' : ''}
${isTodayDate && !isSelected ? 'text-brand-600 ring-1 ring-brand-100 dark:ring-brand-500/30 rounded-xl' : ''}
`}
>
<span>{format(day, "d")}</span>
</button>
);
})}
</div>
</div>
</PopoverPanel>
</Transition>
</Popover>
);
}