diff --git a/app/(site)/docs/components/date-time-picker/page.tsx b/app/(site)/docs/components/date-time-picker/page.tsx new file mode 100644 index 0000000..f964728 --- /dev/null +++ b/app/(site)/docs/components/date-time-picker/page.tsx @@ -0,0 +1,338 @@ +import { ComponentPreview } from "@/components/docs/component-preview"; + +export default function Date-time-pickerPage() { + return ( + \n Click me\n \n );\n}", + "language": "tsx" + } +]} + componentCode={`import * as React from "react"; +import { View, Text, Pressable, Modal, Animated } from "react-native"; +import { cn } from "@/lib/utils"; +import { Calendar } from "@/components/ui/calendar"; +import { Ionicons } from "@expo/vector-icons"; +import { format } from "date-fns"; +import { enUS } from "date-fns/locale"; + +interface DateRange { + from: Date; + to: Date; +} + +interface TimeConfig { + minuteInterval?: 1 | 5 | 10 | 15 | 30; + minTime?: string; + maxTime?: string; + disabledTimes?: string[]; +} + +interface DateTimePickerProps { + mode?: "single" | "range" | "datetime"; + value?: Date | Date[] | DateRange; + onValueChange?: (value: Date | Date[] | DateRange | undefined) => void; + placeholder?: string; + disabled?: boolean; + className?: string; + showOutsideDays?: boolean; + disabledDates?: (date: Date) => boolean; + disableWeekends?: boolean; + fromDate?: Date; + toDate?: Date; + timeConfig?: TimeConfig; + firstDayOfWeek?: 0 | 1; + enableQuickMonthYear?: boolean; + variant?: "default" | "outline"; + size?: "sm" | "md" | "lg"; +} + +const isDateRange = (value: any): value is DateRange => { + return value && typeof value === "object" && "from" in value && "to" in value; +}; + +const formatDisplayValue = ( + value: Date | Date[] | DateRange | undefined, + mode: "single" | "range" | "datetime", + placeholder: string +): string => { + if (!value) return placeholder; + + switch (mode) { + case "single": + if (value instanceof Date) { + return format(value, "PPP", { locale: enUS }); + } + break; + case "datetime": + if (value instanceof Date) { + return format(value, "PPP 'at' HH:mm", { locale: enUS }); + } + break; + case "range": + if (isDateRange(value)) { + const fromFormatted = format(value.from, "PP", { locale: enUS }); + const toFormatted = format(value.to, "PP", { locale: enUS }); + return \`"" - ""\`; + } + break; + } + return placeholder; +}; + +const getInputIcon = (mode: "single" | "range" | "datetime") => { + switch (mode) { + case "datetime": + return "calendar-outline"; + case "range": + return "calendar-outline"; + default: + return "calendar-outline"; + } +}; + +const DateTimePicker = React.forwardRef( + ( + { + mode = "single", + value, + onValueChange, + placeholder = "Select date", + disabled = false, + className, + showOutsideDays = true, + disabledDates, + disableWeekends = false, + fromDate, + toDate, + timeConfig, + firstDayOfWeek = 1, + enableQuickMonthYear = false, + variant = "default", + size = "md", + ...props + }, + ref + ) => { + const [isOpen, setIsOpen] = React.useState(false); + const [isFocused, setIsFocused] = React.useState(false); + const fadeAnim = React.useRef(new Animated.Value(0)).current; + const scaleAnim = React.useRef(new Animated.Value(0.95)).current; + + const displayValue = formatDisplayValue(value, mode, placeholder); + const iconName = getInputIcon(mode); + + const sizeClasses = { + sm: "h-10 px-3 text-sm", + md: "h-12 px-3 text-base", + lg: "h-14 px-4 text-lg", + }; + + const iconSizes = { + sm: 18, + md: 20, + lg: 22, + }; + + const openPicker = React.useCallback(() => { + if (disabled) return; + setIsOpen(true); + setIsFocused(true); + + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(scaleAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + }, [disabled, fadeAnim, scaleAnim]); + + const closePicker = React.useCallback(() => { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(scaleAnim, { + toValue: 0.95, + duration: 250, + useNativeDriver: true, + }), + ]).start(() => { + setIsOpen(false); + setIsFocused(false); + }); + }, [fadeAnim, scaleAnim]); + + const handleSelect = React.useCallback( + (selectedValue: Date | Date[] | DateRange | undefined) => { + onValueChange?.(selectedValue); + if ( + mode === "single" || + (mode === "range" && + isDateRange(selectedValue) && + selectedValue.from !== selectedValue.to) + ) { + closePicker(); + } + }, + [onValueChange, mode, closePicker] + ); + + return ( + <> + + + + + + {displayValue} + + + + {/* Calendar Modal */} + + + + + + {/* Modal Header */} + + + + Cancel + + + + {mode === "range" + ? "Select dates" + : mode === "datetime" + ? "Select date & time" + : "Select date"} + + + + + Done + + + + + + {/* Calendar */} + + + + + + + + ); + } +); + +DateTimePicker.displayName = "DateTimePicker"; + +export { DateTimePicker, type DateTimePickerProps }; +`} + previewCode={`import { Date-time-picker } from "@nativeui/ui"; + +export default function Date-time-pickerDemo() { + return ( +
+ Default Date-time-picker + Delete + Outline + Secondary + Ghost + Link +
+ ); +}`} + registryName="date-time-picker" + packageName="@nativeui/ui" + /> + ); +} diff --git a/public/r/date-time-picker.json b/public/r/date-time-picker.json new file mode 100644 index 0000000..5487127 --- /dev/null +++ b/public/r/date-time-picker.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "date-time-picker", + "type": "registry:component", + "title": "Date Time Picker", + "description": "A date time picker component for React Native applications.", + "dependencies": [ + "react-native", + "date-fns", + "@expo/vector-icons" + ], + "registryDependencies": [ + "https://nativeui.io/registry/calendar.json" + ], + "files": [ + { + "path": "registry/date-time-picker/date-time-picker.tsx", + "content": "import * as React from \"react\";\nimport { View, Text, Pressable, Modal, Animated } from \"react-native\";\nimport { cn } from \"@/lib/utils\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Ionicons } from \"@expo/vector-icons\";\nimport { format } from \"date-fns\";\nimport { enUS } from \"date-fns/locale\";\n\ninterface DateRange {\n from: Date;\n to: Date;\n}\n\ninterface TimeConfig {\n minuteInterval?: 1 | 5 | 10 | 15 | 30;\n minTime?: string;\n maxTime?: string;\n disabledTimes?: string[];\n}\n\ninterface DateTimePickerProps {\n mode?: \"single\" | \"range\" | \"datetime\";\n value?: Date | Date[] | DateRange;\n onValueChange?: (value: Date | Date[] | DateRange | undefined) => void;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n showOutsideDays?: boolean;\n disabledDates?: (date: Date) => boolean;\n disableWeekends?: boolean;\n fromDate?: Date;\n toDate?: Date;\n timeConfig?: TimeConfig;\n firstDayOfWeek?: 0 | 1;\n enableQuickMonthYear?: boolean;\n variant?: \"default\" | \"outline\";\n size?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst isDateRange = (value: any): value is DateRange => {\n return value && typeof value === \"object\" && \"from\" in value && \"to\" in value;\n};\n\nconst formatDisplayValue = (\n value: Date | Date[] | DateRange | undefined,\n mode: \"single\" | \"range\" | \"datetime\",\n placeholder: string\n): string => {\n if (!value) return placeholder;\n\n switch (mode) {\n case \"single\":\n if (value instanceof Date) {\n return format(value, \"PPP\", { locale: enUS });\n }\n break;\n case \"datetime\":\n if (value instanceof Date) {\n return format(value, \"PPP 'at' HH:mm\", { locale: enUS });\n }\n break;\n case \"range\":\n if (isDateRange(value)) {\n const fromFormatted = format(value.from, \"PP\", { locale: enUS });\n const toFormatted = format(value.to, \"PP\", { locale: enUS });\n return `${fromFormatted} - ${toFormatted}`;\n }\n break;\n }\n return placeholder;\n};\n\nconst getInputIcon = (mode: \"single\" | \"range\" | \"datetime\") => {\n switch (mode) {\n case \"datetime\":\n return \"calendar-outline\";\n case \"range\":\n return \"calendar-outline\";\n default:\n return \"calendar-outline\";\n }\n};\n\nconst DateTimePicker = React.forwardRef(\n (\n {\n mode = \"single\",\n value,\n onValueChange,\n placeholder = \"Select date\",\n disabled = false,\n className,\n showOutsideDays = true,\n disabledDates,\n disableWeekends = false,\n fromDate,\n toDate,\n timeConfig,\n firstDayOfWeek = 1,\n enableQuickMonthYear = false,\n variant = \"default\",\n size = \"md\",\n ...props\n },\n ref\n ) => {\n const [isOpen, setIsOpen] = React.useState(false);\n const [isFocused, setIsFocused] = React.useState(false);\n const fadeAnim = React.useRef(new Animated.Value(0)).current;\n const scaleAnim = React.useRef(new Animated.Value(0.95)).current;\n\n const displayValue = formatDisplayValue(value, mode, placeholder);\n const iconName = getInputIcon(mode);\n\n const sizeClasses = {\n sm: \"h-10 px-3 text-sm\",\n md: \"h-12 px-3 text-base\",\n lg: \"h-14 px-4 text-lg\",\n };\n\n const iconSizes = {\n sm: 18,\n md: 20,\n lg: 22,\n };\n\n const openPicker = React.useCallback(() => {\n if (disabled) return;\n setIsOpen(true);\n setIsFocused(true);\n\n Animated.parallel([\n Animated.timing(fadeAnim, {\n toValue: 1,\n duration: 300,\n useNativeDriver: true,\n }),\n Animated.timing(scaleAnim, {\n toValue: 1,\n duration: 300,\n useNativeDriver: true,\n }),\n ]).start();\n }, [disabled, fadeAnim, scaleAnim]);\n\n const closePicker = React.useCallback(() => {\n Animated.parallel([\n Animated.timing(fadeAnim, {\n toValue: 0,\n duration: 250,\n useNativeDriver: true,\n }),\n Animated.timing(scaleAnim, {\n toValue: 0.95,\n duration: 250,\n useNativeDriver: true,\n }),\n ]).start(() => {\n setIsOpen(false);\n setIsFocused(false);\n });\n }, [fadeAnim, scaleAnim]);\n\n const handleSelect = React.useCallback(\n (selectedValue: Date | Date[] | DateRange | undefined) => {\n onValueChange?.(selectedValue);\n if (\n mode === \"single\" ||\n (mode === \"range\" &&\n isDateRange(selectedValue) &&\n selectedValue.from !== selectedValue.to)\n ) {\n closePicker();\n }\n },\n [onValueChange, mode, closePicker]\n );\n\n return (\n <>\n \n \n \n \n \n {displayValue}\n \n \n\n {/* Calendar Modal */}\n \n \n \n\n \n {/* Modal Header */}\n \n \n \n Cancel\n \n\n \n {mode === \"range\"\n ? \"Select dates\"\n : mode === \"datetime\"\n ? \"Select date & time\"\n : \"Select date\"}\n \n\n \n \n Done\n \n \n \n \n\n {/* Calendar */}\n \n \n\n \n \n \n \n );\n }\n);\n\nDateTimePicker.displayName = \"DateTimePicker\";\n\nexport { DateTimePicker, type DateTimePickerProps };\n", + "type": "registry:component" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index 7aa0eb6..d272db2 100644 --- a/registry.json +++ b/registry.json @@ -497,6 +497,20 @@ ], "dependencies": ["react-native", "date-fns", "@expo/vector-icons", "@react-native-community/datetimepicker"], "registryDependencies": [] + }, + { + "name": "date-time-picker", + "type": "registry:component", + "title": "Date Time Picker", + "description": "A date time picker component for React Native applications.", + "files": [ + { + "path": "registry/date-time-picker/date-time-picker.tsx", + "type": "registry:component" + } + ], + "dependencies": ["react-native", "date-fns", "@expo/vector-icons"], + "registryDependencies": ["https://nativeui.io/registry/calendar.json"] } ] } diff --git a/registry/date-time-picker/date-time-picker.tsx b/registry/date-time-picker/date-time-picker.tsx new file mode 100644 index 0000000..a724c03 --- /dev/null +++ b/registry/date-time-picker/date-time-picker.tsx @@ -0,0 +1,303 @@ +import * as React from "react"; +import { View, Text, Pressable, Modal, Animated } from "react-native"; +import { cn } from "@/lib/utils"; +import { Calendar } from "@/components/ui/calendar"; +import { Ionicons } from "@expo/vector-icons"; +import { format } from "date-fns"; +import { enUS } from "date-fns/locale"; + +interface DateRange { + from: Date; + to: Date; +} + +interface TimeConfig { + minuteInterval?: 1 | 5 | 10 | 15 | 30; + minTime?: string; + maxTime?: string; + disabledTimes?: string[]; +} + +interface DateTimePickerProps { + mode?: "single" | "range" | "datetime"; + value?: Date | Date[] | DateRange; + onValueChange?: (value: Date | Date[] | DateRange | undefined) => void; + placeholder?: string; + disabled?: boolean; + className?: string; + showOutsideDays?: boolean; + disabledDates?: (date: Date) => boolean; + disableWeekends?: boolean; + fromDate?: Date; + toDate?: Date; + timeConfig?: TimeConfig; + firstDayOfWeek?: 0 | 1; + enableQuickMonthYear?: boolean; + variant?: "default" | "outline"; + size?: "sm" | "md" | "lg"; +} + +const isDateRange = (value: any): value is DateRange => { + return value && typeof value === "object" && "from" in value && "to" in value; +}; + +const formatDisplayValue = ( + value: Date | Date[] | DateRange | undefined, + mode: "single" | "range" | "datetime", + placeholder: string +): string => { + if (!value) return placeholder; + + switch (mode) { + case "single": + if (value instanceof Date) { + return format(value, "PPP", { locale: enUS }); + } + break; + case "datetime": + if (value instanceof Date) { + return format(value, "PPP 'at' HH:mm", { locale: enUS }); + } + break; + case "range": + if (isDateRange(value)) { + const fromFormatted = format(value.from, "PP", { locale: enUS }); + const toFormatted = format(value.to, "PP", { locale: enUS }); + return `${fromFormatted} - ${toFormatted}`; + } + break; + } + return placeholder; +}; + +const getInputIcon = (mode: "single" | "range" | "datetime") => { + switch (mode) { + case "datetime": + return "calendar-outline"; + case "range": + return "calendar-outline"; + default: + return "calendar-outline"; + } +}; + +const DateTimePicker = React.forwardRef( + ( + { + mode = "single", + value, + onValueChange, + placeholder = "Select date", + disabled = false, + className, + showOutsideDays = true, + disabledDates, + disableWeekends = false, + fromDate, + toDate, + timeConfig, + firstDayOfWeek = 1, + enableQuickMonthYear = false, + variant = "default", + size = "md", + ...props + }, + ref + ) => { + const [isOpen, setIsOpen] = React.useState(false); + const [isFocused, setIsFocused] = React.useState(false); + const fadeAnim = React.useRef(new Animated.Value(0)).current; + const scaleAnim = React.useRef(new Animated.Value(0.95)).current; + + const displayValue = formatDisplayValue(value, mode, placeholder); + const iconName = getInputIcon(mode); + + const sizeClasses = { + sm: "h-10 px-3 text-sm", + md: "h-12 px-3 text-base", + lg: "h-14 px-4 text-lg", + }; + + const iconSizes = { + sm: 18, + md: 20, + lg: 22, + }; + + const openPicker = React.useCallback(() => { + if (disabled) return; + setIsOpen(true); + setIsFocused(true); + + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(scaleAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + }, [disabled, fadeAnim, scaleAnim]); + + const closePicker = React.useCallback(() => { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(scaleAnim, { + toValue: 0.95, + duration: 250, + useNativeDriver: true, + }), + ]).start(() => { + setIsOpen(false); + setIsFocused(false); + }); + }, [fadeAnim, scaleAnim]); + + const handleSelect = React.useCallback( + (selectedValue: Date | Date[] | DateRange | undefined) => { + onValueChange?.(selectedValue); + if ( + mode === "single" || + (mode === "range" && + isDateRange(selectedValue) && + selectedValue.from !== selectedValue.to) + ) { + closePicker(); + } + }, + [onValueChange, mode, closePicker] + ); + + return ( + <> + + + + + + {displayValue} + + + + {/* Calendar Modal */} + + + + + + {/* Modal Header */} + + + + Cancel + + + + {mode === "range" + ? "Select dates" + : mode === "datetime" + ? "Select date & time" + : "Select date"} + + + + + Done + + + + + + {/* Calendar */} + + + + + + + + ); + } +); + +DateTimePicker.displayName = "DateTimePicker"; + +export { DateTimePicker, type DateTimePickerProps };