Load locale ondemand + spanish locale (#249)

This commit is contained in:
Luke Vella 2022-07-28 10:39:58 +01:00 committed by GitHub
parent 0f35bd0518
commit c2aea134ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 700 additions and 455 deletions

View file

@ -1,11 +1,11 @@
import clsx from "clsx";
import dayjs from "dayjs";
import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "next-i18next";
import { usePlausible } from "next-plausible";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { useDayjs } from "../../utils/dayjs";
import { requiredString } from "../../utils/form-validation";
import { trpc } from "../../utils/trpc";
import { Button } from "../button";
@ -25,6 +25,7 @@ interface CommentForm {
}
const Discussion: React.VoidFunctionComponent = () => {
const { dayjs } = useDayjs();
const queryClient = trpc.useContext();
const { t } = useTranslation("app");
const { poll } = usePoll();

View file

@ -1,5 +1,6 @@
import Head from "next/head";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { Button } from "@/components/button";
@ -19,6 +20,7 @@ const ErrorPage: React.VoidFunctionComponent<ComponentProps> = ({
title,
description,
}) => {
const { t } = useTranslation("errors");
return (
<div className="mx-auto flex h-full max-w-full items-center justify-center bg-gray-50 px-4 py-8 lg:w-[1024px]">
<Head>
@ -28,16 +30,16 @@ const ErrorPage: React.VoidFunctionComponent<ComponentProps> = ({
<div className="flex items-start">
<div className="text-center">
<Icon className="mb-4 inline-block w-24 text-slate-400" />
<div className="text-primary-500 mb-2 text-3xl font-bold ">
<div className="mb-2 text-3xl font-bold text-primary-500 ">
{title}
</div>
<p>{description}</p>
<div className="flex justify-center space-x-3">
<Link href="/" passHref={true}>
<a className="btn-default">Go to home</a>
<a className="btn-default">{t("goToHome")}</a>
</Link>
<Button icon={<Chat />} onClick={showCrispChat}>
Start chat
{t("startChat")}
</Button>
</div>
</div>

View file

@ -1,16 +1,14 @@
import clsx from "clsx";
import dayjs from "dayjs";
import { useTranslation } from "next-i18next";
import { usePlausible } from "next-plausible";
import * as React from "react";
import { usePreferences } from "@/components/preferences/use-preferences";
import {
expectTimeOption,
getDateProps,
removeAllOptionsForDay,
} from "../../../../utils/date-time-utils";
import { useDayjs } from "../../../../utils/dayjs";
import { Button } from "../../../button";
import CompactButton from "../../../compact-button";
import DateCard from "../../../date-card";
@ -38,6 +36,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
duration,
onChangeDuration,
}) => {
const { dayjs, weekStartsOn } = useDayjs();
const { t } = useTranslation("app");
const isTimedEvent = options.some((option) => option.type === "timeSlot");
@ -76,8 +75,6 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
);
}, [optionsByDay]);
const { weekStartsOn } = usePreferences();
const datepicker = useHeadlessDatePicker({
selection: datepickerSelection,
onNavigationChange: onNavigate,

View file

@ -7,11 +7,11 @@ import {
} from "@floating-ui/react-dom-interactions";
import { Listbox } from "@headlessui/react";
import clsx from "clsx";
import dayjs from "dayjs";
import * as React from "react";
import { stopPropagation } from "@/utils/stop-propagation";
import { useDayjs } from "../../../../utils/dayjs";
import ChevronDown from "../../../icons/chevron-down.svg";
import { styleMenuItem } from "../../../menu-styles";
@ -28,6 +28,7 @@ const TimePicker: React.VoidFunctionComponent<TimePickerProps> = ({
className,
startFrom,
}) => {
const { dayjs } = useDayjs();
const { reference, floating, x, y, strategy, refs } = useFloating({
strategy: "fixed",
middleware: [

View file

@ -1,18 +1,15 @@
import clsx from "clsx";
import dayjs from "dayjs";
import React from "react";
import { Calendar } from "react-big-calendar";
import { useMount } from "react-use";
import { getDuration } from "../../../utils/date-time-utils";
import { usePreferences } from "../../preferences/use-preferences";
import { useDayjs } from "../../../utils/dayjs";
import DateNavigationToolbar from "./date-navigation-toolbar";
import dayjsLocalizer from "./dayjs-localizer";
import { DateTimeOption, DateTimePickerProps } from "./types";
import { formatDateWithoutTime, formatDateWithoutTz } from "./utils";
const localizer = dayjsLocalizer(dayjs);
const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
title,
options,
@ -23,8 +20,9 @@ const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
onChangeDuration,
}) => {
const [scrollToTime, setScrollToTime] = React.useState<Date>();
const { dayjs, timeFormat } = useDayjs();
const localizer = React.useMemo(() => dayjsLocalizer(dayjs), [dayjs]);
const { timeFormat } = usePreferences();
useMount(() => {
// Bit of a hack to force rbc to scroll to the right time when we close/open a modal
setScrollToTime(dayjs(date).add(-60, "minutes").toDate());

View file

@ -1,6 +1,7 @@
import dayjs from "dayjs";
import React from "react";
import { useDayjs } from "../utils/dayjs";
interface DayProps {
date: Date;
day: string;
@ -33,6 +34,7 @@ export const useHeadlessDatePicker = (
selection: Date[];
toggle: (date: Date) => void;
} => {
const { dayjs } = useDayjs();
const [localSelection, setSelection] = React.useState<Date[]>([]);
const selection = options?.selection ?? localSelection;
const [localNavigationDate, setNavigationDate] = React.useState(today);

View file

@ -3,6 +3,7 @@ import Link from "next/link";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { DayjsProvider } from "../../utils/dayjs";
import { UserAvatarProvider } from "../poll/user-avatar";
import PollDemo from "./poll-demo";
import ScribbleArrow from "./scribble-arrow.svg";
@ -43,36 +44,38 @@ const Hero: React.VoidFunctionComponent = () => {
</div>
<div className="pointer-events-none mt-24 hidden h-[380px] select-none items-end justify-center md:flex lg:mt-8 lg:ml-24">
<UserAvatarProvider seed="mock" names={names}>
<div className="relative inline-block">
<motion.div
className="absolute z-20 h-full rounded-2xl border-4 border-primary-500 bg-primary-200/10 shadow-md"
initial={{ opacity: 0, width: 100, scale: 1.1, x: 480 }}
animate={{ opacity: 1, x: 381 }}
transition={{ type: "spring", delay: 1 }}
/>
<motion.div
className="absolute z-20 rounded-full bg-primary-500 py-1 px-3 text-sm text-slate-100"
initial={{
opacity: 0,
right: 190,
top: -65,
translateY: 50,
}}
animate={{ opacity: 1, translateY: 0 }}
transition={{ type: "spring", delay: 2 }}
>
{t("perfect")} 🤩
<ScribbleArrow className="absolute -right-8 top-3 text-slate-400" />
</motion.div>
<motion.div
className="rounded-lg"
transition={{ type: "spring", delay: 0.5 }}
initial={{ opacity: 0, translateY: -100 }}
animate={{ opacity: 1, translateY: 0 }}
>
<PollDemo />
</motion.div>
</div>
<DayjsProvider>
<div className="relative inline-block">
<motion.div
className="absolute z-20 h-full rounded-2xl border-4 border-primary-500 bg-primary-200/10 shadow-md"
initial={{ opacity: 0, width: 100, scale: 1.1, x: 480 }}
animate={{ opacity: 1, x: 381 }}
transition={{ type: "spring", delay: 1 }}
/>
<motion.div
className="absolute z-20 rounded-full bg-primary-500 py-1 px-3 text-sm text-slate-100"
initial={{
opacity: 0,
right: 190,
top: -65,
translateY: 50,
}}
animate={{ opacity: 1, translateY: 0 }}
transition={{ type: "spring", delay: 2 }}
>
{t("perfect")} 🤩
<ScribbleArrow className="absolute -right-8 top-3 text-slate-400" />
</motion.div>
<motion.div
className="rounded-lg"
transition={{ type: "spring", delay: 0.5 }}
initial={{ opacity: 0, translateY: -100 }}
animate={{ opacity: 1, translateY: 0 }}
>
<PollDemo />
</motion.div>
</div>
</DayjsProvider>
</UserAvatarProvider>
</div>
</div>

View file

@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { useDayjs } from "../../utils/dayjs";
import { ParticipantRowView } from "../poll/desktop-poll/participant-row";
import { ScoreSummary } from "../poll/score-summary";
@ -35,6 +35,7 @@ const options = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];
const PollDemo: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
const { dayjs } = useDayjs();
return (
<div
className="rounded-lg bg-white py-1 shadow-huge"

View file

@ -12,9 +12,9 @@ import {
} from "@/utils/date-time-utils";
import { GetPollApiResponse } from "@/utils/trpc/types";
import { useDayjs } from "../utils/dayjs";
import ErrorPage from "./error-page";
import { useParticipants } from "./participants-provider";
import { usePreferences } from "./preferences/use-preferences";
import { useSession } from "./session";
import { useRequiredContext } from "./use-required-context";
@ -88,7 +88,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
[participants],
);
const { timeFormat } = usePreferences();
const { timeFormat } = useDayjs();
const contextValue = React.useMemo<PollContextValue>(() => {
const highScore = poll.options.reduce((acc, curr) => {

View file

@ -27,7 +27,6 @@ import VoteIcon from "./poll/vote-icon";
import { usePoll } from "./poll-context";
import { useSession } from "./session";
import Sharing from "./sharing";
import StandardLayout from "./standard-layout";
const Discussion = React.lazy(() => import("@/components/discussion"));
@ -120,159 +119,151 @@ const PollPage: NextPage = () => {
);
return (
<UserAvatarProvider seed={poll.id} names={names}>
<StandardLayout>
<div className="relative max-w-full py-4 md:px-4">
<Head>
<title>{poll.title}</title>
<meta name="robots" content="noindex,nofollow" />
</Head>
<div
className="mx-auto max-w-full lg:mx-0"
style={{
width: Math.max(768, poll.options.length * 95 + 200 + 160),
}}
>
{admin ? (
<>
<div className="mb-4 flex space-x-2 px-4 md:justify-end md:px-0">
<NotificationsToggle />
<ManagePoll
placement={isWideScreen ? "bottom-end" : "bottom-start"}
/>
<Button
type="primary"
icon={<Share />}
onClick={() => {
setSharingVisible((value) => !value);
<div className="relative max-w-full py-4 md:px-4">
<Head>
<title>{poll.title}</title>
<meta name="robots" content="noindex,nofollow" />
</Head>
<div
className="mx-auto max-w-full lg:mx-0"
style={{
width: Math.max(768, poll.options.length * 95 + 200 + 160),
}}
>
{admin ? (
<>
<div className="mb-4 flex space-x-2 px-4 md:justify-end md:px-0">
<NotificationsToggle />
<ManagePoll
placement={isWideScreen ? "bottom-end" : "bottom-start"}
/>
<Button
type="primary"
icon={<Share />}
onClick={() => {
setSharingVisible((value) => !value);
}}
>
{t("share")}
</Button>
</div>
<AnimatePresence initial={false}>
{isSharingVisible ? (
<motion.div
initial={{
opacity: 0,
scale: 0.8,
height: 0,
}}
animate={{
opacity: 1,
scale: 1,
height: "auto",
marginBottom: 16,
}}
exit={{
opacity: 0,
scale: 0.8,
height: 0,
marginBottom: 0,
}}
className="overflow-hidden"
>
{t("share")}
</Button>
<Sharing
onHide={() => {
setSharingVisible(false);
}}
/>
</motion.div>
) : null}
</AnimatePresence>
{poll.verified === false ? (
<div className="m-4 overflow-hidden rounded-lg border p-4 md:mx-0 md:mt-0">
<UnverifiedPollNotice />
</div>
<AnimatePresence initial={false}>
{isSharingVisible ? (
<motion.div
initial={{
opacity: 0,
scale: 0.8,
height: 0,
}}
animate={{
opacity: 1,
scale: 1,
height: "auto",
marginBottom: 16,
}}
exit={{
opacity: 0,
scale: 0.8,
height: 0,
marginBottom: 0,
}}
className="overflow-hidden"
>
<Sharing
onHide={() => {
setSharingVisible(false);
}}
/>
</motion.div>
) : null}
</AnimatePresence>
{poll.verified === false ? (
<div className="m-4 overflow-hidden rounded-lg border p-4 md:mx-0 md:mt-0">
<UnverifiedPollNotice />
) : null}
</>
) : null}
{!poll.admin && poll.adminUrlId ? (
<div className="mb-4 items-center justify-between rounded-lg px-4 md:flex md:space-x-4 md:border md:p-2 md:pl-4">
<div className="mb-4 font-medium md:mb-0">
{t("pollOwnerNotice", { name: poll.user.name })}
</div>
<a href={`/admin/${poll.adminUrlId}`} className="btn-default">
{t("goToAdmin")} &rarr;
</a>
</div>
) : null}
{poll.closed ? (
<div className="flex bg-sky-100 py-3 px-4 text-sky-700 md:mb-4 md:rounded-lg md:shadow-sm">
<div className="mr-2 rounded-md">
<LockClosed className="w-6" />
</div>
<div>
<div className="font-medium">{t("pollHasBeenLocked")}</div>
</div>
</div>
) : null}
<div className="md:card mb-4 border-t bg-white md:overflow-hidden md:p-0">
<div className="p-4 md:border-b md:p-6">
<div className="space-y-4">
<div>
<div
className="mb-1 text-2xl font-semibold text-slate-700 md:text-left md:text-3xl"
data-testid="poll-title"
>
{preventWidows(poll.title)}
</div>
<PollSubheader />
</div>
{poll.description ? (
<div className="border-primary whitespace-pre-line lg:text-lg">
<TruncatedLinkify>
{preventWidows(poll.description)}
</TruncatedLinkify>
</div>
) : null}
{poll.location ? (
<div className="lg:text-lg">
<div className="text-sm text-slate-500">
{t("location")}
</div>
<TruncatedLinkify>{poll.location}</TruncatedLinkify>
</div>
) : null}
</>
) : null}
{!poll.admin && poll.adminUrlId ? (
<div className="mb-4 items-center justify-between rounded-lg px-4 md:flex md:space-x-4 md:border md:p-2 md:pl-4">
<div className="mb-4 font-medium md:mb-0">
{t("pollOwnerNotice", { name: poll.user.name })}
</div>
<a href={`/admin/${poll.adminUrlId}`} className="btn-default">
{t("goToAdmin")} &rarr;
</a>
</div>
) : null}
{poll.closed ? (
<div className="flex bg-sky-100 py-3 px-4 text-sky-700 md:mb-4 md:rounded-lg md:shadow-sm">
<div className="mr-2 rounded-md">
<LockClosed className="w-6" />
</div>
<div>
<div className="font-medium">{t("pollHasBeenLocked")}</div>
</div>
</div>
) : null}
<div className="md:card mb-4 border-t bg-white md:overflow-hidden md:p-0">
<div className="p-4 md:border-b md:p-6">
<div className="space-y-4">
<div>
<div
className="mb-1 text-2xl font-semibold text-slate-700 md:text-left md:text-3xl"
data-testid="poll-title"
>
{preventWidows(poll.title)}
</div>
<PollSubheader />
<div className="mb-2 text-sm text-slate-500">
{t("possibleAnswers")}
</div>
{poll.description ? (
<div className="border-primary whitespace-pre-line lg:text-lg">
<TruncatedLinkify>
{preventWidows(poll.description)}
</TruncatedLinkify>
</div>
) : null}
{poll.location ? (
<div className="lg:text-lg">
<div className="text-sm text-slate-500">
{t("location")}
</div>
<TruncatedLinkify>{poll.location}</TruncatedLinkify>
</div>
) : null}
<div>
<div className="mb-2 text-sm text-slate-500">
{t("possibleAnswers")}
</div>
<div className="flex items-center space-x-3">
<span className="inline-flex items-center space-x-1">
<VoteIcon type="yes" />
<span className="text-xs text-slate-500">
{t("yes")}
</span>
<div className="flex items-center space-x-3">
<span className="inline-flex items-center space-x-1">
<VoteIcon type="yes" />
<span className="text-xs text-slate-500">{t("yes")}</span>
</span>
<span className="inline-flex items-center space-x-1">
<VoteIcon type="ifNeedBe" />
<span className="text-xs text-slate-500">
{t("ifNeedBe")}
</span>
<span className="inline-flex items-center space-x-1">
<VoteIcon type="ifNeedBe" />
<span className="text-xs text-slate-500">
{t("ifNeedBe")}
</span>
</span>
<span className="inline-flex items-center space-x-1">
<VoteIcon type="no" />
<span className="text-xs text-slate-500">
{t("no")}
</span>
</span>
</div>
</span>
<span className="inline-flex items-center space-x-1">
<VoteIcon type="no" />
<span className="text-xs text-slate-500">{t("no")}</span>
</span>
</div>
</div>
</div>
<React.Suspense fallback={null}>
{participants ? <PollComponent /> : null}
</React.Suspense>
</div>
<React.Suspense
fallback={<div className="p-4">{t("loading")}</div>}
>
<Discussion />
<React.Suspense fallback={null}>
{participants ? <PollComponent /> : null}
</React.Suspense>
</div>
<React.Suspense fallback={<div className="p-4">{t("loading")}</div>}>
<Discussion />
</React.Suspense>
</div>
</StandardLayout>
</div>
</UserAvatarProvider>
);
};

View file

@ -1,18 +1,16 @@
import clsx from "clsx";
import Cookies from "js-cookie";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
export const LanguageSelect: React.VoidFunctionComponent<{
className?: string;
onChange?: (language: string) => void;
}> = ({ className, onChange }) => {
const { t } = useTranslation("common");
const router = useRouter();
const { t, i18n } = useTranslation("common");
return (
<select
className={clsx("input", className)}
defaultValue={router.locale}
defaultValue={i18n.language}
onChange={(e) => {
Cookies.set("NEXT_LOCALE", e.target.value, {
expires: 365,
@ -21,6 +19,7 @@ export const LanguageSelect: React.VoidFunctionComponent<{
}}
>
<option value="en">{t("english")}</option>
<option value="es">{t("spanish")}</option>
<option value="fr">{t("french")}</option>
<option value="de">{t("german")}</option>
<option value="sv">{t("swedish")}</option>

View file

@ -1,11 +1,12 @@
import dayjs from "dayjs";
import { useTranslation } from "next-i18next";
import { usePoll } from "@/components/poll-context";
import { useDayjs } from "../../../utils/dayjs";
import { useParticipants } from "../../participants-provider";
export const useCsvExporter = () => {
const { dayjs } = useDayjs();
const { poll, options } = usePoll();
const { t } = useTranslation("app");
const { participants } = useParticipants();

View file

@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { useDayjs } from "../../utils/dayjs";
import Badge from "../badge";
import { usePoll } from "../poll-context";
import Tooltip from "../tooltip";
@ -9,7 +9,7 @@ import Tooltip from "../tooltip";
const PollSubheader: React.VoidFunctionComponent = () => {
const { poll } = usePoll();
const { t } = useTranslation("app");
const { dayjs } = useDayjs();
return (
<div className="text-slate-500/75 lg:text-lg">
<div className="md:inline">

View file

@ -4,15 +4,14 @@ import { useTranslation } from "next-i18next";
import { usePlausible } from "next-plausible";
import React from "react";
import { useDayjs } from "../utils/dayjs";
import { LanguageSelect } from "./poll/language-selector";
import { usePreferences } from "./preferences/use-preferences";
const Preferences: React.VoidFunctionComponent = () => {
const { t } = useTranslation(["app", "common"]);
const { weekStartsOn, setWeekStartsOn, timeFormat, setTimeFormat } =
usePreferences();
useDayjs();
const router = useRouter();
const plausible = usePlausible();

View file

@ -1,85 +0,0 @@
import dayjs from "dayjs";
import de from "dayjs/locale/de";
import en from "dayjs/locale/en";
import fr from "dayjs/locale/fr";
import sv from "dayjs/locale/sv";
import duration from "dayjs/plugin/duration";
import isBetween from "dayjs/plugin/isBetween";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import localeData from "dayjs/plugin/localeData";
import localizedFormat from "dayjs/plugin/localizedFormat";
import minMax from "dayjs/plugin/minMax";
import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { useRouter } from "next/router";
import * as React from "react";
import { useLocalStorage } from "react-use";
type TimeFormat = "12h" | "24h";
type StartOfWeek = "monday" | "sunday";
const dayJsLocales = {
de,
en,
fr,
sv,
};
dayjs.extend(localizedFormat);
dayjs.extend(relativeTime);
dayjs.extend(localeData);
dayjs.extend(isSameOrBefore);
dayjs.extend(isBetween);
dayjs.extend(minMax);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(duration);
export const PreferencesContext =
React.createContext<{
weekStartsOn: StartOfWeek;
timeFormat: TimeFormat;
setWeekStartsOn: React.Dispatch<
React.SetStateAction<StartOfWeek | undefined>
>;
setTimeFormat: React.Dispatch<React.SetStateAction<TimeFormat | undefined>>;
} | null>(null);
PreferencesContext.displayName = "PreferencesContext";
const PreferencesProvider: React.VoidFunctionComponent<{
children?: React.ReactNode;
}> = ({ children }) => {
const [weekStartsOn = "monday", setWeekStartsOn] =
useLocalStorage<StartOfWeek>("rallly-week-starts-on");
const router = useRouter();
const userLocale = dayJsLocales[router.locale ?? "en"];
const [timeFormat = "12h", setTimeFormat] =
useLocalStorage<TimeFormat>("rallly-time-format");
dayjs.locale({
...userLocale,
weekStart: weekStartsOn === "monday" ? 1 : 0,
formats: { LT: timeFormat === "12h" ? "h:mm A" : "HH:mm" },
});
const contextValue = React.useMemo(
() => ({
weekStartsOn,
timeFormat,
setWeekStartsOn,
setTimeFormat,
}),
[setTimeFormat, setWeekStartsOn, timeFormat, weekStartsOn],
);
return (
<PreferencesContext.Provider value={contextValue}>
{children}
</PreferencesContext.Provider>
);
};
export default PreferencesProvider;

View file

@ -1,6 +0,0 @@
import { useRequiredContext } from "../use-required-context";
import { PreferencesContext } from "./preferences-provider";
export const usePreferences = () => {
return useRequiredContext(PreferencesContext);
};

View file

@ -1,4 +1,3 @@
import dayjs from "dayjs";
import Head from "next/head";
import Link from "next/link";
import { useTranslation } from "next-i18next";
@ -8,6 +7,7 @@ import Calendar from "@/components/icons/calendar.svg";
import Pencil from "@/components/icons/pencil.svg";
import User from "@/components/icons/user.svg";
import { useDayjs } from "../utils/dayjs";
import { trpc } from "../utils/trpc";
import { EmptyState } from "./empty-state";
import LoginForm from "./login-form";
@ -16,6 +16,7 @@ import { useSession } from "./session";
export const Profile: React.VoidFunctionComponent = () => {
const { user } = useSession();
const { dayjs } = useDayjs();
const { t } = useTranslation("app");
const { data: userPolls } = trpc.useQuery(["user.getPolls"]);

View file

@ -9,6 +9,7 @@ import User from "@/components/icons/user.svg";
import UserCircle from "@/components/icons/user-circle.svg";
import Logo from "~/public/logo.svg";
import { DayjsProvider } from "../utils/dayjs";
import Dropdown, { DropdownItem, DropdownProps } from "./dropdown";
import Adjustments from "./icons/adjustments.svg";
import Cash from "./icons/cash.svg";
@ -23,7 +24,7 @@ import Support from "./icons/support.svg";
import Twitter from "./icons/twitter.svg";
import LoginForm from "./login-form";
import { useModal } from "./modal";
import { useModalContext } from "./modal/modal-provider";
import ModalProvider, { useModalContext } from "./modal/modal-provider";
import Popover from "./popover";
import Preferences from "./preferences";
import { useSession } from "./session";
@ -244,154 +245,164 @@ const StandardLayout: React.VoidFunctionComponent<{
});
return (
<div
className="relative flex min-h-full flex-col bg-gray-50 lg:flex-row"
{...rest}
>
{loginModal}
<MobileNavigation openLoginModal={openLoginModal} />
<div className="hidden grow px-4 pt-6 pb-5 lg:block">
<div className="sticky top-6 float-right w-48 items-start">
<div className="mb-8 px-3">
<HomeLink />
</div>
<div className="mb-4">
<Link href="/new">
<a className="group mb-1 flex items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
<Pencil className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">{t("app:newPoll")}</span>
</a>
</Link>
<a
target="_blank"
href="https://support.rallly.co"
className="group mb-1 flex items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20"
rel="noreferrer"
>
<Support className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">{t("common:support")}</span>
</a>
<Popover
placement="right-start"
trigger={
<button className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">{t("app:preferences")}</span>
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
</button>
}
>
<Preferences />
</Popover>
{user ? null : (
<button
onClick={openLoginModal}
className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20"
>
<Login className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">{t("app:login")}</span>
</button>
)}
</div>
<AnimatePresence initial={false}>
{user ? (
<UserDropdown
className="mb-4 w-full"
placement="bottom-end"
openLoginModal={openLoginModal}
trigger={
<motion.button
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0, transition: { duration: 0.2 } }}
className="group w-full rounded-lg p-2 px-3 text-left text-inherit transition-colors hover:bg-slate-500/10 active:bg-slate-500/20"
>
<div className="flex w-full items-center space-x-3">
<div className="relative">
<UserCircle className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
</div>
<div className="grow overflow-hidden">
<div className="truncate font-medium leading-snug text-slate-600">
{user.shortName}
</div>
<div className="truncate text-xs text-slate-500">
{user.isGuest ? t("app:guest") : t("app:user")}
</div>
</div>
<ModalProvider>
<DayjsProvider>
<div
className="relative flex min-h-full flex-col bg-gray-50 lg:flex-row"
{...rest}
>
{loginModal}
<MobileNavigation openLoginModal={openLoginModal} />
<div className="hidden grow px-4 pt-6 pb-5 lg:block">
<div className="sticky top-6 float-right w-48 items-start">
<div className="mb-8 px-3">
<HomeLink />
</div>
<div className="mb-4">
<Link href="/new">
<a className="group mb-1 flex items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
<Pencil className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">{t("app:newPoll")}</span>
</a>
</Link>
<a
target="_blank"
href="https://support.rallly.co"
className="group mb-1 flex items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20"
rel="noreferrer"
>
<Support className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">{t("common:support")}</span>
</a>
<Popover
placement="right-start"
trigger={
<button className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">
{t("app:preferences")}
</span>
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
</motion.button>
}
/>
) : null}
</AnimatePresence>
</div>
</div>
<div className="min-w-0 grow">
<div className="max-w-full pt-12 md:w-[1024px] lg:min-h-[calc(100vh-64px)] lg:pt-0">
{children}
</div>
<div className="flex flex-col items-center space-y-4 px-6 pt-3 pb-6 text-slate-400 lg:h-16 lg:flex-row lg:space-y-0 lg:space-x-6 lg:py-0 lg:px-8 lg:pb-3">
<div>
<Link href="https://rallly.co">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
<Logo className="h-5" />
</a>
</Link>
</button>
}
>
<Preferences />
</Popover>
{user ? null : (
<button
onClick={openLoginModal}
className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20"
>
<Login className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">{t("app:login")}</span>
</button>
)}
</div>
<AnimatePresence initial={false}>
{user ? (
<UserDropdown
className="mb-4 w-full"
placement="bottom-end"
openLoginModal={openLoginModal}
trigger={
<motion.button
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{
x: -20,
opacity: 0,
transition: { duration: 0.2 },
}}
className="group w-full rounded-lg p-2 px-3 text-left text-inherit transition-colors hover:bg-slate-500/10 active:bg-slate-500/20"
>
<div className="flex w-full items-center space-x-3">
<div className="relative">
<UserCircle className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
</div>
<div className="grow overflow-hidden">
<div className="truncate font-medium leading-snug text-slate-600">
{user.shortName}
</div>
<div className="truncate text-xs text-slate-500">
{user.isGuest ? t("app:guest") : t("app:user")}
</div>
</div>
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
</motion.button>
}
/>
) : null}
</AnimatePresence>
</div>
</div>
<div className="hidden text-slate-300 lg:block">&bull;</div>
<div className="flex items-center justify-center space-x-6 md:justify-start">
<a
target="_blank"
href="https://support.rallly.co"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
rel="noreferrer"
>
{t("common:support")}
</a>
<Link href="https://github.com/lukevella/rallly/discussions">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
{t("common:discussions")}
</a>
</Link>
<Link href="https://blog.rallly.co">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
{t("common:blog")}
</a>
</Link>
<div className="hidden text-slate-300 lg:block">&bull;</div>
<div className="flex items-center space-x-6">
<div className="min-w-0 grow">
<div className="max-w-full pt-12 md:w-[1024px] lg:min-h-[calc(100vh-64px)] lg:pt-0">
{children}
</div>
<div className="flex flex-col items-center space-y-4 px-6 pt-3 pb-6 text-slate-400 lg:h-16 lg:flex-row lg:space-y-0 lg:space-x-6 lg:py-0 lg:px-8 lg:pb-3">
<div>
<Link href="https://rallly.co">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
<Logo className="h-5" />
</a>
</Link>
</div>
<div className="hidden text-slate-300 lg:block">&bull;</div>
<div className="flex items-center justify-center space-x-6 md:justify-start">
<a
target="_blank"
href="https://support.rallly.co"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
rel="noreferrer"
>
{t("common:support")}
</a>
<Link href="https://github.com/lukevella/rallly/discussions">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
{t("common:discussions")}
</a>
</Link>
<Link href="https://blog.rallly.co">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
{t("common:blog")}
</a>
</Link>
<div className="hidden text-slate-300 lg:block">&bull;</div>
<div className="flex items-center space-x-6">
<a
href="https://twitter.com/ralllyco"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
>
<Twitter className="h-5 w-5" />
</a>
<a
href="https://github.com/lukevella/rallly"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
>
<Github className="h-5 w-5" />
</a>
<a
href="https://discord.gg/uzg4ZcHbuM"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
>
<Discord className="h-5 w-5" />
</a>
</div>
</div>
<div className="hidden text-slate-300 lg:block">&bull;</div>
<a
href="https://twitter.com/ralllyco"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
className="inline-flex h-8 items-center rounded-full bg-slate-100 pl-2 pr-3 text-sm text-slate-400 transition-colors hover:bg-primary-500 hover:text-white hover:no-underline focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 active:bg-primary-600"
>
<Twitter className="h-5 w-5" />
</a>
<a
href="https://github.com/lukevella/rallly"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
>
<Github className="h-5 w-5" />
</a>
<a
href="https://discord.gg/uzg4ZcHbuM"
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
>
<Discord className="h-5 w-5" />
<Cash className="mr-1 inline-block w-5" />
<span>{t("app:donate")}</span>
</a>
</div>
</div>
<div className="hidden text-slate-300 lg:block">&bull;</div>
<a
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
className="inline-flex h-8 items-center rounded-full bg-slate-100 pl-2 pr-3 text-sm text-slate-400 transition-colors hover:bg-primary-500 hover:text-white hover:no-underline focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 active:bg-primary-600"
>
<Cash className="mr-1 inline-block w-5" />
<span>{t("app:donate")}</span>
</a>
</div>
</div>
</div>
</DayjsProvider>
</ModalProvider>
);
};