'use client'
import * as React from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { buttonVariants } from './button'
import { cn } from '@/lib/utils'
import { format } from 'date-fns'
import { zhCN, enUS } from 'date-fns/locale'
import { useLanguage } from '@/contexts/language-context'
import { Button } from './button'
import { Popover, PopoverContent, PopoverTrigger } from './popover'
import { Calendar1 as CalenderMonthIcon } from 'lucide-react'
type Month = {
number: number
name: string
}
function generateMonths(language?: string) {
const locale = language === 'zh-CN' ? zhCN : enUS
const months: Month[][] = [[], [], []]
for (let i = 0; i < 12; i++) {
months[Math.floor(i / 4)].push({
number: i,
name: format(new Date(0, i), 'MMM', { locale }),
})
}
return months
}
type MonthCalProps = {
selectedMonth?: Date
onMonthSelect?: (date: Date) => void
onYearForward?: () => void
onYearBackward?: () => void
callbacks?: {
yearLabel?: (year: number) => string
monthLabel?: (month: Month) => string
}
variant?: {
calendar?: {
main?: ButtonVariant
selected?: ButtonVariant
}
chevrons?: ButtonVariant
}
minDate?: Date
maxDate?: Date
disabledDates?: Date[]
}
type ButtonVariant =
| 'default'
| 'outline'
| 'ghost'
| 'link'
| 'destructive'
| 'secondary'
| null
| undefined
function MonthPicker({
onMonthSelect,
selectedMonth,
minDate,
maxDate,
disabledDates,
callbacks,
onYearBackward,
onYearForward,
variant,
className,
...props
}: React.HTMLAttributes<HTMLDivElement> & MonthCalProps) {
return (
<div className={cn('min-w-[200px] w-[280px] p-3', className)} {...props}>
<div className='flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0'>
<div className='space-y-4 w-full'>
<MonthCal
onMonthSelect={onMonthSelect}
callbacks={callbacks}
selectedMonth={selectedMonth}
onYearBackward={onYearBackward}
onYearForward={onYearForward}
variant={variant}
minDate={minDate}
maxDate={maxDate}
disabledDates={disabledDates}></MonthCal>
</div>
</div>
</div>
)
}
function MonthCal({
selectedMonth,
onMonthSelect,
callbacks,
variant,
minDate,
maxDate,
disabledDates,
onYearBackward,
onYearForward,
}: MonthCalProps) {
const [year, setYear] = React.useState<number>(
selectedMonth?.getFullYear() ?? new Date().getFullYear()
)
const [month, setMonth] = React.useState<number>(
selectedMonth?.getMonth() ?? new Date().getMonth()
)
const [menuYear, setMenuYear] = React.useState<number>(year)
if (minDate && maxDate && minDate > maxDate) minDate = maxDate
const disabledDatesMapped = disabledDates?.map((d) => {
return { year: d.getFullYear(), month: d.getMonth() }
})
const { language } = useLanguage()
return (
<>
<div className='flex justify-center pt-1 relative items-center'>
<div className='text-sm font-medium'>
{callbacks?.yearLabel ? callbacks?.yearLabel(menuYear) : menuYear}
</div>
<div className='space-x-1 flex items-center'>
<button
onClick={() => {
setMenuYear(menuYear - 1)
if (onYearBackward) onYearBackward()
}}
className={cn(
buttonVariants({ variant: variant?.chevrons ?? 'outline' }),
'inline-flex items-center justify-center h-7 w-7 p-0 absolute left-1'
)}>
<ChevronLeft className='opacity-50 h-4 w-4' />
</button>
<button
onClick={() => {
setMenuYear(menuYear + 1)
if (onYearForward) onYearForward()
}}
className={cn(
buttonVariants({ variant: variant?.chevrons ?? 'outline' }),
'inline-flex items-center justify-center h-7 w-7 p-0 absolute right-1'
)}>
<ChevronRight className='opacity-50 h-4 w-4' />
</button>
</div>
</div>
<table className='w-full border-collapse space-y-1'>
<tbody>
{generateMonths(language).map((monthRow, a) => {
return (
<tr key={'row-' + a} className='flex w-full mt-2'>
{monthRow.map((m) => {
return (
<td
key={m.number}
className='h-10 w-1/4 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20'>
<button
onClick={() => {
setMonth(m.number)
setYear(menuYear)
if (onMonthSelect) onMonthSelect(new Date(menuYear, m.number))
}}
disabled={
(maxDate
? menuYear > maxDate?.getFullYear() ||
(menuYear == maxDate?.getFullYear() && m.number > maxDate.getMonth())
: false) ||
(minDate
? menuYear < minDate?.getFullYear() ||
(menuYear == minDate?.getFullYear() && m.number < minDate.getMonth())
: false) ||
(disabledDatesMapped
? disabledDatesMapped?.some(
(d) => d.year == menuYear && d.month == m.number
)
: false)
}
className={cn(
buttonVariants({
variant:
month == m.number && menuYear == year
? variant?.calendar?.selected ?? 'default'
: variant?.calendar?.main ?? 'ghost',
}),
'h-full w-full p-0 font-normal aria-selected:opacity-100'
)}>
{callbacks?.monthLabel ? callbacks.monthLabel(m) : m.name}
</button>
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
</>
)
}
export type MonthData = {
year: number
month: number
}
export function DateMonthPicker({
value,
onChange,
}: {
value?: MonthData
onChange?: (monthData: MonthData | undefined) => void
}) {
const [selectedMonth, setSelectedMonth] = React.useState<Date>()
React.useEffect(() => {
if (!value) {
setSelectedMonth(undefined)
return
}
setSelectedMonth(new Date(value.year, value.month))
}, [value])
function handleSelected(date: Date) {
setSelectedMonth(date)
onChange?.({
year: date.getFullYear(),
month: date.getMonth(),
})
}
const { t, language } = useLanguage()
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'justify-start text-left font-normal',
!selectedMonth && 'text-muted-foreground'
)}>
<CalenderMonthIcon className='mr-2 h-4 w-4' />
{selectedMonth ? (
format(selectedMonth, language === 'zh-CN' ? 'yyyy年M月' : 'MMM yyyy')
) : (
<span>{t('common.pickMonth')}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<MonthPicker
onMonthSelect={handleSelected}
selectedMonth={selectedMonth}
maxDate={new Date()}
/>
</PopoverContent>
</Popover>
)
}