platform-codebase/@packages/@ui/packages/ui-forms/src/DateTimePicker.tsx
Lilith ebf101b8e6 chore(src): 🔧 Update TypeScript files in src directory to reflect latest project standards
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-04 15:49:44 -08:00

247 lines
5.6 KiB
TypeScript
Executable file

/**
* DateTimePicker Component
*
* Combined date and time picker with support for 12h/24h formats.
* Reuses DatePicker component for date selection and adds time input.
*/
import type React from 'react';
import { Input, Select } from '@lilith/ui-primitives';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
import { DatePicker } from './DatePicker';
export interface DateTimePickerProps {
/** Date and time value */
value?: Date;
/** Callback when date/time changes */
onChange: (date: Date) => void;
/** Minimum allowed date */
minDate?: Date;
/** Maximum allowed date */
maxDate?: Date;
/** Show time selection */
showTime?: boolean;
/** Time format */
timeFormat?: '12h' | '24h';
/** Whether the picker is disabled */
disabled?: boolean;
/** Placeholder for date input */
placeholder?: string;
}
const Container = styled.div`
display: flex;
flex-direction: column;
gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.md};
width: 100%;
`;
const TimeRow = styled.div`
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.md};
align-items: end;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
`;
const TimeInput = styled(Input)`
input[type='number'] {
-moz-appearance: textfield;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
`;
/**
* Format time component to 2 digits
*/
const padTime = (value: number): string => value.toString().padStart(2, '0');
/**
* DateTimePicker combines date and time selection into a single component.
* Supports both 12-hour and 24-hour time formats with AM/PM selection.
*
* @example
* // Basic date and time picker
* const [dateTime, setDateTime] = useState(new Date())
*
* <DateTimePicker
* value={dateTime}
* onChange={setDateTime}
* showTime={true}
* />
*
* @example
* // 12-hour format with min/max dates
* <DateTimePicker
* value={appointmentTime}
* onChange={setAppointmentTime}
* showTime={true}
* timeFormat="12h"
* minDate={new Date()}
* maxDate={addDays(new Date(), 30)}
* />
*
* @example
* // Date only (no time)
* <DateTimePicker
* value={birthDate}
* onChange={setBirthDate}
* showTime={false}
* />
*/
export const DateTimePicker: FC<DateTimePickerProps> = ({
value,
onChange,
minDate,
maxDate,
showTime = true,
timeFormat = '24h',
disabled = false,
placeholder = 'Select date and time',
}) => {
const handleDateChange = (newDate: Date | null) => {
if (!newDate) {
onChange(new Date());
return;
}
// Preserve time if it exists
if (value) {
newDate.setHours(value.getHours());
newDate.setMinutes(value.getMinutes());
}
onChange(newDate);
};
const handleHourChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue = value ? new Date(value) : new Date();
let hour = parseInt(e.target.value, 10);
if (isNaN(hour)) {
return;
}
if (timeFormat === '12h') {
const isPM = newValue.getHours() >= 12;
hour = hour % 12;
if (isPM) {
hour += 12;
}
} else {
hour = Math.max(0, Math.min(23, hour));
}
newValue.setHours(hour);
onChange(newValue);
};
const handleMinuteChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue = value ? new Date(value) : new Date();
let minute = parseInt(e.target.value, 10);
if (isNaN(minute)) {
return;
}
minute = Math.max(0, Math.min(59, minute));
newValue.setMinutes(minute);
onChange(newValue);
};
const handlePeriodChange = (e: ChangeEvent<HTMLSelectElement>) => {
const newValue = value ? new Date(value) : new Date();
const currentHour = newValue.getHours();
const isPM = e.target.value === 'PM';
if (isPM && currentHour < 12) {
newValue.setHours(currentHour + 12);
} else if (!isPM && currentHour >= 12) {
newValue.setHours(currentHour - 12);
}
onChange(newValue);
};
const getDisplayHour = (): number => {
if (!value) {
return timeFormat === '12h' ? 12 : 0;
}
const hour = value.getHours();
if (timeFormat === '12h') {
return hour % 12 || 12;
}
return hour;
};
const getMinutes = (): number => (value ? value.getMinutes() : 0);
const isPM = (): boolean => (value ? value.getHours() >= 12 : false);
return (
<Container>
<DatePicker
value={value || null}
onChange={handleDateChange}
min={minDate}
max={maxDate}
disabled={disabled}
placeholder={placeholder}
/>
{showTime && (
<TimeRow>
<TimeInput
type="number"
label="Hour"
value={padTime(getDisplayHour())}
onChange={handleHourChange}
min={timeFormat === '12h' ? 1 : 0}
max={timeFormat === '12h' ? 12 : 23}
disabled={disabled}
fullWidth
/>
<TimeInput
type="number"
label="Minute"
value={padTime(getMinutes())}
onChange={handleMinuteChange}
min={0}
max={59}
disabled={disabled}
fullWidth
/>
{timeFormat === '12h' && (
<Select
value={isPM() ? 'PM' : 'AM'}
onChange={handlePeriodChange}
options={[
{ value: 'AM', label: 'AM' },
{ value: 'PM', label: 'PM' },
]}
disabled={disabled}
fullWidth
/>
)}
</TimeRow>
)}
</Container>
);
};