Require logon for Zone pages

This commit is contained in:
Timothy Armes
2024-04-11 13:31:21 +02:00
parent 9e9f5fd975
commit 50943685c0
10 changed files with 155 additions and 90 deletions
+6 -79
View File
@@ -1,65 +1,13 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { useLocale, useTranslations } from "next-intl";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { clearMessage } from "@/components/InfoMessage";
import InfoMessage from "@/components/InfoMessage";
import { TextField } from "@/components/form/TextField";
const signInSchema = z.object({
email: z.string().email(),
});
type SignInFormValues = z.infer<typeof signInSchema>;
import { SignInForm } from "@/components/SignInForm";
export default function SignIn() {
const t = useTranslations("SignIn");
const locale = useLocale();
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const form = useForm<SignInFormValues>({
resolver: zodResolver(signInSchema),
});
const onSubmit = async ({ email }: SignInFormValues) => {
try {
const result = await signIn("nodemailer", {
email,
redirect: false,
callbackUrl: `/${locale}/tournaments`,
});
if (result?.error) {
throw new Error(result.error);
} else if (result?.ok) {
setResponseMessage({
isSuccessful: true,
message: t("checkEmail"),
});
form.reset();
}
} catch (err: unknown) {
console.log(err);
setResponseMessage({
isSuccessful: false,
message: t("failure"),
});
clearMessage(setResponseMessage);
}
};
const params = useSearchParams();
return (
<section className="grid place-items-center bg-white pb-20 dark:bg-gray-800">
@@ -74,30 +22,9 @@ export default function SignIn() {
{t("info")}
</p>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<TextField
name="email"
control={form.control}
label={t("emailLabel")}
placeholder={t("emailPlaceholder")}
required
<SignInForm
callbackPath={params.get("callbackUrl") ?? "/tournaments"}
/>
<button
disabled={form.formState.isSubmitting}
type="submit"
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
>
{t("signInButton")}
</button>
<InfoMessage
className="text-center"
responseMessage={responseMessage}
/>
</form>
</FormProvider>
</div>
</section>
);
@@ -35,7 +35,7 @@ const CreateZone = () => {
};
return (
<div className="mx-auto max-w-screen-md px-4 pb-20 pt-8 lg:pt-16">
<div>
<h2
className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
data-test="header2"
@@ -2,7 +2,7 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
@@ -17,7 +17,6 @@ const EditZone = () => {
const t = useTranslations("Zones");
const router = useRouter();
const params = useParams();
const queryClient = useQueryClient();
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
@@ -75,7 +74,7 @@ const EditZone = () => {
};
return (
<div className="mx-auto max-w-screen-md px-4 pb-20 pt-8 lg:pt-16">
<div>
<h2
className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
data-test="header2"
+36
View File
@@ -0,0 +1,36 @@
"use client";
import { ReactNode } from "react";
import { useSession } from "next-auth/react";
import { useTranslations } from "next-intl";
import { SignInForm } from "@/components/SignInForm";
export default function ZonesLayout({ children }: { children: ReactNode }) {
const { data: sessionData } = useSession();
const t = useTranslations("Zones");
return (
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
{!sessionData ? (
<div>
<h2
className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
data-test="header2"
>
{t("title")}
</h2>
<p className="mb-8 text-center font-light text-gray-500 dark:text-gray-400 sm:text-xl lg:mb-16">
{t("logonRequired")}
</p>
<SignInForm />
</div>
) : (
children
)}
</div>
);
}
+1 -1
View File
@@ -34,7 +34,7 @@ const Zones = () => {
const zones = data?.data ?? [];
return (
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
<div>
<h2
className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
data-test="header2"
+96
View File
@@ -0,0 +1,96 @@
"use client";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { useLocale, useTranslations } from "next-intl";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { clearMessage } from "@/components/InfoMessage";
import InfoMessage from "@/components/InfoMessage";
import { TextField } from "@/components/form/TextField";
import { usePathname } from "@/utils/navigation";
const signInSchema = z.object({
email: z.string().email(),
});
type SignInFormValues = z.infer<typeof signInSchema>;
type SignInFormProps = {
callbackPath?: string;
};
export const SignInForm = ({ callbackPath }: SignInFormProps) => {
const t = useTranslations("SignIn");
const locale = useLocale();
const path = usePathname();
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const form = useForm<SignInFormValues>({
resolver: zodResolver(signInSchema),
});
const onSubmit = async ({ email }: SignInFormValues) => {
try {
const result = await signIn("nodemailer", {
email,
redirect: false,
callbackUrl: callbackPath ?? `/${locale}/${path}`,
});
if (result?.error) {
throw new Error(result.error);
} else if (result?.ok) {
setResponseMessage({
isSuccessful: true,
message: t("checkEmail"),
});
form.reset();
}
} catch (err: unknown) {
console.log(err);
setResponseMessage({
isSuccessful: false,
message: t("failure"),
});
clearMessage(setResponseMessage);
}
};
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<TextField
name="email"
control={form.control}
label={t("emailLabel")}
placeholder={t("emailPlaceholder")}
required
/>
<button
disabled={form.formState.isSubmitting}
type="submit"
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
>
{t("signInButton")}
</button>
<InfoMessage
className="text-center"
responseMessage={responseMessage}
/>
</form>
</FormProvider>
);
};
+3
View File
@@ -219,6 +219,7 @@
"SignIn": {
"title": "Sign In",
"info": "Sign in to access additional features, such as receiving email notifications for tournaments in your area. There's no need to create an account, just sign in with your email address and we'll send you a link.",
"emailLabel": "Email",
"emailPlaceholder": "Your email",
"signInButton": "Sign In",
@@ -236,6 +237,8 @@
"Zones": {
"title": "Your Zones",
"info": "You can set up zones to receive email notifications for tournaments in these areas. These zones can also be used to filter tournaments on the main map.",
"logonRequired": "You must be signed in to access this feature so that we can store your personalized Zones. There's no need to create an account, just sign in with your email address and we'll send you a link.",
"noNotifications": "You have not enabled email notifications for this zone.",
"withNotifications": "You will receive email notifications for new tournaments with <b>{list}</b> time control in this region.",
"editButton": "Edit",
+2
View File
@@ -237,6 +237,8 @@
"Zones": {
"title": "Vos régions",
"info": "Vous pouvez définir des zones personnalisées pour recevoir des notifications par e-mail pour les tournois dans ces zones. Ces zones peuvent aussi être utiliser pour filtrer les tournois sur la carte.",
"logonRequired": "Vous devez être connecté pour accéder à cette fonctionnalité afin que nous puissions stocker vos régions personnalisées. Vous n'avez pas besoin de créer un compte, connectez-vous simplement avec votre adresse e-mail et nous vous enverrons un lien.",
"noNotifications": "Vous n'avez pas activé les notifications par email pour cette région.",
"withNotifications": "Vous allez recevoir des notifications par e-mail pour les nouveaux tournois de cadences <b>{list}</b> dans cette région.",
"editButton": "Modifier",
-2
View File
@@ -41,8 +41,6 @@ export const pathnames = {
fr: "/contactez-nous",
en: "/contact-us",
},
"[...rest]": "[...rest]",
} satisfies Pathnames<typeof locales>;
export const { Link, redirect, usePathname, useRouter, getPathname } =
+7 -3
View File
@@ -133,16 +133,20 @@ export default function Nodemailer(
maxAge: 24 * 60 * 60,
async sendVerificationRequest(params) {
const { identifier, url, provider } = params;
let locale = "fr";
const { searchParams } = new URL(params.url);
const { searchParams } = new URL(url);
if (searchParams.has("callbackUrl")) {
const { pathname } = new URL(searchParams.get("callbackUrl")!);
locale = pathname.split("/")[1];
const maybeLocale = pathname.split("/")[1];
if (["en", "fr"].includes(maybeLocale)) {
locale = maybeLocale;
}
}
const t = await getTranslations({ locale, namespace: "SignIn" });
const { identifier, url, provider } = params;
const { host } = new URL(url);
const transport = createTransport(provider.server);
const result = await transport.sendMail({