Files
beenvoice-app/components/ui/DateTimeField.tsx
T
soconnor 6d2711e36e Polish mobile app for App Store review and expand CRUD.
Default to beenvoice.soconnor.dev with server settings hidden behind Advanced; add Entities tab with clients/businesses, invoice creation, UI fixes for dashboard layout, date fields, FAB position, and card-matched button radius.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 23:14:58 -04:00

185 lines
5.0 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import DateTimePicker, {
type DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
import { useState } from "react";
import { Modal, Platform, Pressable, StyleSheet, Text, View } from "react-native";
import { fonts, radii, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { formatDate, formatDateTime } from "@/lib/format";
type DateTimeFieldProps = {
label: string;
value: Date;
mode?: "date" | "datetime";
maximumDate?: Date;
minimumDate?: Date;
onChange: (date: Date) => void;
};
export function DateTimeField({
label,
value,
mode = "datetime",
maximumDate = new Date(),
minimumDate,
onChange,
}: DateTimeFieldProps) {
const { colors, isDark } = useAppTheme();
const [open, setOpen] = useState(false);
const [draft, setDraft] = useState(value);
function openPicker() {
setDraft(value);
setOpen(true);
}
function applyDate(next: Date) {
const clamped =
next.getTime() > maximumDate.getTime()
? maximumDate
: minimumDate && next.getTime() < minimumDate.getTime()
? minimumDate
: next;
onChange(clamped);
}
function handleChange(event: DateTimePickerEvent, selected?: Date) {
if (Platform.OS === "android") {
setOpen(false);
if (event.type === "set" && selected) {
applyDate(selected);
}
return;
}
if (selected) {
setDraft(selected);
}
}
return (
<View style={styles.wrapper}>
<Text style={[styles.label, { color: colors.mutedForeground }]}>{label}</Text>
<Pressable
accessibilityRole="button"
onPress={openPicker}
style={({ pressed }) => [
styles.trigger,
{
borderColor: colors.border,
backgroundColor: colors.cardGlass,
},
pressed && styles.triggerPressed,
]}
>
<Text style={[styles.value, { color: colors.foreground }]}>
{mode === "date" ? formatDate(value) : formatDateTime(value)}
</Text>
<Ionicons name="calendar-outline" size={18} color={colors.mutedForeground} />
</Pressable>
{Platform.OS === "ios" ? (
<Modal visible={open} transparent animationType="slide" onRequestClose={() => setOpen(false)}>
<Pressable style={styles.backdrop} onPress={() => setOpen(false)}>
<Pressable
style={[styles.sheet, { backgroundColor: colors.card }]}
onPress={(event) => event.stopPropagation()}
>
<View style={[styles.sheetHeader, { borderBottomColor: colors.border }]}>
<Pressable onPress={() => setOpen(false)}>
<Text style={[styles.sheetAction, { color: colors.mutedForeground }]}>Cancel</Text>
</Pressable>
<Text style={[styles.sheetTitle, { color: colors.foreground }]}>{label}</Text>
<Pressable
onPress={() => {
applyDate(draft);
setOpen(false);
}}
>
<Text style={[styles.sheetAction, { color: colors.primary }]}>Done</Text>
</Pressable>
</View>
<DateTimePicker
value={draft}
mode={mode}
display="spinner"
maximumDate={maximumDate}
minimumDate={minimumDate}
themeVariant={isDark ? "dark" : "light"}
onChange={handleChange}
/>
</Pressable>
</Pressable>
</Modal>
) : open ? (
<DateTimePicker
value={draft}
mode={mode}
maximumDate={maximumDate}
minimumDate={minimumDate}
onChange={handleChange}
/>
) : null}
</View>
);
}
const styles = StyleSheet.create({
wrapper: {
gap: spacing.xs,
alignSelf: "stretch",
width: "100%",
},
label: {
fontSize: 13,
fontFamily: fonts.bodyMedium,
},
trigger: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
alignSelf: "stretch",
width: "100%",
borderWidth: 1,
borderRadius: radii.lg,
paddingHorizontal: spacing.md,
minHeight: 48,
paddingVertical: spacing.sm,
},
triggerPressed: {
opacity: 0.92,
},
value: {
fontSize: 15,
fontFamily: fonts.body,
flex: 1,
},
backdrop: {
flex: 1,
justifyContent: "flex-end",
backgroundColor: "rgba(0,0,0,0.45)",
},
sheet: {
borderTopLeftRadius: radii.lg,
borderTopRightRadius: radii.lg,
paddingBottom: spacing.lg,
},
sheetHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
borderBottomWidth: 1,
},
sheetTitle: {
fontSize: 15,
fontFamily: fonts.bodySemiBold,
},
sheetAction: {
fontSize: 15,
fontFamily: fonts.bodyMedium,
},
});