react-native-taqweem
Version:
**A dual calendar component (Hijri + Gregorian) for React Native** — minimal, customizable, and theme-ready. Perfect for apps needing culturally-aware calendars, Islamic date pickers, or just modern UX flexibility.
314 lines (311 loc) • 10 kB
JavaScript
"use strict";
import { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import moment from 'moment-hijri';
import { useCalendarDays, useCalendarInfo, useWeekdays } from "../hooks.js";
import { getCalendarTitle, getCalendarTitleFormat, getFullDateFormat, getNextMonth, getPreviousMonth, getYearMonthFormat } from "../utils.js";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
export function DualCalendar({
initialSelectedDate,
currentYearMonth,
minDate,
maxDate,
renderHeader,
calendarTheme = {},
showAdjacentMonths = true,
calendarMode = 'gregorian',
allowFutureDates = true,
onDateChange = () => {}
}) {
const [isHijri, setIsHijri] = useState(calendarMode === 'hijri');
const fullDateFormat = getFullDateFormat(isHijri);
const yearMonthFormat = getYearMonthFormat(isHijri);
const titleFormat = getCalendarTitleFormat(isHijri);
const defaultMinDate = isHijri ? '1400-1' : '1900-1';
const defaultMaxDate = isHijri ? moment().add(10, 'iYear').format('iYYYY-iM') : moment().add(10, 'year').format('YYYY-M');
const effectiveMinDate = moment(minDate || defaultMinDate, yearMonthFormat);
const effectiveMaxDate = moment(maxDate || defaultMaxDate, yearMonthFormat);
const todaysDate = moment().format(fullDateFormat);
const initialYearMonth = currentYearMonth ? moment(currentYearMonth, yearMonthFormat) : initialSelectedDate ? moment(initialSelectedDate, fullDateFormat) : moment();
const [currentDate, setCurrentDate] = useState(initialYearMonth);
const [selectedDateStr, setSelectedDateStr] = useState(initialSelectedDate ?? null);
const _previousMonth = getPreviousMonth(isHijri, currentDate);
const _nextMonth = getNextMonth(isHijri, currentDate);
const canGoPreviousMonth = !_previousMonth.isBefore(effectiveMinDate, 'month');
const canGoNextMonth = !_nextMonth.isAfter(effectiveMaxDate, 'month') && (allowFutureDates || !_nextMonth.isAfter(moment(), 'month'));
useEffect(() => {
setIsHijri(calendarMode === 'hijri');
}, [calendarMode]);
useEffect(() => {
if (!currentYearMonth) {
return;
}
const parsedKey = moment(currentYearMonth, yearMonthFormat);
const min = effectiveMinDate;
const max = effectiveMaxDate;
const clamped = parsedKey.isBefore(min) ? min : parsedKey.isAfter(max) ? max : parsedKey;
setCurrentDate(clamped);
}, [currentYearMonth, yearMonthFormat, effectiveMaxDate, effectiveMinDate]);
const {
year,
month,
daysInMonth
} = useCalendarInfo(isHijri, currentDate);
const days = useCalendarDays(isHijri, year, month, daysInMonth, showAdjacentMonths);
const handleSelectDay = dayObj => {
const selectedKey = dayObj.date.format(fullDateFormat);
const gregorianDate = dayObj.date.format('YYYY-MM-DD');
const hijriDate = dayObj.date.format('iYYYY-iMM-iDD');
setSelectedDateStr(prev => {
const isToggling = prev === selectedKey;
onDateChange(isToggling ? {
calendarDate: '',
gregorianDate: '',
hijriDate: '',
momentObj: null
} : {
calendarDate: selectedKey,
gregorianDate,
hijriDate,
momentObj: dayObj.date
});
return isToggling ? null : selectedKey;
});
if (!dayObj.isCurrentMonth) {
setCurrentDate(dayObj.date.clone());
}
};
const goToPrevMonth = () => {
let prev = getPreviousMonth(isHijri, currentDate);
if (prev.isBefore(effectiveMinDate, 'month')) {
return;
}
setCurrentDate(prev);
};
const goToNextMonth = () => {
let next = getNextMonth(isHijri, currentDate);
// 🔒 Block navigation if allowFutureDates is false
if (!allowFutureDates && next.isAfter(moment(), 'month')) {
return;
}
// 🔒 Still respect maxDate limit
if (next.isAfter(effectiveMaxDate, 'month')) {
return;
}
setCurrentDate(next);
};
const resetSelection = () => {
onDateChange({
calendarDate: '',
gregorianDate: '',
hijriDate: '',
momentObj: null
});
setSelectedDateStr(null);
};
const toggleCalendarMode = () => {
setIsHijri(prev => {
resetSelection();
if (prev === true) {
return false;
}
return true;
});
};
const monthYearTitle = getCalendarTitle(currentDate, titleFormat);
const headerProps = {
calendarTitle: monthYearTitle,
canGoNextMonth,
canGoPreviousMonth,
goToNextMonth,
goToPrevMonth,
toggleCalendarMode,
isHijri,
dateObj: currentDate
};
const dayGridProps = {
days,
allowFutureDates,
fullDateFormat,
handleSelectDay,
todaysDate,
selectedDateStr
};
return /*#__PURE__*/_jsxs(View, {
style: [styles.calendarView, calendarTheme.calendarView],
children: [renderHeader ? renderHeader(headerProps) : /*#__PURE__*/_jsx(CalendarHeader, {
...headerProps,
headerTheme: {
header: calendarTheme.header,
headerTextStyle: calendarTheme.headerTextStyle
}
}), /*#__PURE__*/_jsx(MonthView, {
...dayGridProps,
dayGridTheme: {
calendarGrid: calendarTheme.calendarGrid,
dayCell: calendarTheme.dayCell,
dayNameCell: calendarTheme.dayNameCell,
dayTextStyle: calendarTheme.dayTextStyle,
nonCurrentMonthTextStyle: calendarTheme.nonCurrentMonthTextStyle,
selectedDayStyle: calendarTheme.selectedDayStyle,
todayCellStyle: calendarTheme.todayCellStyle,
todayTextStyle: calendarTheme.todayTextStyle,
selectedDayTextStyle: calendarTheme.selectedDayTextStyle
}
})]
});
}
function CalendarHeader({
calendarTitle = '',
canGoPreviousMonth = false,
canGoNextMonth = false,
headerTheme = {},
goToNextMonth = () => {},
goToPrevMonth = () => {}
}) {
return /*#__PURE__*/_jsxs(View, {
style: [styles.header, headerTheme.header],
children: [canGoPreviousMonth && /*#__PURE__*/_jsx(TouchableOpacity, {
style: styles.arrow,
onPress: goToPrevMonth,
children: /*#__PURE__*/_jsx(Text, {
style: [styles.nav, headerTheme.headerTextStyle],
children: "\u2039"
})
}), /*#__PURE__*/_jsx(View, {
style: [styles.titleWrapper],
children: /*#__PURE__*/_jsx(Text, {
style: [styles.monthTitleStyle, headerTheme.headerTextStyle],
children: calendarTitle
})
}), canGoNextMonth && /*#__PURE__*/_jsx(TouchableOpacity, {
style: styles.arrow,
onPress: goToNextMonth,
children: /*#__PURE__*/_jsx(Text, {
style: [styles.nav, headerTheme.headerTextStyle],
children: "\u203A"
})
})]
});
}
function WeekDayNames({
weekDayTheme = {}
}) {
const weekDayNames = useWeekdays();
return weekDayNames.map((dayName, i) => /*#__PURE__*/_jsx(Text, {
style: [styles.dayNameCell, weekDayTheme.dayNameCell],
children: dayName
}, i));
}
function MonthView(props) {
return /*#__PURE__*/_jsxs(View, {
style: [styles.calendarGrid, props.dayGridTheme?.calendarGrid],
children: [/*#__PURE__*/_jsx(WeekDayNames, {
weekDayTheme: {
dayNameCell: props.dayGridTheme?.dayNameCell
}
}), /*#__PURE__*/_jsx(DayGrid, {
...props
})]
});
}
function DayGrid({
allowFutureDates,
fullDateFormat,
selectedDateStr,
todaysDate,
dayGridTheme = {},
days = [],
handleSelectDay = () => {}
}) {
return /*#__PURE__*/_jsx(_Fragment, {
children: days.map((dayObj, idx) => {
if (!dayObj) {
return /*#__PURE__*/_jsx(View, {
style: [styles.dayCell, dayGridTheme.dayCell]
}, idx);
}
if (!allowFutureDates && dayObj.date.isAfter(moment(), 'day')) {
return /*#__PURE__*/_jsx(View, {
style: [styles.dayCell, dayGridTheme.dayCell],
children: /*#__PURE__*/_jsx(Text, {
style: [styles.nonCurrentMonthTextStyle, dayGridTheme.nonCurrentMonthTextStyle],
children: dayObj.label
})
}, idx);
}
const key = dayObj.date.format(fullDateFormat);
const isSelected = key === selectedDateStr;
const isToday = dayObj.date.format(fullDateFormat) === todaysDate;
return /*#__PURE__*/_jsx(TouchableOpacity, {
style: [styles.dayCell, dayGridTheme.dayCell, isToday && styles.todayCellStyle, isToday && dayGridTheme.todayCellStyle, isSelected && styles.selectedDayStyle, isSelected && dayGridTheme.selectedDayStyle],
onPress: () => handleSelectDay(dayObj),
children: /*#__PURE__*/_jsx(Text, {
style: [styles.dayTextStyle, dayGridTheme.dayTextStyle, isToday && dayGridTheme.todayTextStyle, isSelected && dayGridTheme.selectedDayTextStyle, !dayObj.isCurrentMonth && styles.nonCurrentMonthTextStyle, !dayObj.isCurrentMonth && dayGridTheme.nonCurrentMonthTextStyle],
children: dayObj.label
})
}, idx);
})
});
}
const styles = StyleSheet.create({
calendarView: {
padding: 16,
backgroundColor: '#f8f9fa'
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12
},
arrow: {
width: 40,
alignItems: 'center'
},
titleWrapper: {
flex: 1,
alignItems: 'center'
},
nav: {
fontSize: 24
},
monthTitleStyle: {
fontSize: 18,
fontWeight: 'bold'
},
calendarGrid: {
flexDirection: 'row',
flexWrap: 'wrap'
},
dayNameCell: {
width: '14.28%',
textAlign: 'center',
textAlignVertical: 'center',
height: 35,
fontWeight: 'bold'
},
dayCell: {
width: '14.28%',
height: 50,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 50
},
selectedDayStyle: {
backgroundColor: '#f7ede2',
borderWidth: 1
},
todayCellStyle: {
borderWidth: 1,
borderColor: '#415a77',
borderStyle: 'dashed'
},
dayTextStyle: {
color: '#000'
},
nonCurrentMonthTextStyle: {
color: '#aaa'
}
});
//# sourceMappingURL=index.js.map