diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 9f76e2645..5969d960b 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -154,9 +154,7 @@ "noTransfer": "No, take me home", "share": "Share", "timeShownIn": "Times shown in {timeZone}", - "timeShownInLocalTime": "Times shown in local time", "editDetailsDescription": "Change the details of your event.", - "editOptionsDescription": "Change the options available in your poll.", "finalizeDescription": "Select a final date for your event.", "notificationsGuestTooltip": "Create an account or login to turn get notifications", "planFree": "Free", @@ -213,5 +211,7 @@ "hideScoresDescription": "Only show scores until after a participant has voted", "disableComments": "Disable comments", "disableCommentsDescription": "Remove the option to leave a comment on the poll", - "planCustomizablePollSettings": "Customizable poll settings" + "planCustomizablePollSettings": "Customizable poll settings", + "clockPreferences": "Clock Preferences", + "clockPreferencesDescription": "Set your preferred time zone and time format." } diff --git a/apps/web/src/components/clock.tsx b/apps/web/src/components/clock.tsx new file mode 100644 index 000000000..2c9cf9078 --- /dev/null +++ b/apps/web/src/components/clock.tsx @@ -0,0 +1,146 @@ +import { trpc } from "@rallly/backend"; +import { GlobeIcon } from "@rallly/icons"; +import { cn } from "@rallly/ui"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@rallly/ui/dialog"; +import { Label } from "@rallly/ui/label"; +import dayjs from "dayjs"; +import React from "react"; +import { useInterval } from "react-use"; +import spacetime from "spacetime"; +import soft from "timezone-soft"; + +import { TimeFormatPicker } from "@/components/time-format-picker"; +import { TimeZoneSelect } from "@/components/time-zone-picker/time-zone-select"; +import { Trans } from "@/components/trans"; +import { useDayjs } from "@/utils/dayjs"; + +export const TimePreferences = () => { + const { timeZone, timeFormat } = useDayjs(); + const queryClient = trpc.useContext(); + + const { data } = trpc.userPreferences.get.useQuery(); + + const updatePreferences = trpc.userPreferences.update.useMutation({ + onMutate: (newPreferences) => { + queryClient.userPreferences.get.setData(undefined, (oldPreferences) => { + if (!oldPreferences) { + return null; + } + return { + ...oldPreferences, + timeFormat: newPreferences.timeFormat ?? oldPreferences?.timeFormat, + timeZone: newPreferences.timeZone ?? oldPreferences?.timeZone ?? null, + weekStart: newPreferences.weekStart ?? oldPreferences?.weekStart, + }; + }); + }, + onSuccess: () => { + queryClient.userPreferences.get.invalidate(); + }, + }); + + if (data === undefined) { + return null; + } + + return ( +
+
+ + { + updatePreferences.mutate({ + timeZone: newTimeZone, + }); + }} + /> +
+
+ + { + updatePreferences.mutate({ + timeFormat: newTimeFormat, + }); + }} + /> +
+
+ ); +}; + +export const Clock = ({ className }: { className?: string }) => { + const { timeZone, timeFormat } = useDayjs(); + const timeZoneDisplayFormat = soft(timeZone)[0]; + const now = spacetime.now(timeZone); + const standardAbbrev = timeZoneDisplayFormat.standard.abbr; + const dstAbbrev = timeZoneDisplayFormat.daylight?.abbr; + const abbrev = now.isDST() ? dstAbbrev : standardAbbrev; + const [time, setTime] = React.useState(new Date()); + useInterval(() => { + setTime(new Date()); + }, 1000); + + return ( + {`${dayjs(time).tz(timeZone).format("LT")} ${abbrev}`} + ); +}; + +export const TimesShownIn = () => { + const { timeZone } = useDayjs(); + const timeZoneDisplayFormat = soft(timeZone)[0]; + const now = spacetime.now(timeZone); + const standard = timeZoneDisplayFormat.standard.name; + const dst = timeZoneDisplayFormat.daylight?.name; + const timeZoneName = now.isDST() ? dst : standard; + + return ( + + + + ); +}; + +export const ClockPreferences = ({ children }: React.PropsWithChildren) => { + return ( + + {children} + + + + + + + + + +
+ +
+ +
+
+ ); +}; diff --git a/apps/web/src/components/layouts/standard-layout.tsx b/apps/web/src/components/layouts/standard-layout.tsx index abe2b4f20..43b49c9e0 100644 --- a/apps/web/src/components/layouts/standard-layout.tsx +++ b/apps/web/src/components/layouts/standard-layout.tsx @@ -1,23 +1,20 @@ -import { ClockIcon, ListIcon, LogInIcon } from "@rallly/icons"; +import { ListIcon, LogInIcon } from "@rallly/icons"; import { cn } from "@rallly/ui"; +import { Button } from "@rallly/ui/button"; import clsx from "clsx"; -import dayjs from "dayjs"; import { AnimatePresence, m } from "framer-motion"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import React from "react"; import { Toaster } from "react-hot-toast"; -import { useInterval } from "react-use"; -import spacetime from "spacetime"; -import soft from "timezone-soft"; +import { Clock, ClockPreferences } from "@/components/clock"; import { Container } from "@/components/container"; import FeedbackButton from "@/components/feedback"; import { Spinner } from "@/components/spinner"; import { Trans } from "@/components/trans"; import { UserDropdown } from "@/components/user-dropdown"; -import { useDayjs } from "@/utils/dayjs"; import { IconComponent, NextPageWithLayout } from "../../types"; import ModalProvider from "../modal/modal-provider"; @@ -28,27 +25,32 @@ const NavMenuItem = ({ target, label, icon: Icon, + className, }: { icon: IconComponent; href: string; target?: string; label: React.ReactNode; + className?: string; }) => { const router = useRouter(); return ( - - - {label} - + ); }; @@ -93,28 +95,6 @@ const Logo = () => { ); }; -const Clock = () => { - const { timeZone } = useDayjs(); - const timeZoneDisplayFormat = soft(timeZone)[0]; - const now = spacetime.now(timeZone); - const standardAbbrev = timeZoneDisplayFormat.standard.abbr; - const dstAbbrev = timeZoneDisplayFormat.daylight?.abbr; - const abbrev = now.isDST() ? dstAbbrev : standardAbbrev; - const [time, setTime] = React.useState(new Date()); - - useInterval(() => { - setTime(new Date()); - }, 1000); - - return ( - - ); -}; - const MainNav = () => { return ( {
-
diff --git a/apps/web/src/components/poll/desktop-poll.tsx b/apps/web/src/components/poll/desktop-poll.tsx index f1396ed22..497efa75e 100644 --- a/apps/web/src/components/poll/desktop-poll.tsx +++ b/apps/web/src/components/poll/desktop-poll.tsx @@ -10,9 +10,9 @@ import { Trans, useTranslation } from "next-i18next"; import * as React from "react"; import { useMeasure, useUpdateEffect } from "react-use"; +import { TimesShownIn } from "@/components/clock"; import { usePermissions } from "@/contexts/permissions"; import { useRole } from "@/contexts/role"; -import { TimePreferences } from "@/contexts/time-preferences"; import { useNewParticipantModal } from "../new-participant-modal"; import { @@ -193,8 +193,8 @@ const Poll: React.FunctionComponent = () => { {poll.options[0].duration !== 0 ? ( -
- +
+
) : null}
diff --git a/apps/web/src/components/poll/manage-poll/finalize-poll-dialog.tsx b/apps/web/src/components/poll/manage-poll/finalize-poll-dialog.tsx index d18cd18d2..f0a20fbfa 100644 --- a/apps/web/src/components/poll/manage-poll/finalize-poll-dialog.tsx +++ b/apps/web/src/components/poll/manage-poll/finalize-poll-dialog.tsx @@ -18,8 +18,7 @@ import { DateIcon } from "@/components/date-icon"; import { useParticipants } from "@/components/participants-provider"; import { ConnectedScoreSummary } from "@/components/poll/score-summary"; import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar"; -import { usePoll } from "@/contexts/poll"; -import { useDateFormatter } from "@/contexts/time-preferences"; +import { useDateFormatter, usePoll } from "@/contexts/poll"; const formSchema = z.object({ selectedOptionId: z.string(), diff --git a/apps/web/src/components/poll/manage-poll/notify-participants-form.tsx b/apps/web/src/components/poll/manage-poll/notify-participants-form.tsx index 088bd0594..32bd4d906 100644 --- a/apps/web/src/components/poll/manage-poll/notify-participants-form.tsx +++ b/apps/web/src/components/poll/manage-poll/notify-participants-form.tsx @@ -18,8 +18,7 @@ import { DateIcon } from "@/components/date-icon"; import { useParticipants } from "@/components/participants-provider"; import { ConnectedScoreSummary } from "@/components/poll/score-summary"; import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar"; -import { usePoll } from "@/contexts/poll"; -import { useDateFormatter } from "@/contexts/time-preferences"; +import { useDateFormatter, usePoll } from "@/contexts/poll"; const formSchema = z.object({ selectedOptionId: z.string(), diff --git a/apps/web/src/components/poll/mobile-poll.tsx b/apps/web/src/components/poll/mobile-poll.tsx index 51d56600f..25fb0978b 100644 --- a/apps/web/src/components/poll/mobile-poll.tsx +++ b/apps/web/src/components/poll/mobile-poll.tsx @@ -8,10 +8,10 @@ import { FormProvider, useForm } from "react-hook-form"; import { useUpdateEffect } from "react-use"; import smoothscroll from "smoothscroll-polyfill"; +import { TimesShownIn } from "@/components/clock"; import { ParticipantDropdown } from "@/components/participant-dropdown"; import { useOptions, usePoll } from "@/components/poll-context"; import { usePermissions } from "@/contexts/permissions"; -import { TimePreferences } from "@/contexts/time-preferences"; import { styleMenuItem } from "../menu-styles"; import { useNewParticipantModal } from "../new-participant-modal"; @@ -216,8 +216,8 @@ const MobilePoll: React.FunctionComponent = () => {
{poll.options[0].duration !== 0 ? ( -
- +
+
) : null} void }>, -) => { - return ( - - - - { - updatePreferences.mutate({ - timeZone: value, - }); - setIsOpen(false); - }} - /> - - - -
- { + + + + + + + { updatePreferences.mutate({ - timeFormat: newTimeFormat, + timeZone: value, }); + setIsOpen(false); }} /> -
-
+ + ); }; export const useDateFormatter = () => { diff --git a/packages/ui/button.tsx b/packages/ui/button.tsx index 2cc001227..49835f950 100644 --- a/packages/ui/button.tsx +++ b/packages/ui/button.tsx @@ -18,7 +18,7 @@ const buttonVariants = cva( "rounded-md px-3.5 py-2.5 hover:shadow-sm active:shadow-none data-[state=open]:shadow-none data-[state=open]:bg-gray-100 active:bg-gray-100 hover:bg-white/50 bg-gray-50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "border-transparent hover:bg-gray-200/50 active:bg-gray-200", + ghost: "border-transparent hover:bg-gray-200 active:bg-gray-300", link: "underline-offset-4 hover:underline text-primary", }, size: { diff --git a/packages/ui/dialog.tsx b/packages/ui/dialog.tsx index f8fc7ae4d..cae5e9917 100644 --- a/packages/ui/dialog.tsx +++ b/packages/ui/dialog.tsx @@ -9,6 +9,7 @@ import { cn } from "./lib/utils"; const Dialog = DialogPrimitive.Root; const DialogTrigger = DialogPrimitive.Trigger; +const DialogClose = DialogPrimitive.Close; const DialogPortal = ({ className, @@ -128,6 +129,7 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, diff --git a/packages/ui/popover.tsx b/packages/ui/popover.tsx index 698d9fed4..0aabe3dcc 100644 --- a/packages/ui/popover.tsx +++ b/packages/ui/popover.tsx @@ -18,8 +18,9 @@ const PopoverContent = React.forwardRef< ref={ref} align={align} sideOffset={sideOffset} + collisionPadding={12} className={cn( - "animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 rounded-md border bg-white p-4 shadow-md outline-none", + "animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 shadow-huge z-50 rounded-md border bg-white p-4 outline-none", className, )} {...props}