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 (
+
+ );
+};
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 (
-
- );
-};
-
const TimeFormatPicker = ({
disabled,
value,
onChange,
}: TimeFormatPickerProps) => {
return (
-
- {
- onChange?.("hours12");
- }}
- checked={value === "hours12"}
- >
-
-
- {
- onChange?.("hours24");
- }}
- checked={value === "hours24"}
- >
-
-
-
+
+
+
+
+
+
);
};
diff --git a/apps/web/src/components/time-zone-picker/time-zone-select.tsx b/apps/web/src/components/time-zone-picker/time-zone-select.tsx
index e47896896..07692fcef 100644
--- a/apps/web/src/components/time-zone-picker/time-zone-select.tsx
+++ b/apps/web/src/components/time-zone-picker/time-zone-select.tsx
@@ -57,11 +57,11 @@ export const TimeZoneCommand = ({ onSelect, value }: TimeZoneCommandProps) => {
onSelect?.(option.value)}
- className="flex min-w-0 gap-x-4"
+ className="flex min-w-0 gap-x-2.5"
>
@@ -84,7 +84,7 @@ export const TimeZoneSelect = React.forwardRef(
const popoverContentId = "timeZoneSelect__popoverContent";
return (
-
+
);
};
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}