mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 09:59:00 +02:00
Update login and registration (#437)
This commit is contained in:
parent
4e67254022
commit
29eb477792
56 changed files with 1788 additions and 695 deletions
18
src/components/auth/auth-layout.tsx
Normal file
18
src/components/auth/auth-layout.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from "react";
|
||||
|
||||
import Logo from "~/public/logo.svg";
|
||||
|
||||
export const AuthLayout = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="h-full bg-slate-500/10 p-8">
|
||||
<div className="flex h-full items-start justify-center">
|
||||
<div className="w-[480px] max-w-full overflow-hidden rounded-lg border bg-white shadow-sm">
|
||||
<div className="bg-pattern border-b border-t-4 border-t-primary-500 bg-slate-500/5 p-4 text-center sm:p-8">
|
||||
<Logo className="inline-block h-6 text-primary-500 sm:h-7" />
|
||||
</div>
|
||||
<div className="p-4 sm:p-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
376
src/components/auth/login-form.tsx
Normal file
376
src/components/auth/login-form.tsx
Normal file
|
@ -0,0 +1,376 @@
|
|||
import Link from "next/link";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { requiredString, validEmail } from "../../utils/form-validation";
|
||||
import { trpcNext } from "../../utils/trpc";
|
||||
import { Button } from "../button";
|
||||
import { TextInput } from "../text-input";
|
||||
|
||||
const VerifyCode: React.VoidFunctionComponent<{
|
||||
email: string;
|
||||
onSubmit: (code: string) => Promise<void>;
|
||||
onResend: () => Promise<void>;
|
||||
onChange: () => void;
|
||||
}> = ({ onChange, onSubmit, email, onResend }) => {
|
||||
const { register, handleSubmit, setError, formState } =
|
||||
useForm<{ code: string }>();
|
||||
const { t } = useTranslation("app");
|
||||
const [resendStatus, setResendStatus] =
|
||||
React.useState<"ok" | "busy" | "disabled">("ok");
|
||||
|
||||
const handleResend = async () => {
|
||||
setResendStatus("busy");
|
||||
try {
|
||||
await onResend();
|
||||
setResendStatus("disabled");
|
||||
setTimeout(() => {
|
||||
setResendStatus("ok");
|
||||
}, 1000 * 30);
|
||||
} catch {
|
||||
setResendStatus("ok");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
onSubmit={handleSubmit(async ({ code }) => {
|
||||
try {
|
||||
await onSubmit(code);
|
||||
} catch {
|
||||
setError("code", {
|
||||
type: "not_found",
|
||||
message: t("wrongVerificationCode"),
|
||||
});
|
||||
}
|
||||
})}
|
||||
>
|
||||
<fieldset>
|
||||
<div className="mb-1 text-2xl font-bold">{t("verifyYourEmail")}</div>
|
||||
<p className="text-slate-500">
|
||||
{t("stepSummary", {
|
||||
current: 2,
|
||||
total: 2,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="verificationCodeSent"
|
||||
values={{ email }}
|
||||
components={{
|
||||
b: <strong className="whitespace-nowrap" />,
|
||||
a: (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<TextInput
|
||||
autoFocus={true}
|
||||
proportions="lg"
|
||||
error={!!formState.errors.code}
|
||||
className="w-full"
|
||||
placeholder={t("verificationCodePlaceholder")}
|
||||
{...register("code", {
|
||||
validate: requiredString,
|
||||
})}
|
||||
/>
|
||||
{formState.errors.code?.message ? (
|
||||
<p className="mt-2 text-sm text-rose-500">
|
||||
{formState.errors.code.message}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
{t("verificationCodeHelp")}
|
||||
</p>
|
||||
</fieldset>
|
||||
<div className="space-y-4 sm:flex sm:space-y-0 sm:space-x-3">
|
||||
<Button
|
||||
loading={formState.isSubmitting || formState.isSubmitSuccessful}
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
className="h-12 w-full px-6 sm:w-auto"
|
||||
>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleResend}
|
||||
loading={resendStatus === "busy"}
|
||||
disabled={resendStatus === "disabled"}
|
||||
className="h-12 w-full rounded-lg px-4 text-slate-500 transition-colors hover:bg-slate-500/10 active:bg-slate-500/20 sm:w-auto"
|
||||
>
|
||||
{t("resendVerificationCode")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type RegisterFormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const RegisterForm: React.VoidFunctionComponent<{
|
||||
onClickLogin?: React.MouseEventHandler;
|
||||
onRegistered: () => void;
|
||||
defaultValues?: Partial<RegisterFormData>;
|
||||
}> = ({ onClickLogin, onRegistered, defaultValues }) => {
|
||||
const { t } = useTranslation("app");
|
||||
const { register, handleSubmit, getValues, setError, formState } =
|
||||
useForm<RegisterFormData>({
|
||||
defaultValues,
|
||||
});
|
||||
const requestRegistration = trpcNext.auth.requestRegistration.useMutation();
|
||||
const authenticateRegistration =
|
||||
trpcNext.auth.authenticateRegistration.useMutation();
|
||||
const [token, setToken] = React.useState<string>();
|
||||
|
||||
if (token) {
|
||||
return (
|
||||
<VerifyCode
|
||||
onSubmit={async (code) => {
|
||||
const res = await authenticateRegistration.mutateAsync({
|
||||
token,
|
||||
code,
|
||||
});
|
||||
|
||||
if (!res.user) {
|
||||
throw new Error("Failed to authenticate user");
|
||||
}
|
||||
|
||||
onRegistered();
|
||||
posthog.identify(res.user.id, {
|
||||
email: res.user.email,
|
||||
name: res.user.name,
|
||||
});
|
||||
}}
|
||||
onResend={async () => {
|
||||
const values = getValues();
|
||||
await requestRegistration.mutateAsync({
|
||||
email: values.email,
|
||||
name: values.name,
|
||||
});
|
||||
}}
|
||||
onChange={() => setToken(undefined)}
|
||||
email={getValues("email")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(async (data) => {
|
||||
const res = await requestRegistration.mutateAsync({
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
switch (res.code) {
|
||||
case "userAlreadyExists":
|
||||
setError("email", {
|
||||
message: t("userAlreadyExists"),
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
setToken(res.token);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="mb-1 text-2xl font-bold">{t("createAnAccount")}</div>
|
||||
<p className="text-slate-500">
|
||||
{t("stepSummary", {
|
||||
current: 1,
|
||||
total: 2,
|
||||
})}
|
||||
</p>
|
||||
<fieldset className="mb-4">
|
||||
<label htmlFor="name" className="text-slate-500">
|
||||
{t("name")}
|
||||
</label>
|
||||
<TextInput
|
||||
className="w-full"
|
||||
proportions="lg"
|
||||
autoFocus={true}
|
||||
error={!!formState.errors.name}
|
||||
disabled={formState.isSubmitting}
|
||||
placeholder={t("namePlaceholder")}
|
||||
{...register("name", { validate: requiredString })}
|
||||
/>
|
||||
{formState.errors.name?.message ? (
|
||||
<div className="mt-2 text-sm text-rose-500">
|
||||
{formState.errors.name.message}
|
||||
</div>
|
||||
) : null}
|
||||
</fieldset>
|
||||
<fieldset className="mb-4">
|
||||
<label htmlFor="email" className="text-slate-500">
|
||||
{t("email")}
|
||||
</label>
|
||||
<TextInput
|
||||
className="w-full"
|
||||
proportions="lg"
|
||||
error={!!formState.errors.email}
|
||||
disabled={formState.isSubmitting}
|
||||
placeholder={t("emailPlaceholder")}
|
||||
{...register("email", { validate: validEmail })}
|
||||
/>
|
||||
{formState.errors.email?.message ? (
|
||||
<div className="mt-1 text-sm text-rose-500">
|
||||
{formState.errors.email.message}
|
||||
</div>
|
||||
) : null}
|
||||
</fieldset>
|
||||
<Button
|
||||
loading={formState.isSubmitting}
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
className="h-12 px-6"
|
||||
>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<div className="mt-4 border-t pt-4 text-slate-500 sm:text-base">
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="alreadyRegistered"
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-link"
|
||||
onClick={onClickLogin}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoginForm: React.VoidFunctionComponent<{
|
||||
onClickRegister?: (
|
||||
e: React.MouseEvent<HTMLAnchorElement>,
|
||||
email: string,
|
||||
) => void;
|
||||
onAuthenticated: () => void;
|
||||
}> = ({ onAuthenticated, onClickRegister }) => {
|
||||
const { t } = useTranslation("app");
|
||||
const { register, handleSubmit, getValues, formState, setError } =
|
||||
useForm<{ email: string }>();
|
||||
const requestLogin = trpcNext.auth.requestLogin.useMutation();
|
||||
const authenticateLogin = trpcNext.auth.authenticateLogin.useMutation();
|
||||
|
||||
const [token, setToken] = React.useState<string>();
|
||||
|
||||
if (token) {
|
||||
return (
|
||||
<VerifyCode
|
||||
onSubmit={async (code) => {
|
||||
const res = await authenticateLogin.mutateAsync({
|
||||
code,
|
||||
token,
|
||||
});
|
||||
|
||||
if (!res.user) {
|
||||
throw new Error("Failed to authenticate user");
|
||||
} else {
|
||||
onAuthenticated();
|
||||
posthog.identify(res.user.id, {
|
||||
email: res.user.email,
|
||||
name: res.user.name,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onResend={async () => {
|
||||
const values = getValues();
|
||||
const res = await requestLogin.mutateAsync({
|
||||
email: values.email,
|
||||
});
|
||||
|
||||
setToken(res.token);
|
||||
}}
|
||||
onChange={() => setToken(undefined)}
|
||||
email={getValues("email")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(async (data) => {
|
||||
const res = await requestLogin.mutateAsync({
|
||||
email: data.email,
|
||||
});
|
||||
|
||||
if (!res.token) {
|
||||
setError("email", {
|
||||
type: "not_found",
|
||||
message: t("userNotFound"),
|
||||
});
|
||||
} else {
|
||||
setToken(res.token);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="mb-1 text-2xl font-bold">{t("login")}</div>
|
||||
<p className="text-slate-500">
|
||||
{t("stepSummary", {
|
||||
current: 1,
|
||||
total: 2,
|
||||
})}
|
||||
</p>
|
||||
<fieldset className="mb-4">
|
||||
<label htmlFor="email" className="text-slate-500">
|
||||
{t("email")}
|
||||
</label>
|
||||
<TextInput
|
||||
className="w-full"
|
||||
proportions="lg"
|
||||
autoFocus={true}
|
||||
error={!!formState.errors.email}
|
||||
disabled={formState.isSubmitting}
|
||||
placeholder={t("emailPlaceholder")}
|
||||
{...register("email", { validate: validEmail })}
|
||||
/>
|
||||
{formState.errors.email?.message ? (
|
||||
<div className="mt-2 text-sm text-rose-500">
|
||||
{formState.errors.email.message}
|
||||
</div>
|
||||
) : null}
|
||||
</fieldset>
|
||||
<div className="mb-4 space-y-3">
|
||||
<Button
|
||||
loading={formState.isSubmitting}
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
className="h-12 w-full px-6"
|
||||
>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<Link
|
||||
href="/register"
|
||||
className="btn-default h-12 w-full px-6"
|
||||
onClick={(e) => {
|
||||
onClickRegister?.(e, getValues("email"));
|
||||
}}
|
||||
>
|
||||
{t("notRegistered")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
87
src/components/auth/login-modal.tsx
Normal file
87
src/components/auth/login-modal.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import Logo from "~/public/logo.svg";
|
||||
|
||||
import { useModalContext } from "../modal/modal-provider";
|
||||
import { useUser } from "../user-provider";
|
||||
import { LoginForm, RegisterForm } from "./login-form";
|
||||
|
||||
export const LoginModal: React.VoidFunctionComponent<{
|
||||
onDone: () => void;
|
||||
}> = ({ onDone }) => {
|
||||
const [hasAccount, setHasAccount] = React.useState(false);
|
||||
const [defaultEmail, setDefaultEmail] = React.useState("");
|
||||
|
||||
return (
|
||||
<div className="w-[420px] max-w-full overflow-hidden rounded-lg bg-white shadow-sm">
|
||||
<div className="bg-pattern border-b border-t-4 border-t-primary-500 bg-slate-500/5 p-4 text-center sm:p-8">
|
||||
<Logo className="inline-block h-6 text-primary-500 sm:h-7" />
|
||||
</div>
|
||||
<div className="p-4 sm:p-6">
|
||||
{hasAccount ? (
|
||||
<RegisterForm
|
||||
defaultValues={{ email: defaultEmail }}
|
||||
onRegistered={onDone}
|
||||
onClickLogin={(e) => {
|
||||
e.preventDefault();
|
||||
setHasAccount(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LoginForm
|
||||
onAuthenticated={onDone}
|
||||
onClickRegister={(e, email) => {
|
||||
e.preventDefault();
|
||||
setDefaultEmail(email);
|
||||
setHasAccount(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLoginModal = () => {
|
||||
const modalContext = useModalContext();
|
||||
const { refresh } = useUser();
|
||||
|
||||
const openLoginModal = () => {
|
||||
modalContext.render({
|
||||
overlayClosable: false,
|
||||
showClose: true,
|
||||
content: function Content({ close }) {
|
||||
return (
|
||||
<LoginModal
|
||||
onDone={() => {
|
||||
refresh();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
footer: null,
|
||||
});
|
||||
};
|
||||
return { openLoginModal };
|
||||
};
|
||||
|
||||
export const LoginLink = ({
|
||||
children,
|
||||
className,
|
||||
}: React.PropsWithChildren<{ className?: string }>) => {
|
||||
const { openLoginModal } = useLoginModal();
|
||||
return (
|
||||
<Link
|
||||
href="/login"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openLoginModal();
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
|
@ -9,7 +9,6 @@ import {
|
|||
import { Menu } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
||||
import { transformOriginByPlacement } from "@/utils/constants";
|
||||
|
@ -83,41 +82,14 @@ const Dropdown: React.VoidFunctionComponent<DropdownProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const AnchorLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
{
|
||||
href?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
>(function AnchorLink(
|
||||
{ href = "", className, children, ...forwardProps },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
href={href}
|
||||
passHref
|
||||
className={clsx(
|
||||
"font-normal hover:text-white hover:no-underline",
|
||||
className,
|
||||
)}
|
||||
{...forwardProps}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
export const DropdownItem: React.VoidFunctionComponent<{
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
label?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
}> = ({ icon: Icon, label, onClick, disabled, href }) => {
|
||||
const Element = href ? AnchorLink : "button";
|
||||
const Element = href ? "a" : "button";
|
||||
return (
|
||||
<Menu.Item disabled={disabled}>
|
||||
{({ active }) => (
|
||||
|
|
|
@ -33,7 +33,12 @@ const Bonus: React.VoidFunctionComponent = () => {
|
|||
t={t}
|
||||
i18nKey={"openSourceDescription"}
|
||||
components={{
|
||||
a: <a href="https://github.com/lukevella/rallly" />,
|
||||
a: (
|
||||
<a
|
||||
className="text-link"
|
||||
href="https://github.com/lukevella/rallly"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import * as React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { Button } from "@/components/button";
|
||||
import Magic from "@/components/icons/magic.svg";
|
||||
import { validEmail } from "@/utils/form-validation";
|
||||
|
||||
import { trpc } from "../utils/trpc";
|
||||
|
||||
const LoginForm: React.VoidFunctionComponent = () => {
|
||||
const { t } = useTranslation("app");
|
||||
const { register, formState, handleSubmit, getValues } =
|
||||
useForm<{ email: string }>();
|
||||
|
||||
const login = trpc.useMutation(["login"]);
|
||||
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="hidden items-center rounded-tl-lg rounded-bl-lg bg-slate-50 p-6 md:flex">
|
||||
<Magic className="h-24 text-slate-300" />
|
||||
</div>
|
||||
<div className="max-w-sm p-6">
|
||||
<div className="mb-2 text-xl font-semibold">
|
||||
{t("loginViaMagicLink")}
|
||||
</div>
|
||||
{!formState.isSubmitSuccessful ? (
|
||||
<form
|
||||
onSubmit={handleSubmit(async ({ email }) => {
|
||||
posthog.capture("login requested", { email });
|
||||
await login.mutateAsync({ email, path: router.asPath });
|
||||
})}
|
||||
>
|
||||
<div className="mb-2 text-slate-500">
|
||||
{t("loginViaMagicLinkDescription")}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
autoFocus={true}
|
||||
readOnly={formState.isSubmitting}
|
||||
className={clsx("input w-full", {
|
||||
"input-error": formState.errors.email,
|
||||
})}
|
||||
placeholder="john.doe@email.com"
|
||||
{...register("email", { validate: validEmail })}
|
||||
/>
|
||||
{formState.errors.email ? (
|
||||
<div className="mt-1 text-sm text-rose-500">
|
||||
{t("loginWithValidEmail")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Button
|
||||
htmlType="submit"
|
||||
loading={formState.isSubmitting}
|
||||
type="primary"
|
||||
>
|
||||
{t("loginSendMagicLink")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-slate-500">{t("loginMagicLinkSent")}</div>
|
||||
<div className="font-mono text-primary-500">
|
||||
{getValues("email")}
|
||||
</div>
|
||||
<div className="mt-2 text-slate-500">{t("loginCheckInbox")}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
|
@ -28,7 +28,11 @@ export const useModalContext = () => {
|
|||
const ModalProvider: React.VoidFunctionComponent<ModalProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [modals, { push, removeAt, updateAt }] = useList<ModalConfig>([]);
|
||||
const counter = React.useRef(0);
|
||||
|
||||
const [modals, { push, removeAt, updateAt }] = useList<
|
||||
ModalConfig & { id: number }
|
||||
>([]);
|
||||
|
||||
const removeModalAt = (index: number) => {
|
||||
updateAt(index, { ...modals[index], visible: false });
|
||||
|
@ -40,14 +44,14 @@ const ModalProvider: React.VoidFunctionComponent<ModalProviderProps> = ({
|
|||
<ModalContext.Provider
|
||||
value={{
|
||||
render: (props) => {
|
||||
push(props);
|
||||
push({ ...props, id: counter.current++ });
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{modals.map((props, i) => (
|
||||
<Modal
|
||||
key={i}
|
||||
key={`modal-${props.id}`}
|
||||
visible={true}
|
||||
{...props}
|
||||
content={
|
||||
|
|
|
@ -144,7 +144,7 @@ const Footer: React.VoidFunctionComponent = () => {
|
|||
/>
|
||||
<a
|
||||
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"
|
||||
className="inline-flex items-center rounded-md border px-3 py-2 text-xs text-slate-500 hover:border-primary-500 hover:text-primary-500"
|
||||
>
|
||||
<Translate className="mr-2 h-5 w-5" />
|
||||
{t("volunteerTranslator")} →
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
|
@ -10,7 +11,6 @@ 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";
|
||||
import { UserDetails } from "./profile/user-details";
|
||||
import { useUser } from "./user-provider";
|
||||
|
||||
|
@ -22,16 +22,16 @@ export const Profile: React.VoidFunctionComponent = () => {
|
|||
const { data: userPolls } = trpc.useQuery(["user.getPolls"]);
|
||||
|
||||
const createdPolls = userPolls?.polls;
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user.isGuest) {
|
||||
router.push("/profile");
|
||||
}
|
||||
}, [router, user.isGuest]);
|
||||
|
||||
if (user.isGuest) {
|
||||
return (
|
||||
<div className="card my-4 p-0">
|
||||
<Head>
|
||||
<title>{t("profileLogin")}</title>
|
||||
</Head>
|
||||
<LoginForm />
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -10,6 +10,7 @@ import UserCircle from "@/components/icons/user-circle.svg";
|
|||
import Logo from "~/public/logo.svg";
|
||||
|
||||
import { DayjsProvider } from "../utils/dayjs";
|
||||
import { LoginLink, useLoginModal } from "./auth/login-modal";
|
||||
import Dropdown, { DropdownItem, DropdownProps } from "./dropdown";
|
||||
import Adjustments from "./icons/adjustments.svg";
|
||||
import Cash from "./icons/cash.svg";
|
||||
|
@ -22,8 +23,6 @@ import Pencil from "./icons/pencil.svg";
|
|||
import Question from "./icons/question-mark-circle.svg";
|
||||
import Support from "./icons/support.svg";
|
||||
import Twitter from "./icons/twitter.svg";
|
||||
import LoginForm from "./login-form";
|
||||
import { useModal } from "./modal";
|
||||
import ModalProvider, { useModalContext } from "./modal/modal-provider";
|
||||
import Popover from "./popover";
|
||||
import Preferences from "./preferences";
|
||||
|
@ -37,9 +36,7 @@ const HomeLink = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const MobileNavigation: React.VoidFunctionComponent<{
|
||||
openLoginModal: () => void;
|
||||
}> = ({ openLoginModal }) => {
|
||||
const MobileNavigation: React.VoidFunctionComponent = () => {
|
||||
const { user } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
return (
|
||||
|
@ -52,18 +49,14 @@ const MobileNavigation: React.VoidFunctionComponent<{
|
|||
</div>
|
||||
<div className="flex items-center">
|
||||
{user ? null : (
|
||||
<button
|
||||
onClick={openLoginModal}
|
||||
className="flex w-full cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<LoginLink className="flex w-full cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300">
|
||||
<Login className="h-5 opacity-75" />
|
||||
<span className="inline-block">{t("app:login")}</span>
|
||||
</button>
|
||||
</LoginLink>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{user ? (
|
||||
<UserDropdown
|
||||
openLoginModal={openLoginModal}
|
||||
placement="bottom-end"
|
||||
trigger={
|
||||
<motion.button
|
||||
|
@ -126,7 +119,6 @@ const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
|
|||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
console.log("logo", Logo);
|
||||
return (
|
||||
<div className={clsx("space-y-1", className)}>
|
||||
<Link
|
||||
|
@ -149,11 +141,13 @@ const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const UserDropdown: React.VoidFunctionComponent<
|
||||
DropdownProps & { openLoginModal: () => void }
|
||||
> = ({ children, openLoginModal, ...forwardProps }) => {
|
||||
const UserDropdown: React.VoidFunctionComponent<DropdownProps> = ({
|
||||
children,
|
||||
...forwardProps
|
||||
}) => {
|
||||
const { logout, user } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
const { openLoginModal } = useLoginModal();
|
||||
const modalContext = useModalContext();
|
||||
if (!user) {
|
||||
return null;
|
||||
|
@ -245,12 +239,6 @@ const StandardLayout: React.VoidFunctionComponent<{
|
|||
}> = ({ children, ...rest }) => {
|
||||
const { user } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
const [loginModal, openLoginModal] = useModal({
|
||||
footer: null,
|
||||
overlayClosable: true,
|
||||
showClose: true,
|
||||
content: <LoginForm />,
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalProvider>
|
||||
|
@ -259,8 +247,7 @@ const StandardLayout: React.VoidFunctionComponent<{
|
|||
className="relative flex min-h-full flex-col bg-gray-50 lg:flex-row"
|
||||
{...rest}
|
||||
>
|
||||
{loginModal}
|
||||
<MobileNavigation openLoginModal={openLoginModal} />
|
||||
<MobileNavigation />
|
||||
<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">
|
||||
|
@ -298,13 +285,10 @@ const StandardLayout: React.VoidFunctionComponent<{
|
|||
<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"
|
||||
>
|
||||
<LoginLink 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>
|
||||
</LoginLink>
|
||||
)}
|
||||
</div>
|
||||
<AnimatePresence initial={false}>
|
||||
|
@ -312,7 +296,6 @@ const StandardLayout: React.VoidFunctionComponent<{
|
|||
<UserDropdown
|
||||
className="mb-4 w-full"
|
||||
placement="bottom-end"
|
||||
openLoginModal={openLoginModal}
|
||||
trigger={
|
||||
<motion.button
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
|
|
|
@ -12,7 +12,7 @@ export const UserContext =
|
|||
React.createContext<{
|
||||
user: UserSession & { shortName: string };
|
||||
refresh: () => void;
|
||||
logout: () => Promise<void>;
|
||||
logout: () => void;
|
||||
ownsObject: (obj: { userId: string | null }) => boolean;
|
||||
} | null>(null);
|
||||
|
||||
|
@ -50,27 +50,24 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
|
|||
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||
const { t } = useTranslation("app");
|
||||
|
||||
const { data: user, refetch } = trpcNext.whoami.get.useQuery();
|
||||
const queryClient = trpcNext.useContext();
|
||||
const { data: user, isFetching } = trpcNext.whoami.get.useQuery();
|
||||
|
||||
const logout = trpcNext.whoami.destroy.useMutation({
|
||||
onSuccess: () => {
|
||||
posthog.reset();
|
||||
queryClient.whoami.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
useMount(() => {
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY ?? "fake token", {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
|
||||
opt_out_capturing_by_default: false,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
autocapture: false,
|
||||
loaded: (posthog) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
|
||||
posthog.opt_out_capturing();
|
||||
}
|
||||
if (user && posthog.get_distinct_id() !== user.id) {
|
||||
|
@ -85,7 +82,9 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
|||
});
|
||||
});
|
||||
|
||||
const shortName = user
|
||||
const shortName = isFetching
|
||||
? t("loading")
|
||||
: user
|
||||
? user.isGuest === false
|
||||
? user.name
|
||||
: user.id.substring(0, 10)
|
||||
|
@ -99,16 +98,17 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
|||
<UserContext.Provider
|
||||
value={{
|
||||
user: { ...user, shortName },
|
||||
refresh: refetch,
|
||||
refresh: () => {
|
||||
return queryClient.whoami.invalidate();
|
||||
},
|
||||
ownsObject: ({ userId }) => {
|
||||
if (userId && user.id === userId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
logout: async () => {
|
||||
await logout.mutateAsync();
|
||||
refetch();
|
||||
logout: () => {
|
||||
logout.mutate();
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue