diff --git a/components/ModalContent/NewTokenModal.tsx b/components/ModalContent/NewTokenModal.tsx index 4d397e2..90e98a7 100644 --- a/components/ModalContent/NewTokenModal.tsx +++ b/components/ModalContent/NewTokenModal.tsx @@ -3,10 +3,10 @@ import TextInput from "@/components/TextInput"; import { TokenExpiry } from "@/types/global"; import toast from "react-hot-toast"; import Modal from "../Modal"; -import useTokenStore from "@/store/tokens"; import { dropdownTriggerer } from "@/lib/client/utils"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; +import { useAddToken } from "@/hooks/store/tokens"; type Props = { onClose: Function; @@ -15,7 +15,7 @@ type Props = { export default function NewTokenModal({ onClose }: Props) { const { t } = useTranslation(); const [newToken, setNewToken] = useState(""); - const { addToken } = useTokenStore(); + const addToken = useAddToken(); const initial = { name: "", @@ -28,16 +28,12 @@ export default function NewTokenModal({ onClose }: Props) { const submit = async () => { if (!submitLoader) { setSubmitLoader(true); - const load = toast.loading(t("creating_token")); - const { ok, data } = await addToken(token); - - toast.dismiss(load); - - if (ok) { - toast.success(t("token_created")); - setNewToken((data as any).secretKey); - } else toast.error(data as string); + await addToken.mutateAsync(token, { + onSuccess: (data) => { + setNewToken(data.secretKey); + }, + }); setSubmitLoader(false); } diff --git a/components/ModalContent/RevokeTokenModal.tsx b/components/ModalContent/RevokeTokenModal.tsx index 6b741e6..0e128c9 100644 --- a/components/ModalContent/RevokeTokenModal.tsx +++ b/components/ModalContent/RevokeTokenModal.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useState } from "react"; -import useTokenStore from "@/store/tokens"; -import toast from "react-hot-toast"; import Modal from "../Modal"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; import { AccessToken } from "@prisma/client"; +import { useRevokeToken } from "@/hooks/store/tokens"; type Props = { onClose: Function; @@ -15,26 +14,18 @@ export default function DeleteTokenModal({ onClose, activeToken }: Props) { const { t } = useTranslation(); const [token, setToken] = useState(activeToken); - const { revokeToken } = useTokenStore(); + const revokeToken = useRevokeToken(); useEffect(() => { setToken(activeToken); }, [activeToken]); const deleteLink = async () => { - const load = toast.loading(t("deleting")); - - const response = await revokeToken(token.id as number); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("token_revoked")); - } else { - toast.error(response.data as string); - } - - onClose(); + await revokeToken.mutateAsync(token.id, { + onSuccess: () => { + onClose(); + }, + }); }; return ( diff --git a/hooks/store/tokens.tsx b/hooks/store/tokens.tsx new file mode 100644 index 0000000..18bcd9d --- /dev/null +++ b/hooks/store/tokens.tsx @@ -0,0 +1,84 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; +import { AccessToken } from "@prisma/client"; + +const useTokens = () => { + return useQuery({ + queryKey: ["tokens"], + queryFn: async () => { + const response = await fetch("/api/v1/tokens"); + + if (!response.ok) throw new Error("Failed to fetch tokens."); + + const data = await response.json(); + return data.response as AccessToken[]; + }, + }); +}; + +const useAddToken = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (body: Partial) => { + const load = toast.loading(t("creating_token")); + + const response = await fetch("/api/v1/tokens", { + body: JSON.stringify(body), + method: "POST", + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data) => { + queryClient.setQueryData(["tokens"], (oldData: AccessToken[]) => [ + ...oldData, + data.token, + ]); + toast.success(t("token_added")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useRevokeToken = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (tokenId: number) => { + const load = toast.loading(t("deleting")); + + const response = await fetch(`/api/v1/tokens/${tokenId}`, { + method: "DELETE", + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data, variables) => { + queryClient.setQueryData(["tokens"], (oldData: AccessToken[]) => + oldData.filter((token: Partial) => token.id !== variables) + ); + toast.success(t("token_revoked")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { useTokens, useAddToken, useRevokeToken }; diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx index e526e2f..9e1cfd6 100644 --- a/pages/settings/access-tokens.tsx +++ b/pages/settings/access-tokens.tsx @@ -1,11 +1,11 @@ import SettingsLayout from "@/layouts/SettingsLayout"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import NewTokenModal from "@/components/ModalContent/NewTokenModal"; import RevokeTokenModal from "@/components/ModalContent/RevokeTokenModal"; import { AccessToken } from "@prisma/client"; -import useTokenStore from "@/store/tokens"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTokens } from "@/hooks/store/tokens"; export default function AccessTokens() { const [newTokenModal, setNewTokenModal] = useState(false); @@ -18,15 +18,7 @@ export default function AccessTokens() { setRevokeTokenModal(true); }; - const { setTokens, tokens } = useTokenStore(); - - useEffect(() => { - fetch("/api/v1/tokens") - .then((res) => res.json()) - .then((data) => { - if (data.response) setTokens(data.response as AccessToken[]); - }); - }, []); + const { data: tokens = [] } = useTokens(); return (