// @ts-nocheck
'use client';
import React from 'react';
import { parseDate } from 'chrono-node';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/website/ui/popover';
import { ActiveModifiers } from 'react-day-picker';
import { Calendar, CalendarProps } from '@/components/website/ui/calendar';
import { Button, buttonVariants } from '@/components/website/ui/button';
import { cn } from '@/lib/utils';
import { Calendar as CalendarIcon, LucideTextCursorInput } from 'lucide-react';
import { ScrollArea } from '@/components/website/ui/scroll-area';
/* -------------------------------------------------------------------------- */
/* Inspired By: */
/* @steventey */
/* ------------------https://dub.co/blog/smart-datetime-picker--------------- */
/* -------------------------------------------------------------------------- */
/**
* Utility function that parses dates.
* Parses a given date string using the `chrono-node` library.
*
* @param str - A string representation of a date and time.
* @returns A `Date` object representing the parsed date and time, or `null` if the string could not be parsed.
*/
export const parseDateTime = (str: Date | string) => {
if (str instanceof Date) return str;
return parseDate(str);
};
/**
* Converts a given timestamp or the current date and time to a string representation in the local time zone.
* format: `HH:mm`, adjusted for the local time zone.
*
* @param timestamp {Date | string}
* @returns A string representation of the timestamp
*/
export const getDateTimeLocal = (timestamp?: Date): string => {
const d = timestamp ? new Date(timestamp) : new Date();
if (d.toString() === 'Invalid Date') return '';
return new Date(d.getTime() - d.getTimezoneOffset() * 60000)
.toISOString()
.split(':')
.slice(0, 2)
.join(':');
};
/**
* Formats a given date and time object or string into a human-readable string representation.
* "MMM D, YYYY h:mm A" (e.g. "Jan 1, 2023 12:00 PM").
*
* @param datetime - {Date | string}
* @returns A string representation of the date and time
*/
const formatTimeOnly = (datetime: Date | string) => {
return new Date(datetime).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
});
};
const formatDateOnly = (datetime: Date | string) => {
return new Date(datetime).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
};
const formatDateTime = (
datetime: Date | string,
showCalendar: boolean,
showTimePicker: boolean
) => {
if (!showCalendar && showTimePicker) {
return formatTimeOnly(datetime);
}
if (showCalendar && !showTimePicker) {
return formatDateOnly(datetime);
}
return new Date(datetime).toLocaleTimeString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
});
};
const inputBase =
'bg-transparent focus:outline-hidden focus:ring-0 focus-within:outline-hidden focus-within:ring-0 sm:text-sm disabled:cursor-not-allowed disabled:opacity-50';
// @source: https://www.perplexity.ai/search/in-javascript-how-RfI7fMtITxKr5c.V9Lv5KA#1
// use this pattern to validate the transformed date string for the natural language input
const naturalInputValidationPattern =
'^[A-Z][a-z]{2}sd{1,2},sd{4},sd{1,2}:d{2}s[AP]M$';
const DEFAULT_SIZE = 96;
/**
* Smart time input Docs: {@link: https://shadcn-extension.vercel.app/docs/smart-time-input}
*/
interface SmartDatetimeInputProps {
value?: Date;
onValueChange: (date: Date) => void;
showCalendar?: boolean;
showTimePicker?: boolean;
}
interface SmartDatetimeInputContextProps extends SmartDatetimeInputProps {
Time: string;
onTimeChange: (time: string) => void;
}
const SmartDatetimeInputContext =
React.createContext<SmartDatetimeInputContextProps | null>(null);
const useSmartDateInput = () => {
const context = React.useContext(SmartDatetimeInputContext);
if (!context) {
throw new Error(
'useSmartDateInput must be used within SmartDateInputProvider'
);
}
return context;
};
export const SmartDatetimeInput = React.forwardRef<
HTMLInputElement,
Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'type' | 'ref' | 'value' | 'defaultValue' | 'onBlur'
> &
SmartDatetimeInputProps
>(
(
{
className,
value,
onValueChange,
placeholder,
disabled,
showCalendar = true,
showTimePicker = true,
},
ref
) => {
const [Time, setTime] = React.useState<string>('');
const onTimeChange = React.useCallback((time: string) => {
setTime(time);
}, []);
// If neither calendar nor timepicker is specified, show both
const shouldShowBoth = showCalendar === showTimePicker;
return (
<SmartDatetimeInputContext.Provider
value={{
value,
onValueChange,
Time,
onTimeChange,
showCalendar: shouldShowBoth ? true : showCalendar,
showTimePicker: shouldShowBoth ? true : showTimePicker,
}}
>
<div className='flex items-center justify-center dark:bg-neutral-950 bg-neutral-50'>
<div
className={cn(
'flex gap-1 w-full p-1 items-center justify-between rounded-md border-2 transition-all',
'focus-within:outline-0 focus:outline-0 focus:ring-0',
'placeholder:text-muted-foreground focus-visible:outline-0 ',
className
)}
>
<DateTimeLocalInput />
<NaturalLanguageInput
placeholder={placeholder}
disabled={disabled}
ref={ref}
/>
</div>
</div>
</SmartDatetimeInputContext.Provider>
);
}
);
SmartDatetimeInput.displayName = 'DatetimeInput';
// Make it a standalone component
const TimePicker = () => {
const { value, onValueChange, Time, onTimeChange } = useSmartDateInput();
const [activeIndex, setActiveIndex] = React.useState(-1);
const timestamp = 15;
const formateSelectedTime = React.useCallback(
(time: string, hour: number, partStamp: number) => {
onTimeChange(time);
let newVal = value ? new Date(value) : new Date();
// If no value exists, use current date but only set the time
newVal.setHours(
hour,
partStamp === 0 ? parseInt('00') : timestamp * partStamp
);
onValueChange(newVal);
},
[value, onValueChange, onTimeChange]
);
const handleKeydown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
e.stopPropagation();
if (!document) return;
const moveNext = () => {
const nextIndex =
activeIndex + 1 > DEFAULT_SIZE - 1 ? 0 : activeIndex + 1;
const currentElm = document.getElementById(`time-${nextIndex}`);
currentElm?.focus();
setActiveIndex(nextIndex);
};
const movePrev = () => {
const prevIndex =
activeIndex - 1 < 0 ? DEFAULT_SIZE - 1 : activeIndex - 1;
const currentElm = document.getElementById(`time-${prevIndex}`);
currentElm?.focus();
setActiveIndex(prevIndex);
};
const setElement = () => {
const currentElm = document.getElementById(`time-${activeIndex}`);
if (!currentElm) return;
currentElm.focus();
const timeValue = currentElm.textContent ?? '';
// this should work now haha that hour is what does the trick
const PM_AM = timeValue.split(' ')[1];
const PM_AM_hour = parseInt(timeValue.split(' ')[0].split(':')[0]);
const hour =
PM_AM === 'AM'
? PM_AM_hour === 12
? 0
: PM_AM_hour
: PM_AM_hour === 12
? 12
: PM_AM_hour + 12;
const part = Math.floor(
parseInt(timeValue.split(' ')[0].split(':')[1]) / 15
);
formateSelectedTime(timeValue, hour, part);
};
const reset = () => {
const currentElm = document.getElementById(`time-${activeIndex}`);
currentElm?.blur();
setActiveIndex(-1);
};
switch (e.key) {
case 'ArrowUp':
movePrev();
break;
case 'ArrowDown':
moveNext();
break;
case 'Escape':
reset();
break;
case 'Enter':
setElement();
break;
}
},
[activeIndex, formateSelectedTime]
);
const handleClick = React.useCallback(
(hour: number, part: number, PM_AM: string, currentIndex: number) => {
formateSelectedTime(
`${hour}:${part === 0 ? '00' : timestamp * part} ${PM_AM}`,
hour,
part
);
setActiveIndex(currentIndex);
},
[formateSelectedTime]
);
const currentTime = React.useMemo(() => {
const timeVal = Time.split(' ')[0];
return {
hours: parseInt(timeVal.split(':')[0]),
minutes: parseInt(timeVal.split(':')[1]),
};
}, [Time]);
React.useEffect(() => {
const getCurrentElementTime = () => {
const timeVal = Time.split(' ')[0];
const hours = parseInt(timeVal.split(':')[0]);
const minutes = parseInt(timeVal.split(':')[1]);
const PM_AM = Time.split(' ')[1];
const formatIndex =
PM_AM === 'AM' ? hours : hours === 12 ? hours : hours + 12;
const formattedHours = formatIndex;
console.log(formatIndex);
for (let j = 0; j <= 3; j++) {
const diff = Math.abs(j * timestamp - minutes);
const selected =
PM_AM === (formattedHours >= 12 ? 'PM' : 'AM') &&
(minutes <= 53 ? diff < Math.ceil(timestamp / 2) : diff < timestamp);
if (selected) {
const trueIndex =
activeIndex === -1 ? formattedHours * 4 + j : activeIndex;
setActiveIndex(trueIndex);
const currentElm = document.getElementById(`time-${trueIndex}`);
currentElm?.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
}
}
};
getCurrentElementTime();
}, [Time, activeIndex]);
const height = React.useMemo(() => {
if (!document) return;
const calendarElm = document.getElementById('calendar');
if (!calendarElm) return;
return calendarElm.style.height;
}, []);
return (
<div className='space-y-2 pr-3 py-3 relative '>
<h3 className='text-sm font-medium text-center'>Time</h3>
<ScrollArea
onKeyDown={handleKeydown}
className='h-[90%] w-full focus-visible:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-0 py-0.5'
style={{
height,
}}
>
<ul
className={cn(
'flex items-center flex-col gap-1 h-full max-h-56 w-28 px-1 py-0.5'
)}
>
{Array.from({ length: 24 }).map((_, i) => {
const PM_AM = i >= 12 ? 'PM' : 'AM';
const formatIndex = i > 12 ? i % 12 : i === 0 || i === 12 ? 12 : i;
return Array.from({ length: 4 }).map((_, part) => {
const diff = Math.abs(part * timestamp - currentTime.minutes);
const trueIndex = i * 4 + part;
// ? refactor : add the select of the default time on the current device (H:MM)
const isSelected =
(currentTime.hours === i ||
currentTime.hours === formatIndex) &&
Time.split(' ')[1] === PM_AM &&
(currentTime.minutes <= 53
? diff < Math.ceil(timestamp / 2)
: diff < timestamp);
const isSuggested = !value && isSelected;
const currentValue = `${formatIndex}:${
part === 0 ? '00' : timestamp * part
} ${PM_AM}`;
return (
<li
tabIndex={isSelected ? 0 : -1}
id={`time-${trueIndex}`}
key={`time-${trueIndex}`}
aria-label='currentTime'
className={cn(
buttonVariants({
variant: isSuggested
? 'secondary'
: isSelected
? 'default'
: 'outline-solid',
}),
'h-8 px-3 w-full text-sm focus-visible:outline-0 outline-0 focus-visible:border-0 cursor-default ring-0'
)}
onClick={() => handleClick(i, part, PM_AM, trueIndex)}
onFocus={() => isSuggested && setActiveIndex(trueIndex)}
>
{currentValue}
</li>
);
});
})}
</ul>
</ScrollArea>
</div>
);
};
const getDefaultPlaceholder = (
showCalendar: boolean,
showTimePicker: boolean
) => {
if (!showCalendar && showTimePicker) {
return 'e.g. "5pm" or "in 2 hours"';
}
if (showCalendar && !showTimePicker) {
return 'e.g. "tomorrow" or "next monday"';
}
return 'e.g. "tomorrow at 5pm" or "in 2 hours"';
};
const NaturalLanguageInput = React.forwardRef<
HTMLInputElement,
{
placeholder?: string;
disabled?: boolean;
}
>(({ placeholder, ...props }, ref) => {
const {
value,
onValueChange,
Time,
onTimeChange,
showCalendar,
showTimePicker,
} = useSmartDateInput();
const _placeholder =
placeholder ?? getDefaultPlaceholder(showCalendar, showTimePicker);
const [inputValue, setInputValue] = React.useState<string>('');
React.useEffect(() => {
if (!value) {
setInputValue('');
return;
}
const formattedValue = formatDateTime(value, showCalendar, showTimePicker);
setInputValue(formattedValue);
// Only update time if time picker is shown
if (showTimePicker) {
const hour = value.getHours();
const timeVal = `${hour >= 12 ? hour % 12 || 12 : hour || 12}:${String(
value.getMinutes()
).padStart(2, '0')} ${hour >= 12 ? 'PM' : 'AM'}`;
onTimeChange(timeVal);
}
}, [value, showCalendar, showTimePicker]);
const handleParse = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const parsedDateTime = parseDateTime(e.currentTarget.value);
if (parsedDateTime) {
// If only showing time picker, preserve the current date
if (!showCalendar && showTimePicker && value) {
parsedDateTime.setFullYear(
value.getFullYear(),
value.getMonth(),
value.getDate()
);
}
// If only showing calendar, preserve the current time
if (showCalendar && !showTimePicker && value) {
parsedDateTime.setHours(0, 0, 0, 0);
}
// console.log(parsedDateTime);
onValueChange(parsedDateTime);
setInputValue(
formatDateTime(parsedDateTime, showCalendar, showTimePicker)
);
if (showTimePicker) {
const PM_AM = parsedDateTime.getHours() >= 12 ? 'PM' : 'AM';
const PM_AM_hour = parsedDateTime.getHours();
const hour =
PM_AM_hour > 12
? PM_AM_hour % 12
: PM_AM_hour === 0 || PM_AM_hour === 12
? 12
: PM_AM_hour;
onTimeChange(
`${hour}:${String(parsedDateTime.getMinutes()).padStart(
2,
'0'
)} ${PM_AM}`
);
}
}
},
[value, showCalendar, showTimePicker]
);
const handleKeydown = React.useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleParse(e as any);
}
},
[handleParse]
);
return (
<input
ref={ref}
type='text'
placeholder={_placeholder}
value={inputValue}
onChange={(e) => setInputValue(e.currentTarget.value)}
onKeyDown={handleKeydown}
onBlur={handleParse}
className={cn(
'px-2 mr-0.5 dark:bg-neutral-800 bg-neutral-50 flex-1 border-none h-8 rounded-sm',
inputBase
)}
{...props}
/>
);
});
NaturalLanguageInput.displayName = 'NaturalLanguageInput';
type DateTimeLocalInputProps = {} & CalendarProps;
const DateTimeLocalInput = ({
className,
...props
}: DateTimeLocalInputProps) => {
const { value, onValueChange, Time, showCalendar, showTimePicker } =
useSmartDateInput();
const formateSelectedDate = React.useCallback(
(
date: Date | undefined,
selectedDate: Date,
m: ActiveModifiers,
e: React.MouseEvent
) => {
const parsedDateTime = new Date(selectedDate);
if (!showTimePicker) {
// If only calendar is shown, set time to start of day
parsedDateTime.setHours(0, 0, 0, 0);
} else if (value) {
// If time picker is shown, preserve existing time
parsedDateTime.setHours(
value.getHours(),
value.getMinutes(),
value.getSeconds(),
value.getMilliseconds()
);
}
onValueChange(parsedDateTime);
},
[value, showTimePicker, onValueChange]
);
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
size={'icon'}
className={cn(
'size-9 flex items-center justify-center font-normal dark:bg-neutral-800 bg-neutral-200',
!value && 'text-muted-foreground'
)}
>
<CalendarIcon className='size-4' />
<span className='sr-only'>calendar</span>
</Button>
</PopoverTrigger>
<PopoverContent
className='w-auto p-0 dark:bg-neutral-800 bg-neutral-50'
sideOffset={8}
>
<div className='flex gap-1'>
{showCalendar && (
<Calendar
{...props}
id={'calendar'}
className={cn('peer flex justify-end', inputBase, className)}
mode='single'
selected={value}
onSelect={formateSelectedDate}
initialFocus
/>
)}
{showTimePicker && <TimePicker />}
</div>
</PopoverContent>
</Popover>
);
};
DateTimeLocalInput.displayName = 'DateTimeLocalInput';