import React, { useState, useMemo, useCallback, useRef } from 'react'; import { View, Text, Pressable, Modal, StyleSheet, Dimensions, ScrollView, PanResponder, } from 'react-native'; import Ionicons from '@react-native-vector-icons/ionicons'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { useTheme, fontFamily } from '../../theme'; const SCREEN_WIDTH = Dimensions.get('window').width; const DAY_SIZE = Math.floor((SCREEN_WIDTH - 80) / 7); const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; const MONTHS_SHORT = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; // Generate year range: 1950 → current year + 10 const CURRENT_YEAR = new Date().getFullYear(); const YEAR_RANGE_START = 1950; const YEAR_RANGE_END = CURRENT_YEAR + 10; const YEARS = Array.from({ length: YEAR_RANGE_END - YEAR_RANGE_START + 1 }, (_, i) => YEAR_RANGE_START + i); interface DatePickerProps { label?: string; placeholder?: string; value?: string; // DD/MM/YYYY onDateChange: (dateStr: string) => void; error?: string; /** Max selectable date. Defaults to today */ maxDate?: Date; /** Min selectable date */ minDate?: Date; } type PickerMode = 'days' | 'months' | 'years'; const parseDDMMYYYY = (str: string | undefined): Date | null => { if (!str) return null; const parts = str.split('/'); if (parts.length !== 3) return null; const part0 = parts[0]; const part1 = parts[1]; const part2 = parts[2]; if (!part0 || !part1 || !part2) return null; const d = parseInt(part0, 10); const m = parseInt(part1, 10) - 1; const y = parseInt(part2, 10); if (isNaN(d) || isNaN(m) || isNaN(y)) return null; return new Date(y, m, d); }; const formatDDMMYYYY = (date: Date): string => { const d = String(date.getDate()).padStart(2, '0'); const m = String(date.getMonth() + 1).padStart(2, '0'); const y = date.getFullYear(); return `${d}/${m}/${y}`; }; const isSameDay = (a: Date, b: Date) => a.getDate() === b.getDate() && a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear(); export const DatePicker = ({ label, placeholder = 'DD/MM/YYYY', value, onDateChange, error, maxDate, minDate, }: DatePickerProps) => { const theme = useTheme(); const today = useMemo(() => new Date(), []); const selectedDate = useMemo(() => parseDDMMYYYY(value), [value]); const [visible, setVisible] = useState(false); const [viewMonth, setViewMonth] = useState(selectedDate?.getMonth() ?? today.getMonth()); const [viewYear, setViewYear] = useState(selectedDate?.getFullYear() ?? today.getFullYear()); const [pickerMode, setPickerMode] = useState('days'); const yearScrollRef = useRef(null); const openCalendar = useCallback(() => { const ref = selectedDate ?? today; setViewMonth(ref.getMonth()); setViewYear(ref.getFullYear()); setPickerMode('days'); setVisible(true); }, [selectedDate, today]); // ── Month navigation ────────────────────────────────────── const prevMonth = useCallback(() => { if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1); } else { setViewMonth(m => m - 1); } }, [viewMonth]); const nextMonth = useCallback(() => { if (viewMonth === 11) { setViewMonth(0); setViewYear(y => y + 1); } else { setViewMonth(m => m + 1); } }, [viewMonth]); // ── Swipe handler (PanResponder) ────────────────────────── const swipeThreshold = 50; const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => false, onMoveShouldSetPanResponder: (_, gestureState) => { // Only capture horizontal swipes return Math.abs(gestureState.dx) > 15 && Math.abs(gestureState.dy) < 30; }, onPanResponderRelease: (_, gestureState) => { if (gestureState.dx > swipeThreshold) { // Swipe right → previous month (functional updates avoid stale closure) setViewMonth(m => { if (m === 0) { setViewYear(y => y - 1); return 11; } return m - 1; }); } else if (gestureState.dx < -swipeThreshold) { // Swipe left → next month setViewMonth(m => { if (m === 11) { setViewYear(y => y + 1); return 0; } return m + 1; }); } }, }) ).current; // ── Day calculations ────────────────────────────────────── const daysInMonth = useMemo(() => new Date(viewYear, viewMonth + 1, 0).getDate(), [viewYear, viewMonth]); const firstDayOfWeek = useMemo(() => new Date(viewYear, viewMonth, 1).getDay(), [viewYear, viewMonth]); const calendarDays = useMemo(() => { const days: (number | null)[] = []; for (let i = 0; i < firstDayOfWeek; i++) days.push(null); for (let d = 1; d <= daysInMonth; d++) days.push(d); return days; }, [daysInMonth, firstDayOfWeek]); const isDayDisabled = useCallback( (day: number) => { const d = new Date(viewYear, viewMonth, day); if (maxDate && d > maxDate) return true; if (minDate && d < minDate) return true; return false; }, [viewYear, viewMonth, maxDate, minDate], ); const handleSelect = (day: number) => { const d = new Date(viewYear, viewMonth, day); onDateChange(formatDDMMYYYY(d)); setVisible(false); }; // ── Month picker handler ────────────────────────────────── const handleMonthSelect = (monthIndex: number) => { setViewMonth(monthIndex); setPickerMode('days'); }; // ── Year picker handler ─────────────────────────────────── const handleYearSelect = (year: number) => { setViewYear(year); setPickerMode('days'); }; // Auto-scroll to current year when year picker opens const handleOpenYearPicker = useCallback(() => { setPickerMode('years'); // Scroll to the current viewYear position after render setTimeout(() => { const yearIndex = YEARS.indexOf(viewYear); if (yearIndex >= 0 && yearScrollRef.current) { yearScrollRef.current.scrollTo({ y: Math.max(0, (yearIndex - 2) * 48), animated: false }); } }, 100); }, [viewYear]); // ── Render: Days grid ───────────────────────────────────── const renderDaysView = () => ( <> {/* Header: tappable month & year */} setPickerMode('months')} style={styles.headerTappable}> {MONTHS[viewMonth]} {viewYear} {/* Weekday headers */} {WEEKDAYS.map(wd => ( {wd} ))} {/* Days grid with swipe */} {calendarDays.map((day, idx) => { if (day === null) { return ; } const dateObj = new Date(viewYear, viewMonth, day); const isToday = isSameDay(dateObj, today); const isSelected = selectedDate ? isSameDay(dateObj, selectedDate) : false; const disabled = isDayDisabled(day); return ( handleSelect(day)} style={[ styles.dayCell, { width: DAY_SIZE, height: DAY_SIZE }, isSelected && { backgroundColor: theme.colors.primary, borderRadius: DAY_SIZE / 2, }, isToday && !isSelected && { borderWidth: 1.5, borderColor: theme.colors.primary, borderRadius: DAY_SIZE / 2, }, ]} > {day} ); })} {/* Footer */} setVisible(false)} style={[styles.footerBtn, { borderColor: theme.colors.cardBorder }]}> Cancel { setViewMonth(today.getMonth()); setViewYear(today.getFullYear()); handleSelect(today.getDate()); }} style={[styles.footerBtn, styles.todayBtn, { backgroundColor: `${theme.colors.primary}12` }]} > Today ); // ── Render: Month picker grid ───────────────────────────── const renderMonthsView = () => ( <> setPickerMode('days')} hitSlop={12}> Select Month {MONTHS_SHORT.map((m, idx) => { const isActive = idx === viewMonth; return ( handleMonthSelect(idx)} style={[ styles.monthCell, { backgroundColor: isActive ? theme.colors.primary : `${theme.colors.primary}08`, borderColor: isActive ? theme.colors.primary : theme.colors.cardBorder, }, ]} > {m} ); })} ); // ── Render: Year picker list ────────────────────────────── const renderYearsView = () => ( <> setPickerMode('days')} hitSlop={12}> Select Year {YEARS.map(year => { const isActive = year === viewYear; return ( handleYearSelect(year)} style={[ styles.yearRow, { backgroundColor: isActive ? `${theme.colors.primary}12` : 'transparent', borderColor: isActive ? theme.colors.primary : 'transparent', }, ]} > {year} {isActive && ( )} ); })} ); return ( {label && ( {label.toUpperCase()} )} {/* Tappable field */} {value || placeholder} {error && {error}} {/* Calendar Modal */} setVisible(false)}> setVisible(false)}> {}} // prevent dismiss when tapping inside > {pickerMode === 'days' && renderDaysView()} {pickerMode === 'months' && renderMonthsView()} {pickerMode === 'years' && renderYearsView()} ); }; const styles = StyleSheet.create({ container: { gap: 6, }, label: { fontSize: 11, fontFamily: fontFamily.bold, letterSpacing: 0.5, marginLeft: 4, }, inputWrapper: { flexDirection: 'row', alignItems: 'center', height: 52, borderRadius: 12, borderWidth: 1, paddingHorizontal: 16, }, inputText: { flex: 1, fontSize: 15, fontFamily: fontFamily.medium, }, errorText: { fontSize: 12, fontFamily: fontFamily.medium, marginTop: 2, marginLeft: 4, }, overlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, }, calendarCard: { width: SCREEN_WIDTH - 40, borderRadius: 20, borderWidth: 1, padding: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.15, shadowRadius: 20, elevation: 10, }, // ── Header ─────────────────────────────────── calendarHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, navBtn: { width: 36, height: 36, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, monthYearWrap: { alignItems: 'center', }, headerTappable: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 3, }, monthText: { fontSize: 18, fontFamily: fontFamily.bold, }, yearText: { fontSize: 13, fontFamily: fontFamily.semibold, marginTop: 2, }, // ── Days grid ──────────────────────────────── weekdayRow: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 4, }, weekdayText: { fontSize: 12, fontFamily: fontFamily.bold, letterSpacing: 0.5, }, daysGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'flex-start', }, dayCell: { alignItems: 'center', justifyContent: 'center', }, dayText: { fontSize: 14, fontFamily: fontFamily.medium, }, // ── Footer ─────────────────────────────────── calendarFooter: { flexDirection: 'row', justifyContent: 'flex-end', gap: 12, marginTop: 16, paddingTop: 12, borderTopWidth: 1, }, footerBtn: { paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10, borderWidth: 1, borderColor: 'transparent', }, todayBtn: { borderWidth: 0, }, footerBtnText: { fontSize: 14, fontFamily: fontFamily.semibold, }, // ── Month / Year picker shared ─────────────── pickerHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, }, pickerTitle: { fontSize: 17, fontFamily: fontFamily.bold, }, // ── Month picker grid ──────────────────────── monthGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, justifyContent: 'center', }, monthCell: { width: (SCREEN_WIDTH - 100) / 3, paddingVertical: 14, borderRadius: 12, borderWidth: 1, alignItems: 'center', justifyContent: 'center', }, monthCellText: { fontSize: 15, fontFamily: fontFamily.bold, }, // ── Year picker list ───────────────────────── yearList: { maxHeight: 300, }, yearListContent: { paddingBottom: 8, }, yearRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 20, borderRadius: 10, borderWidth: 1.5, marginBottom: 4, }, yearRowText: { fontSize: 16, }, });