147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useMemo } from 'react'
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface MiniCalendarProps {
|
|
selectedDate: string
|
|
onSelectDate: (date: string) => void
|
|
}
|
|
|
|
const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
|
|
|
|
function getDaysInMonth(year: number, month: number): number {
|
|
return new Date(year, month + 1, 0).getDate()
|
|
}
|
|
|
|
function getFirstDayOfMonth(year: number, month: number): number {
|
|
return new Date(year, month, 1).getDay()
|
|
}
|
|
|
|
function formatDate(year: number, month: number, day: number): string {
|
|
return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
}
|
|
|
|
export function MiniCalendar({ selectedDate, onSelectDate }: MiniCalendarProps) {
|
|
const selected = selectedDate ? new Date(selectedDate) : new Date()
|
|
const [viewYear, setViewYear] = useState(selected.getFullYear())
|
|
const [viewMonth, setViewMonth] = useState(selected.getMonth())
|
|
|
|
const today = new Date()
|
|
const todayStr = formatDate(
|
|
today.getFullYear(),
|
|
today.getMonth(),
|
|
today.getDate()
|
|
)
|
|
|
|
const selectedStr = selectedDate?.split('T')[0]?.split(' ')[0] || todayStr
|
|
|
|
const days = useMemo(() => {
|
|
const daysInMonth = getDaysInMonth(viewYear, viewMonth)
|
|
const firstDay = getFirstDayOfMonth(viewYear, viewMonth)
|
|
const result: (number | null)[] = []
|
|
|
|
// Leading empty cells
|
|
for (let i = 0; i < firstDay; i++) {
|
|
result.push(null)
|
|
}
|
|
// Days of month
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
result.push(d)
|
|
}
|
|
return result
|
|
}, [viewYear, viewMonth])
|
|
|
|
function prevMonth() {
|
|
if (viewMonth === 0) {
|
|
setViewYear(viewYear - 1)
|
|
setViewMonth(11)
|
|
} else {
|
|
setViewMonth(viewMonth - 1)
|
|
}
|
|
}
|
|
|
|
function nextMonth() {
|
|
if (viewMonth === 11) {
|
|
setViewYear(viewYear + 1)
|
|
setViewMonth(0)
|
|
} else {
|
|
setViewMonth(viewMonth + 1)
|
|
}
|
|
}
|
|
|
|
function goToToday() {
|
|
setViewYear(today.getFullYear())
|
|
setViewMonth(today.getMonth())
|
|
onSelectDate(todayStr)
|
|
}
|
|
|
|
const monthLabel = new Date(viewYear, viewMonth).toLocaleDateString('en-US', {
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={prevMonth}>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
<button
|
|
onClick={goToToday}
|
|
className="text-sm font-medium hover:underline"
|
|
>
|
|
{monthLabel}
|
|
</button>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={nextMonth}>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-0">
|
|
{WEEKDAYS.map((wd) => (
|
|
<div
|
|
key={wd}
|
|
className="py-1 text-center text-[10px] font-medium text-muted-foreground"
|
|
>
|
|
{wd}
|
|
</div>
|
|
))}
|
|
{days.map((day, idx) => {
|
|
if (day === null) {
|
|
return <div key={`empty-${idx}`} className="h-7" />
|
|
}
|
|
const dateStr = formatDate(viewYear, viewMonth, day)
|
|
const isToday = dateStr === todayStr
|
|
const isSelected = dateStr === selectedStr
|
|
|
|
return (
|
|
<button
|
|
key={dateStr}
|
|
onClick={() => onSelectDate(dateStr)}
|
|
className={cn(
|
|
'flex h-7 w-full items-center justify-center rounded-md text-xs transition-colors hover:bg-accent',
|
|
isToday && !isSelected && 'font-bold text-primary',
|
|
isSelected && 'bg-primary text-primary-foreground hover:bg-primary/90'
|
|
)}
|
|
>
|
|
{day}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full text-xs"
|
|
onClick={goToToday}
|
|
>
|
|
Today
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|