feat: add calendar date picker, nav icons, and input-focused keyboard scrolling
This commit is contained in:
93
src/components/DatePickerModal.js
Normal file
93
src/components/DatePickerModal.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Modal, Pressable, Text, View } from 'react-native';
|
||||
import { styles } from '../styles';
|
||||
import { todayYMD } from '../utils/date';
|
||||
|
||||
const WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
||||
|
||||
function toYMD(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = `${date.getMonth() + 1}`.padStart(2, '0');
|
||||
const d = `${date.getDate()}`.padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function parseFromYMD(value) {
|
||||
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return new Date();
|
||||
const date = new Date(`${value}T00:00:00`);
|
||||
return Number.isNaN(date.getTime()) ? new Date() : date;
|
||||
}
|
||||
|
||||
function monthLabel(date) {
|
||||
return date.toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
function buildMonthGrid(viewDate) {
|
||||
const first = new Date(viewDate.getFullYear(), viewDate.getMonth(), 1);
|
||||
const startWeekday = (first.getDay() + 6) % 7;
|
||||
const daysInMonth = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 0).getDate();
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < startWeekday; i += 1) cells.push(null);
|
||||
for (let day = 1; day <= daysInMonth; day += 1) {
|
||||
cells.push(new Date(viewDate.getFullYear(), viewDate.getMonth(), day));
|
||||
}
|
||||
|
||||
while (cells.length % 7 !== 0) cells.push(null);
|
||||
return cells;
|
||||
}
|
||||
|
||||
export default function DatePickerModal({ visible, value, onClose, onSelect, title = 'Pick date' }) {
|
||||
const [viewDate, setViewDate] = useState(parseFromYMD(value || todayYMD()));
|
||||
|
||||
const grid = useMemo(() => buildMonthGrid(viewDate), [viewDate]);
|
||||
const selected = value || todayYMD();
|
||||
|
||||
const goMonth = (diff) => {
|
||||
setViewDate((prev) => new Date(prev.getFullYear(), prev.getMonth() + diff, 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
||||
<View style={styles.dateModalBackdrop}>
|
||||
<View style={styles.dateModalCard}>
|
||||
<View style={styles.sectionRow}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<Pressable onPress={onClose}>
|
||||
<Text style={styles.closeText}>Close</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.calendarHeader}>
|
||||
<Pressable style={styles.calendarNavBtn} onPress={() => goMonth(-1)}>
|
||||
<Text style={styles.calendarNavText}>‹</Text>
|
||||
</Pressable>
|
||||
<Text style={styles.calendarMonthText}>{monthLabel(viewDate)}</Text>
|
||||
<Pressable style={styles.calendarNavBtn} onPress={() => goMonth(1)}>
|
||||
<Text style={styles.calendarNavText}>›</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.calendarWeekRow}>
|
||||
{WEEKDAYS.map((w) => (
|
||||
<Text key={w} style={styles.calendarWeekday}>{w}</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.calendarGrid}>
|
||||
{grid.map((cell, idx) => {
|
||||
if (!cell) return <View key={`empty-${idx}`} style={styles.calendarCell} />;
|
||||
const ymd = toYMD(cell);
|
||||
const isSelected = ymd === selected;
|
||||
return (
|
||||
<Pressable key={ymd} style={[styles.calendarCell, isSelected && styles.calendarCellActive]} onPress={() => onSelect(ymd)}>
|
||||
<Text style={[styles.calendarCellText, isSelected && styles.calendarCellTextActive]}>{cell.getDate()}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user