Add information about first notification email

This commit is contained in:
Timothy Armes
2024-10-21 15:16:38 +02:00
parent b4e1b6d24c
commit 3dfbde0ef9
12 changed files with 196 additions and 103 deletions
@@ -9,7 +9,7 @@ import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { clearMessage } from "@/components/InfoMessage"; import { InfoMessageType } from "@/components/InfoMessage";
import InfoMessage from "@/components/InfoMessage"; import InfoMessage from "@/components/InfoMessage";
import LoadingMap from "@/components/LoadingMap"; import LoadingMap from "@/components/LoadingMap";
import { DateField } from "@/components/form/DateField"; import { DateField } from "@/components/form/DateField";
@@ -32,10 +32,10 @@ const TournamentForm = () => {
const t = useTranslations("AddTournament"); const t = useTranslations("AddTournament");
const at = useTranslations("App"); const at = useTranslations("App");
const [responseMessage, setResponseMessage] = useState({ const [responseMessage, setResponseMessage] = useState<{
isSuccessful: false, type: InfoMessageType;
message: "", message: string;
}); } | null>(null);
const form = useForm<TournamentFormValues>({ const form = useForm<TournamentFormValues>({
resolver: zodResolver(addTournamentSchema), resolver: zodResolver(addTournamentSchema),
@@ -51,20 +51,18 @@ const TournamentForm = () => {
try { try {
await addTournament(data); await addTournament(data);
setResponseMessage({ setResponseMessage({
isSuccessful: true, type: "success",
message: t("success"), message: t("success"),
}); });
clearMessage(setResponseMessage);
form.reset(); form.reset();
} catch (err: unknown) { } catch (err: unknown) {
console.log(err); console.log(err);
setResponseMessage({ setResponseMessage({
isSuccessful: false, type: "error",
message: t("failure"), message: t("failure"),
}); });
clearMessage(setResponseMessage);
} }
}; };
@@ -220,7 +218,14 @@ const TournamentForm = () => {
{t("clearForm")} {t("clearForm")}
</Button> </Button>
</div> </div>
<InfoMessage responseMessage={responseMessage} />
{responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</form> </form>
</FormProvider> </FormProvider>
); );
+1 -1
View File
@@ -52,7 +52,7 @@ const ClubTable = () => {
<div className="flex flex-col gap-0 text-gray-900 dark:text-white"></div> <div className="flex flex-col gap-0 text-gray-900 dark:text-white"></div>
</div> </div>
<div className="overflow-x-scroll bg-red-50"> <div className="overflow-x-scroll">
<table className="relative min-w-full table-fixed text-center text-xs lg:min-w-full"> <table className="relative min-w-full table-fixed text-center text-xs lg:min-w-full">
<thead> <thead>
<tr> <tr>
@@ -9,7 +9,7 @@ import { z } from "zod";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import InfoMessage from "@/components/InfoMessage"; import InfoMessage from "@/components/InfoMessage";
import { clearMessage } from "@/components/InfoMessage"; import { InfoMessageType } from "@/components/InfoMessage";
import { TextAreaField } from "@/components/form/TextAreaField"; import { TextAreaField } from "@/components/form/TextAreaField";
import { TextField } from "@/components/form/TextField"; import { TextField } from "@/components/form/TextField";
import { contactUsSchema } from "@/schemas"; import { contactUsSchema } from "@/schemas";
@@ -20,10 +20,10 @@ type TournamentFormValues = z.infer<typeof contactUsSchema>;
const ContactForm = () => { const ContactForm = () => {
const t = useTranslations("Contact"); const t = useTranslations("Contact");
const [responseMessage, setResponseMessage] = useState({ const [responseMessage, setResponseMessage] = useState<{
isSuccessful: false, type: InfoMessageType;
message: "", message: string;
}); } | null>(null);
const form = useForm<TournamentFormValues>({ const form = useForm<TournamentFormValues>({
resolver: zodResolver(contactUsSchema), resolver: zodResolver(contactUsSchema),
@@ -33,20 +33,17 @@ const ContactForm = () => {
try { try {
await contactUs(data); await contactUs(data);
setResponseMessage({ setResponseMessage({
isSuccessful: true, type: "success",
message: t("success"), message: t("success"),
}); });
clearMessage(setResponseMessage);
form.reset(); form.reset();
} catch (err: unknown) { } catch (err: unknown) {
console.log(err); console.log(err);
setResponseMessage({ setResponseMessage({
isSuccessful: false, type: "error",
message: t("failure"), message: t("failure"),
}); });
clearMessage(setResponseMessage);
} }
}; };
@@ -82,7 +79,13 @@ const ContactForm = () => {
{form.formState.isSubmitting ? t("sending") : t("sendButton")} {form.formState.isSubmitting ? t("sending") : t("sendButton")}
</Button> </Button>
<InfoMessage responseMessage={responseMessage} /> {responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</form> </form>
</FormProvider> </FormProvider>
); );
@@ -50,10 +50,7 @@ export default function Contact() {
</h2> </h2>
{deleted ? ( {deleted ? (
<InfoMessage <InfoMessage className="mb-8" type="success" message={t("success")} />
className="text-center"
responseMessage={{ isSuccessful: true, message: t("success") }}
/>
) : ( ) : (
<> <>
<p className="mb-8 text-center font-light text-gray-500 dark:text-gray-400 sm:text-xl lg:mb-16"> <p className="mb-8 text-center font-light text-gray-500 dark:text-gray-400 sm:text-xl lg:mb-16">
@@ -62,8 +59,9 @@ export default function Contact() {
{error && ( {error && (
<InfoMessage <InfoMessage
className="mb-8 text-center" className="mb-8"
responseMessage={{ isSuccessful: false, message: t("failure") }} type="error"
message={t("failure")}
/> />
)} )}
@@ -6,6 +6,7 @@ import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import InfoMessage from "@/components/InfoMessage";
import { InlineSwitchField } from "@/components/form/InlineSwitchField"; import { InlineSwitchField } from "@/components/form/InlineSwitchField";
import { Label } from "@/components/form/Label"; import { Label } from "@/components/form/Label";
import { TextField } from "@/components/form/TextField"; import { TextField } from "@/components/form/TextField";
@@ -20,6 +21,7 @@ type ZoneFormProps = {
zone?: Zone; zone?: Zone;
onSubmit: (data: ZoneFormValues) => Promise<void>; onSubmit: (data: ZoneFormValues) => Promise<void>;
onCancel: () => void; onCancel: () => void;
withInfo?: boolean;
submitTitle?: string; submitTitle?: string;
cancelTitle?: string; cancelTitle?: string;
}; };
@@ -28,6 +30,7 @@ export const ZoneForm = ({
zone, zone,
onSubmit, onSubmit,
onCancel, onCancel,
withInfo,
submitTitle, submitTitle,
cancelTitle, cancelTitle,
}: ZoneFormProps) => { }: ZoneFormProps) => {
@@ -58,8 +61,14 @@ export const ZoneForm = ({
}, },
}); });
const features = form.watch("features"); const [cn, rn, bn, on] = form.watch([
console.log(features); "classicNotifications",
"rapidNotifications",
"blitzNotifications",
"otherNotifications",
]);
const requestingNotifications = cn || rn || bn || on;
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
@@ -89,22 +98,34 @@ export const ZoneForm = ({
control={form.control} control={form.control}
label={at("timeControlEnum", { tc: TimeControl.Classic })} label={at("timeControlEnum", { tc: TimeControl.Classic })}
/> />
<InlineSwitchField <InlineSwitchField
name="rapidNotifications" name="rapidNotifications"
control={form.control} control={form.control}
label={at("timeControlEnum", { tc: TimeControl.Rapid })} label={at("timeControlEnum", { tc: TimeControl.Rapid })}
/> />
<InlineSwitchField <InlineSwitchField
name="blitzNotifications" name="blitzNotifications"
control={form.control} control={form.control}
label={at("timeControlEnum", { tc: TimeControl.Blitz })} label={at("timeControlEnum", { tc: TimeControl.Blitz })}
/> />
<InlineSwitchField <InlineSwitchField
name="otherNotifications" name="otherNotifications"
control={form.control} control={form.control}
label={at("timeControlEnum", { tc: TimeControl.Other })} label={at("timeControlEnum", { tc: TimeControl.Other })}
/> />
{withInfo && requestingNotifications && (
<div className="col-span-4">
<InfoMessage
type="info"
message={t.rich("firstNotificationInfo", { br: () => <br /> })}
/>
</div>
)}
<section id="map" className="z-0 col-span-4 flex h-auto"> <section id="map" className="z-0 col-span-4 flex h-auto">
<ZoneEditorField name="features" control={form.control} /> <ZoneEditorField name="features" control={form.control} />
</section> </section>
+19 -10
View File
@@ -4,7 +4,7 @@ import { useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import InfoMessage, { clearMessage } from "@/components/InfoMessage"; import InfoMessage, { InfoMessageType } from "@/components/InfoMessage";
import { useZones } from "@/hooks/useZones"; import { useZones } from "@/hooks/useZones";
import { createZone } from "@/server/createZone"; import { createZone } from "@/server/createZone";
import { useRouter } from "@/utils/routing"; import { useRouter } from "@/utils/routing";
@@ -15,10 +15,10 @@ const CreateZone = () => {
const t = useTranslations("Zones"); const t = useTranslations("Zones");
const router = useRouter(); const router = useRouter();
const [responseMessage, setResponseMessage] = useState({ const [responseMessage, setResponseMessage] = useState<{
isSuccessful: false, type: InfoMessageType;
message: "", message: string;
}); } | null>(null);
const { refetch } = useZones(); const { refetch } = useZones();
@@ -31,11 +31,9 @@ const CreateZone = () => {
} catch (err: unknown) { } catch (err: unknown) {
console.log(err); console.log(err);
setResponseMessage({ setResponseMessage({
isSuccessful: false, type: "error",
message: t("createFailure"), message: t("createFailure"),
}); });
clearMessage(setResponseMessage);
} }
}; };
@@ -48,8 +46,19 @@ const CreateZone = () => {
{t("createTitle")} {t("createTitle")}
</h2> </h2>
<ZoneForm onSubmit={onSubmit} onCancel={() => router.push("/zones")} /> <ZoneForm
<InfoMessage responseMessage={responseMessage} /> withInfo
onSubmit={onSubmit}
onCancel={() => router.push("/zones")}
/>
{responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</div> </div>
); );
}; };
@@ -5,7 +5,7 @@ import { useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import InfoMessage, { clearMessage } from "@/components/InfoMessage"; import InfoMessage, { InfoMessageType } from "@/components/InfoMessage";
import { useZones } from "@/hooks/useZones"; import { useZones } from "@/hooks/useZones";
import { editZone } from "@/server/editZone"; import { editZone } from "@/server/editZone";
import { useRouter } from "@/utils/routing"; import { useRouter } from "@/utils/routing";
@@ -17,10 +17,10 @@ const EditZone = () => {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [responseMessage, setResponseMessage] = useState({ const [responseMessage, setResponseMessage] = useState<{
isSuccessful: false, type: InfoMessageType;
message: "", message: string;
}); } | null>(null);
const { zones, isFetching, refetch } = useZones(); const { zones, isFetching, refetch } = useZones();
@@ -51,11 +51,9 @@ const EditZone = () => {
router.push("/zones"); router.push("/zones");
} catch (error) { } catch (error) {
setResponseMessage({ setResponseMessage({
isSuccessful: false, type: "error",
message: t("createFailure"), message: t("createFailure"),
}); });
clearMessage(setResponseMessage);
} }
}; };
@@ -74,7 +72,13 @@ const EditZone = () => {
zone={zone} zone={zone}
/> />
<InfoMessage responseMessage={responseMessage} /> {responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</div> </div>
); );
}; };
+13 -10
View File
@@ -7,7 +7,7 @@ import dynamic from "next/dynamic";
import { IoAdd } from "react-icons/io5"; import { IoAdd } from "react-icons/io5";
import { Button, buttonVariants } from "@/components/Button"; import { Button, buttonVariants } from "@/components/Button";
import InfoMessage, { clearMessage } from "@/components/InfoMessage"; import InfoMessage, { InfoMessageType } from "@/components/InfoMessage";
import { Modal } from "@/components/Modal"; import { Modal } from "@/components/Modal";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { useZones } from "@/hooks/useZones"; import { useZones } from "@/hooks/useZones";
@@ -27,11 +27,10 @@ const Zones = () => {
const format = useFormatter(); const format = useFormatter();
const [deletingZoneId, setDeletingZoneId] = useState<string | null>(null); const [deletingZoneId, setDeletingZoneId] = useState<string | null>(null);
const [responseMessage, setResponseMessage] = useState({ const [responseMessage, setResponseMessage] = useState<{
isSuccessful: false, type: InfoMessageType;
message: "", message: string;
}); } | null>(null);
const { zones, isFetching, refetch } = useZones(); const { zones, isFetching, refetch } = useZones();
const onDelete = async () => { const onDelete = async () => {
@@ -46,11 +45,9 @@ const Zones = () => {
} catch (err: unknown) { } catch (err: unknown) {
console.log(err); console.log(err);
setResponseMessage({ setResponseMessage({
isSuccessful: false, type: "error",
message: t("createFailure"), message: t("createFailure"),
}); });
clearMessage(setResponseMessage);
} }
}; };
@@ -154,7 +151,13 @@ const Zones = () => {
title={t("deleteZoneTitle")} title={t("deleteZoneTitle")}
subTitle={t("deleteZoneInfo")} subTitle={t("deleteZoneInfo")}
> >
<InfoMessage responseMessage={responseMessage} /> {responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
<div className="flex items-center justify-between space-x-4 text-sm font-bold"> <div className="flex items-center justify-between space-x-4 text-sm font-bold">
<Button <Button
+74 -26
View File
@@ -1,37 +1,85 @@
import { Dispatch, SetStateAction } from "react"; import {
HiCheckCircle,
HiInformationCircle,
HiXCircle,
HiXMark,
} from "react-icons/hi2";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { ResponseMessage } from "@/types"; export type InfoMessageType = "success" | "error" | "info";
const InfoMessage = ({ const InfoMessage = ({
responseMessage, message,
type,
className, className,
onDismiss,
}: { }: {
responseMessage: ResponseMessage; message: React.ReactNode;
type: "success" | "error" | "info";
onDismiss?: () => void;
className?: string; className?: string;
}) => ( }) => {
<p const icons = {
className={twMerge( success: HiCheckCircle,
"italic", error: HiXCircle,
responseMessage.isSuccessful ? "text-green-600" : "text-red-600", info: HiInformationCircle,
className, };
)}
data-test="info-message"
>
{responseMessage.message}
</p>
);
export const clearMessage = ( const colorClasses = {
setResponseMessage: Dispatch<SetStateAction<ResponseMessage>>, success: {
) => { text: "text-success",
setTimeout(() => { bg: "bg-success/30",
setResponseMessage({ button:
isSuccessful: false, "bg-transparent hover:bg-success hover:text-white focus:ring-success focus:ring-offset-success/50",
message: "", },
}); error: {
}, 10000); text: "text-error",
bg: "bg-error/30",
button:
"bg-transparent hover:bg-error hover:text-white focus:ring-error focus:ring-offset-error/50",
},
info: {
text: "text-primary",
bg: "bg-primary/30",
button:
"bg-transparent hover:bg-primary-800 hover:text-white focus:ring-primary-800 focus:ring-offset-primary-50",
},
};
const Icon = icons[type];
const { text, bg, button } = colorClasses[type];
return (
<div className={twMerge("rounded-md p-4", bg, className)}>
<div className="flex">
<div className="flex-shrink-0">
<Icon aria-hidden="true" className={twMerge("h-5 w-5", text)} />
</div>
<div className="ml-3">
<p className={twMerge("text-sm font-medium", text)}>{message}</p>
</div>
{onDismiss && (
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
className={twMerge(
"inline-flex rounded-md border-0 p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2",
text,
bg,
button,
)}
>
<HiXMark aria-hidden="true" className="h-5 w-5" />
</button>
</div>
</div>
)}
</div>
</div>
);
}; };
export default InfoMessage; export default InfoMessage;
+15 -14
View File
@@ -9,7 +9,7 @@ import { usePathname as useRawPathname } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { clearMessage } from "@/components/InfoMessage"; import { InfoMessageType } from "@/components/InfoMessage";
import InfoMessage from "@/components/InfoMessage"; import InfoMessage from "@/components/InfoMessage";
import { TextField } from "@/components/form/TextField"; import { TextField } from "@/components/form/TextField";
@@ -31,10 +31,10 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
// We use the version from next/navigation to get the raw pathname in the current locale // We use the version from next/navigation to get the raw pathname in the current locale
const path = useRawPathname(); const path = useRawPathname();
const [responseMessage, setResponseMessage] = useState({ const [responseMessage, setResponseMessage] = useState<{
isSuccessful: false, type: InfoMessageType;
message: "", message: string;
}); } | null>(null);
const form = useForm<SignInFormValues>({ const form = useForm<SignInFormValues>({
resolver: zodResolver(signInSchema), resolver: zodResolver(signInSchema),
@@ -44,13 +44,13 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
// Clear the response message when the email field changes // Clear the response message when the email field changes
const subscription = form.watch((value, { name, type }) => { const subscription = form.watch((value, { name, type }) => {
if (type === "change") { if (type === "change") {
if (name === "email" && responseMessage.message === "") { if (name === "email" && responseMessage !== null) {
setResponseMessage({ isSuccessful: true, message: "" }); setResponseMessage(null);
} }
} }
}); });
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, [form, form.watch, responseMessage.message]); }, [form, form.watch, responseMessage]);
const onSubmit = async ({ email }: SignInFormValues) => { const onSubmit = async ({ email }: SignInFormValues) => {
try { try {
@@ -64,7 +64,7 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
throw new Error(result.error); throw new Error(result.error);
} else if (result?.ok) { } else if (result?.ok) {
setResponseMessage({ setResponseMessage({
isSuccessful: true, type: "success",
message: t("checkEmail"), message: t("checkEmail"),
}); });
@@ -74,11 +74,9 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
console.log(err); console.log(err);
setResponseMessage({ setResponseMessage({
isSuccessful: false, type: "error",
message: t("failure"), message: t("failure"),
}); });
clearMessage(setResponseMessage);
} }
}; };
@@ -87,6 +85,7 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<TextField <TextField
name="email" name="email"
type="email"
control={form.control} control={form.control}
label={t("emailLabel")} label={t("emailLabel")}
placeholder={t("emailPlaceholder")} placeholder={t("emailPlaceholder")}
@@ -97,10 +96,12 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
{t("signInButton")} {t("signInButton")}
</Button> </Button>
{responseMessage && (
<InfoMessage <InfoMessage
className="text-center" message={responseMessage.message}
responseMessage={responseMessage} type={responseMessage.type}
/> />
)}
</form> </form>
</FormProvider> </FormProvider>
); );
+1 -1
View File
@@ -237,7 +237,7 @@
"zoneNamePlaceholder": "Name for your zone", "zoneNamePlaceholder": "Name for your zone",
"notificationsLabel": "Notifications", "notificationsLabel": "Notifications",
"notificationsInfo": "You can choose to turn on notifications for the time controls that interest you, and will receive an email when a new tournament is added to this zone.", "notificationsInfo": "You can choose to turn on notifications for the time controls that interest you, and will receive an email when a new tournament is added to this zone.",
"firstNotificationInfo": "The first email you receive will list all the current tournaments in this zone.<br></br>Subsequent notifications will only list new tournaments.",
"createFailure": "Oops, something went wrong. Please try again later.", "createFailure": "Oops, something went wrong. Please try again later.",
"deleteZoneTitle": "Delete Zone", "deleteZoneTitle": "Delete Zone",
+1
View File
@@ -238,6 +238,7 @@
"zoneNamePlaceholder": "Votre nom pour cette région", "zoneNamePlaceholder": "Votre nom pour cette région",
"notificationsLabel": "Notifications", "notificationsLabel": "Notifications",
"notificationsInfo": "Vous pouvez choisir d'activer les notifications pour les cadences qui vous intéressent, et vous recevrez un e-mail lorsqu'un nouveau tournoi est ajouté dans cette région.", "notificationsInfo": "Vous pouvez choisir d'activer les notifications pour les cadences qui vous intéressent, et vous recevrez un e-mail lorsqu'un nouveau tournoi est ajouté dans cette région.",
"firstNotificationInfo": "Le premier email que vous recevrez listera tous les tournois actuels dans cette zone.<br></br>Les notifications suivantes ne listeront que les nouveaux tournois.",
"createFailure": "Oups, quelque chose s'est mal passé. Veuillez réessayer.", "createFailure": "Oups, quelque chose s'est mal passé. Veuillez réessayer.",