diff --git a/apps/web/package.json b/apps/web/package.json index 1bcfd6b55..d784ba4e7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,7 +21,6 @@ "dependencies": { "@auth/prisma-adapter": "^1.0.3", "@floating-ui/react-dom-interactions": "^0.13.3", - "@headlessui/react": "^1.7.7", "@hookform/resolvers": "^3.3.1", "@next/bundle-analyzer": "^12.3.4", "@radix-ui/react-slot": "^1.0.1", diff --git a/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx index 826f88610..d9e29e07b 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx @@ -123,7 +123,7 @@ const Page = () => { if (optionsToDeleteThatHaveVotes.length > 0) { modalContext.render({ title: t("areYouSure"), - description: ( + content: ( }} @@ -131,7 +131,7 @@ const Page = () => { ), onOk, okButtonProps: { - type: "danger", + variant: "destructive", }, okText: t("delete"), cancelText: t("cancel"), diff --git a/apps/web/src/components/compact-button.tsx b/apps/web/src/components/compact-button.tsx deleted file mode 100644 index acfab585d..000000000 --- a/apps/web/src/components/compact-button.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; - -export interface CompactButtonProps { - icon?: React.ComponentType<{ - className?: string; - style?: React.CSSProperties; - }>; - children?: React.ReactNode; - onClick?: () => void; -} - -const CompactButton = React.forwardRef( - ({ icon: Icon, children, onClick }, ref) => { - return ( - - ); - }, -); - -CompactButton.displayName = "CompactButton"; - -export default CompactButton; diff --git a/apps/web/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx b/apps/web/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx index 7aa0a1dce..d1539f9f2 100644 --- a/apps/web/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx +++ b/apps/web/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx @@ -4,8 +4,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@rallly/ui/dropdown-menu"; import { Icon } from "@rallly/ui/icon"; @@ -14,6 +12,7 @@ import clsx from "clsx"; import dayjs from "dayjs"; import { CalendarIcon, + CalendarXIcon, ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, @@ -34,7 +33,6 @@ import { getDateProps, removeAllOptionsForDay, } from "../../../../utils/date-time-utils"; -import CompactButton from "../../../compact-button"; import DateCard from "../../../date-card"; import { useHeadlessDatePicker } from "../../../headless-date-picker"; import { DateTimeOption } from ".."; @@ -330,15 +328,20 @@ const MonthCalendar: React.FunctionComponent = ({ ); }} /> - { onChange([ ...options.slice(0, index), ...options.slice(index + 1), ]); }} - /> + > + + + + ); })} @@ -380,15 +383,13 @@ const MonthCalendar: React.FunctionComponent = ({ - + - - - - { const times = optionsForDay.map( @@ -430,7 +431,9 @@ const MonthCalendar: React.FunctionComponent = ({ onChange(newOptions); }} > - + + + = ({ ); }} > - + + + diff --git a/apps/web/src/components/forms/poll-options-form/month-calendar/time-picker.tsx b/apps/web/src/components/forms/poll-options-form/month-calendar/time-picker.tsx index 936975c3f..27c8957aa 100644 --- a/apps/web/src/components/forms/poll-options-form/month-calendar/time-picker.tsx +++ b/apps/web/src/components/forms/poll-options-form/month-calendar/time-picker.tsx @@ -1,23 +1,18 @@ +import { Button } from "@rallly/ui/button"; import { - flip, - FloatingPortal, - offset, - size, - useFloating, -} from "@floating-ui/react-dom-interactions"; -import { Listbox } from "@headlessui/react"; -import clsx from "clsx"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@rallly/ui/select"; import dayjs from "dayjs"; -import { ChevronDownIcon } from "lucide-react"; import * as React from "react"; import { getDuration } from "@/utils/date-time-utils"; -import { stopPropagation } from "@/utils/stop-propagation"; - -import { styleMenuItem } from "../../../menu-styles"; export interface TimePickerProps { - value: Date; + value?: Date; after?: Date; className?: string; onChange?: (value: Date) => void; @@ -29,87 +24,69 @@ const TimePicker: React.FunctionComponent = ({ className, after, }) => { - const { reference, floating, x, y, strategy, refs } = useFloating({ - placement: "bottom-start", - strategy: "fixed", - middleware: [ - offset(5), - flip(), - size({ - apply: ({ rects }) => { - if (refs.floating.current) { - Object.assign(refs.floating.current.style, { - minWidth: `${rects.reference.width}px`, - }); - } - }, - }), - ], - }); - - const renderOptions = () => { - const startFromDate = after ? dayjs(after) : dayjs(value).startOf("day"); - - const options: React.ReactNode[] = []; - - for (let i = 1; i <= 96; i++) { - const optionValue = startFromDate.add(i * 15, "minutes"); - options.push( - -
- {optionValue.format("LT")} - {after ? ( - - {getDuration(dayjs(after), optionValue)} - - ) : null} -
-
, - ); + const [open, setOpen] = React.useState(false); + const getOptions = React.useCallback(() => { + if (!open) { + return [dayjs(value).toISOString()]; } - return options; - }; + let cursor = after + ? dayjs(after).add(15, "minutes") + : dayjs(value).startOf("day"); + + const res: string[] = []; + while (cursor.isSame(value, "day")) { + res.push(cursor.toISOString()); + cursor = cursor.add(15, "minutes"); + } + return res; + }, [after, open, value]); return ( - { - onChange?.(new Date(newValue)); + ); }; diff --git a/apps/web/src/components/forms/poll-options-form/poll-options-form.tsx b/apps/web/src/components/forms/poll-options-form/poll-options-form.tsx index 550ba5822..9d7fa4608 100644 --- a/apps/web/src/components/forms/poll-options-form/poll-options-form.tsx +++ b/apps/web/src/components/forms/poll-options-form/poll-options-form.tsx @@ -2,6 +2,14 @@ import { cn } from "@rallly/ui"; import { Button } from "@rallly/ui/button"; import { Card, CardDescription, CardHeader, CardTitle } from "@rallly/ui/card"; import { CommandDialog } from "@rallly/ui/command"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + useDialog, +} from "@rallly/ui/dialog"; import { FormField, FormMessage } from "@rallly/ui/form"; import { Label } from "@rallly/ui/label"; import { Switch } from "@rallly/ui/switch"; @@ -15,7 +23,6 @@ import { useFormContext } from "react-hook-form"; import { TimeZoneCommand } from "@/components/time-zone-picker/time-zone-select"; import { getBrowserTimeZone } from "../../../utils/date-time-utils"; -import { useModal } from "../../modal"; import { NewEventData } from "../types"; import MonthCalendar from "./month-calendar"; import { DateTimeOption } from "./types"; @@ -73,38 +80,17 @@ const PollOptionsForm = ({ options.length === 0 || options.some((option) => option.type !== "timeSlot"); - const [dateOrTimeRangeModal, openDateOrTimeRangeModal] = useModal({ - title: t("mixedOptionsTitle"), - description: t("mixedOptionsDescription"), - okText: t("mixedOptionsKeepTimes"), - onOk: () => { - setValue( - "options", - watchOptions.filter((option) => option.type === "timeSlot"), - ); - if (!watchTimeZone) { - setValue("timeZone", getBrowserTimeZone()); - } - }, - cancelText: t("mixedOptionsKeepDates"), - onCancel: () => { - setValue( - "options", - watchOptions.filter((option) => option.type === "date"), - ); - setValue("timeZone", ""); - }, - }); + const dateOrTimeRangeDialog = useDialog(); React.useEffect(() => { if (watchOptions.length > 1) { const optionType = watchOptions[0].type; // all options needs to be the same type if (watchOptions.some((option) => option.type !== optionType)) { - openDateOrTimeRangeModal(); + dateOrTimeRangeDialog.trigger(); } } - }, [watchOptions, openDateOrTimeRangeModal]); + }, [watchOptions, dateOrTimeRangeDialog]); const watchNavigationDate = watch("navigationDate"); const navigationDate = new Date(watchNavigationDate ?? Date.now()); @@ -146,7 +132,47 @@ const PollOptionsForm = ({ - {dateOrTimeRangeModal} + + + + + + + +

+ +

+ + + + +
+
= ({ visible={true} {...modal.config} content={modal.content} - footer={null} onCancel={() => { remove(id); }} diff --git a/apps/web/src/components/modal/modal.tsx b/apps/web/src/components/modal/modal.tsx index 8b8f2790b..8dd70f623 100644 --- a/apps/web/src/components/modal/modal.tsx +++ b/apps/web/src/components/modal/modal.tsx @@ -1,128 +1,66 @@ -import { Dialog } from "@headlessui/react"; -import { AnimatePresence, m } from "framer-motion"; -import { XIcon } from "lucide-react"; +import { Button, ButtonProps } from "@rallly/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@rallly/ui/dialog"; import * as React from "react"; -import { ButtonProps, LegacyButton } from "../button"; - export interface ModalProps { - description?: React.ReactNode; title?: React.ReactNode; okText?: string; cancelText?: string; okButtonProps?: ButtonProps; onOk?: () => void; onCancel?: () => void; - footer?: React.ReactNode; content?: React.ReactNode; overlayClosable?: boolean; visible?: boolean; - showClose?: boolean; } const Modal: React.FunctionComponent = ({ - description, title, okText, - cancelText, - okButtonProps, - footer, content, - overlayClosable, onCancel, + cancelText, onOk, + okButtonProps, visible, - showClose, }) => { - const initialFocusRef = React.useRef(null); return ( - - {visible ? ( - { - if (overlayClosable) onCancel?.(); - }} - > - { + if (!open) { + onCancel?.(); + } + }} + > + + + {title} + +

{content}

+ + + + + - ) : null} - {content ?? ( -
- {title ? ( - - {title} - - ) : null} - {description ? ( - - {description} - - ) : null} -
- )} - {footer === undefined ? ( -
- {cancelText ? ( - { - onCancel?.(); - }} - > - {cancelText} - - ) : null} - {okText ? ( - { - onOk?.(); - }} - {...okButtonProps} - > - {okText} - - ) : null} -
- ) : null} -
- - - - ) : null} - + {okText} + + + + ); }; diff --git a/apps/web/src/components/modal/use-modal.tsx b/apps/web/src/components/modal/use-modal.tsx index 6d77f4234..2872e1407 100644 --- a/apps/web/src/components/modal/use-modal.tsx +++ b/apps/web/src/components/modal/use-modal.tsx @@ -25,12 +25,3 @@ export const useModal = ( ); return [modal, () => setVisible(true), () => setVisible(false)]; }; - -export const useModalState = (): [boolean, OpenModalFn, CloseModalFn] => { - const [visible, setVisible] = React.useState(false); - - const hide = React.useCallback(() => setVisible(false), []); - const show = React.useCallback(() => setVisible(true), []); - - return [visible, show, hide]; -}; diff --git a/apps/web/src/components/switch.tsx b/apps/web/src/components/switch.tsx deleted file mode 100644 index a025a5fa4..000000000 --- a/apps/web/src/components/switch.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Switch as HeadlessSwitch } from "@headlessui/react"; -import clsx from "clsx"; -import * as React from "react"; - -export interface SwitchProps { - checked?: boolean; - onChange: (checked: boolean) => void; - srDescription?: string; -} - -const Switch: React.FunctionComponent = ({ - checked = false, - onChange, - srDescription, - ...rest -}) => { - return ( - - {srDescription ? {srDescription} : null} - - ); -}; - -export default Switch; diff --git a/apps/web/tests/edit-options.spec.ts b/apps/web/tests/edit-options.spec.ts index 9198f2fb3..451af7abe 100644 --- a/apps/web/tests/edit-options.spec.ts +++ b/apps/web/tests/edit-options.spec.ts @@ -26,7 +26,8 @@ test.describe("edit options", () => { editOptionsPage.switchToSpecifyTimes(); await page.click("text='12:00 PM'"); - await page.click("text='1:00 PM'"); + const listbox = page.getByRole("listbox"); + listbox.getByText("1:00 PM", { exact: true }).click(); await page.getByRole("button", { name: "Save" }).click(); await expect(page.locator('text="Are you sure?"')).toBeVisible(); await page.click("text='Delete'"); diff --git a/yarn.lock b/yarn.lock index 764132d6c..9360f235e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1969,13 +1969,6 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@headlessui/react@^1.7.7": - version "1.7.12" - resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.12.tgz" - integrity sha512-FhSx5V+Qp0GvbTpaxyS+ymGDDNntCacClWsk/d8Upbr19g3AsPbjfPk4+m2CgJGcuCB5Dz7LpUIOAbvQTyjL2g== - dependencies: - client-only "^0.0.1" - "@heroicons/react@^1.0.6": version "1.0.6" resolved "https://registry.npmjs.org/@heroicons/react/-/react-1.0.6.tgz" @@ -5750,7 +5743,7 @@ cli-spinners@^2.5.0: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz" integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== -client-only@0.0.1, client-only@^0.0.1: +client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==