أداة اختيار للتاريخ مخصصة (shadcn.ui, تاريخ بالهجري, عرض الأرقام العربية الهندية)
Mohanad Alrwaihy
September 24, 2023
685
3
أحد أهم المكونات في الويب هو Date Picker ومعرفة كيفية إنشاء واحد أو استخدام منتقي تاريخ جاهز أمر ضروري لسهولة استخدام تطبيقك.
7 دقائق للقراءة
هناك العديد من الطرق للحصول على منتقي التاريخ في موقعك إما باستخدام منتقي تاريخ مكتبة مشهورة للعناصر مثل MUI Date Picker أو باستخدام اضافات مثل react-datepicker أو react-day-picker.
خيارات منتقي التاريخ
react-datepicker- عنصر كلاسيكي و بسيط قابل لإعادة الاستخدام و يستخدم على نطاق واسع حول الويب ويمكن أن يحتوي على الكثير من خيارات التخصيص. (معاينة, Github)react-datetime-picker- عنصر جميل المظهر, سريع, ويوفر الكثير من المرونة في الأستخدام وعدة خيارات يمكنك الأختيار تم تطويرها المطور Wojciech Maj. (معاينة, NPM)react-dates- عنصر لأختيار التاريخ مناسبة للمواقع التي توفر أكثر من خيار للغة وتتميز بسهولة الأستخدام ومتوافقة مع الهاتف المحمول بشكل جيد. (Github, معاينة).react-day-picker- عنصر لأختيار التاريخ لـ React. يقدم تقويمًا شهريًا لتحديد الأيام. DayPicker قابل للتخصيص ، ويعمل بشكل رائع مع حقول الإدخال ويمكن تعديله ليتناسب مع أي تصميم. (معاينة, Github).
التقويم المخصص
في هذا المنشور ، سأوضح كيف يمكننا إنشاء منتقي تاريخ مصمم خصيصًا بتوفير هذه الميزات:
- التقويم الميلادي والهجري.
- أظهار التاريخ بالأرقام العربية الهندية.
- الترجمة العربية.
استخدام مكون Shadcn/ui calendar الذي تم بناؤه فوق react-day-picker ويستخدم date-fns لمعالجة تواريخ JavaScritp.
أنظمة الترقيم المختلفة:
انتشار الأرقام الهندية العربية في تقاليد الرياضيات العملية الأوروبية
النتيجة النهائية
أدوات
هذه هي الأدوات التي استخدمتها لبناء منتقي التاريخ المخصص:
- React (Vite).
- Tailwind CSS.
- Shadcn / ui - مكونات مخصصة.
إضافة تقويم shadcn/ui
لن أقوم بشرح طريقة إضافة shadcn/ui للمشروع، يمكنك متابعة صفحة التثبيت لمزيد من المعلومات أو التحقق من آخر منشور لي هنا شرحت فيه كيف يمكننا استخدام shadc/ui في مشروعنا.
تثبيت تقويم shadcn/ui
لإضافة تقويم Shadcn/ui 👇
POWERSHELL# NPM
npx shadcn-ui@latest add calendar
# PNPM
pnpm dlx shadcn-ui@latest add calendarتعديل التقويم
يمكننا التعديل على التقويم من الملف الجديد المنشئ في /components/ui/calendar.tsx.
تعديل التصميم الأساسي
يحتوي مكون DayPicker على classNames لكل شيء داخل التقويم الذي يمكنك من التصميم والتخصيص كما تريد.
فيما يلي التغيرات التي قمت بها 👇
/components/ui/calendar.tsxTSX // ...
<DayPicker
// ...
className={cn('w-full overflow-x-auto p-1.5 sm:p-3', className)}
classNames={{
months:
'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0 w-full',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm text-center font-medium mx-12',
nav: 'space-x-1 flex items-center pt-1',
nav_button: cn(
buttonVariants({ variant: 'secondary' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'space-y-2 w-full',
head_row: 'flex justify-between gap-1',
head_cell: 'text-muted-foreground w-9 font-bold text-[0.7rem]',
row: 'flex mt-2 justify-between',
cell: 'text-center text-sm w-9 h-9 p-0 relative rounded-lg focus-within:z-20 self-center my-auto flex flex-col items-center justify-center',
day: cn(
buttonVariants({ variant: 'ghost' }),
'w-full p-0 font-normal aria-selected:opacity-100 font-medium'
),
day_selected:
'bg-foreground text-background focus:bg-foreground focus:text-background',
day_today: 'bg-accent text-accent-foreground',
day_outside: 'text-muted-foreground opacity-50',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-foreground/70 aria-selected:text-background',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: () => <ChevronLeft className='h-4 w-4 rtl:rotate-180' />,
IconRight: () => <ChevronRight className='h-4 w-4 rtl:rotate-180' />,
}}
{...props}
/>خيارات الإعدادات المحلية
لدعم تغييرات الإعدادات المحلية في مكون التقويم ، نحتاج إلى تمرير سمة الإعدادات المحلية إلى مكون التقويم ومن ثم يمكننا إظهار أرقام التواريخ أو أسماء الأشهر بناء على الإعدادات المحلية وتغيير موقع منتقي التاريخ من اليمين إلى اليسار 👇
TSX...
import { arSA } from 'date-fns/locale'
function Calendar({
className,
classNames,
locale,
showOutsideDays = true,
...props
}: CalendarProps) {
const NU_LOCALE = locale === arSA
? 'ar-u-ca-nu-arab'
: 'en-u-ca-nu-latn'
const ISLAMIC_CALENDAR = locale === arSA
? 'ar-u-ca-islamic-umalqura-nu-arab'
: 'ar-u-ca-islamic-umalqura-nu-latn'
return (
<DayPicker
showOutsideDays={showOutsideDays}
locale={locale}
dir={locale === arSA ? 'rtl' : 'ltr'}
formatters={...}
disabled={...}
defaultMonth={...}
fromMonth={...}
fromYear={...}
toYear={...}
className={...}
classNames={...}
components={...}
{...props}
/>
)
}المتغير NU_LOCALE هو متغير مهم جدا سنستخدمه لإظهار القيم بأستخدام التنسيق الأوروبي أو الأرقام العربية الهندية.
كما سنستخدم متغير ISLAMIC_CALENDAR لإظهار التاريخ الهجري - أم القرى. 👇
TSXconst NU_LOCALE =
locale === arSA
? 'ar-u-ca-nu-arab' // Arabic-indic (٠, ١, ٢, ٣, ٤, ٥, ٦, ٧, ٨, ٩)
: 'en-u-ca-nu-latn' // Euoropean (0, 1, 2, 3, 4, 5, 6, 7 ,8, 9)
const ISLAMIC_CALENDAR =
locale === arSA
? 'ar-u-ca-islamic-umalqura-nu-arab'
: 'ar-u-ca-islamic-umalqura-nu-latn'
تحذير
لاحظ أنه في المتغير ISLAMIC_CALENDAR استخدمت ar-u-ca-islamic-umalqura-nu-latn عندما لا تكون اللغة العربية حيث كان بإمكاني استخدام en-u-ca-islamic-umalqura-nu-latn والتي في الواقع ستظهر أسماء الأشهر الهجرية بالالإنجليزية بدلا من العربية لكنني تجنبت ذلك لأنني وجدت خطأ عند فتح الموقع عن طريق الجوال حيث سيظهر الشهر الهجري بأسم الشهور الميلادية مع السنة الهجرية الصحيحة متبوعة ب BC في النهاية والتي لا معنى لها على الإطلاق.
سوف نقوم بتنسيق شيئين:
- خانة المعلومات التوضيحية في رأس التقويم (Caption Label) - إظهار الشهر الهجري والأرقام العربية الهندية عندما تكون اللغة العربية.
- مربع اليوم - لإظهار اليوم الهجري تحت التاريخ الميلادي.
لحسن الحظ ، يمنحنا React DayPicker خيارا سهلا لتنسيق الكثير من الأشياء داخل منتقي التاريخ المخصص باستخدام وظيفة forammter.
تنسيق التسمية التوضيحية
لتنسيق رأس التقويم حيث تكون الأسهم والشهر والسنة الحاليين مرئيين.
TSX...
import { DayPicker, type DateFormatter } from 'react-day-picker'
import { arSA } from 'date-fns/locale'
import { addMonths } from 'date-fns'
function Calendar({
className,
classNames,
locale,
showOutsideDays = true,
...props
}: CalendarProps) {
const NU_LOCALE = locale === arSA
? 'ar-u-ca-nu-arab'
: 'en-u-ca-nu-latn'
const ISLAMIC_CALENDAR = locale === arSA
? 'ar-u-ca-islamic-umalqura-nu-arab'
: 'ar-u-ca-islamic-umalqura-nu-latn'
const formatCaption: DateFormatter = (date) => {
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
}
const dateGregorian = date.toLocaleDateString(NU_LOCALE,options)
const dateHajri = date.toLocaleDateString(ISLAMIC_CALENDAR, options)
const nextMonth = addMonths(date, 1)
const dateHajriNext = nextMonth.toLocaleDateString(NU_LOCALE,options)
return (
<div>
<span>{dateGregorian}</span>
<span className='block text-[0.7rem] leading-4 text-orange-400'>
{dateHajri} - {dateHajriNext}
</span>
</div>
)
}
return (
<DayPicker
showOutsideDays={showOutsideDays}
locale={locale}
dir={locale === arSA ? 'rtl' : 'ltr'}
formatters={{ formatCaption, ... }}
disabled={...}
defaultMonth={...}
fromMonth={...}
fromYear={...}
toYear={...}
className={...}
classNames={...}
components={...}
{...props}
/>
)
}تنسيق مربع اليوم
لتنسيق كل خلية تعرض يوما في الشهر.
TSX...
function Calendar({
className,
classNames,
locale,
showOutsideDays = true,
...props
}: CalendarProps) {
const NU_LOCALE = locale === arSA
? 'ar-u-ca-nu-arab'
: 'en-u-ca-nu-latn'
const ISLAMIC_CALENDAR = locale === arSA
? 'ar-u-ca-islamic-umalqura-nu-arab'
: 'ar-u-ca-islamic-umalqura-nu-latn'
const formatCaption: DateFormatter = (date) => {...}
const formatDay: DateFormatter = (day) => {
const options: Intl.DateTimeFormatOptions = { day: 'numeric' }
const dateGregorian = day.toLocaleDateString(NU_LOCALE, options)
const dateHajri = day.toLocaleDateString(ISLAMIC_CALENDAR, options)
return (
<div>
<span className='text-sm'>{dateGregorian}</span>
<span className='block text-[0.7rem] leading-3 text-orange-400 '>
{dateHajri}
</span>
</div>
)
}
return (
<DayPicker
showOutsideDays={showOutsideDays}
locale={locale}
dir={locale === arSA ? 'rtl' : 'ltr'}
formatters={{ formatCaption, formatDay }}
disabled={...}
defaultMonth={...}
fromMonth={...}
fromYear={...}
toYear={...}
className={...}
classNames={...}
components={...}
{...props}
/>
)
}
إضافة تاريخي البدء والانتهاء
هذه الطريقة مفيدة بالنسبة لي للحصول على الprops التي يمكنني تمريرها لتعيين التاريخ في نطاق يبدأ على سبيل المثال في 1950 إلى 2030 أو أنشاء قيم افتراضية تحدد تاريخ البدء إلى التاريخ الحالي وتاريخ الانتهاء إلى 50 سنة قادمة كمثال.
المتغيرات الافتراضية
هناك قائمة بالمتغيرات الافتراضية التي يمكننا تعيينها لاستخدامها عندما لا يتم توفير خيارات بواسطة الprops.
month- سيتم استخدامها لإظهار الشهر المحدد الحالي.setMonth- تغيير الشهر الحالي.year- سيتم استخدام هذا لإظهار السنة الحالية المحددة.setYear- تغيير السنة الحالية.isHajri- استبدل نظام الترقيم الافتراضي للهجري.setIsHajri- لضبط نظام الترقيم الافتراضي.defaultMonth- قم بتعيين الشهر الافتراضي من الprops أو اختر الشهر الحالي من السنة.
للتحكم في تاريخ التقويم يدويا ، يمكننا استخدام الmonth prop ، من أجل تعيين تاريخ التقويم و onMonthChange لتغيير تاريخ العرض الحالي لمنتقي التاريخ.
TSX...
import {useState} from 'react'
export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
start?: Date
end?: Date
hajri?: boolean
}
function Calendar({
className,
classNames,
locale,
showOutsideDays = true,
start,
end,
hajri,
...props
}: CalendarProps) {
const startDate = start ?? addDays(new Date(), -1)
const endDate = end ?? addYears(startDate, 50)
const [date, setDate] = useState(startDate)
const [month, setMonth] = useState(startDate.getMonth() + 1)
const [year, setYear] = useState(startDate.getFullYear())
const [isHajri, setIsHajri] = useState(hajri ?? false)
const defaultMonth =
props.defaultMonth ??
new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
return (
<DayPicker
month={date}
onMonthChange={setDate}
{...}
{...props}
/>
)
}تعطيل التواريخ
يمكننا استخدام disabled لتحديد النطاق الذي نريد تعطيل التواريخ فيه.
لتعطيل التورايخ بأستخدام متغيرات البداية والنهاية👇
TSX...
function Calendar({
className,
classNames,
locale,
showOutsideDays = true,
start,
end,
hajri,
...props
}: CalendarProps) {
const startDate = start ?? addDays(new Date(), -1)
const endDate = end ?? addYears(startDate, 50)
const [date, setDate] = useState(startDate)
const [month, setMonth] = useState(startDate.getMonth() + 1)
const [year, setYear] = useState(startDate.getFullYear())
const [isHajri, setIsHajri] = useState(hajri ?? false)
const defaultMonth =
props.defaultMonth ??
new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
return (
<DayPicker
month={date}
onMonthChange={setDate}
disabled={(date: Date) => startDate > date || endDate < date}
{...}
{...props}
/>
)
}الشهر الافتراضي
عند تعيين التاريخ المعطل disabled ، سيتعين علينا تعيين defaultMonth و fromMonth لإيقاف التنقل للتواريخ السابقة التي يتعذر الوصول إليها 👇
TSX...
function Calendar({
className,
classNames,
locale,
showOutsideDays = true,
start,
end,
hajri,
...props
}: CalendarProps) {
const startDate = start ?? addDays(new Date(), -1)
const endDate = end ?? addYears(startDate, 50)
const [date, setDate] = useState(startDate)
const [month, setMonth] = useState(startDate.getMonth() + 1)
const [year, setYear] = useState(startDate.getFullYear())
const [isHajri, setIsHajri] = useState(hajri ?? false)
const defaultMonth =
props.defaultMonth ??
new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
return (
<DayPicker
month={date}
onMonthChange={setDate}
disabled={(date: Date) => startDate > date || endDate < date}
defaultMonth={defaultMonth}
fromMonth={defaultMonth}
{...}
{...props}
/>
)
}تحديد اختيارات السنوات
للحد من اختيار السنوات ، يمكننا استخدام سمات fromYear و toYear للحد من الاختيار بين تاريخي البدء والانتهاء 👇
TSX...
function Calendar({
className,
classNames,
locale,
showOutsideDays = true,
start,
end,
hajri,
...props
}: CalendarProps) {
const startDate = start ?? addDays(new Date(), -1)
const endDate = end ?? addYears(startDate, 50)
const [date, setDate] = useState(startDate)
const [month, setMonth] = useState(startDate.getMonth() + 1)
const [year, setYear] = useState(startDate.getFullYear())
const [isHajri, setIsHajri] = useState(hajri ?? false)
const defaultMonth =
props.defaultMonth ??
new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
return (
<DayPicker
month={date}
onMonthChange={setDate}
disabled={(date: Date) => startDate > date || endDate < date}
defaultMonth={defaultMonth}
fromMonth={defaultMonth}
fromYear={startDate.getFullYear()}
toYear={endDate.getFullYear()}
{...}
{...props}
/>
)
}تغيير الشهر والسنة
واحدة من أهم الميزات للتقويم التي يمكننا إضافتها هي القدرة على تغيير الأشهر أو السنوات بسلاسة وللقيام بذلك ، يقدم react-day-picker خيار dropdown عن طريق إضافة captionLayout="dropdown" لعرض خيارات القائمة المنسدلة للتنقل بين الأشهر بدلا من استخدام الأسهم.
تعد إضافة القائمة المنسدلة بهذه الطريقة جيدة وتعمل بشكل جيد ولكن يمكننا تحسينها من خلال إنشاء قائمة منسدلة مخصصة باستخدام مكون select الذي يوفره shadcn/ui.
ستكون جميع التغييرات ضمن وظيفة formatCaption.
تثبيت مكون select
لإضافة shadcn/ui Select 👇
POWERSHELL# NPM
npx shadcn-ui@latest add select
# PNPM
pnpm dlx shadcn-ui@latest add selectوظائف مساعدة
TSX
// تغيير الشهر المختار
function handleMonth(month: number) {
setMonth(month)
setDate(() => new Date(`${year.toString()}-${month}`))
}
// تغيير السنة المختارة
function handleYear(year: number) {
setYear(year)
if (
startDate.getFullYear() === year &&
month < defaultMonth.getMonth() + 1
) {
setMonth(() => defaultMonth.getMonth() + 1)
setDate(
() => new Date(`${year.toString()}-${defaultMonth.getMonth() + 1}`)
)
} else setDate(() => new Date(`${year.toString()}-${month}`))
}
// للحصول على التنسيق الصحيح للتقويم
function getCalendar(reverse?: boolean) {
if (reverse) return !isHajri ? ISLAMIC_CALENDAR : NU_LOCALE
return isHajri ? ISLAMIC_CALENDAR : NU_LOCALE
}
// للحصول على قائمة بالسنوات المتاحة في التقويم
function getYears() {
return Array.from(
{ length: endDate.getFullYear() - startDate.getFullYear() + 1 },
(_, i) => startDate.getFullYear() + i
)
}
// للحصول على قائمة أرقام للعدد 12 تمثل عدد الأشهر في السنة
function getMonths() {
return Array.from({ length: 12 }, (_, i) => i + 1)
}اختر السنة والشهر
لنتمكن من تحديد الأشهر ، نحتاج إلى تقسيم خانة المعلومات التوضيحية الذي يعرض الشهر الحالي والعام الحالي إلى متغيرين مختلفين:
dateMainYear- الحصول على السنة الحالية (ميلادي أو هاجري).dateMainMonth- احصل على الشهر الحالي (ميلادي أو هاجري).
نحتاج أيضا إلى تحويل خيار التاريخ الثانوي المرئي:
dateSecondary- إظهار التاريخ المعاكس.dateNextSecondary- إظهار الشهر التالي من التقويم المعاكس.dateNextMainMonth- سيتم استخدامه إذا تم اختيار التقويم الهجري لعرضdateMainMonthوdateNextMinMonthبسبب ان هيكلة التقويم مبنية على التقويم الميلادي ولا يمكننا تحويل ليعرض بناء على التاريخ الهجري
TSX
const formatCaption: DateFormatter = (date: Date) => {
const dateMainYear = date.toLocaleDateString(getCalendar(), {
year: 'numeric',
})
const dateMainMonth = date.toLocaleDateString(getCalendar(), {
month: 'long',
})
const dateSecondaryOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
}
const dateSecondary = date.toLocaleDateString(
getCalendar(true),
dateSecondaryOptions
)
const nextMonth = addMonths(date, 1)
const dateNextMainMonth = nextMonth.toLocaleDateString(getCalendar(), {
month: 'long',
})
const dateNextSecondary = nextMonth.toLocaleDateString(
getCalendar(true),
dateSecondaryOptions
)
return (
<div className='flex flex-col gap-1'>
<div className='flex justify-between gap-5'>
<Select
dir={locale === arSA ? 'rtl' : 'ltr'}
onValueChange={(val: string) => handleMonth(Number(val))}
value={month.toString()}
>
<SelectTrigger className='h-full gap-2 border-none p-0 rtl:text-base'>
<p>
{!isHajri
? dateMainMonth
: `${dateMainMonth} - ${dateNextMainMonth}`}
</p>
</SelectTrigger>
<SelectContent>
{getMonths().map((currMonth) => (
<SelectItem
key={currMonth}
value={currMonth.toString()}
disabled={
isBefore(new Date(`${year}-${currMonth + 1}`), startDate) ||
isAfter(new Date(`${year}-${currMonth}`), endDate)
}
>
{new Date(`${year}-${currMonth}`).toLocaleDateString(
getCalendar(),
{ month: 'long' }
)}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
dir={locale === arSA ? 'rtl' : 'ltr'}
onValueChange={(val: string) => handleYear(Number(val))}
value={year.toString()}
>
<SelectTrigger className='h-full gap-2 border-none p-0 rtl:text-base'>
<p>{dateMainYear}</p>
</SelectTrigger>
<SelectContent className='max-h-72 overflow-y-auto'>
{getYears().map((year) => (
<SelectItem key={year} value={year.toString()}>
{new Date(year.toString()).toLocaleDateString(getCalendar(), {
year: 'numeric',
})}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<span className='block text-[0.7rem] leading-4 text-orange-400'>
{isHajri ? dateSecondary : `${dateSecondary} - ${dateNextSecondary}`}
</span>
</div>
)
}كيفية الاستخدام
يعد استخدام مكون Calendar أمرا سهلا للغاية ويمكننا تجاوز الإعدادات الافتراضية لمطابقة حالة الاستخدام الخاصة بنا.
تعديل التقويم
يدعم DayPicker 3 أوضاع تحديد اساسية لعرض الأيام كما هو محدد. قم بتمكين وضع التحديد عن طريق تعيين خاصية mode.
- الوضع الفردي
mode="single": يمكن اختيار يوم واحد فقط - وضع متعدد
mode="multiple": السماح بتحديد أيام متعددة - وضع النطاق
mode="range"السماح باختيار نطاق الأيام
الوضع الفردي
TSXimport {useState} from 'react'
export default function App(){
const [date, setDate] = useState<Date>()
return (
<Calendar
locale={enUS} // Enable (LTR) && Euoropean Numbers
mode='single' // Enable Single mode
selected={date}
onSelect={setDate}
/>
)
}الوضع المتعدد
TSXimport {useState} from 'react'
export default function App(){
const [multiple, setMultiple] = useState<Date[] | undefined>([]);
return (
<Calendar
locale={enUS} // Enable (LTR) && Euoropean Numbers
mode='multiple' // Enable Multiple mode
selected={multiple}
onSelect={setMultiple}
/>
)
}وضع النطاق
TSXimport {useState} from 'react'
import { type DateRange } from 'react-day-picker'
export default function App(){
const [range, setRange] = useState<DateRange | undefined>(defaultSelected)
return (
<Calendar
locale={enUS} // Enable (LTR) && Euoropean Numbers
mode='range' // Enable Single mode
selected={range}
onSelect={setRange}
/>
)
}خيارات أخرى
هناك الكثير من الخيارات التي يوفرها React DayPicker لتغيير كيفية عمل مكون Calendar ، يمكنك القاء نظرة على الوثائق لمعرفة المزيد حول جميع الخيارات المختلفة.
