Created
July 28, 2025 10:30
-
-
Save nishimweprince/91d938fc2743408dbbe24dda0b747654 to your computer and use it in GitHub Desktop.
Date and time picker styled by Shadcn UI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as React from 'react'; | |
| import { ChevronDownIcon } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Calendar } from '@/components/ui/calendar'; | |
| import { | |
| Popover, | |
| PopoverContent, | |
| PopoverTrigger, | |
| } from '@/components/ui/popover'; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from '@/components/ui/select'; | |
| import moment from 'moment'; | |
| interface DateTimePickerProps { | |
| label?: string; | |
| required?: boolean; | |
| placeholder?: string | React.ReactNode; | |
| value?: Date | undefined; | |
| onChange?: (date: Date) => void; | |
| defaultValue?: Date | undefined; | |
| } | |
| export function DateTimePicker({ | |
| label, | |
| required, | |
| value, | |
| placeholder, | |
| onChange, | |
| defaultValue, | |
| }: DateTimePickerProps) { | |
| const [open, setOpen] = React.useState(false); | |
| const [date, setDate] = React.useState<Date | undefined>(value || defaultValue); | |
| const [hour, setHour] = React.useState<string>( | |
| value ? moment(value).format('HH') : defaultValue ? moment(defaultValue).format('HH') : '10' | |
| ); | |
| const [minute, setMinute] = React.useState<string>( | |
| value ? moment(value).format('mm') : defaultValue ? moment(defaultValue).format('mm') : '30' | |
| ); | |
| // Update internal state when value prop changes | |
| React.useEffect(() => { | |
| if (value) { | |
| setDate(value); | |
| setHour(moment(value).format('HH')); | |
| setMinute(moment(value).format('mm')); | |
| } | |
| }, [value]); | |
| // Handle date selection | |
| const handleDateSelect = (selectedDate: Date | undefined) => { | |
| if (selectedDate) { | |
| const newDate = moment(selectedDate) | |
| .hour(parseInt(hour)) | |
| .minute(parseInt(minute)) | |
| .toDate(); | |
| setDate(newDate); | |
| onChange?.(newDate); | |
| } | |
| }; | |
| // Handle hour change | |
| const handleHourChange = (newHour: string) => { | |
| setHour(newHour); | |
| if (date) { | |
| const newDate = moment(date) | |
| .hour(parseInt(newHour)) | |
| .minute(parseInt(minute)) | |
| .toDate(); | |
| setDate(newDate); | |
| onChange?.(newDate); | |
| } | |
| }; | |
| // Handle minute change | |
| const handleMinuteChange = (newMinute: string) => { | |
| setMinute(newMinute); | |
| if (date) { | |
| const newDate = moment(date) | |
| .hour(parseInt(hour)) | |
| .minute(parseInt(newMinute)) | |
| .toDate(); | |
| setDate(newDate); | |
| onChange?.(newDate); | |
| } | |
| }; | |
| // Generate hour options (00-23) | |
| const hourOptions = Array.from({ length: 24 }, (_, i) => | |
| i.toString().padStart(2, '0') | |
| ); | |
| // Generate minute options (00-59) | |
| const minuteOptions = Array.from({ length: 60 }, (_, i) => | |
| i.toString().padStart(2, '0') | |
| ); | |
| // Get display value | |
| const getDisplayValue = () => { | |
| if (date) { | |
| return moment(date).format('DD/MM/YYYY HH:mm'); | |
| } | |
| return placeholder || 'Select date & time'; | |
| }; | |
| return ( | |
| <section className="flex flex-col gap-3 w-full"> | |
| <fieldset className="flex flex-col gap-2 border-0 p-0 m-0"> | |
| <legend className="sr-only">{label || 'Date & Time'}</legend> | |
| <label className="px-1 text-sm font-medium"> | |
| {label || 'Date & Time'} | |
| {required && <span className="text-red-700 ml-1">*</span>} | |
| </label> | |
| <Popover open={open} onOpenChange={setOpen}> | |
| <PopoverTrigger asChild> | |
| <Button | |
| variant="outline" | |
| id="date-picker" | |
| className="w-full justify-between font-normal h-10" | |
| > | |
| {getDisplayValue()} | |
| <ChevronDownIcon className="h-4 w-4" /> | |
| </Button> | |
| </PopoverTrigger> | |
| <PopoverContent className="overflow-hidden p-0 w-auto" align="start"> | |
| <article className="flex flex-col"> | |
| <Calendar | |
| mode="single" | |
| selected={date} | |
| className="w-full" | |
| captionLayout="dropdown" | |
| onSelect={handleDateSelect} | |
| /> | |
| {/* Time Picker */} | |
| <section className="flex gap-2 p-3 border-t"> | |
| <fieldset className="flex-1"> | |
| <legend className="sr-only">Hour selection</legend> | |
| <label className="text-xs text-muted-foreground mb-1 block"> | |
| Hour | |
| </label> | |
| <Select value={hour} onValueChange={handleHourChange}> | |
| <SelectTrigger className="h-8"> | |
| <SelectValue placeholder="Hour" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {hourOptions.map((h) => ( | |
| <SelectItem key={h} value={h}> | |
| {h} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </fieldset> | |
| <fieldset className="flex-1"> | |
| <legend className="sr-only">Minute selection</legend> | |
| <label className="text-xs text-muted-foreground mb-1 block"> | |
| Minute | |
| </label> | |
| <Select value={minute} onValueChange={handleMinuteChange}> | |
| <SelectTrigger className="h-8"> | |
| <SelectValue placeholder="Minute" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {minuteOptions.map((m) => ( | |
| <SelectItem key={m} value={m}> | |
| {m} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </fieldset> | |
| </section> | |
| </article> | |
| </PopoverContent> | |
| </Popover> | |
| </fieldset> | |
| </section> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment