// Interactive date field: quick chips (Today / Tomorrow / Next week) plus an // expandable month calendar. Pure RN views — identical on Android, iOS, web. import { useMemo, useState } from "react"; import { Pressable, StyleSheet, Text, View } from "react-native"; import { haptic } from "../lib/haptics"; import { C } from "../lib/theme"; const DAY_MS = 86_400_000; const WEEKDAYS = ["M", "T", "W", "T", "F", "S", "S"]; const MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; function iso(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function fromIso(s: string): Date { const [y, m, d] = s.split("-").map(Number); return new Date(y, m - 1, d); } function pretty(s: string): string { const d = fromIso(s); const today = new Date(); const diff = Math.round((d.getTime() - new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime()) / DAY_MS); if (diff === 0) return "Today"; if (diff === 1) return "Tomorrow"; if (diff === -1) return "Yesterday"; const wd = d.toLocaleDateString("en-AU", { weekday: "short" }); return `${wd} ${d.getDate()} ${MONTHS[d.getMonth()].slice(0, 3)}${d.getFullYear() !== today.getFullYear() ? ` ${d.getFullYear()}` : ""}`; } function Calendar({ value, onPick }: { value: string | null; onPick: (v: string) => void }) { const initial = value ? fromIso(value) : new Date(); const [year, setYear] = useState(initial.getFullYear()); const [month, setMonth] = useState(initial.getMonth()); const todayIso = iso(new Date()); const weeks = useMemo(() => { const first = new Date(year, month, 1); const start = new Date(first); start.setDate(1 - ((first.getDay() + 6) % 7)); // back to Monday return Array.from({ length: 6 }, (_, w) => Array.from({ length: 7 }, (_, i) => { const d = new Date(start); d.setDate(start.getDate() + w * 7 + i); return d; }), ); }, [year, month]); const shift = (delta: number) => { haptic.select(); const d = new Date(year, month + delta, 1); setYear(d.getFullYear()); setMonth(d.getMonth()); }; return ( shift(-1)} hitSlop={10} style={s.arrow}> ‹ {MONTHS[month]} {year} shift(1)} hitSlop={10} style={s.arrow}> › {WEEKDAYS.map((w, i) => ( {w} ))} {weeks.map((week, wi) => ( {week.map((d) => { const dIso = iso(d); const inMonth = d.getMonth() === month; const selected = dIso === value; const isToday = dIso === todayIso; return ( { haptic.select(); onPick(dIso); }} > {d.getDate()} ); })} ))} ); } export default function DateField({ label, value, onChange, clearable, }: { label: string; value: string | null; onChange: (v: string | null) => void; clearable?: boolean; }) { const [open, setOpen] = useState(false); const today = new Date(); const quick = [ { label: "Today", v: iso(today) }, { label: "Tomorrow", v: iso(new Date(today.getTime() + DAY_MS)) }, { label: "Next week", v: iso(new Date(today.getTime() + 7 * DAY_MS)) }, ]; return ( {label} {value != null && {pretty(value)}} {quick.map((q) => ( { haptic.select(); onChange(value === q.v && clearable ? null : q.v); }} > {q.label} ))} { haptic.select(); setOpen(!open); }} > 📆 Pick… {clearable && value != null && ( { haptic.tap(); onChange(null); }} > Clear )} {open && ( { onChange(v); setOpen(false); }} /> )} ); } const s = StyleSheet.create({ labelRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 }, label: { color: C.muted, fontSize: 13, textTransform: "uppercase", letterSpacing: 0.5 }, chosen: { color: C.accent, fontSize: 14, fontWeight: "600" }, rowWrap: { flexDirection: "row", flexWrap: "wrap", gap: 8 }, chip: { paddingVertical: 8, paddingHorizontal: 14, borderRadius: 20, backgroundColor: C.surface, borderWidth: 1, borderColor: C.border, }, chipActive: { backgroundColor: C.surface2, borderColor: C.accent }, chipText: { color: C.muted, fontSize: 14 }, calendar: { marginTop: 10, backgroundColor: C.surface, borderRadius: 14, borderWidth: 1, borderColor: C.border, padding: 10, }, calHead: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }, calTitle: { color: C.text, fontSize: 15, fontWeight: "600" }, arrow: { paddingHorizontal: 14, paddingVertical: 4 }, arrowText: { color: C.accent, fontSize: 22, fontWeight: "600" }, week: { flexDirection: "row" }, weekday: { flex: 1, textAlign: "center", color: C.muted, fontSize: 11, paddingVertical: 4 }, day: { flex: 1, aspectRatio: 1.15, alignItems: "center", justifyContent: "center", borderRadius: 999, margin: 1, }, daySelected: { backgroundColor: C.accent }, dayToday: { borderWidth: 1, borderColor: C.accent }, dayText: { color: C.text, fontSize: 14 }, });