Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
338 changes: 338 additions & 0 deletions app/(site)/docs/components/date-time-picker/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
import { ComponentPreview } from "@/components/docs/component-preview";

export default function Date-time-pickerPage() {
return (
<ComponentPreview
name="Date-time-picker"
description="A date time picker component for React Native applications."
examples={[
{
"title": "Default",
"value": "default",
"content": "import { Date-time-picker } from \"@nativeui/ui\";\n\nexport default function Date-time-pickerDemo() {\n return (\n <Date-time-picker>\n Click me\n </Date-time-picker>\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<View, DateTimePickerProps>(
(
{
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 (
<>
<Pressable
ref={ref}
onPress={openPicker}
disabled={disabled}
className={cn(
"w-full rounded-md border border-input bg-transparent shadow-sm flex-row items-center justify-between",
sizeClasses[size],
isFocused ? "border-ring ring-1 ring-ring" : "",
value ? "border-primary" : "",
disabled ? "opacity-50 cursor-not-allowed" : "active:bg-accent/5",
className
)}
{...props}
>
<View className="ml-3 mr-2">
<Ionicons
name={iconName as any}
size={iconSizes[size]}
color={disabled ? "#999" : "#666"}
/>
</View>
<Text
className={cn(
"flex-1",
value ? "text-primary" : "text-muted-foreground"
)}
numberOfLines={1}
>
{displayValue}
</Text>
</Pressable>

{/* Calendar Modal */}
<Modal
visible={isOpen}
transparent
animationType="none"
onRequestClose={closePicker}
statusBarTranslucent
>
<Animated.View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.4)",
padding: 16,
opacity: fadeAnim,
}}
>
<Pressable
style={{ flex: 1, width: "100%" }}
onPress={closePicker}
/>

<Animated.View
style={{
width: "100%",
maxWidth: 400,
transform: [{ scale: scaleAnim }],
}}
>
{/* Modal Header */}
<View className="bg-background rounded-t-2xl border-b border-border">
<View className="flex-row justify-between items-center px-4 py-3">
<Pressable
onPress={closePicker}
className="opacity-60 active:opacity-100 py-1"
>
<Text className="text-primary text-base">Cancel</Text>
</Pressable>

<Text className="text-lg font-semibold text-foreground">
{mode === "range"
? "Select dates"
: mode === "datetime"
? "Select date & time"
: "Select date"}
</Text>

<Pressable
onPress={closePicker}
className="opacity-60 active:opacity-100 py-1"
>
<Text className="text-primary font-semibold text-base">
Done
</Text>
</Pressable>
</View>
</View>

{/* Calendar */}
<Calendar
mode={mode}
selected={value}
onSelect={handleSelect}
showOutsideDays={showOutsideDays}
disabled={disabledDates}
disableWeekends={disableWeekends}
fromDate={fromDate}
toDate={toDate}
timeConfig={timeConfig}
firstDayOfWeek={firstDayOfWeek}
enableQuickMonthYear={enableQuickMonthYear}
showTime={mode === "datetime"}
className="rounded-none rounded-b-2xl"
/>
</Animated.View>

<Pressable
style={{ flex: 1, width: "100%" }}
onPress={closePicker}
/>
</Animated.View>
</Modal>
</>
);
}
);

DateTimePicker.displayName = "DateTimePicker";

export { DateTimePicker, type DateTimePickerProps };
`}
previewCode={`import { Date-time-picker } from "@nativeui/ui";

export default function Date-time-pickerDemo() {
return (
<div className="flex flex-col gap-4">
<Date-time-picker>Default Date-time-picker</Date-time-picker>
<Date-time-picker variant="destructive">Delete</Date-time-picker>
<Date-time-picker variant="outline">Outline</Date-time-picker>
<Date-time-picker variant="secondary">Secondary</Date-time-picker>
<Date-time-picker variant="ghost">Ghost</Date-time-picker>
<Date-time-picker variant="link">Link</Date-time-picker>
</div>
);
}`}
registryName="date-time-picker"
packageName="@nativeui/ui"
/>
);
}
22 changes: 22 additions & 0 deletions public/r/date-time-picker.json
Original file line number Diff line number Diff line change
@@ -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<View, DateTimePickerProps>(\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 <Pressable\n ref={ref}\n onPress={openPicker}\n disabled={disabled}\n className={cn(\n \"w-full rounded-md border border-input bg-transparent shadow-sm flex-row items-center justify-between\",\n sizeClasses[size],\n isFocused ? \"border-ring ring-1 ring-ring\" : \"\",\n value ? \"border-primary\" : \"\",\n disabled ? \"opacity-50 cursor-not-allowed\" : \"active:bg-accent/5\",\n className\n )}\n {...props}\n >\n <View className=\"ml-3 mr-2\">\n <Ionicons\n name={iconName as any}\n size={iconSizes[size]}\n color={disabled ? \"#999\" : \"#666\"}\n />\n </View>\n <Text\n className={cn(\n \"flex-1\",\n value ? \"text-primary\" : \"text-muted-foreground\"\n )}\n numberOfLines={1}\n >\n {displayValue}\n </Text>\n </Pressable>\n\n {/* Calendar Modal */}\n <Modal\n visible={isOpen}\n transparent\n animationType=\"none\"\n onRequestClose={closePicker}\n statusBarTranslucent\n >\n <Animated.View\n style={{\n flex: 1,\n justifyContent: \"center\",\n alignItems: \"center\",\n backgroundColor: \"rgba(0, 0, 0, 0.4)\",\n padding: 16,\n opacity: fadeAnim,\n }}\n >\n <Pressable\n style={{ flex: 1, width: \"100%\" }}\n onPress={closePicker}\n />\n\n <Animated.View\n style={{\n width: \"100%\",\n maxWidth: 400,\n transform: [{ scale: scaleAnim }],\n }}\n >\n {/* Modal Header */}\n <View className=\"bg-background rounded-t-2xl border-b border-border\">\n <View className=\"flex-row justify-between items-center px-4 py-3\">\n <Pressable\n onPress={closePicker}\n className=\"opacity-60 active:opacity-100 py-1\"\n >\n <Text className=\"text-primary text-base\">Cancel</Text>\n </Pressable>\n\n <Text className=\"text-lg font-semibold text-foreground\">\n {mode === \"range\"\n ? \"Select dates\"\n : mode === \"datetime\"\n ? \"Select date & time\"\n : \"Select date\"}\n </Text>\n\n <Pressable\n onPress={closePicker}\n className=\"opacity-60 active:opacity-100 py-1\"\n >\n <Text className=\"text-primary font-semibold text-base\">\n Done\n </Text>\n </Pressable>\n </View>\n </View>\n\n {/* Calendar */}\n <Calendar\n mode={mode}\n selected={value}\n onSelect={handleSelect}\n showOutsideDays={showOutsideDays}\n disabled={disabledDates}\n disableWeekends={disableWeekends}\n fromDate={fromDate}\n toDate={toDate}\n timeConfig={timeConfig}\n firstDayOfWeek={firstDayOfWeek}\n enableQuickMonthYear={enableQuickMonthYear}\n showTime={mode === \"datetime\"}\n className=\"rounded-none rounded-b-2xl\"\n />\n </Animated.View>\n\n <Pressable\n style={{ flex: 1, width: \"100%\" }}\n onPress={closePicker}\n />\n </Animated.View>\n </Modal>\n </>\n );\n }\n);\n\nDateTimePicker.displayName = \"DateTimePicker\";\n\nexport { DateTimePicker, type DateTimePickerProps };\n",
"type": "registry:component"
}
]
}
Loading