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 { Button } from "@/components/Button";
import { clearMessage } from "@/components/InfoMessage";
import { InfoMessageType } from "@/components/InfoMessage";
import InfoMessage from "@/components/InfoMessage";
import LoadingMap from "@/components/LoadingMap";
import { DateField } from "@/components/form/DateField";
@@ -32,10 +32,10 @@ const TournamentForm = () => {
const t = useTranslations("AddTournament");
const at = useTranslations("App");
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const [responseMessage, setResponseMessage] = useState<{
type: InfoMessageType;
message: string;
} | null>(null);
const form = useForm<TournamentFormValues>({
resolver: zodResolver(addTournamentSchema),
@@ -51,20 +51,18 @@ const TournamentForm = () => {
try {
await addTournament(data);
setResponseMessage({
isSuccessful: true,
type: "success",
message: t("success"),
});
clearMessage(setResponseMessage);
form.reset();
} catch (err: unknown) {
console.log(err);
setResponseMessage({
isSuccessful: false,
type: "error",
message: t("failure"),
});
clearMessage(setResponseMessage);
}
};
@@ -220,7 +218,14 @@ const TournamentForm = () => {
{t("clearForm")}
</Button>
</div>
<InfoMessage responseMessage={responseMessage} />
{responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</form>
</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>
<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">
<thead>
<tr>
@@ -9,7 +9,7 @@ import { z } from "zod";
import { Button } from "@/components/Button";
import InfoMessage from "@/components/InfoMessage";
import { clearMessage } from "@/components/InfoMessage";
import { InfoMessageType } from "@/components/InfoMessage";
import { TextAreaField } from "@/components/form/TextAreaField";
import { TextField } from "@/components/form/TextField";
import { contactUsSchema } from "@/schemas";
@@ -20,10 +20,10 @@ type TournamentFormValues = z.infer<typeof contactUsSchema>;
const ContactForm = () => {
const t = useTranslations("Contact");
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const [responseMessage, setResponseMessage] = useState<{
type: InfoMessageType;
message: string;
} | null>(null);
const form = useForm<TournamentFormValues>({
resolver: zodResolver(contactUsSchema),
@@ -33,20 +33,17 @@ const ContactForm = () => {
try {
await contactUs(data);
setResponseMessage({
isSuccessful: true,
type: "success",
message: t("success"),
});
clearMessage(setResponseMessage);
form.reset();
} catch (err: unknown) {
console.log(err);
setResponseMessage({
isSuccessful: false,
type: "error",
message: t("failure"),
});
clearMessage(setResponseMessage);
}
};
@@ -82,7 +79,13 @@ const ContactForm = () => {
{form.formState.isSubmitting ? t("sending") : t("sendButton")}
</Button>
<InfoMessage responseMessage={responseMessage} />
{responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</form>
</FormProvider>
);
@@ -50,10 +50,7 @@ export default function Contact() {
</h2>
{deleted ? (
<InfoMessage
className="text-center"
responseMessage={{ isSuccessful: true, message: t("success") }}
/>
<InfoMessage className="mb-8" type="success" message={t("success")} />
) : (
<>
<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 && (
<InfoMessage
className="mb-8 text-center"
responseMessage={{ isSuccessful: false, message: t("failure") }}
className="mb-8"
type="error"
message={t("failure")}
/>
)}
@@ -6,6 +6,7 @@ import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/Button";
import InfoMessage from "@/components/InfoMessage";
import { InlineSwitchField } from "@/components/form/InlineSwitchField";
import { Label } from "@/components/form/Label";
import { TextField } from "@/components/form/TextField";
@@ -20,6 +21,7 @@ type ZoneFormProps = {
zone?: Zone;
onSubmit: (data: ZoneFormValues) => Promise<void>;
onCancel: () => void;
withInfo?: boolean;
submitTitle?: string;
cancelTitle?: string;
};
@@ -28,6 +30,7 @@ export const ZoneForm = ({
zone,
onSubmit,
onCancel,
withInfo,
submitTitle,
cancelTitle,
}: ZoneFormProps) => {
@@ -58,8 +61,14 @@ export const ZoneForm = ({
},
});
const features = form.watch("features");
console.log(features);
const [cn, rn, bn, on] = form.watch([
"classicNotifications",
"rapidNotifications",
"blitzNotifications",
"otherNotifications",
]);
const requestingNotifications = cn || rn || bn || on;
return (
<FormProvider {...form}>
@@ -89,22 +98,34 @@ export const ZoneForm = ({
control={form.control}
label={at("timeControlEnum", { tc: TimeControl.Classic })}
/>
<InlineSwitchField
name="rapidNotifications"
control={form.control}
label={at("timeControlEnum", { tc: TimeControl.Rapid })}
/>
<InlineSwitchField
name="blitzNotifications"
control={form.control}
label={at("timeControlEnum", { tc: TimeControl.Blitz })}
/>
<InlineSwitchField
name="otherNotifications"
control={form.control}
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">
<ZoneEditorField name="features" control={form.control} />
</section>
+19 -10
View File
@@ -4,7 +4,7 @@ import { useState } from "react";
import { useTranslations } from "next-intl";
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
import InfoMessage, { InfoMessageType } from "@/components/InfoMessage";
import { useZones } from "@/hooks/useZones";
import { createZone } from "@/server/createZone";
import { useRouter } from "@/utils/routing";
@@ -15,10 +15,10 @@ const CreateZone = () => {
const t = useTranslations("Zones");
const router = useRouter();
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const [responseMessage, setResponseMessage] = useState<{
type: InfoMessageType;
message: string;
} | null>(null);
const { refetch } = useZones();
@@ -31,11 +31,9 @@ const CreateZone = () => {
} catch (err: unknown) {
console.log(err);
setResponseMessage({
isSuccessful: false,
type: "error",
message: t("createFailure"),
});
clearMessage(setResponseMessage);
}
};
@@ -48,8 +46,19 @@ const CreateZone = () => {
{t("createTitle")}
</h2>
<ZoneForm onSubmit={onSubmit} onCancel={() => router.push("/zones")} />
<InfoMessage responseMessage={responseMessage} />
<ZoneForm
withInfo
onSubmit={onSubmit}
onCancel={() => router.push("/zones")}
/>
{responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</div>
);
};
@@ -5,7 +5,7 @@ import { useState } from "react";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
import InfoMessage, { InfoMessageType } from "@/components/InfoMessage";
import { useZones } from "@/hooks/useZones";
import { editZone } from "@/server/editZone";
import { useRouter } from "@/utils/routing";
@@ -17,10 +17,10 @@ const EditZone = () => {
const router = useRouter();
const params = useParams();
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const [responseMessage, setResponseMessage] = useState<{
type: InfoMessageType;
message: string;
} | null>(null);
const { zones, isFetching, refetch } = useZones();
@@ -51,11 +51,9 @@ const EditZone = () => {
router.push("/zones");
} catch (error) {
setResponseMessage({
isSuccessful: false,
type: "error",
message: t("createFailure"),
});
clearMessage(setResponseMessage);
}
};
@@ -74,7 +72,13 @@ const EditZone = () => {
zone={zone}
/>
<InfoMessage responseMessage={responseMessage} />
{responseMessage && (
<InfoMessage
message={responseMessage.message}
type={responseMessage.type}
onDismiss={() => setResponseMessage(null)}
/>
)}
</div>
);
};
+13 -10
View File
@@ -7,7 +7,7 @@ import dynamic from "next/dynamic";
import { IoAdd } from "react-icons/io5";
import { Button, buttonVariants } from "@/components/Button";
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
import InfoMessage, { InfoMessageType } from "@/components/InfoMessage";
import { Modal } from "@/components/Modal";
import { Spinner } from "@/components/Spinner";
import { useZones } from "@/hooks/useZones";
@@ -27,11 +27,10 @@ const Zones = () => {
const format = useFormatter();
const [deletingZoneId, setDeletingZoneId] = useState<string | null>(null);
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const [responseMessage, setResponseMessage] = useState<{
type: InfoMessageType;
message: string;
} | null>(null);
const { zones, isFetching, refetch } = useZones();
const onDelete = async () => {
@@ -46,11 +45,9 @@ const Zones = () => {
} catch (err: unknown) {
console.log(err);
setResponseMessage({
isSuccessful: false,
type: "error",
message: t("createFailure"),
});
clearMessage(setResponseMessage);
}
};
@@ -154,7 +151,13 @@ const Zones = () => {
title={t("deleteZoneTitle")}
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">
<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 { ResponseMessage } from "@/types";
export type InfoMessageType = "success" | "error" | "info";
const InfoMessage = ({
responseMessage,
message,
type,
className,
onDismiss,
}: {
responseMessage: ResponseMessage;
message: React.ReactNode;
type: "success" | "error" | "info";
onDismiss?: () => void;
className?: string;
}) => (
<p
className={twMerge(
"italic",
responseMessage.isSuccessful ? "text-green-600" : "text-red-600",
className,
)}
data-test="info-message"
>
{responseMessage.message}
</p>
);
}) => {
const icons = {
success: HiCheckCircle,
error: HiXCircle,
info: HiInformationCircle,
};
export const clearMessage = (
setResponseMessage: Dispatch<SetStateAction<ResponseMessage>>,
) => {
setTimeout(() => {
setResponseMessage({
isSuccessful: false,
message: "",
});
}, 10000);
const colorClasses = {
success: {
text: "text-success",
bg: "bg-success/30",
button:
"bg-transparent hover:bg-success hover:text-white focus:ring-success focus:ring-offset-success/50",
},
error: {
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;
+15 -14
View File
@@ -9,7 +9,7 @@ import { usePathname as useRawPathname } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { clearMessage } from "@/components/InfoMessage";
import { InfoMessageType } from "@/components/InfoMessage";
import InfoMessage from "@/components/InfoMessage";
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
const path = useRawPathname();
const [responseMessage, setResponseMessage] = useState({
isSuccessful: false,
message: "",
});
const [responseMessage, setResponseMessage] = useState<{
type: InfoMessageType;
message: string;
} | null>(null);
const form = useForm<SignInFormValues>({
resolver: zodResolver(signInSchema),
@@ -44,13 +44,13 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
// Clear the response message when the email field changes
const subscription = form.watch((value, { name, type }) => {
if (type === "change") {
if (name === "email" && responseMessage.message === "") {
setResponseMessage({ isSuccessful: true, message: "" });
if (name === "email" && responseMessage !== null) {
setResponseMessage(null);
}
}
});
return () => subscription.unsubscribe();
}, [form, form.watch, responseMessage.message]);
}, [form, form.watch, responseMessage]);
const onSubmit = async ({ email }: SignInFormValues) => {
try {
@@ -64,7 +64,7 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
throw new Error(result.error);
} else if (result?.ok) {
setResponseMessage({
isSuccessful: true,
type: "success",
message: t("checkEmail"),
});
@@ -74,11 +74,9 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
console.log(err);
setResponseMessage({
isSuccessful: false,
type: "error",
message: t("failure"),
});
clearMessage(setResponseMessage);
}
};
@@ -87,6 +85,7 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<TextField
name="email"
type="email"
control={form.control}
label={t("emailLabel")}
placeholder={t("emailPlaceholder")}
@@ -97,10 +96,12 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
{t("signInButton")}
</Button>
{responseMessage && (
<InfoMessage
className="text-center"
responseMessage={responseMessage}
message={responseMessage.message}
type={responseMessage.type}
/>
)}
</form>
</FormProvider>
);
+1 -1
View File
@@ -237,7 +237,7 @@
"zoneNamePlaceholder": "Name for your zone",
"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.",
"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.",
"deleteZoneTitle": "Delete Zone",
+1
View File
@@ -238,6 +238,7 @@
"zoneNamePlaceholder": "Votre nom pour cette région",
"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.",
"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.",