Add locale support (#228)
|
@ -156,7 +156,7 @@ const Page: NextPage<CreatePollPageProps> = ({
|
|||
<div className="max-w-full py-4 md:px-3 lg:px-6">
|
||||
<div className="mx-auto w-fit max-w-full lg:mx-0">
|
||||
<div className="mb-4 flex items-center justify-center space-x-4 px-4 lg:justify-start">
|
||||
<h1 className="m-0">New Poll</h1>
|
||||
<h1 className="m-0">{t("newPoll")}</h1>
|
||||
<Steps current={currentStepIndex} total={steps.length} />
|
||||
</div>
|
||||
<div className="overflow-hidden border-t border-b bg-white shadow-sm md:rounded-lg md:border">
|
||||
|
|
|
@ -66,7 +66,7 @@ const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
);
|
||||
}}
|
||||
components={{
|
||||
toolbar: (props) => {
|
||||
toolbar: function Toolbar(props) {
|
||||
return (
|
||||
<DateNavigationToolbar
|
||||
year={props.date.getFullYear()}
|
||||
|
@ -83,7 +83,7 @@ const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
/>
|
||||
);
|
||||
},
|
||||
eventWrapper: (props) => {
|
||||
eventWrapper: function EventWraper(props) {
|
||||
const start = dayjs(props.event.start);
|
||||
const end = dayjs(props.event.end);
|
||||
return (
|
||||
|
@ -105,7 +105,7 @@ const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
},
|
||||
week: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
header: ({ date }: any) => {
|
||||
header: function Header({ date }: any) {
|
||||
const dateString = formatDateWithoutTime(date);
|
||||
const selectedOption = options.find((option) => {
|
||||
return option.type === "date" && option.date === dateString;
|
||||
|
@ -143,7 +143,7 @@ const WeekCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
);
|
||||
},
|
||||
},
|
||||
timeSlotWrapper: ({ children }) => {
|
||||
timeSlotWrapper: function TimeSlotWrapper({ children }) {
|
||||
return <div className="h-8 text-xs text-gray-500">{children}</div>;
|
||||
},
|
||||
}}
|
||||
|
|
|
@ -10,7 +10,7 @@ import Ban from "./ban-ads.svg";
|
|||
const Bonus: React.VoidFunctionComponent = () => {
|
||||
const { t } = useTranslation("homepage");
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-8 pt-8 pb-24">
|
||||
<div className="mx-auto max-w-7xl px-8 py-8">
|
||||
<h2 className="heading">{t("principles")}</h2>
|
||||
<p className="subheading">{t("principlesSubheading")}</p>
|
||||
<div className="grid grid-cols-4 gap-16">
|
||||
|
|
|
@ -26,12 +26,12 @@ const Hero: React.VoidFunctionComponent = () => {
|
|||
</h1>
|
||||
<div className="mb-12 text-xl text-gray-400">{t("heroSubText")}</div>
|
||||
<div className="space-x-3">
|
||||
<Link href="/new">
|
||||
<Link href="/new" locale={false}>
|
||||
<a className="rounded-lg bg-primary-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-primary-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-primary-600/90">
|
||||
{t("getStarted")}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/demo">
|
||||
<Link href="/demo" locale={false}>
|
||||
<a
|
||||
className="rounded-lg bg-slate-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-slate-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-slate-600/90"
|
||||
rel="nofollow"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M14.06 3.44a1.5 1.5 0 0 1 0 2.12l-7 7a1.5 1.5 0 0 1-2.12 0l-3-3a1.5 1.5 0 1 1 2.12-2.12L6 9.378l5.94-5.94a1.5 1.5 0 0 1 2.12 0Z" clip-rule="evenodd" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 211 B |
3
src/components/icons/discord.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.317 4.537a19.596 19.596 0 0 0-4.885-1.536.074.074 0 0 0-.079.038c-.21.38-.444.877-.608 1.267-1.845-.28-3.68-.28-5.487 0a12.891 12.891 0 0 0-.617-1.267A.077.077 0 0 0 8.562 3c-1.714.3-3.354.824-4.885 1.536a.07.07 0 0 0-.032.028C.533 9.278-.32 13.875.099 18.414a.084.084 0 0 0 .031.057 19.797 19.797 0 0 0 5.993 3.071.077.077 0 0 0 .084-.028c.462-.639.874-1.313 1.226-2.022a.078.078 0 0 0-.041-.107 13.021 13.021 0 0 1-1.872-.904.079.079 0 0 1-.008-.13c.126-.095.252-.195.372-.295a.073.073 0 0 1 .078-.01c3.927 1.817 8.18 1.817 12.061 0a.073.073 0 0 1 .079.009c.12.1.245.2.372.296a.079.079 0 0 1-.006.13 12.23 12.23 0 0 1-1.873.903.078.078 0 0 0-.041.108c.36.708.772 1.382 1.225 2.021a.076.076 0 0 0 .084.03 19.731 19.731 0 0 0 6.002-3.072.078.078 0 0 0 .032-.056c.5-5.248-.838-9.807-3.549-13.849a.061.061 0 0 0-.031-.029ZM8.02 15.65c-1.182 0-2.157-1.1-2.157-2.452 0-1.352.956-2.453 2.157-2.453 1.21 0 2.176 1.11 2.157 2.453 0 1.351-.956 2.452-2.157 2.452Zm7.975 0c-1.183 0-2.157-1.1-2.157-2.452 0-1.352.955-2.453 2.157-2.453 1.21 0 2.176 1.11 2.157 2.453 0 1.351-.946 2.452-2.157 2.452Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
3
src/components/icons/information-circle.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
After Width: | Height: | Size: 254 B |
|
@ -1,3 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 540 B |
3
src/components/icons/translate.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
|
||||
</svg>
|
After Width: | Height: | Size: 314 B |
|
@ -1,4 +1,5 @@
|
|||
import clsx from "clsx";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
import UserAvatar from "./poll/user-avatar";
|
||||
|
@ -16,6 +17,7 @@ const NameInput: React.ForwardRefRenderFunction<
|
|||
HTMLInputElement,
|
||||
NameInputProps
|
||||
> = ({ value, defaultValue, className, ...forwardProps }, ref) => {
|
||||
const { t } = useTranslation("app");
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<UserAvatar
|
||||
|
@ -25,7 +27,7 @@ const NameInput: React.ForwardRefRenderFunction<
|
|||
<input
|
||||
ref={ref}
|
||||
className={clsx("input pl-[35px]", className)}
|
||||
placeholder="Your name…"
|
||||
placeholder={t("yourName")}
|
||||
value={value}
|
||||
{...forwardProps}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from "clsx";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
|
@ -24,6 +23,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
|
|||
className,
|
||||
}) => {
|
||||
const { pathname } = useRouter();
|
||||
const { t } = useTranslation("common");
|
||||
return (
|
||||
<nav className={className}>
|
||||
<Link href="/">
|
||||
|
@ -36,7 +36,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
|
|||
},
|
||||
)}
|
||||
>
|
||||
Home
|
||||
{t("home")}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="https://blog.rallly.co">
|
||||
|
@ -45,14 +45,14 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
|
|||
"text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2",
|
||||
)}
|
||||
>
|
||||
Blog
|
||||
{t("blog")}
|
||||
</a>
|
||||
</Link>
|
||||
<a
|
||||
href="https://support.rallly.co"
|
||||
className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2"
|
||||
>
|
||||
Support
|
||||
{t("support")}
|
||||
</a>
|
||||
<Link href="https://github.com/lukevella/rallly">
|
||||
<a className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2">
|
||||
|
@ -70,9 +70,6 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
|
|||
const { t } = useTranslation("homepage");
|
||||
return (
|
||||
<div className="bg-pattern min-h-full overflow-x-hidden">
|
||||
<Head>
|
||||
<title>Rallly - Support</title>
|
||||
</Head>
|
||||
<div className="mx-auto flex max-w-7xl items-center py-8 px-8">
|
||||
<div className="grow">
|
||||
<div className="relative inline-block">
|
||||
|
|
|
@ -1,115 +1,151 @@
|
|||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import GitHubButton from "react-github-btn";
|
||||
|
||||
import Discord from "@/components/icons/discord.svg";
|
||||
import Star from "@/components/icons/star.svg";
|
||||
import Translate from "@/components/icons/translate.svg";
|
||||
import Twitter from "@/components/icons/twitter.svg";
|
||||
import DigitalOcean from "~/public/digitalocean.svg";
|
||||
import Logo from "~/public/logo.svg";
|
||||
import Sentry from "~/public/sentry.svg";
|
||||
import Vercel from "~/public/vercel-logotype-dark.svg";
|
||||
|
||||
import { LanguageSelect } from "../poll/language-selector";
|
||||
|
||||
const Footer: React.VoidFunctionComponent = () => {
|
||||
const { t } = useTranslation("homepage");
|
||||
const { t } = useTranslation(["common", "homepage"]);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="mt-16 bg-slate-50/70">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-10 gap-8 py-20 px-8">
|
||||
<div className="col-span-12 md:col-span-4">
|
||||
<Logo className="mb-4 w-32 text-gray-400" />
|
||||
<p className="text-sm text-gray-400">
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="footerCredit"
|
||||
components={{
|
||||
a: (
|
||||
<a
|
||||
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
|
||||
href="https://twitter.com/imlukevella"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<GitHubButton
|
||||
<div className="mx-auto max-w-7xl space-y-8 p-8 lg:grid lg:grid-cols-12 lg:gap-16 lg:space-y-0">
|
||||
<div className=" lg:col-span-4">
|
||||
<Logo className="w-32 text-slate-400" />
|
||||
<div className="mb-8 mt-4 text-slate-400">
|
||||
<p>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="common:footerSponsor"
|
||||
components={{
|
||||
a: (
|
||||
<a
|
||||
className="font-normal leading-loose text-slate-400 underline hover:text-slate-800 hover:underline"
|
||||
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="common:footerCredit"
|
||||
components={{
|
||||
a: (
|
||||
<a
|
||||
className="font-normal leading-loose text-slate-400 underline hover:text-slate-800 hover:underline"
|
||||
href="https://twitter.com/imlukevella"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 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://discord.gg/m5UFXavc2C"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
>
|
||||
<Discord className="h-5 w-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/lukevella/rallly"
|
||||
data-icon="octicon-star"
|
||||
aria-label="Star lukevella/rallly on GitHub"
|
||||
data-show-count={true}
|
||||
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"
|
||||
>
|
||||
{t("star")}
|
||||
</GitHubButton>
|
||||
<GitHubButton
|
||||
href="https://github.com/sponsors/lukevella"
|
||||
data-icon="octicon-heart"
|
||||
aria-label="Sponsor @lukevella on GitHub"
|
||||
>
|
||||
{t("sponsorThisProject")}
|
||||
</GitHubButton>
|
||||
<Star className="mr-2 inline-block w-5" />
|
||||
<span>{t("common:starOnGithub")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 md:col-span-2">
|
||||
<div className="mb-4 font-medium">{t("links")}</div>
|
||||
<div className="lg:col-span-2">
|
||||
<div className="mb-4 font-medium">{t("homepage:links")}</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
|
||||
className="font-normal leading-loose text-slate-400 hover:text-slate-800 hover:no-underline"
|
||||
href="https://github.com/lukevella/rallly/discussions"
|
||||
>
|
||||
{t("discussions")}
|
||||
{t("homepage:discussions")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="https://blog.rallly.co">
|
||||
<a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline">
|
||||
{t("blog")}
|
||||
<a className="font-normal leading-loose text-slate-400 hover:text-slate-800 hover:no-underline">
|
||||
{t("homepage:blog")}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://support.rallly.co"
|
||||
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
|
||||
className="font-normal leading-loose text-slate-400 hover:text-slate-800 hover:no-underline"
|
||||
>
|
||||
{t("support")}
|
||||
{t("homepage:support")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/privacy-policy">
|
||||
<a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline">
|
||||
{t("privacyPolicy")}
|
||||
<a className="font-normal leading-loose text-slate-400 hover:text-slate-800 hover:no-underline">
|
||||
{t("homepage:privacyPolicy")}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-span-6 md:col-span-2">
|
||||
<div className="mb-4 font-medium">{t("follow")}</div>
|
||||
<ul>
|
||||
<li>
|
||||
<div className="lg:col-span-3">
|
||||
<div className="mb-4 font-medium">{t("homepage:poweredBy")}</div>
|
||||
<div className="block space-y-4">
|
||||
<div>
|
||||
<a
|
||||
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
|
||||
href="https://github.com/lukevella/rallly"
|
||||
href="https://vercel.com?utm_source=rallly&utm_campaign=oss"
|
||||
className="inline-block text-white"
|
||||
>
|
||||
Github
|
||||
<Vercel className="h-5" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
|
||||
href="https://twitter.com/ralllyco"
|
||||
>
|
||||
Twitter
|
||||
</div>
|
||||
<div>
|
||||
<a className="inline-block" href="https://m.do.co/c/f91efc9c9e50">
|
||||
<DigitalOcean className="h-7" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<a className="inline-block" href="https://sentry.io">
|
||||
<Sentry className="h-6" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 md:col-span-2">
|
||||
<div className="lg:col-span-3">
|
||||
<div className="mb-4 font-medium">{t("common:language")}</div>
|
||||
<LanguageSelect
|
||||
className="mb-4 w-full"
|
||||
onChange={(locale) => {
|
||||
router.push(router.asPath, router.asPath, { locale });
|
||||
}}
|
||||
/>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=rallly&utm_campaign=oss"
|
||||
className="inline-block text-white"
|
||||
href="https://github.com/lukevella/rallly/wiki/Guide-for-translators"
|
||||
className="inline-flex items-center rounded-md border px-3 py-2 text-xs text-slate-500"
|
||||
>
|
||||
<span className="mb-1 inline-block w-full text-right text-xs italic text-gray-400">
|
||||
{t("poweredBy")}
|
||||
</span>
|
||||
<Vercel className="w-24" />
|
||||
<Translate className="mr-2 h-5 w-5" />
|
||||
{t("common:volunteerTranslator")} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -174,13 +174,6 @@ const PollPage: NextPage = () => {
|
|||
<Sharing
|
||||
onHide={() => {
|
||||
setSharingVisible(false);
|
||||
router.replace(
|
||||
`/admin/${router.query.urlId}`,
|
||||
undefined,
|
||||
{
|
||||
shallow: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import { useMeasure } from "react-use";
|
||||
import smoothscroll from "smoothscroll-polyfill";
|
||||
|
||||
import ArrowLeft from "@/components/icons/arrow-left.svg";
|
||||
import ArrowRight from "@/components/icons/arrow-right.svg";
|
||||
import Check from "@/components/icons/check.svg";
|
||||
import Plus from "@/components/icons/plus-sm.svg";
|
||||
|
||||
import { Button } from "../button";
|
||||
import ArrowLeft from "../icons/arrow-left.svg";
|
||||
import ArrowRight from "../icons/arrow-right.svg";
|
||||
import { useParticipants } from "../participants-provider";
|
||||
import { usePoll } from "../poll-context";
|
||||
import TimeZonePicker from "../time-zone-picker";
|
||||
|
@ -14,11 +16,10 @@ import ParticipantRow from "./desktop-poll/participant-row";
|
|||
import ParticipantRowForm from "./desktop-poll/participant-row-form";
|
||||
import { PollContext } from "./desktop-poll/poll-context";
|
||||
import PollHeader from "./desktop-poll/poll-header";
|
||||
import { useAddParticipantMutation } from "./mutations";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
smoothscroll.polyfill();
|
||||
}
|
||||
import {
|
||||
useAddParticipantMutation,
|
||||
useUpdateParticipantMutation,
|
||||
} from "./mutations";
|
||||
|
||||
const MotionButton = motion(Button);
|
||||
|
||||
|
@ -27,7 +28,8 @@ const minSidebarWidth = 200;
|
|||
const Poll: React.VoidFunctionComponent = () => {
|
||||
const { t } = useTranslation("app");
|
||||
|
||||
const { poll, options, targetTimeZone, setTargetTimeZone } = usePoll();
|
||||
const { poll, options, targetTimeZone, setTargetTimeZone, userAlreadyVoted } =
|
||||
usePoll();
|
||||
|
||||
const { participants } = useParticipants();
|
||||
|
||||
|
@ -35,7 +37,7 @@ const Poll: React.VoidFunctionComponent = () => {
|
|||
const [editingParticipantId, setEditingParticipantId] =
|
||||
React.useState<string | null>(null);
|
||||
|
||||
const actionColumnWidth = 140;
|
||||
const actionColumnWidth = 100;
|
||||
const columnWidth = Math.min(
|
||||
130,
|
||||
Math.max(
|
||||
|
@ -65,7 +67,8 @@ const Poll: React.VoidFunctionComponent = () => {
|
|||
const maxScrollPosition =
|
||||
columnWidth * options.length - columnWidth * numberOfVisibleColumns;
|
||||
|
||||
const shouldShowNewParticipantForm = !poll.closed;
|
||||
const [shouldShowNewParticipantForm, setShouldShowNewParticipantForm] =
|
||||
React.useState(!poll.closed && !userAlreadyVoted);
|
||||
|
||||
const pollWidth =
|
||||
sidebarWidth + options.length * columnWidth + actionColumnWidth;
|
||||
|
@ -87,6 +90,8 @@ const Poll: React.VoidFunctionComponent = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const updateParticipant = useUpdateParticipantMutation();
|
||||
|
||||
const participantListContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<PollContext.Provider
|
||||
|
@ -192,28 +197,93 @@ const Poll: React.VoidFunctionComponent = () => {
|
|||
isEditing ? participant.id : null,
|
||||
);
|
||||
}}
|
||||
onSubmit={async ({ name, votes }) => {
|
||||
await updateParticipant.mutateAsync({
|
||||
participantId: participant.id,
|
||||
pollId: poll.id,
|
||||
votes,
|
||||
name,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{shouldShowNewParticipantForm ? (
|
||||
{shouldShowNewParticipantForm &&
|
||||
!poll.closed &&
|
||||
!editingParticipantId ? (
|
||||
<ParticipantRowForm
|
||||
className="border-t bg-gray-50"
|
||||
className="shrink-0 border-t bg-gray-50"
|
||||
onSubmit={async ({ name, votes }) => {
|
||||
const participant = await addParticipant.mutateAsync({
|
||||
await addParticipant.mutateAsync({
|
||||
name,
|
||||
votes,
|
||||
pollId: poll.id,
|
||||
});
|
||||
setTimeout(() => {
|
||||
participantListContainerRef.current
|
||||
?.querySelector(`[data-participantid=${participant.id}]`)
|
||||
?.scrollIntoView();
|
||||
}, 100);
|
||||
setShouldShowNewParticipantForm(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{!poll.closed ? (
|
||||
<div className="flex h-14 shrink-0 items-center border-t bg-gray-50 px-3">
|
||||
{shouldShowNewParticipantForm || editingParticipantId ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
key="submit"
|
||||
form="participant-row-form"
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
icon={<Check />}
|
||||
loading={
|
||||
addParticipant.isLoading || updateParticipant.isLoading
|
||||
}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (editingParticipantId) {
|
||||
setEditingParticipantId(null);
|
||||
} else {
|
||||
setShouldShowNewParticipantForm(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<div className="text-sm">
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="saveInstruction"
|
||||
values={{
|
||||
save: t("save"),
|
||||
}}
|
||||
components={{ b: <strong /> }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full items-center space-x-3">
|
||||
<Button
|
||||
key="add-participant"
|
||||
onClick={() => {
|
||||
setShouldShowNewParticipantForm(true);
|
||||
}}
|
||||
icon={<Plus />}
|
||||
>
|
||||
{t("addParticipant")}
|
||||
</Button>
|
||||
{userAlreadyVoted ? (
|
||||
<div className="flex items-center text-sm text-gray-400">
|
||||
<Check className="mr-1 h-5" />
|
||||
<div>{t("alreadyVoted")}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</PollContext.Provider>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import clsx from "clsx";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import CompactButton from "@/components/compact-button";
|
||||
import Check from "@/components/icons/check.svg";
|
||||
import X from "@/components/icons/x.svg";
|
||||
import ArrowRight from "@/components/icons/arrow-right.svg";
|
||||
|
||||
import { requiredString } from "../../../utils/form-validation";
|
||||
import { Button } from "../../button";
|
||||
|
@ -17,6 +16,7 @@ import { VoteSelector } from "../vote-selector";
|
|||
import ControlledScrollArea from "./controlled-scroll-area";
|
||||
import { usePollContext } from "./poll-context";
|
||||
|
||||
const MotionButton = motion(Button);
|
||||
export interface ParticipantRowFormProps {
|
||||
defaultValues?: Partial<ParticipantForm>;
|
||||
onSubmit: (data: ParticipantFormSubmitted) => Promise<void>;
|
||||
|
@ -43,7 +43,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
|
|||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, submitCount, isSubmitting },
|
||||
formState: { errors, submitCount },
|
||||
reset,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
|
@ -69,6 +69,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
|
|||
|
||||
return (
|
||||
<form
|
||||
id="participant-row-form"
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(async ({ name, votes }) => {
|
||||
await onSubmit({
|
||||
|
@ -91,7 +92,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
|
|||
className={clsx("w-full", {
|
||||
"input-error": errors.name && submitCount > 0,
|
||||
})}
|
||||
placeholder="Your name"
|
||||
placeholder={t("yourName")}
|
||||
{...field}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === "Tab" && scrollPosition > 0) {
|
||||
|
@ -126,7 +127,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
|
|||
return (
|
||||
<div
|
||||
key={optionId}
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
className="flex shrink-0 items-center justify-center px-2"
|
||||
style={{ width: columnWidth }}
|
||||
>
|
||||
<VoteSelector
|
||||
|
@ -162,28 +163,25 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
|
|||
/>
|
||||
|
||||
<div className="flex items-center space-x-2 px-2 transition-all">
|
||||
{scrollPosition >= maxScrollPosition ? (
|
||||
<Button
|
||||
htmlType="submit"
|
||||
icon={<Check />}
|
||||
type="primary"
|
||||
loading={isSubmitting}
|
||||
data-testid="submitNewParticipant"
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
) : null}
|
||||
{scrollPosition < maxScrollPosition ? (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToNextPage();
|
||||
}}
|
||||
>
|
||||
{t("next")} →
|
||||
</Button>
|
||||
<AnimatePresence initial={false}>
|
||||
{scrollPosition < maxScrollPosition ? (
|
||||
<MotionButton
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="text-xs"
|
||||
rounded={true}
|
||||
onClick={() => {
|
||||
goToNextPage();
|
||||
}}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</MotionButton>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
{onCancel ? <CompactButton onClick={onCancel} icon={X} /> : null}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import Trash from "@/components/icons/trash.svg";
|
|||
import { usePoll } from "@/components/poll-context";
|
||||
import { useSession } from "@/components/session";
|
||||
|
||||
import { useUpdateParticipantMutation } from "../mutations";
|
||||
import { ParticipantFormSubmitted } from "../types";
|
||||
import { useDeleteParticipantModal } from "../use-delete-participant-modal";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import VoteIcon from "../vote-icon";
|
||||
|
@ -18,8 +18,9 @@ import { usePollContext } from "./poll-context";
|
|||
|
||||
export interface ParticipantRowProps {
|
||||
participant: Participant & { votes: Vote[] };
|
||||
editMode: boolean;
|
||||
onChangeEditMode: (value: boolean) => void;
|
||||
editMode?: boolean;
|
||||
onChangeEditMode?: (editMode: boolean) => void;
|
||||
onSubmit?: (data: ParticipantFormSubmitted) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ParticipantRowView: React.VoidFunctionComponent<{
|
||||
|
@ -49,7 +50,7 @@ export const ParticipantRowView: React.VoidFunctionComponent<{
|
|||
<div
|
||||
data-testid="participant-row"
|
||||
data-participantid={participantId}
|
||||
className="group flex h-14"
|
||||
className="group flex h-14 items-center"
|
||||
>
|
||||
<div
|
||||
className="flex shrink-0 items-center px-4"
|
||||
|
@ -74,12 +75,12 @@ export const ParticipantRowView: React.VoidFunctionComponent<{
|
|||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="relative shrink-0 transition-colors"
|
||||
className="relative flex shrink-0 items-center justify-center px-2 transition-colors"
|
||||
style={{ width: columnWidth }}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute inset-1 flex items-center justify-center rounded-lg",
|
||||
"flex h-10 w-full items-center justify-center rounded-md",
|
||||
{
|
||||
"bg-green-50": vote === "yes",
|
||||
"bg-amber-50": vote === "ifNeedBe",
|
||||
|
@ -100,12 +101,11 @@ export const ParticipantRowView: React.VoidFunctionComponent<{
|
|||
const ParticipantRow: React.VoidFunctionComponent<ParticipantRowProps> = ({
|
||||
participant,
|
||||
editMode,
|
||||
onSubmit,
|
||||
onChangeEditMode,
|
||||
}) => {
|
||||
const { columnWidth, sidebarWidth } = usePollContext();
|
||||
|
||||
const updateParticipant = useUpdateParticipantMutation();
|
||||
|
||||
const confirmDeleteParticipant = useDeleteParticipantModal();
|
||||
|
||||
const session = useSession();
|
||||
|
@ -128,12 +128,7 @@ const ParticipantRow: React.VoidFunctionComponent<ParticipantRowProps> = ({
|
|||
}),
|
||||
}}
|
||||
onSubmit={async ({ name, votes }) => {
|
||||
await updateParticipant.mutateAsync({
|
||||
participantId: participant.id,
|
||||
pollId: poll.id,
|
||||
votes,
|
||||
name,
|
||||
});
|
||||
await onSubmit?.({ name, votes });
|
||||
onChangeEditMode?.(false);
|
||||
}}
|
||||
onCancel={() => onChangeEditMode?.(false)}
|
||||
|
|
27
src/components/poll/language-selector.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
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();
|
||||
return (
|
||||
<select
|
||||
className={clsx("input", className)}
|
||||
defaultValue={router.locale}
|
||||
onChange={(e) => {
|
||||
Cookies.set("NEXT_LOCALE", e.target.value, {
|
||||
expires: 365,
|
||||
});
|
||||
onChange?.(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="en">{t("english")}</option>
|
||||
<option value="de">{t("german")}</option>
|
||||
</select>
|
||||
);
|
||||
};
|
|
@ -213,16 +213,18 @@ const ManagePoll: React.VoidFunctionComponent<{
|
|||
onClick={() => {
|
||||
modalContext.render({
|
||||
overlayClosable: true,
|
||||
content: ({ close }) => (
|
||||
<DeletePollForm
|
||||
onConfirm={async () => {
|
||||
close();
|
||||
setDeleted(true);
|
||||
}}
|
||||
onCancel={close}
|
||||
urlId={urlId}
|
||||
/>
|
||||
),
|
||||
content: function Content({ close }) {
|
||||
return (
|
||||
<DeletePollForm
|
||||
onConfirm={async () => {
|
||||
close();
|
||||
setDeleted(true);
|
||||
}}
|
||||
onCancel={close}
|
||||
urlId={urlId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
footer: null,
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -229,6 +229,7 @@ const PollOption: React.VoidFunctionComponent<PollOptionProps> = ({
|
|||
ref={selectorRef}
|
||||
value={vote}
|
||||
onChange={onChange}
|
||||
className="w-9"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
@ -29,6 +29,10 @@ export const useAddParticipantMutation = () => {
|
|||
return [...existingParticipants, participant];
|
||||
},
|
||||
);
|
||||
queryClient.invalidateQueries([
|
||||
"polls.participants.list",
|
||||
{ pollId: participant.pollId },
|
||||
]);
|
||||
session.refresh();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { VoteType } from "@prisma/client";
|
||||
import clsx from "clsx";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import * as React from "react";
|
||||
|
||||
|
@ -10,6 +11,7 @@ export interface VoteSelectorProps {
|
|||
onFocus?: React.FocusEventHandler<HTMLButtonElement>;
|
||||
onBlur?: React.FocusEventHandler<HTMLButtonElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const orderedVoteTypes: VoteType[] = ["yes", "ifNeedBe", "no"];
|
||||
|
@ -23,7 +25,10 @@ const getNext = (value: VoteType) => {
|
|||
export const VoteSelector = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
VoteSelectorProps
|
||||
>(function VoteSelector({ value, onChange, onFocus, onBlur, onKeyDown }, ref) {
|
||||
>(function VoteSelector(
|
||||
{ value, onChange, onFocus, onBlur, onKeyDown, className },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
data-testid="vote-selector"
|
||||
|
@ -31,7 +36,20 @@ export const VoteSelector = React.forwardRef<
|
|||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
className="focus-visible:ring-primary-500 relative inline-flex h-9 w-9 items-center justify-center rounded-lg border bg-white shadow-sm transition focus-visible:border-0 focus-visible:ring-2 active:scale-95"
|
||||
className={clsx(
|
||||
"group relative inline-flex h-9 w-full items-center justify-center overflow-hidden rounded-md border bg-white transition-all hover:ring-4 focus-visible:border-0 focus-visible:ring-2 focus-visible:ring-primary-500",
|
||||
{
|
||||
"border-green-200 bg-green-50 hover:ring-green-100/50 active:bg-green-100/50":
|
||||
value === "yes",
|
||||
"border-amber-200 bg-amber-50 hover:ring-amber-100/50 active:bg-amber-100/50":
|
||||
value === "ifNeedBe",
|
||||
"border-gray-200 bg-gray-50 hover:ring-gray-100/50 active:bg-gray-100/50":
|
||||
value === "no",
|
||||
"border-gray-200 hover:ring-gray-100/50 active:bg-gray-100/50":
|
||||
value === undefined,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange?.(value ? getNext(value) : orderedVoteTypes[0]);
|
||||
}}
|
||||
|
@ -40,10 +58,10 @@ export const VoteSelector = React.forwardRef<
|
|||
<AnimatePresence initial={false}>
|
||||
<motion.span
|
||||
className="absolute flex items-center justify-center"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, y: -15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 15 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{ opacity: 0, scale: 1.5, y: -45 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.5, y: 45 }}
|
||||
key={value}
|
||||
>
|
||||
<VoteIcon type={value} />
|
||||
|
|
|
@ -1,29 +1,33 @@
|
|||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { usePlausible } from "next-plausible";
|
||||
import React from "react";
|
||||
|
||||
import Calendar from "@/components/icons/calendar.svg";
|
||||
|
||||
import { LanguageSelect } from "./poll/language-selector";
|
||||
import { usePreferences } from "./preferences/use-preferences";
|
||||
|
||||
const Preferences: React.VoidFunctionComponent = () => {
|
||||
const { t } = useTranslation("app");
|
||||
const { t } = useTranslation(["app", "common"]);
|
||||
|
||||
const { weekStartsOn, setWeekStartsOn, timeFormat, setTimeFormat } =
|
||||
usePreferences();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const plausible = usePlausible();
|
||||
return (
|
||||
<div className="-mb-2">
|
||||
<div className="mb-4 flex items-center space-x-2 text-base font-semibold">
|
||||
<Calendar className="inline-block w-5" />
|
||||
<span>{t("timeAndDate")}</span>
|
||||
<div>
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="grow text-sm text-slate-500">
|
||||
{t("common:language")}
|
||||
</div>
|
||||
<LanguageSelect className="w-full" onChange={() => router.reload()} />
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="mb-2">
|
||||
<div className="grow space-y-2">
|
||||
<div>
|
||||
<div className="mb-2 grow text-sm text-slate-500">
|
||||
{t("weekStartsOn")}
|
||||
{t("app:weekStartsOn")}
|
||||
</div>
|
||||
<div>
|
||||
<div className="segment-button inline-flex">
|
||||
|
@ -41,7 +45,7 @@ const Preferences: React.VoidFunctionComponent = () => {
|
|||
}}
|
||||
type="button"
|
||||
>
|
||||
{t("monday")}
|
||||
{t("app:monday")}
|
||||
</button>
|
||||
<button
|
||||
className={clsx({
|
||||
|
@ -57,14 +61,14 @@ const Preferences: React.VoidFunctionComponent = () => {
|
|||
}}
|
||||
type="button"
|
||||
>
|
||||
{t("sunday")}
|
||||
{t("app:sunday")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="">
|
||||
<div className="mb-2 grow text-sm text-slate-500">
|
||||
{t("timeFormat")}
|
||||
{t("app:timeFormat")}
|
||||
</div>
|
||||
<div className="segment-button inline-flex">
|
||||
<button
|
||||
|
@ -81,7 +85,7 @@ const Preferences: React.VoidFunctionComponent = () => {
|
|||
}}
|
||||
type="button"
|
||||
>
|
||||
{t("12h")}
|
||||
{t("app:12h")}
|
||||
</button>
|
||||
<button
|
||||
className={clsx({
|
||||
|
@ -97,7 +101,7 @@ const Preferences: React.VoidFunctionComponent = () => {
|
|||
}}
|
||||
type="button"
|
||||
>
|
||||
{t("24h")}
|
||||
{t("app:24h")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import dayjs from "dayjs";
|
||||
import de from "dayjs/locale/de";
|
||||
import en from "dayjs/locale/en";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
|
@ -9,12 +10,18 @@ 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,
|
||||
};
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localeData);
|
||||
|
@ -43,11 +50,13 @@ const PreferencesProvider: React.VoidFunctionComponent<{
|
|||
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({
|
||||
...en,
|
||||
...userLocale,
|
||||
weekStart: weekStartsOn === "monday" ? 1 : 0,
|
||||
formats: { LT: timeFormat === "12h" ? "h:mm A" : "HH:mm" },
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import Logo from "~/public/logo.svg";
|
|||
import Dropdown, { DropdownItem, DropdownProps } from "./dropdown";
|
||||
import Adjustments from "./icons/adjustments.svg";
|
||||
import Cash from "./icons/cash.svg";
|
||||
import Discord from "./icons/discord.svg";
|
||||
import DotsVertical from "./icons/dots-vertical.svg";
|
||||
import Github from "./icons/github.svg";
|
||||
import Login from "./icons/login.svg";
|
||||
|
@ -293,7 +294,7 @@ const StandardLayout: React.VoidFunctionComponent<{
|
|||
<AnimatePresence initial={false}>
|
||||
{user ? (
|
||||
<UserDropdown
|
||||
className="w-full"
|
||||
className="mb-4 w-full"
|
||||
placement="bottom-end"
|
||||
openLoginModal={openLoginModal}
|
||||
trigger={
|
||||
|
@ -358,25 +359,34 @@ const StandardLayout: React.VoidFunctionComponent<{
|
|||
</Link>
|
||||
<div className="hidden text-slate-300 lg:block">•</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<Link href="https://twitter.com/ralllyco">
|
||||
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
|
||||
<Twitter className="h-5 w-5" />
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="https://github.com/lukevella/rallly">
|
||||
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
|
||||
<Github className="h-5 w-5" />
|
||||
</a>
|
||||
</Link>
|
||||
<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/m5UFXavc2C"
|
||||
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">•</div>
|
||||
<Link href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E">
|
||||
<a 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("donate")}</span>
|
||||
</a>
|
||||
</Link>
|
||||
<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("donate")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|