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) => ( + + + +
+ + +
+
+ + + {title && ( +
+
+ + {title} + +
+ + {subTitle && ( + + {subTitle} + + )} +
+ )} + + {children} +
+
+
+
+
+
+); 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==