247 lines
5.6 KiB
TypeScript
Executable file
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>
|
|
);
|
|
};
|