mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-30 18:56:45 +02:00
✨ Updated clock preferences (#789)
This commit is contained in:
parent
e5b58c6a21
commit
8ec075b80f
14 changed files with 246 additions and 158 deletions
|
@ -154,9 +154,7 @@
|
||||||
"noTransfer": "No, take me home",
|
"noTransfer": "No, take me home",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"timeShownIn": "Times shown in {timeZone}",
|
"timeShownIn": "Times shown in {timeZone}",
|
||||||
"timeShownInLocalTime": "Times shown in local time",
|
|
||||||
"editDetailsDescription": "Change the details of your event.",
|
"editDetailsDescription": "Change the details of your event.",
|
||||||
"editOptionsDescription": "Change the options available in your poll.",
|
|
||||||
"finalizeDescription": "Select a final date for your event.",
|
"finalizeDescription": "Select a final date for your event.",
|
||||||
"notificationsGuestTooltip": "Create an account or login to turn get notifications",
|
"notificationsGuestTooltip": "Create an account or login to turn get notifications",
|
||||||
"planFree": "Free",
|
"planFree": "Free",
|
||||||
|
@ -213,5 +211,7 @@
|
||||||
"hideScoresDescription": "Only show scores until after a participant has voted",
|
"hideScoresDescription": "Only show scores until after a participant has voted",
|
||||||
"disableComments": "Disable comments",
|
"disableComments": "Disable comments",
|
||||||
"disableCommentsDescription": "Remove the option to leave a comment on the poll",
|
"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."
|
||||||
}
|
}
|
||||||
|
|
146
apps/web/src/components/clock.tsx
Normal file
146
apps/web/src/components/clock.tsx
Normal file
|
@ -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 (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
<Trans i18nKey="timeZone" />
|
||||||
|
</Label>
|
||||||
|
<TimeZoneSelect
|
||||||
|
value={timeZone}
|
||||||
|
onValueChange={(newTimeZone) => {
|
||||||
|
updatePreferences.mutate({
|
||||||
|
timeZone: newTimeZone,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
<Trans i18nKey="timeFormat" />
|
||||||
|
</Label>
|
||||||
|
<TimeFormatPicker
|
||||||
|
value={timeFormat}
|
||||||
|
onChange={(newTimeFormat) => {
|
||||||
|
updatePreferences.mutate({
|
||||||
|
timeFormat: newTimeFormat,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<span
|
||||||
|
key={timeFormat}
|
||||||
|
className={cn("inline-block font-medium tabular-nums", className)}
|
||||||
|
>{`${dayjs(time).tz(timeZone).format("LT")} ${abbrev}`}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ClockPreferences>
|
||||||
|
<button className="inline-flex items-center gap-x-2 text-sm hover:underline">
|
||||||
|
<GlobeIcon className="h-4 w-4" />
|
||||||
|
<Trans i18nKey="timeShownIn" values={{ timeZone: timeZoneName }} />
|
||||||
|
</button>
|
||||||
|
</ClockPreferences>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClockPreferences = ({ children }: React.PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<Dialog modal={false}>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-sm">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans i18nKey="clockPreferences" defaults="Clock Preferences" />
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans
|
||||||
|
i18nKey="clockPreferencesDescription"
|
||||||
|
defaults="Set your preferred time zone and time format."
|
||||||
|
/>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="bg-muted grid h-24 items-center justify-center rounded-md text-2xl font-bold">
|
||||||
|
<Clock />
|
||||||
|
</div>
|
||||||
|
<TimePreferences />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,23 +1,20 @@
|
||||||
import { ClockIcon, ListIcon, LogInIcon } from "@rallly/icons";
|
import { ListIcon, LogInIcon } from "@rallly/icons";
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
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 { Container } from "@/components/container";
|
||||||
import FeedbackButton from "@/components/feedback";
|
import FeedbackButton from "@/components/feedback";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { UserDropdown } from "@/components/user-dropdown";
|
import { UserDropdown } from "@/components/user-dropdown";
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
|
||||||
|
|
||||||
import { IconComponent, NextPageWithLayout } from "../../types";
|
import { IconComponent, NextPageWithLayout } from "../../types";
|
||||||
import ModalProvider from "../modal/modal-provider";
|
import ModalProvider from "../modal/modal-provider";
|
||||||
|
@ -28,27 +25,32 @@ const NavMenuItem = ({
|
||||||
target,
|
target,
|
||||||
label,
|
label,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
icon: IconComponent;
|
icon: IconComponent;
|
||||||
href: string;
|
href: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<Link
|
<Button variant="ghost" asChild>
|
||||||
target={target}
|
<Link
|
||||||
href={href}
|
target={target}
|
||||||
className={cn(
|
href={href}
|
||||||
"flex items-center gap-2.5 px-2.5 py-1.5 text-sm font-medium",
|
className={cn(
|
||||||
router.asPath === href
|
"flex items-center gap-2.5 px-2.5 py-1.5 text-sm font-medium",
|
||||||
? "text-foreground"
|
router.asPath === href
|
||||||
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
? "text-foreground"
|
||||||
)}
|
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
||||||
>
|
className,
|
||||||
<Icon className="h-4 w-4" />
|
)}
|
||||||
{label}
|
>
|
||||||
</Link>
|
<Icon className="h-4 w-4" />
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 (
|
|
||||||
<NavMenuItem
|
|
||||||
icon={ClockIcon}
|
|
||||||
href="/settings/preferences"
|
|
||||||
label={`${dayjs(time).tz(timeZone).format("LT")} ${abbrev}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MainNav = () => {
|
const MainNav = () => {
|
||||||
return (
|
return (
|
||||||
<m.div
|
<m.div
|
||||||
|
@ -139,15 +119,20 @@ const MainNav = () => {
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<nav className="hidden gap-x-2 sm:flex">
|
<nav className="flex gap-x-2">
|
||||||
<IfGuest>
|
<IfGuest>
|
||||||
<NavMenuItem
|
<NavMenuItem
|
||||||
|
className="hidden sm:flex"
|
||||||
icon={LogInIcon}
|
icon={LogInIcon}
|
||||||
href="/login"
|
href="/login"
|
||||||
label={<Trans i18nKey="login" defaults="Login" />}
|
label={<Trans i18nKey="login" defaults="Login" />}
|
||||||
/>
|
/>
|
||||||
</IfGuest>
|
</IfGuest>
|
||||||
<Clock />
|
<ClockPreferences>
|
||||||
|
<Button className="text-muted-foreground" variant="ghost">
|
||||||
|
<Clock />
|
||||||
|
</Button>
|
||||||
|
</ClockPreferences>
|
||||||
</nav>
|
</nav>
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,9 +10,9 @@ import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useMeasure, useUpdateEffect } from "react-use";
|
import { useMeasure, useUpdateEffect } from "react-use";
|
||||||
|
|
||||||
|
import { TimesShownIn } from "@/components/clock";
|
||||||
import { usePermissions } from "@/contexts/permissions";
|
import { usePermissions } from "@/contexts/permissions";
|
||||||
import { useRole } from "@/contexts/role";
|
import { useRole } from "@/contexts/role";
|
||||||
import { TimePreferences } from "@/contexts/time-preferences";
|
|
||||||
|
|
||||||
import { useNewParticipantModal } from "../new-participant-modal";
|
import { useNewParticipantModal } from "../new-participant-modal";
|
||||||
import {
|
import {
|
||||||
|
@ -193,8 +193,8 @@ const Poll: React.FunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{poll.options[0].duration !== 0 ? (
|
{poll.options[0].duration !== 0 ? (
|
||||||
<div className="border-b p-3">
|
<div className="border-b bg-gray-50 p-3">
|
||||||
<TimePreferences />
|
<TimesShownIn />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -18,8 +18,7 @@ import { DateIcon } from "@/components/date-icon";
|
||||||
import { useParticipants } from "@/components/participants-provider";
|
import { useParticipants } from "@/components/participants-provider";
|
||||||
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
||||||
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { useDateFormatter, usePoll } from "@/contexts/poll";
|
||||||
import { useDateFormatter } from "@/contexts/time-preferences";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
selectedOptionId: z.string(),
|
selectedOptionId: z.string(),
|
||||||
|
|
|
@ -18,8 +18,7 @@ import { DateIcon } from "@/components/date-icon";
|
||||||
import { useParticipants } from "@/components/participants-provider";
|
import { useParticipants } from "@/components/participants-provider";
|
||||||
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
||||||
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { useDateFormatter, usePoll } from "@/contexts/poll";
|
||||||
import { useDateFormatter } from "@/contexts/time-preferences";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
selectedOptionId: z.string(),
|
selectedOptionId: z.string(),
|
||||||
|
|
|
@ -8,10 +8,10 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { useUpdateEffect } from "react-use";
|
import { useUpdateEffect } from "react-use";
|
||||||
import smoothscroll from "smoothscroll-polyfill";
|
import smoothscroll from "smoothscroll-polyfill";
|
||||||
|
|
||||||
|
import { TimesShownIn } from "@/components/clock";
|
||||||
import { ParticipantDropdown } from "@/components/participant-dropdown";
|
import { ParticipantDropdown } from "@/components/participant-dropdown";
|
||||||
import { useOptions, usePoll } from "@/components/poll-context";
|
import { useOptions, usePoll } from "@/components/poll-context";
|
||||||
import { usePermissions } from "@/contexts/permissions";
|
import { usePermissions } from "@/contexts/permissions";
|
||||||
import { TimePreferences } from "@/contexts/time-preferences";
|
|
||||||
|
|
||||||
import { styleMenuItem } from "../menu-styles";
|
import { styleMenuItem } from "../menu-styles";
|
||||||
import { useNewParticipantModal } from "../new-participant-modal";
|
import { useNewParticipantModal } from "../new-participant-modal";
|
||||||
|
@ -216,8 +216,8 @@ const MobilePoll: React.FunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{poll.options[0].duration !== 0 ? (
|
{poll.options[0].duration !== 0 ? (
|
||||||
<div className="overflow-x-auto border-b bg-gray-50 p-3">
|
<div className="flex border-b bg-gray-50 p-3">
|
||||||
<TimePreferences />
|
<TimesShownIn />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<GroupedOptions
|
<GroupedOptions
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { TimeFormat } from "@rallly/database";
|
import { TimeFormat } from "@rallly/database";
|
||||||
import { cn } from "@rallly/ui";
|
import { RadioGroup, RadioGroupItem } from "@rallly/ui/radio-group";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
|
||||||
|
@ -10,51 +9,28 @@ interface TimeFormatPickerProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RadioButton = (
|
|
||||||
props: React.PropsWithChildren<{ checked?: boolean; onClick?: () => void }>,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
{...props}
|
|
||||||
role="radio"
|
|
||||||
type="button"
|
|
||||||
aria-checked={props.checked}
|
|
||||||
className={cn(
|
|
||||||
props.checked ? "" : "hover:bg-accent",
|
|
||||||
"text-muted-foreground aria-checked:text-foreground grow rounded-none px-2 font-medium aria-checked:bg-white",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TimeFormatPicker = ({
|
const TimeFormatPicker = ({
|
||||||
disabled,
|
disabled,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: TimeFormatPickerProps) => {
|
}: TimeFormatPickerProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<RadioGroup value={value} onValueChange={onChange} disabled={disabled}>
|
||||||
aria-disabled={disabled}
|
<div className="grid gap-y-1">
|
||||||
role="radiogroup"
|
<label className="flex items-center gap-x-2">
|
||||||
className="inline-flex h-9 divide-x overflow-hidden whitespace-nowrap rounded-md border bg-gray-50 text-sm aria-disabled:pointer-events-none aria-disabled:opacity-50"
|
<RadioGroupItem value="hours12" />
|
||||||
>
|
<span>
|
||||||
<RadioButton
|
<Trans i18nKey="12h" />
|
||||||
onClick={() => {
|
</span>
|
||||||
onChange?.("hours12");
|
</label>
|
||||||
}}
|
<label className="flex items-center gap-x-2">
|
||||||
checked={value === "hours12"}
|
<RadioGroupItem value="hours24" />
|
||||||
>
|
<span>
|
||||||
<Trans i18nKey={"12h"} defaults={"12h"} />
|
<Trans i18nKey="24h" />
|
||||||
</RadioButton>
|
</span>
|
||||||
<RadioButton
|
</label>
|
||||||
onClick={() => {
|
</div>
|
||||||
onChange?.("hours24");
|
</RadioGroup>
|
||||||
}}
|
|
||||||
checked={value === "hours24"}
|
|
||||||
>
|
|
||||||
<Trans i18nKey={"24h"} defaults={"24h"} />
|
|
||||||
</RadioButton>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -57,11 +57,11 @@ export const TimeZoneCommand = ({ onSelect, value }: TimeZoneCommandProps) => {
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onSelect={() => onSelect?.(option.value)}
|
onSelect={() => onSelect?.(option.value)}
|
||||||
className="flex min-w-0 gap-x-4"
|
className="flex min-w-0 gap-x-2.5"
|
||||||
>
|
>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"h-4 w-4 shrink-0",
|
||||||
value === option.value ? "opacity-100" : "opacity-0",
|
value === option.value ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -84,7 +84,7 @@ export const TimeZoneSelect = React.forwardRef<HTMLButtonElement, SelectProps>(
|
||||||
const popoverContentId = "timeZoneSelect__popoverContent";
|
const popoverContentId = "timeZoneSelect__popoverContent";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover modal={false} open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild={true}>
|
<PopoverTrigger asChild={true}>
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -93,10 +93,10 @@ export const TimeZoneSelect = React.forwardRef<HTMLButtonElement, SelectProps>(
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-controls={popoverContentId}
|
aria-controls={popoverContentId}
|
||||||
className="bg-input-background flex h-9 w-full items-center gap-x-1.5 rounded-md border px-2 py-2 text-sm focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
className="bg-input-background flex h-9 w-full min-w-0 items-center gap-x-1.5 rounded-md border px-2 py-2 text-sm focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<GlobeIcon className="h-4 w-4" />
|
<GlobeIcon className="h-4 w-4" />
|
||||||
<span className="grow text-left">
|
<span className="grow truncate text-left">
|
||||||
{value ? (
|
{value ? (
|
||||||
options.find((option) => option.value === value)?.label
|
options.find((option) => option.value === value)?.label
|
||||||
) : (
|
) : (
|
||||||
|
@ -112,7 +112,7 @@ export const TimeZoneSelect = React.forwardRef<HTMLButtonElement, SelectProps>(
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
id={popoverContentId}
|
id={popoverContentId}
|
||||||
align="start"
|
align="start"
|
||||||
className="min-w-[var(--radix-popover-trigger-width)] bg-white p-0"
|
className="z-[1000] max-w-[var(--radix-popover-trigger-width)] bg-white p-0"
|
||||||
>
|
>
|
||||||
<TimeZoneCommand
|
<TimeZoneCommand
|
||||||
value={value}
|
value={value}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { trpc } from "@rallly/backend";
|
import { trpc } from "@rallly/backend";
|
||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
|
|
||||||
export const usePoll = () => {
|
export const usePoll = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -20,3 +23,16 @@ export const usePoll = () => {
|
||||||
|
|
||||||
return pollQuery.data;
|
return pollQuery.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDateFormatter = () => {
|
||||||
|
const { timeZone } = usePoll();
|
||||||
|
const { timeZone: preferredTimeZone } = useDayjs();
|
||||||
|
|
||||||
|
return (date: Date | Dayjs) => {
|
||||||
|
if (timeZone) {
|
||||||
|
return dayjs(date).utc().tz(timeZone, true).tz(preferredTimeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjs(date).utc();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -1,23 +1,16 @@
|
||||||
import { trpc } from "@rallly/backend";
|
import { trpc } from "@rallly/backend";
|
||||||
import { GlobeIcon } from "@rallly/icons";
|
import { GlobeIcon } from "@rallly/icons";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Popover, PopoverContent, PopoverTrigger } from "@rallly/ui/popover";
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@rallly/ui/dialog";
|
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { TimeFormatPicker } from "@/components/time-format-picker";
|
import { TimesShownIn } from "@/components/clock";
|
||||||
import {
|
import { TimeZoneCommand } from "@/components/time-zone-picker/time-zone-select";
|
||||||
TimeZoneCommand,
|
|
||||||
timeZones,
|
|
||||||
} from "@/components/time-zone-picker/time-zone-select";
|
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
|
|
||||||
export const TimePreferences = () => {
|
export const TimePreferences = () => {
|
||||||
const poll = usePoll();
|
const { timeZone } = useDayjs();
|
||||||
|
|
||||||
const { timeZone, timeFormat } = useDayjs();
|
|
||||||
const queryClient = trpc.useContext();
|
const queryClient = trpc.useContext();
|
||||||
|
|
||||||
const [open, setIsOpen] = React.useState(false);
|
const [open, setIsOpen] = React.useState(false);
|
||||||
|
@ -31,9 +24,7 @@ export const TimePreferences = () => {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...oldPreferences,
|
...oldPreferences,
|
||||||
timeFormat: newPreferences.timeFormat ?? oldPreferences?.timeFormat,
|
|
||||||
timeZone: newPreferences.timeZone ?? oldPreferences?.timeZone ?? null,
|
timeZone: newPreferences.timeZone ?? oldPreferences?.timeZone ?? null,
|
||||||
weekStart: newPreferences.weekStart ?? oldPreferences?.weekStart,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -47,50 +38,23 @@ export const TimePreferences = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between gap-x-4 overflow-x-auto sm:overflow-x-visible">
|
<Popover open={open} onOpenChange={setIsOpen}>
|
||||||
<Dialog open={open} onOpenChange={setIsOpen}>
|
<PopoverTrigger className="inline-flex items-center gap-x-2 text-sm hover:underline">
|
||||||
<DialogTrigger asChild>
|
<GlobeIcon className="h-4 w-4" />
|
||||||
<Button icon={GlobeIcon} disabled={!poll.timeZone}>
|
<TimesShownIn />
|
||||||
{poll.timeZone ? (
|
</PopoverTrigger>
|
||||||
<Trans
|
<PopoverContent align="center" className="p-0">
|
||||||
i18nKey="timeShownIn"
|
<TimeZoneCommand
|
||||||
defaults="Times shown in {timeZone}"
|
value={timeZone}
|
||||||
values={{
|
onSelect={(value) => {
|
||||||
timeZone: timeZones[timeZone as keyof typeof timeZones],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Trans
|
|
||||||
i18nKey="timeShownInLocalTime"
|
|
||||||
defaults="Times shown in local time"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="p-0">
|
|
||||||
<TimeZoneCommand
|
|
||||||
value={timeZone}
|
|
||||||
onSelect={(value) => {
|
|
||||||
updatePreferences.mutate({
|
|
||||||
timeZone: value,
|
|
||||||
});
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<TimeFormatPicker
|
|
||||||
value={timeFormat}
|
|
||||||
onChange={(newTimeFormat) => {
|
|
||||||
updatePreferences.mutate({
|
updatePreferences.mutate({
|
||||||
timeFormat: newTimeFormat,
|
timeZone: value,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PopoverContent>
|
||||||
</div>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const useDateFormatter = () => {
|
export const useDateFormatter = () => {
|
||||||
|
|
|
@ -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",
|
"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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"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",
|
link: "underline-offset-4 hover:underline text-primary",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { cn } from "./lib/utils";
|
||||||
const Dialog = DialogPrimitive.Root;
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
const DialogPortal = ({
|
const DialogPortal = ({
|
||||||
className,
|
className,
|
||||||
|
@ -128,6 +129,7 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
|
|
|
@ -18,8 +18,9 @@ const PopoverContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
|
collisionPadding={12}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
Loading…
Add table
Reference in a new issue