mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-22 20:16:57 +00:00
Require logon for Zone pages
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 } =
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user