Update login and registration (#437)

This commit is contained in:
Luke Vella 2023-01-30 10:15:25 +00:00 committed by GitHub
parent 4e67254022
commit 29eb477792
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1788 additions and 695 deletions

View 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>
);
};

View 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>
);
};

View 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>
);
};

View file

@ -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 }) => (

View file

@ -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>

View file

@ -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;

View file

@ -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={

View file

@ -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")} &rarr;

View file

@ -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 (

View file

@ -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 }}

View file

@ -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();
},
}}
>