diff --git a/components/project/events/date-time-picker.tsx b/components/project/events/date-time-picker.tsx index d381d1d..d188a6d 100644 --- a/components/project/events/date-time-picker.tsx +++ b/components/project/events/date-time-picker.tsx @@ -1,277 +1,207 @@ "use client"; +import { format } from "date-fns"; +import * as React from "react"; + import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; -import { toStartOfDay } from "@/lib/utils/date"; -import { - type Period, - type TimePickerType, - getArrowByType, - getDateByType, - setDateByType, -} from "@/lib/utils/time"; -import { add, format } from "date-fns"; -import { Calendar as CalendarIcon, Clock } from "lucide-react"; -import * as React from "react"; +import { CalendarIcon } from "lucide-react"; -export interface TimePickerInputProps - extends React.InputHTMLAttributes { - picker: TimePickerType; - date: Date | undefined; - setDate: React.Dispatch>; - period?: Period; - onRightFocus?: () => void; - onLeftFocus?: () => void; +export function DateTimePicker(props: { + name: string; + defaultValue?: Date; + dateOnly?: boolean; + onSelect?: (date: Date) => void; +}) { + return props.dateOnly ? : ; } -interface TimePickerProps { - date: Date | undefined; - setDate: React.Dispatch>; -} +function TimePicker(props: { + name: string; + defaultValue?: Date; + onSelect?: (date: Date) => void; +}) { + const [date, setDate] = React.useState( + props.defaultValue ? new Date(props.defaultValue) : undefined, + ); + const [isOpen, setIsOpen] = React.useState(false); + + const hours = Array.from({ length: 12 }, (_, i) => i + 1); + const handleDateSelect = (selectedDate: Date | undefined) => { + if (selectedDate) { + setDate(selectedDate); + props.onSelect?.(selectedDate); + } + }; -const TimePickerInput = React.forwardRef< - HTMLInputElement, - TimePickerInputProps ->( - ( - { - className, - type = "tel", - value, - id, - name, - date = new Date(new Date().setHours(0, 0, 0, 0)), - setDate, - onChange, - onKeyDown, - picker, - period, - onLeftFocus, - onRightFocus, - ...props - }, - ref, + const handleTimeChange = ( + type: "hour" | "minute" | "ampm", + value: string, ) => { - const [flag, setFlag] = React.useState(false); - const [prevIntKey, setPrevIntKey] = React.useState("0"); - - /** - * allow the user to enter the second digit within 2 seconds - * otherwise start again with entering first digit - */ - React.useEffect(() => { - if (flag) { - const timer = setTimeout(() => { - setFlag(false); - }, 2000); - - return () => clearTimeout(timer); - } - }, [flag]); - - const calculatedValue = React.useMemo(() => { - return getDateByType(date, picker); - }, [date, picker]); - - const calculateNewValue = (key: string) => { - /* - * If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1. - * The second entered digit will break the condition and the value will be set to 10-12. - */ - if (picker === "12hours") { - if (flag && calculatedValue.slice(1, 2) === "1" && prevIntKey === "0") - return `0${key}`; - } - - return !flag ? `0${key}` : calculatedValue.slice(1, 2) + key; - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Tab") return; - e.preventDefault(); - if (e.key === "ArrowRight") onRightFocus?.(); - if (e.key === "ArrowLeft") onLeftFocus?.(); - if (["ArrowUp", "ArrowDown"].includes(e.key)) { - const step = e.key === "ArrowUp" ? 1 : -1; - const newValue = getArrowByType(calculatedValue, step, picker); - if (flag) setFlag(false); - const tempDate = new Date(date); - setDate(setDateByType(tempDate, newValue, picker, period)); - } - if (e.key >= "0" && e.key <= "9") { - if (picker === "12hours") setPrevIntKey(e.key); - - const newValue = calculateNewValue(e.key); - if (flag) onRightFocus?.(); - setFlag((prev) => !prev); - const tempDate = new Date(date); - setDate(setDateByType(tempDate, newValue, picker, period)); + if (date) { + const newDate = new Date(date); + if (type === "hour") { + newDate.setHours( + (Number.parseInt(value) % 12) + (newDate.getHours() >= 12 ? 12 : 0), + ); + } else if (type === "minute") { + newDate.setMinutes(Number.parseInt(value)); + } else if (type === "ampm") { + const currentHours = newDate.getHours(); + newDate.setHours( + value === "PM" ? currentHours + 12 : currentHours - 12, + ); } - }; - - return ( - { - e.preventDefault(); - onChange?.(e); - }} - type={type} - inputMode="decimal" - onKeyDown={(e) => { - onKeyDown?.(e); - handleKeyDown(e); - }} - {...props} - /> - ); - }, -); - -TimePickerInput.displayName = "TimePickerInput"; - -export { TimePickerInput }; - -export function TimePicker({ date, setDate }: TimePickerProps) { - const minuteRef = React.useRef(null); - const hourRef = React.useRef(null); - const secondRef = React.useRef(null); + setDate(newDate); + } + }; return ( -
-
- - minuteRef.current?.focus()} + + + + + + -
-
- - hourRef.current?.focus()} - onRightFocus={() => secondRef.current?.focus()} - /> -
-
- - minuteRef.current?.focus()} - /> -
-
- -
-
+
+ +
+ +
+ {hours.reverse().map((hour) => ( + + ))} +
+ +
+ +
+ {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => ( + + ))} +
+ +
+ +
+ {["AM", "PM"].map((ampm) => ( + + ))} +
+
+
+
+ + ); } -export function DateTimePicker({ - name, - defaultValue, - dateOnly = false, - onSelect, -}: { +function DatePicker(props: { name: string; - defaultValue?: string | undefined; - dateOnly?: boolean; + defaultValue?: Date; onSelect?: (date: Date) => void; }) { const [date, setDate] = React.useState( - defaultValue ? new Date(defaultValue) : undefined, + props.defaultValue ? new Date(props.defaultValue) : undefined, ); - /** - * carry over the current time when a user clicks a new day - * instead of resetting to 00:00 - */ - const handleSelect = (newDay: Date | undefined) => { - if (!newDay) return; - - const newDate = toStartOfDay(newDay); - const diff = newDay.getTime() - newDate.getTime(); - const diffInDays = diff / (1000 * 60 * 60 * 24); - const newDateFull = add(newDate, { days: Math.ceil(diffInDays) }); - setDate(newDateFull); - onSelect?.(newDateFull); - }; - return ( - <> - - - - - - - handleSelect(d)} - initialFocus - /> - {!dateOnly ? ( -
- -
- ) : null} - -
-
- + + + + + + + { + setDate(date); + if (date) props.onSelect?.(date); + }} + initialFocus + /> + + ); } diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx new file mode 100644 index 0000000..0b4a48d --- /dev/null +++ b/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/package.json b/package.json index 1dcc122..bab4f26 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78e4b95..df1d0af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@radix-ui/react-progress': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-scroll-area': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-select': specifier: ^1.2.2 version: 1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1192,6 +1195,9 @@ packages: '@radix-ui/number@1.0.1': resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.0.0': resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} @@ -1201,6 +1207,9 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-arrow@1.0.3': resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -1302,6 +1311,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context-menu@2.2.1': resolution: {integrity: sha512-wvMKKIeb3eOrkJ96s722vcidZ+2ZNfcYZWBPRHIB1VWrF+fiF851Io6LX0kmK5wTDQFKdulCCKJk2c3SBaQHvA==} peerDependencies: @@ -1338,6 +1356,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.0.0': resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} peerDependencies: @@ -1614,6 +1641,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@1.0.0': resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} peerDependencies: @@ -1646,6 +1686,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-progress@1.1.0': resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} peerDependencies: @@ -1672,6 +1725,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-scroll-area@1.2.2': + resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==} + peerDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-select@1.2.2': resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} peerDependencies: @@ -1721,6 +1787,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': 19.0.1 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.1.0': resolution: {integrity: sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==} peerDependencies: @@ -5690,6 +5765,8 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.0.0': dependencies: '@babel/runtime': 7.25.0 @@ -5700,6 +5777,8 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-arrow@1.0.3(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -5790,6 +5869,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + '@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.1)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.1 + '@radix-ui/react-context-menu@2.2.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -5822,6 +5907,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + '@radix-ui/react-context@1.1.1(@types/react@19.0.1)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.1 + '@radix-ui/react-dialog@1.0.0(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6143,6 +6234,16 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-primitive@1.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6169,6 +6270,15 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-primitive@2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-progress@1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.0.0) @@ -6196,6 +6306,23 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-scroll-area@1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-select@1.2.2(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.0 @@ -6256,6 +6383,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.1 + '@radix-ui/react-slot@1.1.1(@types/react@19.0.1)(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.1 + '@radix-ui/react-switch@1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.0