Files
ember-market-frontend/components/ui/date-picker.tsx
2025-11-28 18:33:23 +00:00

413 lines
12 KiB
TypeScript

"use client"
import * as React from "react"
import { format, addDays, startOfDay, endOfDay, isSameDay, isWithinInterval, getMonth, getYear, setMonth, setYear } from "date-fns"
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react"
import { DateRange } from "react-day-picker"
import { cn } from "@/lib/utils/styles"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
interface DatePickerProps {
date?: Date
onDateChange?: (date: Date | undefined) => void
placeholder?: string
className?: string
}
interface DateRangePickerProps {
dateRange?: DateRange
onDateRangeChange?: (range: DateRange | undefined) => void
placeholder?: string
className?: string
showPresets?: boolean
disabled?: boolean
}
interface MonthPickerProps {
selectedMonth?: Date
onMonthChange?: (date: Date | undefined) => void
placeholder?: string
className?: string
disabled?: boolean
}
// Single Date Picker
export function DatePicker({ date, onDateChange, placeholder = "Pick a date", className }: DatePickerProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : placeholder}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={onDateChange}
initialFocus
/>
</PopoverContent>
</Popover>
)
}
// Month Picker Component
export function MonthPicker({ selectedMonth, onMonthChange, placeholder = "Pick a month", className, disabled = false }: MonthPickerProps) {
const [isOpen, setIsOpen] = React.useState(false)
const [selectedYear, setSelectedYear] = React.useState(selectedMonth ? getYear(selectedMonth) : new Date().getFullYear())
const [selectedMonthIndex, setSelectedMonthIndex] = React.useState(selectedMonth ? getMonth(selectedMonth) : new Date().getMonth())
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
]
const years = Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - 5 + i)
React.useEffect(() => {
if (selectedMonth) {
setSelectedYear(getYear(selectedMonth))
setSelectedMonthIndex(getMonth(selectedMonth))
}
}, [selectedMonth])
const handleMonthSelect = (monthIndex: number) => {
const newDate = new Date(selectedYear, monthIndex, 1)
onMonthChange?.(newDate)
setIsOpen(false)
}
const handleYearChange = (year: string) => {
const newYear = parseInt(year)
setSelectedYear(newYear)
if (selectedMonth) {
const newDate = new Date(newYear, selectedMonthIndex, 1)
onMonthChange?.(newDate)
}
}
const formatSelectedMonth = (date?: Date) => {
if (!date) return placeholder
return format(date, "MMMM yyyy")
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!selectedMonth && "text-muted-foreground",
className
)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{formatSelectedMonth(selectedMonth)}
{selectedMonth && (
<div
className="ml-auto h-6 w-6 p-0 flex items-center justify-center rounded-sm hover:bg-accent cursor-pointer"
onClick={(e) => {
e.stopPropagation()
onMonthChange?.(undefined)
}}
>
<X className="h-3 w-3" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-4" align="start">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-medium">Select Month</h4>
<Select value={selectedYear.toString()} onValueChange={handleYearChange}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{years.map((year) => (
<SelectItem key={year} value={year.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-3 gap-2">
{months.map((month, index) => (
<Button
key={month}
variant={selectedMonthIndex === index ? "default" : "outline"}
size="sm"
onClick={() => handleMonthSelect(index)}
className="text-xs"
>
{month}
</Button>
))}
</div>
</div>
</PopoverContent>
</Popover>
)
}
// Date Range Picker with Presets
export function DateRangePicker({
dateRange,
onDateRangeChange,
placeholder = "Pick a date range",
className,
showPresets = true,
disabled = false
}: DateRangePickerProps) {
const [isOpen, setIsOpen] = React.useState(false)
const presets = [
{
label: "Today",
value: {
from: startOfDay(new Date()),
to: endOfDay(new Date())
}
},
{
label: "Yesterday",
value: {
from: startOfDay(addDays(new Date(), -1)),
to: endOfDay(addDays(new Date(), -1))
}
},
{
label: "Last 7 days",
value: {
from: startOfDay(addDays(new Date(), -6)),
to: endOfDay(new Date())
}
},
{
label: "Last 30 days",
value: {
from: startOfDay(addDays(new Date(), -29)),
to: endOfDay(new Date())
}
},
{
label: "This month",
value: {
from: startOfDay(new Date(new Date().getFullYear(), new Date().getMonth(), 1)),
to: endOfDay(new Date())
}
},
{
label: "Last month",
value: {
from: startOfDay(new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1)),
to: endOfDay(new Date(new Date().getFullYear(), new Date().getMonth(), 0))
}
}
]
const handlePresetClick = (preset: typeof presets[0]) => {
onDateRangeChange?.(preset.value)
setIsOpen(false)
}
const handleClear = () => {
onDateRangeChange?.(undefined)
setIsOpen(false)
}
const formatDateRange = (range: DateRange | undefined) => {
if (!range?.from) return placeholder
if (!range.to) {
return format(range.from, "PPP")
}
if (isSameDay(range.from, range.to)) {
return format(range.from, "PPP")
}
return `${format(range.from, "MMM dd")} - ${format(range.to, "MMM dd, yyyy")}`
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!dateRange?.from && "text-muted-foreground",
className
)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{formatDateRange(dateRange)}
{dateRange?.from && (
<div
className="ml-auto h-6 w-6 p-0 flex items-center justify-center rounded-sm hover:bg-accent cursor-pointer"
onClick={(e) => {
e.stopPropagation()
handleClear()
}}
>
<X className="h-3 w-3" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<div className="p-3">
{showPresets && (
<div className="space-y-2 mb-4">
<Label className="text-sm font-medium">Quick Select</Label>
<div className="grid grid-cols-2 gap-2">
{presets.map((preset) => (
<Button
key={preset.label}
variant="outline"
size="sm"
className="text-xs h-8"
onClick={() => handlePresetClick(preset)}
>
{preset.label}
</Button>
))}
</div>
</div>
)}
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange?.from}
selected={dateRange}
onSelect={onDateRangeChange}
numberOfMonths={2}
className="rounded-md border"
/>
</div>
</PopoverContent>
</Popover>
)
}
// Custom Date Range Input Component
export function CustomDateRangeInput({
dateRange,
onDateRangeChange,
className
}: DateRangePickerProps) {
const [fromDate, setFromDate] = React.useState<string>("")
const [toDate, setToDate] = React.useState<string>("")
React.useEffect(() => {
if (dateRange?.from) {
setFromDate(format(dateRange.from, "yyyy-MM-dd"))
}
if (dateRange?.to) {
setToDate(format(dateRange.to, "yyyy-MM-dd"))
}
}, [dateRange])
const handleFromDateChange = (value: string) => {
setFromDate(value)
const from = value ? new Date(value) : undefined
const to = toDate ? new Date(toDate) : dateRange?.to
if (from && to && from > to) {
// If from date is after to date, adjust to date
onDateRangeChange?.({ from, to: from })
setToDate(value)
} else {
onDateRangeChange?.({ from, to })
}
}
const handleToDateChange = (value: string) => {
setToDate(value)
const from = fromDate ? new Date(fromDate) : dateRange?.from
const to = value ? new Date(value) : undefined
if (from && to && from > to) {
// If to date is before from date, adjust from date
onDateRangeChange?.({ from: to, to })
setFromDate(value)
} else {
onDateRangeChange?.({ from, to })
}
}
return (
<div className={cn("flex items-center gap-2", className)}>
<div className="flex items-center gap-2">
<Label htmlFor="from-date" className="text-sm whitespace-nowrap">From:</Label>
<Input
id="from-date"
type="date"
value={fromDate}
onChange={(e) => handleFromDateChange(e.target.value)}
className="w-32"
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="to-date" className="text-sm whitespace-nowrap">To:</Label>
<Input
id="to-date"
type="date"
value={toDate}
onChange={(e) => handleToDateChange(e.target.value)}
className="w-32"
/>
</div>
</div>
)
}
// Date Range Display Component
export function DateRangeDisplay({ dateRange }: { dateRange?: DateRange }) {
if (!dateRange?.from) return null
const daysDiff = dateRange.to
? Math.ceil((dateRange.to.getTime() - dateRange.from.getTime()) / (1000 * 60 * 60 * 24)) + 1
: 1
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
{daysDiff} day{daysDiff !== 1 ? 's' : ''}
</Badge>
<span className="text-sm text-muted-foreground">
{format(dateRange.from, "MMM dd")}
{dateRange.to && !isSameDay(dateRange.from, dateRange.to) && (
<> - {format(dateRange.to, "MMM dd, yyyy")}</>
)}
</span>
</div>
)
}