mirror of
https://github.com/twentyhq/twenty.git
synced 2024-08-17 18:00:29 +03:00
New Datetime field picker (#4907)
### Description New Datetime field picker ### Refs https://github.com/twentyhq/twenty/issues/4376 ### Demo https://github.com/twentyhq/twenty/assets/140154534/32656323-972c-413a-9986-a78efffae1b4 Fixes #4376 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
464a2d5998
commit
efcb5dc6d4
@ -152,6 +152,7 @@
|
||||
"react-hook-form": "^7.45.1",
|
||||
"react-hotkeys-hook": "^4.4.4",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-imask": "^7.6.0",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-phone-number-input": "^3.3.4",
|
||||
|
@ -6,8 +6,6 @@ import SearchBar from '@theme-original/SearchBar';
|
||||
const CustomComponents = {
|
||||
'search-bar': () => {
|
||||
const openSearchModal = () => {
|
||||
console.log('yo');
|
||||
|
||||
const searchInput = document.querySelector('#search-bar');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemToggle } from "@/ui/navigation/menu-item/components/MenuItemToggle";
|
||||
import { IconBell } from '@tabler/icons-react';
|
||||
|
||||
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleToggleChange = (toggled) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
IconCalendarEvent,
|
||||
IconCalendarTime,
|
||||
IconCheck,
|
||||
IconCoins,
|
||||
IconComponent,
|
||||
@ -67,8 +68,8 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
|
||||
defaultValue: true,
|
||||
},
|
||||
[FieldMetadataType.DateTime]: {
|
||||
label: 'Date & Time',
|
||||
Icon: IconCalendarEvent,
|
||||
label: 'Date and Time',
|
||||
Icon: IconCalendarTime,
|
||||
defaultValue: DEFAULT_DATE_VALUE.toISOString(),
|
||||
},
|
||||
[FieldMetadataType.Date]: {
|
||||
|
@ -41,9 +41,9 @@ export type DateInputProps = {
|
||||
|
||||
export const DateInput = ({
|
||||
value,
|
||||
hotkeyScope,
|
||||
onEnter,
|
||||
onEscape,
|
||||
hotkeyScope,
|
||||
onClickOutside,
|
||||
clearable,
|
||||
onChange,
|
||||
@ -65,7 +65,7 @@ export const DateInput = ({
|
||||
],
|
||||
});
|
||||
|
||||
const handleChange = (newDate: Date) => {
|
||||
const handleChange = (newDate: Date | null) => {
|
||||
setInternalValue(newDate);
|
||||
onChange?.(newDate);
|
||||
};
|
||||
@ -96,6 +96,7 @@ export const DateInput = ({
|
||||
}}
|
||||
clearable={clearable ? clearable : false}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
</StyledCalendarContainer>
|
||||
</div>
|
||||
|
@ -0,0 +1,109 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIMask } from 'react-imask';
|
||||
import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks';
|
||||
import { DATE_TIME_MASK } from '@/ui/input/components/internal/date/constants/DateTimeMask';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<{ hasError?: boolean }>`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
outline: none;
|
||||
padding: 8px;
|
||||
font-weight: 500;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
width: 100%;
|
||||
color: ${({ hasError, theme }) => (hasError ? theme.color.red : 'inherit')};
|
||||
`;
|
||||
|
||||
type DateTimeInputProps = {
|
||||
onChange?: (date: Date | null) => void;
|
||||
date: Date | null;
|
||||
isDateTimeInput?: boolean;
|
||||
onError?: (error: Error) => void;
|
||||
};
|
||||
|
||||
export const DateTimeInput = ({
|
||||
date,
|
||||
onChange,
|
||||
isDateTimeInput,
|
||||
}: DateTimeInputProps) => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const parseDateToString = (date: any) => {
|
||||
const dateParsed = DateTime.fromJSDate(date);
|
||||
|
||||
const formattedDate = dateParsed.toFormat('MM/dd/yyyy HH:mm');
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
const parseStringToDate = (str: string) => {
|
||||
setHasError(false);
|
||||
|
||||
const parsedDate = DateTime.fromFormat(str, 'MM/dd/yyyy HH:mm');
|
||||
|
||||
const isValid = parsedDate.isValid;
|
||||
|
||||
if (!isValid) {
|
||||
setHasError(true);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const jsDate = parsedDate.toJSDate();
|
||||
|
||||
return jsDate;
|
||||
};
|
||||
|
||||
const { ref, setValue, value } = useIMask(
|
||||
{
|
||||
mask: Date,
|
||||
pattern: DATE_TIME_MASK,
|
||||
blocks: DATE_TIME_BLOCKS,
|
||||
min: new Date(1970, 0, 1),
|
||||
max: new Date(2100, 0, 1),
|
||||
format: parseDateToString,
|
||||
parse: parseStringToDate,
|
||||
lazy: false,
|
||||
autofix: true,
|
||||
},
|
||||
{
|
||||
onComplete: (value) => {
|
||||
const parsedDate = parseStringToDate(value);
|
||||
|
||||
onChange?.(parsedDate);
|
||||
},
|
||||
onAccept: () => {
|
||||
setHasError(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(parseDateToString(date));
|
||||
}, [date, setValue]);
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
type="text"
|
||||
ref={ref as any}
|
||||
placeholder={`Type date${
|
||||
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
|
||||
}`}
|
||||
value={value}
|
||||
hasError={hasError}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
);
|
||||
};
|
@ -1,9 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import ReactDatePicker from 'react-datepicker';
|
||||
import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IconCalendarX } from 'twenty-ui';
|
||||
import { IconCalendarX, IconChevronLeft, IconChevronRight } from 'twenty-ui';
|
||||
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||
import {
|
||||
MONTH_AND_YEAR_DROPDOWN_ID,
|
||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||
MonthAndYearDropdown,
|
||||
} from '@/ui/input/components/internal/date/components/MonthAndYearDropdown';
|
||||
import { TimeInput } from '@/ui/input/components/internal/date/components/TimeInput';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
||||
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
||||
import { OVERLAY_BACKGROUND } from '@/ui/theme/constants/OverlayBackground';
|
||||
@ -45,13 +54,21 @@ const StyledContainer = styled.div`
|
||||
& .react-datepicker__header {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&
|
||||
.react-datepicker__input-time-container
|
||||
.react-datepicker-time__input-container
|
||||
.react-datepicker-time__input {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
& .react-datepicker__header__dropdown {
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(10)};
|
||||
}
|
||||
|
||||
& .react-datepicker__month-dropdown-container,
|
||||
@ -177,7 +194,7 @@ const StyledContainer = styled.div`
|
||||
}
|
||||
& .react-datepicker__navigation--previous {
|
||||
right: 38px;
|
||||
top: 8px;
|
||||
top: 6px;
|
||||
left: auto;
|
||||
|
||||
& > span {
|
||||
@ -187,7 +204,7 @@ const StyledContainer = styled.div`
|
||||
|
||||
& .react-datepicker__navigation--next {
|
||||
right: 6px;
|
||||
top: 8px;
|
||||
top: 6px;
|
||||
|
||||
& > span {
|
||||
margin-left: 6px;
|
||||
@ -239,32 +256,26 @@ const StyledButton = styled(MenuItemLeftContent)`
|
||||
justify-content: start;
|
||||
`;
|
||||
|
||||
const StyledCustomDatePickerHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export type InternalDatePickerProps = {
|
||||
date: Date | null;
|
||||
date: Date;
|
||||
onMouseSelect?: (date: Date | null) => void;
|
||||
onChange?: (date: Date) => void;
|
||||
onChange?: (date: Date | null) => void;
|
||||
clearable?: boolean;
|
||||
isDateTimeInput?: boolean;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent, date: Date | null) => void;
|
||||
};
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
outline: none;
|
||||
padding: 8px;
|
||||
font-weight: 500;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const PICKER_DATE_FORMAT = 'MM/dd/yyyy';
|
||||
|
||||
export const InternalDatePicker = ({
|
||||
@ -273,92 +284,97 @@ export const InternalDatePicker = ({
|
||||
onMouseSelect,
|
||||
clearable = true,
|
||||
isDateTimeInput,
|
||||
onClickOutside,
|
||||
}: InternalDatePickerProps) => {
|
||||
const internalDate = date ?? new Date();
|
||||
|
||||
const dateFormatted =
|
||||
DateTime.fromJSDate(internalDate).toFormat(PICKER_DATE_FORMAT);
|
||||
|
||||
const { closeDropdown } = useDropdown(MONTH_AND_YEAR_DROPDOWN_ID);
|
||||
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
|
||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||
);
|
||||
const { closeDropdown: closeDropdownYearSelect } = useDropdown(
|
||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||
);
|
||||
|
||||
const handleClear = () => {
|
||||
closeDropdowns();
|
||||
onMouseSelect?.(null);
|
||||
};
|
||||
|
||||
const initialDate = date
|
||||
? DateTime.fromJSDate(date).toFormat(PICKER_DATE_FORMAT)
|
||||
: DateTime.now().toFormat(PICKER_DATE_FORMAT);
|
||||
const closeDropdowns = () => {
|
||||
closeDropdownYearSelect();
|
||||
closeDropdownMonthSelect();
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const [dateValue, setDateValue] = useState(initialDate);
|
||||
const handleClickOutside = (event: any) => {
|
||||
closeDropdowns();
|
||||
onClickOutside?.(event, internalDate);
|
||||
};
|
||||
|
||||
const dateValueAsJSDate = DateTime.fromFormat(dateValue, PICKER_DATE_FORMAT)
|
||||
.isValid
|
||||
? DateTime.fromFormat(dateValue, PICKER_DATE_FORMAT).toJSDate()
|
||||
: null;
|
||||
const handleMouseSelect = (newDate: Date) => {
|
||||
closeDropdowns();
|
||||
onMouseSelect?.(newDate);
|
||||
};
|
||||
|
||||
// TODO: implement keyboard events here
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<div className={clearable ? 'clearable ' : ''}>
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
type="text"
|
||||
placeholder={`Type date${
|
||||
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
|
||||
}`}
|
||||
inputMode="numeric"
|
||||
value={dateValue}
|
||||
onChange={(e) => {
|
||||
const inputValue = e.target.value;
|
||||
setDateValue(inputValue);
|
||||
|
||||
if (!isDateTimeInput) {
|
||||
const parsedInputDate = DateTime.fromFormat(
|
||||
inputValue,
|
||||
PICKER_DATE_FORMAT,
|
||||
{ zone: 'utc' },
|
||||
);
|
||||
|
||||
const isValid = parsedInputDate.isValid;
|
||||
|
||||
if (isValid) {
|
||||
onChange?.(parsedInputDate.toJSDate());
|
||||
}
|
||||
} else {
|
||||
// TODO: implement time also
|
||||
const parsedInputDate = DateTime.fromFormat(
|
||||
inputValue,
|
||||
PICKER_DATE_FORMAT,
|
||||
{ zone: 'utc' },
|
||||
);
|
||||
|
||||
const isValid = parsedInputDate.isValid;
|
||||
|
||||
if (isValid) {
|
||||
onChange?.(parsedInputDate.toJSDate());
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
|
||||
<ReactDatePicker
|
||||
open={true}
|
||||
selected={dateValueAsJSDate}
|
||||
value={dateValue}
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
onChange={() => {
|
||||
// We need to use onSelect here but onChange is almost redundant with onSelect but is require
|
||||
selected={internalDate}
|
||||
value={dateFormatted}
|
||||
onChange={(newDate) => {
|
||||
onChange?.(newDate);
|
||||
}}
|
||||
renderCustomHeader={({
|
||||
decreaseMonth,
|
||||
increaseMonth,
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
}) => (
|
||||
<>
|
||||
<DateTimeInput
|
||||
date={internalDate}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<StyledCustomDatePickerHeader>
|
||||
<TimeInput date={internalDate} onChange={onChange} />
|
||||
<MonthAndYearDropdown date={internalDate} onChange={onChange} />
|
||||
<LightIconButton
|
||||
Icon={IconChevronLeft}
|
||||
onClick={() => decreaseMonth()}
|
||||
size="medium"
|
||||
disabled={prevMonthButtonDisabled}
|
||||
/>
|
||||
<LightIconButton
|
||||
Icon={IconChevronRight}
|
||||
onClick={() => increaseMonth()}
|
||||
size="medium"
|
||||
disabled={nextMonthButtonDisabled}
|
||||
/>
|
||||
</StyledCustomDatePickerHeader>
|
||||
</>
|
||||
)}
|
||||
customInput={<></>}
|
||||
onSelect={(date: Date, event) => {
|
||||
// Setting the time to midnight might sometimes return the previous day
|
||||
// We set to 21:00 to avoid any timezone issues
|
||||
const dateForDateField = new Date(date.setHours(21, 0, 0, 0));
|
||||
|
||||
setDateValue(
|
||||
DateTime.fromJSDate(date).toFormat(PICKER_DATE_FORMAT),
|
||||
);
|
||||
const dateUTC = DateTime.fromJSDate(date, {
|
||||
zone: 'utc',
|
||||
}).toJSDate();
|
||||
|
||||
if (event?.type === 'click') {
|
||||
onMouseSelect?.(isDateTimeInput ? date : dateForDateField);
|
||||
handleMouseSelect?.(dateUTC);
|
||||
} else {
|
||||
onChange?.(isDateTimeInput ? date : dateForDateField);
|
||||
onChange?.(dateUTC);
|
||||
}
|
||||
}}
|
||||
onClickOutside={handleClickOutside}
|
||||
></ReactDatePicker>
|
||||
</div>
|
||||
{clearable && (
|
||||
|
@ -0,0 +1,88 @@
|
||||
import { IconCalendarDue } from 'twenty-ui';
|
||||
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
|
||||
type MonthAndYearDropdownProps = {
|
||||
date: Date;
|
||||
onChange?: (newDate: Date) => void;
|
||||
};
|
||||
|
||||
const months = [
|
||||
{ label: 'January', value: 0 },
|
||||
{ label: 'February', value: 1 },
|
||||
{ label: 'March', value: 2 },
|
||||
{ label: 'April', value: 3 },
|
||||
{ label: 'May', value: 4 },
|
||||
{ label: 'June', value: 5 },
|
||||
{ label: 'July', value: 6 },
|
||||
{ label: 'August', value: 7 },
|
||||
{ label: 'September', value: 8 },
|
||||
{ label: 'October', value: 9 },
|
||||
{ label: 'November', value: 10 },
|
||||
{ label: 'December', value: 11 },
|
||||
];
|
||||
|
||||
const years = Array.from(
|
||||
{ length: 200 },
|
||||
(_, i) => new Date().getFullYear() + 5 - i,
|
||||
).map((year) => ({ label: year.toString(), value: year }));
|
||||
|
||||
export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown';
|
||||
export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
|
||||
'date-picker-month-and-year-dropdown-month-select';
|
||||
export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
|
||||
'date-picker-month-and-year-dropdown-year-select';
|
||||
|
||||
export const MonthAndYearDropdown = ({
|
||||
date,
|
||||
onChange,
|
||||
}: MonthAndYearDropdownProps) => {
|
||||
const handleChangeMonth = (month: number) => {
|
||||
const newDate = new Date(date);
|
||||
newDate.setMonth(month);
|
||||
onChange?.(newDate);
|
||||
};
|
||||
|
||||
const handleChangeYear = (year: number) => {
|
||||
const newDate = new Date(date);
|
||||
newDate.setFullYear(year);
|
||||
onChange?.(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_ID}
|
||||
dropdownHotkeyScope={{
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
}}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={
|
||||
<LightIconButton Icon={IconCalendarDue} size="medium" />
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||
options={months}
|
||||
fullWidth
|
||||
disableBlur
|
||||
onChange={handleChangeMonth}
|
||||
value={date.getMonth()}
|
||||
/>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
||||
onChange={handleChangeYear}
|
||||
value={date.getFullYear()}
|
||||
options={years}
|
||||
fullWidth
|
||||
disableBlur
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,80 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useIMask } from 'react-imask';
|
||||
import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IconClockHour8 } from 'twenty-ui';
|
||||
|
||||
import { TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/TimeBlocks';
|
||||
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
|
||||
|
||||
const StyledIconClock = styled(IconClockHour8)`
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
const StyledTimeInputContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
margin-right: 0;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
text-align: left;
|
||||
width: 136px;
|
||||
height: 32px;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
const StyledTimeInput = styled.input`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
outline: none;
|
||||
font-weight: 500;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
margin-left: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
type TimeInputProps = {
|
||||
onChange?: (date: Date) => void;
|
||||
date: Date;
|
||||
};
|
||||
|
||||
export const TimeInput = ({ date, onChange }: TimeInputProps) => {
|
||||
const handleComplete = (value: string) => {
|
||||
const [hours, minutes] = value.split(':');
|
||||
|
||||
const newDate = new Date(date);
|
||||
|
||||
newDate.setHours(parseInt(hours, 10));
|
||||
newDate.setMinutes(parseInt(minutes, 10));
|
||||
|
||||
onChange?.(newDate);
|
||||
};
|
||||
|
||||
const { ref, setValue } = useIMask(
|
||||
{
|
||||
mask: TIME_MASK,
|
||||
blocks: TIME_BLOCKS,
|
||||
lazy: false,
|
||||
},
|
||||
{
|
||||
onComplete: handleComplete,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const formattedDate = DateTime.fromJSDate(date).toFormat('HH:mm');
|
||||
|
||||
setValue(formattedDate);
|
||||
}, [date, setValue]);
|
||||
|
||||
return (
|
||||
<StyledTimeInputContainer>
|
||||
<StyledIconClock size={16} />
|
||||
<StyledTimeInput type="text" ref={ref as any} />
|
||||
</StyledTimeInputContainer>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { IMask } from 'react-imask';
|
||||
|
||||
import { TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/TimeBlocks';
|
||||
|
||||
export const DATE_TIME_BLOCKS = {
|
||||
YYYY: {
|
||||
mask: IMask.MaskedRange,
|
||||
from: 1970,
|
||||
to: 2100,
|
||||
},
|
||||
MM: {
|
||||
mask: IMask.MaskedRange,
|
||||
from: 1,
|
||||
to: 12,
|
||||
},
|
||||
DD: {
|
||||
mask: IMask.MaskedRange,
|
||||
from: 1,
|
||||
to: 31,
|
||||
},
|
||||
...TIME_BLOCKS,
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
|
||||
|
||||
export const DATE_TIME_MASK = `MM/DD/YYYY ${TIME_MASK}`;
|
@ -0,0 +1,14 @@
|
||||
import { IMask } from 'react-imask';
|
||||
|
||||
export const TIME_BLOCKS = {
|
||||
HH: {
|
||||
mask: IMask.MaskedRange, // Use MaskedRange for valid hour range (0-23)
|
||||
from: 0,
|
||||
to: 23,
|
||||
},
|
||||
mm: {
|
||||
mask: IMask.MaskedRange, // Use MaskedRange for valid minute range (0-59)
|
||||
from: 0,
|
||||
to: 61,
|
||||
},
|
||||
};
|
@ -0,0 +1 @@
|
||||
export const TIME_MASK = 'HH:mm'; // Define blocks for hours and minutes
|
@ -26,7 +26,9 @@ export {
|
||||
IconBriefcase,
|
||||
IconBuildingSkyscraper,
|
||||
IconCalendar,
|
||||
IconCalendarDue,
|
||||
IconCalendarEvent,
|
||||
IconCalendarTime,
|
||||
IconCalendarX,
|
||||
IconCheck,
|
||||
IconCheckbox,
|
||||
@ -40,6 +42,7 @@ export {
|
||||
IconCirclePlus,
|
||||
IconCircleX,
|
||||
IconClick,
|
||||
IconClockHour8,
|
||||
IconCode,
|
||||
IconCoins,
|
||||
IconColorSwatch,
|
||||
|
32
yarn.lock
32
yarn.lock
@ -3387,6 +3387,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime-corejs3@npm:^7.24.4":
|
||||
version: 7.24.4
|
||||
resolution: "@babel/runtime-corejs3@npm:7.24.4"
|
||||
dependencies:
|
||||
core-js-pure: "npm:^3.30.2"
|
||||
regenerator-runtime: "npm:^0.14.0"
|
||||
checksum: 121bec9a0b505e2995c4b71cf480167e006e8ee423f77bccc38975bfbfbfdb191192ff03557c18fad6de8f2b85c12c49aaa4b92d1d5fe0c0e136da664129be1e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/runtime@npm:7.23.5"
|
||||
@ -30042,6 +30052,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"imask@npm:^7.6.0":
|
||||
version: 7.6.0
|
||||
resolution: "imask@npm:7.6.0"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": "npm:^7.24.4"
|
||||
checksum: c754210124efbb5dcaa37e9e21497dc9f166e7fb5759853840e49c4cde1bac61bc12f23ca5c6150a2fa57246d762d3849ad3203f5c803fc22d928e8b546b95d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immer@npm:^10.0.2":
|
||||
version: 10.0.3
|
||||
resolution: "immer@npm:10.0.3"
|
||||
@ -40738,6 +40757,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-imask@npm:^7.6.0":
|
||||
version: 7.6.0
|
||||
resolution: "react-imask@npm:7.6.0"
|
||||
dependencies:
|
||||
imask: "npm:^7.6.0"
|
||||
prop-types: "npm:^15.8.1"
|
||||
peerDependencies:
|
||||
react: ">=0.14.0"
|
||||
checksum: f5e7d9a865943ebf05d1c28819d8489a9f36cdeb2a005de340121636dbcb5e3b265017f2b64d7c40268bb5b16b9cf969721f371f18528bfe68a3b86cd8be2373
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-intersection-observer@npm:^9.5.2":
|
||||
version: 9.5.3
|
||||
resolution: "react-intersection-observer@npm:9.5.3"
|
||||
@ -45980,6 +46011,7 @@ __metadata:
|
||||
react-hook-form: "npm:^7.45.1"
|
||||
react-hotkeys-hook: "npm:^4.4.4"
|
||||
react-icons: "npm:^4.12.0"
|
||||
react-imask: "npm:^7.6.0"
|
||||
react-intersection-observer: "npm:^9.5.2"
|
||||
react-loading-skeleton: "npm:^3.3.1"
|
||||
react-phone-number-input: "npm:^3.3.4"
|
||||
|
Loading…
Reference in New Issue
Block a user