Hebrew Date Picker Guide

הנחיות לבניית בורר תאריכים עברי ב-Next.js

מדריך קצר ומעשי עם קטעי קוד מוכנים להעתקה: התקנה, יצירת קומפוננטת קלנדר עברי, ולבסוף עטיפת Date Picker מלאה עם Popover.

01
התקנת התלויות react-day-picker ו-jewish-date
השלב הראשון הוא התקנת ספריית הלוח והמרת תאריכים עבריים.
1pnpm add react-day-picker jewish-date

אחרי ההתקנה יש לך DayPicker עם תמיכה בלוח עברי דרך import מהנתיב react-day-picker/hebrew, ופונקציות פורמט עברי מתוך jewish-date.

02
יצירת קומפוננטת HebrewCalendar
צרו את הקובץ components/ui/hebrew-calendar.tsx והדביקו את הקוד הבא.
components/ui/hebrew-calendar.tsx
1"use client" 2 3import * as React from "react" 4import { formatJewishDateInHebrew, toJewishDate } from "jewish-date" 5import { DayPicker, he } from "react-day-picker/hebrew" 6import { getDefaultClassNames } from "react-day-picker" 7import { 8 ChevronDownIcon, 9 ChevronLeftIcon, 10 ChevronRightIcon, 11} from "lucide-react" 12 13import { cn } from "@/lib/utils" 14 15const weekdayLabels = ["א", "ב", "ג", "ד", "ה", "ו", "ש"] as const 16 17type HebrewCalendarProps = React.ComponentProps<typeof DayPicker> 18 19export function HebrewCalendar({ 20 className, 21 locale = he, 22 dir = "rtl", 23 formatters, 24 classNames, 25 components, 26 ...props 27}: HebrewCalendarProps) { 28 const defaults = getDefaultClassNames() 29 30 const localFormatters = React.useMemo(() => { 31 return { 32 formatCaption: (date: Date) => 33 formatJewishDateInHebrew(toJewishDate(date), "MMMM YYYY"), 34 formatDay: (date: Date) => 35 formatJewishDateInHebrew(toJewishDate(date), "D"), 36 formatWeekdayName: (date: Date) => weekdayLabels[date.getDay()], 37 formatMonthDropdown: (date: Date) => 38 formatJewishDateInHebrew(toJewishDate(date), "MMMM"), 39 formatYearDropdown: (date: Date) => 40 formatJewishDateInHebrew(toJewishDate(date), "YYYY"), 41 } 42 }, []) 43 44 return ( 45 <DayPicker 46 {...props} 47 locale={locale} 48 dir={dir} 49 captionLayout={props.captionLayout ?? "dropdown"} 50 formatters={{ ...localFormatters, ...formatters }} 51 className={cn("rounded-xl border bg-background p-3", className)} 52 classNames={{ 53 root: cn("w-fit", defaults.root), 54 months: cn("flex flex-col gap-4 md:flex-row", defaults.months), 55 month: cn("flex flex-col gap-4", defaults.month), 56 nav: cn( 57 "absolute inset-x-0 top-0 flex items-center justify-between", 58 defaults.nav 59 ), 60 weekday: cn("text-xs text-muted-foreground", defaults.weekday), 61 day: cn("text-sm", defaults.day), 62 ...classNames, 63 }} 64 components={{ 65 Chevron: ({ orientation, className: iconClassName, ...iconProps }) => { 66 if (orientation === "left") { 67 return ( 68 <ChevronLeftIcon 69 className={cn("size-4 rtl:rotate-180", iconClassName)} 70 {...iconProps} 71 /> 72 ) 73 } 74 75 if (orientation === "right") { 76 return ( 77 <ChevronRightIcon 78 className={cn("size-4 rtl:rotate-180", iconClassName)} 79 {...iconProps} 80 /> 81 ) 82 } 83 84 return ( 85 <ChevronDownIcon className={cn("size-4", iconClassName)} {...iconProps} /> 86 ) 87 }, 88 ...components, 89 }} 90 /> 91 ) 92} 93
03
הוספת קומפוננטת HebrewDatePicker
צרו את הקובץ components/date-pickers/hebrew-date-picker.tsx והדביקו את הקוד הבא.
components/date-pickers/hebrew-date-picker.tsx
1"use client" 2 3import * as React from "react" 4import { CalendarIcon } from "lucide-react" 5import { formatJewishDateInHebrew, toJewishDate } from "jewish-date" 6import type { DateRange } from "react-day-picker" 7 8import { Button } from "@/components/ui/button" 9import { HebrewCalendar } from "@/components/ui/hebrew-calendar" 10import { 11 Popover, 12 PopoverContent, 13 PopoverTrigger, 14} from "@/components/ui/popover" 15import { cn } from "@/lib/utils" 16 17type SinglePickerProps = { 18 mode?: "single" 19 selected?: Date 20 onSelect?: (selected: Date | undefined) => void 21} 22 23type MultiplePickerProps = { 24 mode: "multiple" 25 selected?: Date[] 26 onSelect?: (selected: Date[] | undefined) => void 27} 28 29type RangePickerProps = { 30 mode: "range" 31 selected?: DateRange 32 onSelect?: (selected: DateRange | undefined) => void 33} 34 35type HebrewDatePickerProps = (SinglePickerProps | MultiplePickerProps | RangePickerProps) & 36 Omit<React.ComponentProps<typeof HebrewCalendar>, "mode" | "selected" | "onSelect"> & { 37 placeholder?: string 38 closeOnSelect?: boolean 39 showTodayButton?: boolean 40 todayLabel?: string 41 triggerClassName?: string 42 contentClassName?: string 43 } 44 45function formatHebrewDate(date: Date) { 46 return formatJewishDateInHebrew(toJewishDate(date)) 47} 48 49function buildLabel(props: SinglePickerProps | MultiplePickerProps | RangePickerProps, placeholder: string) { 50 if (props.mode === "multiple") { 51 const selected = props.selected 52 53 if (!selected || selected.length === 0) { 54 return placeholder 55 } 56 57 if (selected.length === 1) { 58 return formatHebrewDate(selected[0]) 59 } 60 61 return String(selected.length) + " תאריכים נבחרו" 62 } 63 64 if (props.mode === "range") { 65 const selected = props.selected 66 67 if (!selected?.from) { 68 return placeholder 69 } 70 71 if (!selected.to) { 72 return formatHebrewDate(selected.from) 73 } 74 75 return formatHebrewDate(selected.from) + " - " + formatHebrewDate(selected.to) 76 } 77 78 if (!props.selected) { 79 return placeholder 80 } 81 82 return formatHebrewDate(props.selected) 83} 84 85export function HebrewDatePicker({ 86 placeholder = "בחר/י תאריך עברי", 87 closeOnSelect, 88 showTodayButton = true, 89 todayLabel = "היום", 90 triggerClassName, 91 contentClassName, 92 ...props 93}: HebrewDatePickerProps) { 94 const [open, setOpen] = React.useState(false) 95 const [month, setMonth] = React.useState(new Date()) 96 97 const currentSelectionMonth = React.useMemo(() => { 98 if (props.mode === "multiple") { 99 return props.selected?.[0] 100 } 101 102 if (props.mode === "range") { 103 return props.selected?.from ?? props.selected?.to 104 } 105 106 return props.selected 107 }, [props.mode, props.selected]) 108 109 React.useEffect(() => { 110 if (currentSelectionMonth) { 111 setMonth(currentSelectionMonth) 112 } 113 }, [currentSelectionMonth]) 114 115 const shouldAutoClose = closeOnSelect ?? props.mode !== "multiple" 116 const triggerLabel = buildLabel(props, placeholder) 117 const isEmpty = triggerLabel === placeholder 118 119 const trigger = ( 120 <Button 121 type="button" 122 variant="outline" 123 className={cn( 124 "w-full justify-between gap-2 text-right font-normal", 125 isEmpty && "text-muted-foreground", 126 triggerClassName 127 )} 128 > 129 <span className="truncate">{triggerLabel}</span> 130 <CalendarIcon className="size-4 opacity-70" /> 131 </Button> 132 ) 133 134 if (props.mode === "multiple") { 135 const { mode, selected, onSelect, ...calendarProps } = props 136 137 return ( 138 <Popover open={open} onOpenChange={setOpen}> 139 <PopoverTrigger asChild>{trigger}</PopoverTrigger> 140 <PopoverContent dir="rtl" className={cn("w-auto p-0", contentClassName)}> 141 <div className="flex flex-col gap-0"> 142 <HebrewCalendar 143 {...calendarProps} 144 mode={mode} 145 month={month} 146 onMonthChange={setMonth} 147 selected={selected} 148 onSelect={(next) => { 149 onSelect?.(next as Date[] | undefined) 150 151 if (shouldAutoClose && next && next.length > 0) { 152 setOpen(false) 153 } 154 }} 155 /> 156 {showTodayButton && ( 157 <div className="border-t p-2"> 158 <Button type="button" variant="secondary" size="xs" className="w-full" onClick={() => setMonth(new Date())}> 159 {todayLabel} 160 </Button> 161 </div> 162 )} 163 </div> 164 </PopoverContent> 165 </Popover> 166 ) 167 } 168 169 if (props.mode === "range") { 170 const { mode, selected, onSelect, ...calendarProps } = props 171 172 return ( 173 <Popover open={open} onOpenChange={setOpen}> 174 <PopoverTrigger asChild>{trigger}</PopoverTrigger> 175 <PopoverContent dir="rtl" className={cn("w-auto p-0", contentClassName)}> 176 <div className="flex flex-col gap-0"> 177 <HebrewCalendar 178 {...calendarProps} 179 mode={mode} 180 month={month} 181 onMonthChange={setMonth} 182 selected={selected} 183 onSelect={(next) => { 184 onSelect?.(next) 185 186 if (shouldAutoClose && next?.from && next.to) { 187 setOpen(false) 188 } 189 }} 190 /> 191 {showTodayButton && ( 192 <div className="border-t p-2"> 193 <Button type="button" variant="secondary" size="xs" className="w-full" onClick={() => setMonth(new Date())}> 194 {todayLabel} 195 </Button> 196 </div> 197 )} 198 </div> 199 </PopoverContent> 200 </Popover> 201 ) 202 } 203 204 const { selected, onSelect, ...calendarProps } = props 205 206 return ( 207 <Popover open={open} onOpenChange={setOpen}> 208 <PopoverTrigger asChild>{trigger}</PopoverTrigger> 209 <PopoverContent dir="rtl" className={cn("w-auto p-0", contentClassName)}> 210 <div className="flex flex-col gap-0"> 211 <HebrewCalendar 212 {...calendarProps} 213 mode="single" 214 month={month} 215 onMonthChange={setMonth} 216 selected={selected} 217 onSelect={(next) => { 218 onSelect?.(next as Date | undefined) 219 220 if (shouldAutoClose && next) { 221 setOpen(false) 222 } 223 }} 224 /> 225 {showTodayButton && ( 226 <div className="border-t p-2"> 227 <Button type="button" variant="secondary" size="xs" className="w-full" onClick={() => setMonth(new Date())}> 228 {todayLabel} 229 </Button> 230 </div> 231 )} 232 </div> 233 </PopoverContent> 234 </Popover> 235 ) 236} 237
04
דוגמת שימוש בסיסית בקומפוננטה
לאחר יצירת הקבצים, אפשר להשתמש מיד ב-HebrewDatePicker בכל עמוד Client.
app/basic-usage-example.tsx
1"use client" 2 3import * as React from "react" 4import { formatJewishDateInHebrew, toJewishDate } from "jewish-date" 5 6import { HebrewDatePicker } from "@/components/date-pickers/hebrew-date-picker" 7 8export default function BasicHebrewDatePickerUsage() { 9 const [selectedDate, setSelectedDate] = React.useState<Date>() 10 11 return ( 12 <div className="max-w-xs space-y-3"> 13 <HebrewDatePicker 14 selected={selectedDate} 15 onSelect={setSelectedDate} 16 todayLabel="היום" 17 /> 18 19 <p className="text-sm text-muted-foreground"> 20 נבחר: {selectedDate ? formatJewishDateInHebrew(toJewishDate(selectedDate)) : "לא נבחר"} 21 </p> 22 </div> 23 ) 24} 25