diff --git a/.vscode/settings.json b/.vscode/settings.json
index a13638a..39e83e3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,6 +8,7 @@
"colour",
"colours",
"contactez",
+ "creer",
"datepicker",
"defaulticon",
"Depfu",
@@ -31,7 +32,7 @@
"spiderified",
"superjson",
"tailwindcss",
- "tanstack",Ò
+ "tanstack",
"timothyarmes",
"tournoi",
"tournois",
diff --git a/package.json b/package.json
index dc72865..085168c 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
},
"dependencies": {
"@auth/mongodb-adapter": "^2.5.0",
- "@headlessui/react": "^1.7.17",
+ "@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"@hookform/resolvers": "^3.3.3",
"@infra-blocks/zod-utils": "^0.4.2",
diff --git a/src/app/[locale]/(main)/zones/create/page.tsx b/src/app/[locale]/(main)/zones/create/page.tsx
index 5a0a280..9d9ac64 100644
--- a/src/app/[locale]/(main)/zones/create/page.tsx
+++ b/src/app/[locale]/(main)/zones/create/page.tsx
@@ -5,6 +5,7 @@ import { useState } from "react";
import { useTranslations } from "next-intl";
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
+import { useZones } from "@/hooks/useZones";
import { createZone } from "@/server/createZone";
import { useRouter } from "@/utils/navigation";
@@ -19,9 +20,13 @@ const CreateZone = () => {
message: "",
});
+ const { refetch } = useZones();
+
const onSubmit = async (data: ZoneFormValues) => {
try {
await createZone(data);
+ await refetch();
+
router.push("/zones");
} catch (err: unknown) {
console.log(err);
diff --git a/src/app/[locale]/(main)/zones/edit/[id]/page.tsx b/src/app/[locale]/(main)/zones/edit/[id]/page.tsx
index c4938e9..f76764b 100644
--- a/src/app/[locale]/(main)/zones/edit/[id]/page.tsx
+++ b/src/app/[locale]/(main)/zones/edit/[id]/page.tsx
@@ -2,13 +2,12 @@
import { useState } from "react";
-import { useMutation, useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
+import { useZones } from "@/hooks/useZones";
import { editZone } from "@/server/editZone";
-import { myZones } from "@/server/myZones";
import { useRouter } from "@/utils/navigation";
import { ZoneForm, ZoneFormValues } from "../../components/ZoneForm";
@@ -23,43 +22,9 @@ const EditZone = () => {
message: "",
});
- const { data, isFetching, error, refetch } = useQuery({
- queryKey: ["zones"],
- queryFn: async () => myZones(),
- refetchOnWindowFocus: false,
- staleTime: Infinity,
- gcTime: 10 * 60 * 1000,
- retry: false,
- });
+ const { zones, isFetching, refetch } = useZones();
- const editZoneMutation = useMutation({
- mutationFn: async (data: ZoneFormValues) => {
- const updatedZone = await editZone({
- id: params.id as string,
- zone: data,
- });
-
- if (updatedZone.serverError) {
- throw new Error(updatedZone.serverError);
- }
-
- return updatedZone;
- },
- onError: (error) => {
- setResponseMessage({
- isSuccessful: false,
- message: t("createFailure"),
- });
-
- clearMessage(setResponseMessage);
- },
- onSuccess: async () => {
- await refetch();
- router.push("/zones");
- },
- });
-
- const zone = (data?.data ?? []).find((zone) => zone.id === params.id);
+ const zone = zones.find((zone) => zone.id === params.id);
if (isFetching) {
return null;
@@ -70,7 +35,26 @@ const EditZone = () => {
}
const onSubmit = async (data: ZoneFormValues) => {
- await editZoneMutation.mutateAsync(data);
+ try {
+ const updatedZone = await editZone({
+ id: params.id as string,
+ zone: data,
+ });
+
+ if (updatedZone.serverError) {
+ throw new Error(updatedZone.serverError);
+ }
+
+ await refetch();
+ router.push("/zones");
+ } catch (error) {
+ setResponseMessage({
+ isSuccessful: false,
+ message: t("createFailure"),
+ });
+
+ clearMessage(setResponseMessage);
+ }
};
return (
@@ -87,6 +71,7 @@ const EditZone = () => {
onCancel={() => router.push("/zones")}
zone={zone}
/>
+
);
diff --git a/src/app/[locale]/(main)/zones/layout.tsx b/src/app/[locale]/(main)/zones/layout.tsx
index 548cc18..1f92fbb 100644
--- a/src/app/[locale]/(main)/zones/layout.tsx
+++ b/src/app/[locale]/(main)/zones/layout.tsx
@@ -6,11 +6,19 @@ import { useSession } from "next-auth/react";
import { useTranslations } from "next-intl";
import { SignInForm } from "@/components/SignInForm";
+import { Spinner } from "@/components/Spinner";
export default function ZonesLayout({ children }: { children: ReactNode }) {
- const { data: sessionData } = useSession();
+ const { data: sessionData, status } = useSession();
const t = useTranslations("Zones");
+ if (status === "loading")
+ return (
+
+
+
+ );
+
return (
{!sessionData ? (
diff --git a/src/app/[locale]/(main)/zones/page.tsx b/src/app/[locale]/(main)/zones/page.tsx
index cc1e646..412f26b 100644
--- a/src/app/[locale]/(main)/zones/page.tsx
+++ b/src/app/[locale]/(main)/zones/page.tsx
@@ -1,16 +1,18 @@
"use client";
-import React from "react";
+import React, { useState } from "react";
-import { useQuery } from "@tanstack/react-query";
import { useFormatter, useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import { IoAdd } from "react-icons/io5";
+import InfoMessage, { clearMessage } from "@/components/InfoMessage";
+import { Modal } from "@/components/Modal";
import { Spinner } from "@/components/Spinner";
-import { myZones } from "@/server/myZones";
+import { useZones } from "@/hooks/useZones";
+import { deleteZone } from "@/server/deleteZone";
import { TimeControl } from "@/types";
-import { Link } from "@/utils/navigation";
+import { Link, useRouter } from "@/utils/navigation";
const ZoneThumbnail = dynamic(() => import("./components/ZoneThumbnail"), {
ssr: false,
@@ -20,103 +22,149 @@ const ZoneThumbnail = dynamic(() => import("./components/ZoneThumbnail"), {
const Zones = () => {
const t = useTranslations("Zones");
const at = useTranslations("App");
+ const router = useRouter();
const format = useFormatter();
- const { data, isFetching, error } = useQuery({
- queryKey: ["zones"],
- queryFn: async () => myZones(),
- refetchOnWindowFocus: false,
- staleTime: Infinity,
- gcTime: 10 * 60 * 1000,
- retry: false,
+ const [deletingZoneId, setDeletingZoneId] = useState
(null);
+ const [responseMessage, setResponseMessage] = useState({
+ isSuccessful: false,
+ message: "",
});
- const zones = data?.data ?? [];
+ const { zones, isFetching, refetch } = useZones();
+
+ const onDelete = async () => {
+ try {
+ if (!deletingZoneId) return;
+
+ await deleteZone({ id: deletingZoneId });
+ await refetch();
+ setDeletingZoneId(null);
+
+ router.push("/zones");
+ } catch (err: unknown) {
+ console.log(err);
+ setResponseMessage({
+ isSuccessful: false,
+ message: t("createFailure"),
+ });
+
+ clearMessage(setResponseMessage);
+ }
+ };
return (
-
-
- {t("title")}
-
-
- {t("info")}
-
+ <>
+
+
+ {t("title")}
+
+
+ {t("info")}
+
- {isFetching ? (
-
-
-
- ) : (
-
- {zones?.map((zone) => {
- const notifications = [
- ...(zone.classicNotifications ? [TimeControl.Classic] : []),
- ...(zone.rapidNotifications ? [TimeControl.Rapid] : []),
- ...(zone.blitzNotifications ? [TimeControl.Blitz] : []),
- ];
+ {isFetching ? (
+
+
+
+ ) : (
+
+ {zones?.map((zone) => {
+ const notifications = [
+ ...(zone.classicNotifications ? [TimeControl.Classic] : []),
+ ...(zone.rapidNotifications ? [TimeControl.Rapid] : []),
+ ...(zone.blitzNotifications ? [TimeControl.Blitz] : []),
+ ];
- const notificationsList = format.list(
- notifications.map((tc) => at("timeControlEnumInline", { tc })),
- { type: "conjunction" },
- );
+ const notificationsList = format.list(
+ notifications.map((tc) => at("timeControlEnumInline", { tc })),
+ { type: "conjunction" },
+ );
- return (
-
-
-
-
- {zone.name}
-
+ return (
+
+
+
+
+ {zone.name}
+
-
- {notifications.length === 0
- ? t("noNotifications")
- : t.rich("withNotifications", {
- list: notificationsList,
- b: (str) => {str},
- })}
-
+
+ {notifications.length === 0
+ ? t("noNotifications")
+ : t.rich("withNotifications", {
+ list: notificationsList,
+ b: (str) => {str},
+ })}
+
-
-
- {t("editButton")}
-
-
+
+
+ {at("editButton")}
+
+
+
-
- );
- })}
-
- )}
+ );
+ })}
+
+ )}
-
-
-
- {t("addZoneButton")}
-
+
+
+
+ {t("addZoneButton")}
+
+
-
+
+
setDeletingZoneId(null)}
+ title={t("deleteZoneTitle")}
+ subTitle={t("deleteZoneInfo")}
+ >
+
+
+
+
+
+
+
+ >
);
};
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
new file mode 100644
index 0000000..69351de
--- /dev/null
+++ b/src/components/Modal.tsx
@@ -0,0 +1,104 @@
+import React, { ComponentProps, Fragment } from "react";
+
+import { Dialog, Transition } from "@headlessui/react";
+import { twMerge } from "tailwind-merge";
+
+type ModalProps = ComponentProps
& {
+ title?: string;
+ subTitle?: string;
+
+ titleClass?: string;
+ subTitleClass?: string;
+ overlayClass?: string;
+ panelClass?: string;
+ children?: React.ReactNode;
+};
+
+export const Modal = ({
+ open,
+ onClose,
+
+ title,
+ subTitle,
+
+ titleClass,
+ subTitleClass,
+ overlayClass,
+ panelClass,
+ children,
+
+ ...dialogProps
+}: ModalProps) => (
+
+
+
+);
diff --git a/src/hooks/useZones.ts b/src/hooks/useZones.ts
new file mode 100644
index 0000000..3e3baf1
--- /dev/null
+++ b/src/hooks/useZones.ts
@@ -0,0 +1,19 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { myZones } from "@/server/myZones";
+
+export const useZones = () => {
+ const query = useQuery({
+ queryKey: ["zones"],
+ queryFn: async () => myZones(),
+ refetchOnWindowFocus: false,
+ staleTime: Infinity,
+ gcTime: 10 * 60 * 1000,
+ retry: false,
+ });
+
+ return {
+ ...query,
+ zones: query.data?.data ?? [],
+ };
+};
diff --git a/src/messages/en.json b/src/messages/en.json
index 1d5115a..91a181b 100644
--- a/src/messages/en.json
+++ b/src/messages/en.json
@@ -17,7 +17,9 @@
"timeControlEnumInline": "{tc, select, Classic {classic} Rapid {rapid} Blitz {blitz} Other {other} other {{tc}}}",
"saveButton": "Save",
- "cancelButton": "Cancel"
+ "cancelButton": "Cancel",
+ "editButton": "Edit",
+ "deleteButton": "Delete"
},
"Errors": {
@@ -241,8 +243,6 @@
"noNotifications": "You have not enabled email notifications for this zone.",
"withNotifications": "You will receive email notifications for new tournaments with {list} time control in this region.",
- "editButton": "Edit",
- "deleteButton": "Delete",
"addZoneButton": "Add a Region",
"createTitle": "Create a new zone",
@@ -252,8 +252,10 @@
"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.",
- "saveButton": "Save",
- "createFailure": "Oops, something went wrong. Please try again later."
+ "createFailure": "Oops, something went wrong. Please try again later.",
+
+ "deleteZoneTitle": "Delete Zone",
+ "deleteZoneInfo": "Are you sure you want to delete this zone? This action cannot be undone."
}
}
diff --git a/src/messages/fr.json b/src/messages/fr.json
index 933fb70..dfb25ac 100644
--- a/src/messages/fr.json
+++ b/src/messages/fr.json
@@ -17,7 +17,9 @@
"timeControlEnumInline": "{tc, select, Classic {lente} Rapid {rapide} Blitz {blitz} Other {autres} other {{tc}}}",
"saveButton": "Sauvegarder",
- "cancelButton": "Annuler"
+ "cancelButton": "Annuler",
+ "editButton": "Modifier",
+ "deleteButton": "Supprimer"
},
"Errors": {
@@ -241,8 +243,6 @@
"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 {list} dans cette région.",
- "editButton": "Modifier",
- "deleteButton": "Supprimer",
"addZoneButton": "Ajouter une région",
"createTitle": "Créer une nouvelle région",
@@ -252,8 +252,10 @@
"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 zone.",
- "saveButton": "Sauvegarder",
- "createFailure": "Oups, quelque chose s'est mal passé. Veuillez réessayer."
+ "createFailure": "Oups, quelque chose s'est mal passé. Veuillez réessayer.",
+
+ "deleteZoneTitle": "Supprimer la région",
+ "deleteZoneInfo": "Êtes-vous sûr de vouloir supprimer cette région\u00a0?"
}
}
diff --git a/src/server/deleteZone.ts b/src/server/deleteZone.ts
new file mode 100644
index 0000000..d4eee5b
--- /dev/null
+++ b/src/server/deleteZone.ts
@@ -0,0 +1,39 @@
+"use server";
+
+import { ObjectId } from "mongodb";
+import { z } from "zod";
+
+import { auth } from "@/auth";
+import { collections, dbConnect } from "@/server/mongodb";
+import { errorLog } from "@/utils/logger";
+
+import { action } from "./safeAction";
+
+const deleteZoneSchema = z.object({
+ id: z.string(),
+});
+
+export const deleteZone = action(deleteZoneSchema, async ({ id }) => {
+ try {
+ await dbConnect();
+
+ const user = await auth();
+ if (!user?.user) {
+ throw new Error("You must be logged in to create a zone");
+ }
+
+ const result = await collections.zones!.deleteOne({
+ _id: new ObjectId(id),
+ userId: new ObjectId(user.user!.id!),
+ });
+
+ if (!result || result.deletedCount !== 1) {
+ throw new Error("ERR_ZONE_DELETE_FAILED");
+ }
+
+ return true;
+ } catch (error) {
+ errorLog(error);
+ throw error;
+ }
+});
diff --git a/yarn.lock b/yarn.lock
index 2257d4b..8d559d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1259,7 +1259,7 @@
dependencies:
tslib "^2.4.0"
-"@headlessui/react@^1.7.17":
+"@headlessui/react@^1.7.18":
version "1.7.18"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.18.tgz#30af4634d2215b2ca1aa29d07f33d02bea82d9d7"
integrity sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==