הנחיות לבניית בורר תאריכים עברי ב-Next.js
מדריך קצר ומעשי עם קטעי קוד מוכנים להעתקה: התקנה, יצירת קומפוננטת קלנדר עברי, ולבסוף עטיפת Date Picker מלאה עם Popover.
מתחילים פרויקט חדש?
אם הפרויקט חדש לגמרי, אפשר להתחיל עם תבנית RTL של shadcn ולשלב את הרכיבים של המדריך הזה מיד אחרי יצירת הפרויקט.
טוב לדעת
הקומפוננטה בנויה כך שנעשה מקסימום שימוש ביכולות הנתמכות ישירות מהקופסה על ידי react-day-picker.
במימוש מינימלי מספיק לייבא DayPicker מתת-הנתיב react-day-picker/hebrew ולהוסיף formatters להצגה עברית.
רוב הלוגיקה כבר נתמכת ישירות על ידי הספריה, ואת התיעוד הרשמי ניתן למצוא כאן: daypicker.dev/localization/hebrew.
למותר לציין שכל props שנתמך בספריה ניתן להעביר גם לקומפוננטה הזו, שעוטפת את DayPicker בסגנון shadcn/ui.
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}
9303
הוספת קומפוננטת 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}
23704
דוגמת שימוש בסיסית בקומפוננטה
לאחר יצירת הקבצים, אפשר להשתמש מיד ב-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