mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 18:26:34 +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",
|
||||
"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."
|
||||
}
|
||||
|
|
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 { 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 (
|
||||
<Link
|
||||
target={target}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 px-2.5 py-1.5 text-sm font-medium",
|
||||
router.asPath === href
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
</Link>
|
||||
<Button variant="ghost" asChild>
|
||||
<Link
|
||||
target={target}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 px-2.5 py-1.5 text-sm font-medium",
|
||||
router.asPath === href
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<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 = () => {
|
||||
return (
|
||||
<m.div
|
||||
|
@ -139,15 +119,20 @@ const MainNav = () => {
|
|||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<nav className="hidden gap-x-2 sm:flex">
|
||||
<nav className="flex gap-x-2">
|
||||
<IfGuest>
|
||||
<NavMenuItem
|
||||
className="hidden sm:flex"
|
||||
icon={LogInIcon}
|
||||
href="/login"
|
||||
label={<Trans i18nKey="login" defaults="Login" />}
|
||||
/>
|
||||
</IfGuest>
|
||||
<Clock />
|
||||
<ClockPreferences>
|
||||
<Button className="text-muted-foreground" variant="ghost">
|
||||
<Clock />
|
||||
</Button>
|
||||
</ClockPreferences>
|
||||
</nav>
|
||||
<UserDropdown />
|
||||
</div>
|
||||
|
|
|
@ -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 = () => {
|
|||
</div>
|
||||
</div>
|
||||
{poll.options[0].duration !== 0 ? (
|
||||
<div className="border-b p-3">
|
||||
<TimePreferences />
|
||||
<div className="border-b bg-gray-50 p-3">
|
||||
<TimesShownIn />
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 = () => {
|
|||
</div>
|
||||
</div>
|
||||
{poll.options[0].duration !== 0 ? (
|
||||
<div className="overflow-x-auto border-b bg-gray-50 p-3">
|
||||
<TimePreferences />
|
||||
<div className="flex border-b bg-gray-50 p-3">
|
||||
<TimesShownIn />
|
||||
</div>
|
||||
) : null}
|
||||
<GroupedOptions
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { TimeFormat } from "@rallly/database";
|
||||
import { cn } from "@rallly/ui";
|
||||
import React from "react";
|
||||
import { RadioGroup, RadioGroupItem } from "@rallly/ui/radio-group";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
|
@ -10,51 +9,28 @@ interface TimeFormatPickerProps {
|
|||
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 = ({
|
||||
disabled,
|
||||
value,
|
||||
onChange,
|
||||
}: TimeFormatPickerProps) => {
|
||||
return (
|
||||
<div
|
||||
aria-disabled={disabled}
|
||||
role="radiogroup"
|
||||
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"
|
||||
>
|
||||
<RadioButton
|
||||
onClick={() => {
|
||||
onChange?.("hours12");
|
||||
}}
|
||||
checked={value === "hours12"}
|
||||
>
|
||||
<Trans i18nKey={"12h"} defaults={"12h"} />
|
||||
</RadioButton>
|
||||
<RadioButton
|
||||
onClick={() => {
|
||||
onChange?.("hours24");
|
||||
}}
|
||||
checked={value === "hours24"}
|
||||
>
|
||||
<Trans i18nKey={"24h"} defaults={"24h"} />
|
||||
</RadioButton>
|
||||
</div>
|
||||
<RadioGroup value={value} onValueChange={onChange} disabled={disabled}>
|
||||
<div className="grid gap-y-1">
|
||||
<label className="flex items-center gap-x-2">
|
||||
<RadioGroupItem value="hours12" />
|
||||
<span>
|
||||
<Trans i18nKey="12h" />
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-x-2">
|
||||
<RadioGroupItem value="hours24" />
|
||||
<span>
|
||||
<Trans i18nKey="24h" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -57,11 +57,11 @@ export const TimeZoneCommand = ({ onSelect, value }: TimeZoneCommandProps) => {
|
|||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => onSelect?.(option.value)}
|
||||
className="flex min-w-0 gap-x-4"
|
||||
className="flex min-w-0 gap-x-2.5"
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
"h-4 w-4 shrink-0",
|
||||
value === option.value ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
|
@ -84,7 +84,7 @@ export const TimeZoneSelect = React.forwardRef<HTMLButtonElement, SelectProps>(
|
|||
const popoverContentId = "timeZoneSelect__popoverContent";
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover modal={false} open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<button
|
||||
ref={ref}
|
||||
|
@ -93,10 +93,10 @@ export const TimeZoneSelect = React.forwardRef<HTMLButtonElement, SelectProps>(
|
|||
role="combobox"
|
||||
aria-expanded={open}
|
||||
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" />
|
||||
<span className="grow text-left">
|
||||
<span className="grow truncate text-left">
|
||||
{value ? (
|
||||
options.find((option) => option.value === value)?.label
|
||||
) : (
|
||||
|
@ -112,7 +112,7 @@ export const TimeZoneSelect = React.forwardRef<HTMLButtonElement, SelectProps>(
|
|||
<PopoverContent
|
||||
id={popoverContentId}
|
||||
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
|
||||
value={value}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
import { useDayjs } from "@/utils/dayjs";
|
||||
|
||||
export const usePoll = () => {
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -20,3 +23,16 @@ export const usePoll = () => {
|
|||
|
||||
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 { GlobeIcon } from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@rallly/ui/dialog";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@rallly/ui/popover";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import React from "react";
|
||||
|
||||
import { TimeFormatPicker } from "@/components/time-format-picker";
|
||||
import {
|
||||
TimeZoneCommand,
|
||||
timeZones,
|
||||
} from "@/components/time-zone-picker/time-zone-select";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { TimesShownIn } from "@/components/clock";
|
||||
import { TimeZoneCommand } from "@/components/time-zone-picker/time-zone-select";
|
||||
import { usePoll } from "@/contexts/poll";
|
||||
import { useDayjs } from "@/utils/dayjs";
|
||||
|
||||
export const TimePreferences = () => {
|
||||
const poll = usePoll();
|
||||
|
||||
const { timeZone, timeFormat } = useDayjs();
|
||||
const { timeZone } = useDayjs();
|
||||
const queryClient = trpc.useContext();
|
||||
|
||||
const [open, setIsOpen] = React.useState(false);
|
||||
|
@ -31,9 +24,7 @@ export const TimePreferences = () => {
|
|||
}
|
||||
return {
|
||||
...oldPreferences,
|
||||
timeFormat: newPreferences.timeFormat ?? oldPreferences?.timeFormat,
|
||||
timeZone: newPreferences.timeZone ?? oldPreferences?.timeZone ?? null,
|
||||
weekStart: newPreferences.weekStart ?? oldPreferences?.weekStart,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -47,50 +38,23 @@ export const TimePreferences = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-x-4 overflow-x-auto sm:overflow-x-visible">
|
||||
<Dialog open={open} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button icon={GlobeIcon} disabled={!poll.timeZone}>
|
||||
{poll.timeZone ? (
|
||||
<Trans
|
||||
i18nKey="timeShownIn"
|
||||
defaults="Times shown in {timeZone}"
|
||||
values={{
|
||||
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) => {
|
||||
<Popover open={open} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger className="inline-flex items-center gap-x-2 text-sm hover:underline">
|
||||
<GlobeIcon className="h-4 w-4" />
|
||||
<TimesShownIn />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="center" className="p-0">
|
||||
<TimeZoneCommand
|
||||
value={timeZone}
|
||||
onSelect={(value) => {
|
||||
updatePreferences.mutate({
|
||||
timeFormat: newTimeFormat,
|
||||
timeZone: value,
|
||||
});
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
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",
|
||||
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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Reference in a new issue