diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index da634cc..f57fcc0 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -5,12 +5,12 @@ import ProfilePhoto from "./ProfilePhoto"; import usePermissions from "@/hooks/usePermissions"; import useLocalSettingsStore from "@/store/localSettings"; import getPublicUserData from "@/lib/client/getPublicUserData"; -import useAccountStore from "@/store/account"; import EditCollectionModal from "./ModalContent/EditCollectionModal"; import EditCollectionSharingModal from "./ModalContent/EditCollectionSharingModal"; import DeleteCollectionModal from "./ModalContent/DeleteCollectionModal"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; +import { useUser } from "@/hooks/store/users"; type Props = { collection: CollectionIncludingMembersAndLinkCount; @@ -20,7 +20,7 @@ type Props = { export default function CollectionCard({ collection, className }: Props) { const { t } = useTranslation(); const { settings } = useLocalSettingsStore(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const formattedDate = new Date(collection.createdAt as string).toLocaleString( "en-US", @@ -45,18 +45,18 @@ export default function CollectionCard({ collection, className }: Props) { useEffect(() => { const fetchOwner = async () => { - if (collection && collection.ownerId !== account.id) { + if (collection && collection.ownerId !== user.id) { const owner = await getPublicUserData(collection.ownerId as number); setCollectionOwner(owner); - } else if (collection && collection.ownerId === account.id) { + } else if (collection && collection.ownerId === user.id) { setCollectionOwner({ - id: account.id as number, - name: account.name, - username: account.username as string, - image: account.image as string, - archiveAsScreenshot: account.archiveAsScreenshot as boolean, - archiveAsMonolith: account.archiveAsMonolith as boolean, - archiveAsPDF: account.archiveAsPDF as boolean, + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsMonolith as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, }); } }; diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index d3b433e..725c0d6 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -13,10 +13,10 @@ import { Collection } from "@prisma/client"; import Link from "next/link"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { useRouter } from "next/router"; -import useAccountStore from "@/store/account"; import toast from "react-hot-toast"; import { useTranslation } from "next-i18next"; import { useCollections, useUpdateCollection } from "@/hooks/store/collections"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; interface ExtendedTreeItem extends TreeItem { data: Collection; @@ -25,54 +25,56 @@ interface ExtendedTreeItem extends TreeItem { const CollectionListing = () => { const { t } = useTranslation(); const updateCollection = useUpdateCollection(); - const { data: { response: collections } = { response: [] } } = - useCollections(); - const { account, updateAccount } = useAccountStore(); + const { data: collections = [] } = useCollections(); + + const { data: user = [] } = useUser(); + const updateUser = useUpdateUser(); const router = useRouter(); const currentPath = router.asPath; + const [tree, setTree] = useState(); + const initialTree = useMemo(() => { - if (collections.length > 0) { + if ( + // !tree && + collections.length > 0 + ) { return buildTreeFromCollections( collections, router, - account.collectionOrder + user.collectionOrder ); - } - return undefined; - }, [collections, router]); - - const [tree, setTree] = useState(initialTree); + } else return undefined; + }, [collections, user, router]); useEffect(() => { + // if (!tree) setTree(initialTree); }, [initialTree]); useEffect(() => { - if (account.username) { + if (user.username) { if ( - (!account.collectionOrder || account.collectionOrder.length === 0) && + (!user.collectionOrder || user.collectionOrder.length === 0) && collections.length > 0 ) - updateAccount({ - ...account, + updateUser.mutate({ + ...user, collectionOrder: collections .filter( (e) => e.parentId === null || !collections.find((i) => i.id === e.parentId) ) // Filter out collections with non-null parentId - .map((e) => e.id as number), // Use "as number" to assert that e.id is a number + .map((e) => e.id as number), }); else { - const newCollectionOrder: number[] = [ - ...(account.collectionOrder || []), - ]; + const newCollectionOrder: number[] = [...(user.collectionOrder || [])]; // Start with collections that are in both account.collectionOrder and collections const existingCollectionIds = collections.map((c) => c.id as number); - const filteredCollectionOrder = account.collectionOrder.filter((id) => + const filteredCollectionOrder = user.collectionOrder.filter((id: any) => existingCollectionIds.includes(id) ); @@ -80,7 +82,7 @@ const CollectionListing = () => { collections.forEach((collection) => { if ( !filteredCollectionOrder.includes(collection.id as number) && - (!collection.parentId || collection.ownerId === account.id) + (!collection.parentId || collection.ownerId === user.id) ) { filteredCollectionOrder.push(collection.id as number); } @@ -89,10 +91,10 @@ const CollectionListing = () => { // check if the newCollectionOrder is the same as the old one if ( JSON.stringify(newCollectionOrder) !== - JSON.stringify(account.collectionOrder) + JSON.stringify(user.collectionOrder) ) { - updateAccount({ - ...account, + updateUser.mutateAsync({ + ...user, collectionOrder: newCollectionOrder, }); } @@ -140,9 +142,9 @@ const CollectionListing = () => { ); if ( - (movedCollection?.ownerId !== account.id && + (movedCollection?.ownerId !== user.id && destination.parentId !== source.parentId) || - (destinationCollection?.ownerId !== account.id && + (destinationCollection?.ownerId !== user.id && destination.parentId !== "root") ) { return toast.error(t("cant_change_collection_you_dont_own")); @@ -150,7 +152,7 @@ const CollectionListing = () => { setTree((currentTree) => moveItemOnTree(currentTree!, source, destination)); - const updatedCollectionOrder = [...account.collectionOrder]; + const updatedCollectionOrder = [...user.collectionOrder]; if (source.parentId !== destination.parentId) { await updateCollection.mutateAsync({ @@ -174,8 +176,8 @@ const CollectionListing = () => { updatedCollectionOrder.splice(destination.index, 0, movedCollectionId); - await updateAccount({ - ...account, + await updateUser.mutateAsync({ + ...user, collectionOrder: updatedCollectionOrder, }); } else if ( @@ -184,8 +186,8 @@ const CollectionListing = () => { ) { updatedCollectionOrder.splice(destination.index, 0, movedCollectionId); - await updateAccount({ - ...account, + updateUser.mutate({ + ...user, collectionOrder: updatedCollectionOrder, }); } else if ( @@ -195,8 +197,8 @@ const CollectionListing = () => { ) { updatedCollectionOrder.splice(source.index, 1); - await updateAccount({ - ...account, + await updateUser.mutateAsync({ + ...user, collectionOrder: updatedCollectionOrder, }); } diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 8a98b9e..81bef39 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -24,8 +24,7 @@ export default function CollectionSelection({ showDefaultValue = true, creatable = true, }: Props) { - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx index 20597b7..189dab4 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkCard.tsx @@ -15,12 +15,12 @@ import Link from "next/link"; import LinkIcon from "./LinkComponents/LinkIcon"; import useOnScreen from "@/hooks/useOnScreen"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -34,10 +34,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); const viewMode = localStorage.getItem("viewMode") || "card"; - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); @@ -133,7 +132,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
- !editMode && window.open(generateLinkHref(link, account), "_blank") + !editMode && window.open(generateLinkHref(link, user), "_blank") } >
diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index c68dd06..0d0b7eb 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -9,9 +9,9 @@ import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal"; import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal"; import useLinkStore from "@/store/links"; import { toast } from "react-hot-toast"; -import useAccountStore from "@/store/account"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -39,7 +39,7 @@ export default function LinkActions({ const [deleteLinkModal, setDeleteLinkModal] = useState(false); const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const { removeLink, updateLink } = useLinkStore(); @@ -50,7 +50,7 @@ export default function LinkActions({ const response = await updateLink({ ...link, - pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }], + pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }], }); toast.dismiss(load); diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index 936d857..71c70cf 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -11,12 +11,12 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon"; import { isPWA } from "@/lib/client/utils"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -33,10 +33,9 @@ export default function LinkCardCompact({ }: Props) { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const { links, setSelectedLinks, selectedLinks } = useLinkStore(); useEffect(() => { @@ -121,7 +120,7 @@ export default function LinkCardCompact({
- !editMode && window.open(generateLinkHref(link, account), "_blank") + !editMode && window.open(generateLinkHref(link, user), "_blank") } >
diff --git a/components/LinkViews/LinkMasonry.tsx b/components/LinkViews/LinkMasonry.tsx index c3c2451..8313eb3 100644 --- a/components/LinkViews/LinkMasonry.tsx +++ b/components/LinkViews/LinkMasonry.tsx @@ -15,12 +15,12 @@ import Link from "next/link"; import LinkIcon from "./LinkComponents/LinkIcon"; import useOnScreen from "@/hooks/useOnScreen"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -33,9 +33,8 @@ type Props = { export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); - const { account } = useAccountStore(); + const { data: collections = [] } = useCollections(); + const { data: user = [] } = useUser(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); @@ -131,7 +130,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
- !editMode && window.open(generateLinkHref(link, account), "_blank") + !editMode && window.open(generateLinkHref(link, user), "_blank") } >
diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index 10831d7..fd2207e 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -3,7 +3,6 @@ import TextInput from "@/components/TextInput"; import toast from "react-hot-toast"; import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import getPublicUserData from "@/lib/client/getPublicUserData"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import ProfilePhoto from "../ProfilePhoto"; import addMemberToCollection from "@/lib/client/addMemberToCollection"; @@ -11,6 +10,7 @@ import Modal from "../Modal"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; import { useUpdateCollection } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { onClose: Function; @@ -46,7 +46,7 @@ export default function EditCollectionSharingModal({ } }; - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const permissions = usePermissions(collection.id as number); const currentURL = new URL(document.URL); @@ -158,7 +158,7 @@ export default function EditCollectionSharingModal({ onKeyDown={(e) => e.key === "Enter" && addMemberToCollection( - account.username as string, + user.username as string, memberUsername || "", collection, setMemberState, @@ -170,7 +170,7 @@ export default function EditCollectionSharingModal({
addMemberToCollection( - account.username as string, + user.username as string, memberUsername || "", collection, setMemberState, diff --git a/components/ModalContent/NewCollectionModal.tsx b/components/ModalContent/NewCollectionModal.tsx index e1ab424..24284e2 100644 --- a/components/ModalContent/NewCollectionModal.tsx +++ b/components/ModalContent/NewCollectionModal.tsx @@ -4,8 +4,6 @@ import { HexColorPicker } from "react-colorful"; import { Collection } from "@prisma/client"; import Modal from "../Modal"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; -import useAccountStore from "@/store/account"; -import { useSession } from "next-auth/react"; import { useTranslation } from "next-i18next"; import { useCreateCollection } from "@/hooks/store/collections"; @@ -24,8 +22,6 @@ export default function NewCollectionModal({ onClose, parent }: Props) { } as Partial; const [collection, setCollection] = useState>(initial); - const { setAccount } = useAccountStore(); - const { data } = useSession(); useEffect(() => { setCollection(initial); diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 605d03c..5164789 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -43,8 +43,7 @@ export default function NewLinkModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 87e84a7..e83d749 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -16,10 +16,10 @@ import { screenshotAvailable, } from "@/lib/shared/getArchiveValidity"; import PreservedFormatRow from "@/components/PreserverdFormatRow"; -import useAccountStore from "@/store/account"; import getPublicUserData from "@/lib/client/getPublicUserData"; import { useTranslation } from "next-i18next"; import { BeatLoader } from "react-spinners"; +import { useUser } from "@/hooks/store/users"; type Props = { onClose: Function; @@ -30,7 +30,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { const { t } = useTranslation(); const session = useSession(); const { getLink } = useLinkStore(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [link, setLink] = useState(activeLink); const router = useRouter(); @@ -49,20 +49,20 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { useEffect(() => { const fetchOwner = async () => { - if (link.collection.ownerId !== account.id) { + if (link.collection.ownerId !== user.id) { const owner = await getPublicUserData( link.collection.ownerId as number ); setCollectionOwner(owner); - } else if (link.collection.ownerId === account.id) { + } else if (link.collection.ownerId === user.id) { setCollectionOwner({ - id: account.id as number, - name: account.name, - username: account.username as string, - image: account.image as string, - archiveAsScreenshot: account.archiveAsScreenshot as boolean, - archiveAsMonolith: account.archiveAsScreenshot as boolean, - archiveAsPDF: account.archiveAsPDF as boolean, + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsScreenshot as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, }); } }; diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index c5c3b5e..8cd2807 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -49,8 +49,7 @@ export default function UploadFileModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index 21ce54e..d610277 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -1,17 +1,17 @@ import useLocalSettingsStore from "@/store/localSettings"; import { dropdownTriggerer } from "@/lib/client/utils"; import ProfilePhoto from "./ProfilePhoto"; -import useAccountStore from "@/store/account"; import Link from "next/link"; import { signOut } from "next-auth/react"; import { useTranslation } from "next-i18next"; +import { useUser } from "@/hooks/store/users"; export default function ProfileDropdown() { const { t } = useTranslation(); const { settings, updateSettings } = useLocalSettingsStore(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); - const isAdmin = account.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1); + const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1); const handleToggle = () => { const newTheme = settings.theme === "dark" ? "light" : "dark"; @@ -27,7 +27,7 @@ export default function ProfileDropdown() { className="btn btn-circle btn-ghost" >
diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index e10461b..70e21fe 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -46,8 +46,7 @@ export default function ReadableView({ link }: Props) { const router = useRouter(); const { getLink } = useLinkStore(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const collection = useMemo(() => { return collections.find( diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 20cfd0b..d3cde02 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -22,8 +22,7 @@ export default function Sidebar({ className }: { className?: string }) { } ); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const { tags } = useTagStore(); const [active, setActive] = useState(""); diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx index 46109dc..fe79d1e 100644 --- a/hooks/store/collections.tsx +++ b/hooks/store/collections.tsx @@ -6,12 +6,10 @@ import toast from "react-hot-toast"; const useCollections = () => { return useQuery({ queryKey: ["collections"], - queryFn: async (): Promise<{ - response: CollectionIncludingMembersAndLinkCount[]; - }> => { + queryFn: async (): Promise => { const response = await fetch("/api/v1/collections"); const data = await response.json(); - return data; + return data.response; }, }); }; @@ -21,11 +19,11 @@ const useCreateCollection = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: any) => { + mutationFn: async (body: any) => { const load = toast.loading(t("creating")); const response = await fetch("/api/v1/collections", { - body: JSON.stringify(data), + body: JSON.stringify(body), headers: { "Content-Type": "application/json", }, @@ -34,14 +32,18 @@ const useCreateCollection = () => { toast.dismiss(load); - return response.json(); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; }, onSuccess: (data) => { toast.success(t("created")); return queryClient.setQueryData(["collections"], (oldData: any) => { - return { - response: [...oldData.response, data.response], - }; + console.log([...oldData, data]); + + return [...oldData, data]; }); }, onError: (error) => { @@ -55,33 +57,43 @@ const useUpdateCollection = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: any) => { + mutationFn: async (body: any) => { const load = toast.loading(t("updating_collection")); - const response = await fetch(`/api/v1/collections/${data.id}`, { + const response = await fetch(`/api/v1/collections/${body.id}`, { method: "PUT", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(data), + body: JSON.stringify(body), }); toast.dismiss(load); - return response.json(); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; }, onSuccess: (data) => { { toast.success(t("updated")); return queryClient.setQueryData(["collections"], (oldData: any) => { - return { - response: oldData.response.map((collection: any) => - collection.id === data.response.id ? data.response : collection - ), - }; + return oldData.map((collection: any) => + collection.id === data.id ? data : collection + ); }); } }, + // onMutate: async (data) => { + // await queryClient.cancelQueries({ queryKey: ["collections"] }); + // queryClient.setQueryData(["collections"], (oldData: any) => { + // return oldData.map((collection: any) => + // collection.id === data.id ? data : collection + // ) + // }); + // }, onError: (error) => { toast.error(error.message); }, @@ -105,16 +117,16 @@ const useDeleteCollection = () => { toast.dismiss(load); - return response.json(); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; }, onSuccess: (data) => { toast.success(t("deleted")); return queryClient.setQueryData(["collections"], (oldData: any) => { - return { - response: oldData.response.filter( - (collection: any) => collection.id !== data.response.id - ), - }; + return oldData.filter((collection: any) => collection.id !== data.id); }); }, onError: (error) => { diff --git a/hooks/store/users.tsx b/hooks/store/users.tsx new file mode 100644 index 0000000..881cf0a --- /dev/null +++ b/hooks/store/users.tsx @@ -0,0 +1,63 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; +import { useSession } from "next-auth/react"; + +const useUser = () => { + const { data } = useSession(); + + const userId = data?.user.id; + + return useQuery({ + queryKey: ["user"], + queryFn: async () => { + const response = await fetch(`/api/v1/users/${userId}`); + if (!response.ok) throw new Error("Failed to fetch user data."); + + const data = await response.json(); + + return data.response; + }, + enabled: !!userId, + }); +}; + +const useUpdateUser = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (user: any) => { + const load = toast.loading(t("applying_settings")); + + const response = await fetch(`/api/v1/users/${user.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(user), + }); + + const data = await response.json(); + + toast.dismiss(load); + + if (!response.ok) throw new Error(data.response); + + return data; + }, + onSuccess: (data) => { + toast.success(t("settings_applied")); + queryClient.setQueryData(["user"], data.response); + }, + onMutate: async (user) => { + await queryClient.cancelQueries({ queryKey: ["user"] }); + queryClient.setQueryData(["user"], (oldData: any) => { + return { ...oldData, ...user }; + }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { useUser, useUpdateUser }; diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts index 992befe..aebb842 100644 --- a/hooks/useCollectivePermissions.ts +++ b/hooks/useCollectivePermissions.ts @@ -1,13 +1,12 @@ -import useAccountStore from "@/store/account"; import { Member } from "@/types/global"; import { useEffect, useState } from "react"; import { useCollections } from "./store/collections"; +import { useUser } from "./store/users"; export default function useCollectivePermissions(collectionIds: number[]) { - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { @@ -16,7 +15,7 @@ export default function useCollectivePermissions(collectionIds: number[]) { if (collection) { let getPermission: Member | undefined = collection.members.find( - (e) => e.userId === account.id + (e) => e.userId === user.id ); if ( @@ -26,10 +25,10 @@ export default function useCollectivePermissions(collectionIds: number[]) { ) getPermission = undefined; - setPermissions(account.id === collection.ownerId || getPermission); + setPermissions(user.id === collection.ownerId || getPermission); } } - }, [account, collections, collectionIds]); + }, [user, collections, collectionIds]); return permissions; } diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index 2da9d9e..bb2686f 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -1,33 +1,29 @@ import { useEffect } from "react"; import { useSession } from "next-auth/react"; import useTagStore from "@/store/tags"; -import useAccountStore from "@/store/account"; import useLocalSettingsStore from "@/store/localSettings"; +import { useUser } from "./store/users"; export default function useInitialData() { const { status, data } = useSession(); // const { setCollections } = useCollectionStore(); const { setTags } = useTagStore(); // const { setLinks } = useLinkStore(); - const { account, setAccount } = useAccountStore(); + const { data: user = [] } = useUser(); const { setSettings } = useLocalSettingsStore(); useEffect(() => { setSettings(); - if (status === "authenticated") { - // Get account info - setAccount(data?.user.id as number); - } }, [status, data]); // Get the rest of the data useEffect(() => { - if (account.id && (!process.env.NEXT_PUBLIC_STRIPE || account.username)) { + if (user.id && (!process.env.NEXT_PUBLIC_STRIPE || user.username)) { // setCollections(); setTags(); // setLinks(); } - }, [account]); + }, [user]); return status; } diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index bf21c18..c6e806f 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -1,13 +1,12 @@ -import useAccountStore from "@/store/account"; import { Member } from "@/types/global"; import { useEffect, useState } from "react"; import { useCollections } from "./store/collections"; +import { useUser } from "./store/users"; export default function usePermissions(collectionId: number) { - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { @@ -15,7 +14,7 @@ export default function usePermissions(collectionId: number) { if (collection) { let getPermission: Member | undefined = collection.members.find( - (e) => e.userId === account.id + (e) => e.userId === user.id ); if ( @@ -25,9 +24,9 @@ export default function usePermissions(collectionId: number) { ) getPermission = undefined; - setPermissions(account.id === collection.ownerId || getPermission); + setPermissions(user.id === collection.ownerId || getPermission); } - }, [account, collections, collectionId]); + }, [user, collections, collectionId]); return permissions; } diff --git a/hooks/useReorderCollection.tsx b/hooks/useReorderCollection.tsx new file mode 100644 index 0000000..e69de29 diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 7f89222..69e372e 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -2,7 +2,7 @@ import { ReactNode, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { useSession } from "next-auth/react"; import useInitialData from "@/hooks/useInitialData"; -import useAccountStore from "@/store/account"; +import { useUser } from "@/hooks/store/users"; interface Props { children: ReactNode; @@ -14,7 +14,7 @@ export default function AuthRedirect({ children }: Props) { const router = useRouter(); const { status } = useSession(); const [shouldRenderChildren, setShouldRenderChildren] = useState(false); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); useInitialData(); @@ -23,7 +23,7 @@ export default function AuthRedirect({ children }: Props) { const isUnauthenticated = status === "unauthenticated"; const isPublicPage = router.pathname.startsWith("/public"); const hasInactiveSubscription = - account.id && !account.subscription?.active && stripeEnabled; + user.id && !user.subscription?.active && stripeEnabled; // There are better ways of doing this... but this one works for now const routes = [ @@ -63,7 +63,7 @@ export default function AuthRedirect({ children }: Props) { setShouldRenderChildren(true); } } - }, [status, account, router.pathname]); + }, [status, user, router.pathname]); function redirectTo(destination: string) { router.push(destination).then(() => setShouldRenderChildren(true)); diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index ccbcae3..e1bb1d9 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -12,7 +12,6 @@ import useLinks from "@/hooks/useLinks"; import usePermissions from "@/hooks/usePermissions"; import NoLinksFound from "@/components/NoLinksFound"; import useLocalSettingsStore from "@/store/localSettings"; -import useAccountStore from "@/store/account"; import getPublicUserData from "@/lib/client/getPublicUserData"; import EditCollectionModal from "@/components/ModalContent/EditCollectionModal"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; @@ -26,6 +25,7 @@ import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import LinkListOptions from "@/components/LinkListOptions"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; export default function Index() { const { t } = useTranslation(); @@ -34,8 +34,7 @@ export default function Index() { const router = useRouter(); const { links } = useLinkStore(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); @@ -52,7 +51,7 @@ export default function Index() { ); }, [router, collections]); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [collectionOwner, setCollectionOwner] = useState({ id: null as unknown as number, @@ -66,20 +65,20 @@ export default function Index() { useEffect(() => { const fetchOwner = async () => { - if (activeCollection && activeCollection.ownerId !== account.id) { + if (activeCollection && activeCollection.ownerId !== user.id) { const owner = await getPublicUserData( activeCollection.ownerId as number ); setCollectionOwner(owner); - } else if (activeCollection && activeCollection.ownerId === account.id) { + } else if (activeCollection && activeCollection.ownerId === user.id) { setCollectionOwner({ - id: account.id as number, - name: account.name, - username: account.username as string, - image: account.image as string, - archiveAsScreenshot: account.archiveAsScreenshot as boolean, - archiveAsMonolith: account.archiveAsScreenshot as boolean, - archiveAsPDF: account.archiveAsPDF as boolean, + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsScreenshot as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, }); } }; diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 3314dfb..f57ddfc 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -13,8 +13,7 @@ import { useCollections } from "@/hooks/store/collections"; export default function Collections() { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); const [sortedCollections, setSortedCollections] = useState(collections); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 40ee933..fe64928 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -22,8 +22,7 @@ import { useCollections } from "@/hooks/store/collections"; export default function Dashboard() { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const { links } = useLinkStore(); const { tags } = useTagStore(); diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index faa4950..db06ec8 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -32,8 +32,7 @@ export default function PublicCollections() { const { settings } = useLocalSettingsStore(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index cb46442..5783bf1 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import useAccountStore from "@/store/account"; import { AccountSettings } from "@/types/global"; import { toast } from "react-hot-toast"; import SettingsLayout from "@/layouts/SettingsLayout"; @@ -17,6 +16,7 @@ import Button from "@/components/ui/Button"; import { i18n } from "next-i18next.config"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; @@ -24,7 +24,8 @@ export default function Account() { const [emailChangeVerificationModal, setEmailChangeVerificationModal] = useState(false); const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); + const { data: account = [] } = useUser(); + const updateUser = useUpdateUser(); const [user, setUser] = useState( !objectIsEmpty(account) ? account @@ -78,25 +79,22 @@ export default function Account() { const submit = async (password?: string) => { setSubmitLoader(true); - const load = toast.loading(t("applying_settings")); - const response = await updateAccount({ - ...user, - // @ts-ignore - password: password ? password : undefined, - }); - - toast.dismiss(load); - - if (response.ok) { - const emailChanged = account.email !== user.email; - - toast.success(t("settings_applied")); - if (emailChanged) { - toast.success(t("email_change_request")); - setEmailChangeVerificationModal(false); + await updateUser.mutateAsync( + { + ...user, + password: password ? password : undefined, + }, + { + onSuccess: (data) => { + if (data.response.email !== user.email) { + toast.success(t("email_change_request")); + setEmailChangeVerificationModal(false); + } + }, } - } else toast.error(response.data as string); + ); + setSubmitLoader(false); }; diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx index 92f25ce..2c7c1b3 100644 --- a/pages/settings/password.tsx +++ b/pages/settings/password.tsx @@ -1,11 +1,11 @@ import SettingsLayout from "@/layouts/SettingsLayout"; import { useState } from "react"; -import useAccountStore from "@/store/account"; import SubmitButton from "@/components/SubmitButton"; import { toast } from "react-hot-toast"; import TextInput from "@/components/TextInput"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; export default function Password() { const { t } = useTranslation(); @@ -13,7 +13,8 @@ export default function Password() { const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); + const { data: account = [] } = useUser(); + const updateUser = useUpdateUser(); const submit = async () => { if (newPassword === "" || oldPassword === "") { @@ -23,23 +24,19 @@ export default function Password() { setSubmitLoader(true); - const load = toast.loading(t("applying_changes")); - - const response = await updateAccount({ - ...account, - newPassword, - oldPassword, - }); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("settings_applied")); - setNewPassword(""); - setOldPassword(""); - } else { - toast.error(response.data as string); - } + await updateUser.mutateAsync( + { + ...account, + newPassword, + oldPassword, + }, + { + onSuccess: () => { + setNewPassword(""); + setOldPassword(""); + }, + } + ); setSubmitLoader(false); }; diff --git a/pages/settings/preference.tsx b/pages/settings/preference.tsx index 55c0167..ae5840b 100644 --- a/pages/settings/preference.tsx +++ b/pages/settings/preference.tsx @@ -1,6 +1,5 @@ import SettingsLayout from "@/layouts/SettingsLayout"; import { useState, useEffect } from "react"; -import useAccountStore from "@/store/account"; import SubmitButton from "@/components/SubmitButton"; import { toast } from "react-hot-toast"; import Checkbox from "@/components/Checkbox"; @@ -8,12 +7,14 @@ import useLocalSettingsStore from "@/store/localSettings"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; // Import getServerSideProps for server-side data fetching import { LinksRouteTo } from "@prisma/client"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; export default function Appearance() { const { t } = useTranslation(); const { updateSettings } = useLocalSettingsStore(); const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); + const { data: account = [] } = useUser(); + const updateUser = useUpdateUser(); const [user, setUser] = useState(account); const [preventDuplicateLinks, setPreventDuplicateLinks] = useState( @@ -73,17 +74,8 @@ export default function Appearance() { const submit = async () => { setSubmitLoader(true); - const load = toast.loading(t("applying_changes")); + await updateUser.mutateAsync({ ...user }); - const response = await updateAccount({ ...user }); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("settings_applied")); - } else { - toast.error(response.data as string); - } setSubmitLoader(false); }; diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index 591863e..c795da9 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -7,7 +7,7 @@ import { Plan } from "@/types/global"; import Button from "@/components/ui/Button"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { Trans, useTranslation } from "next-i18next"; -import useAccountStore from "@/store/account"; +import { useUser } from "@/hooks/store/users"; const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true"; @@ -20,11 +20,11 @@ export default function Subscribe() { const router = useRouter(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); useEffect(() => { const hasInactiveSubscription = - account.id && !account.subscription?.active && stripeEnabled; + user.id && !user.subscription?.active && stripeEnabled; if (session.status === "authenticated" && !hasInactiveSubscription) { router.push("/dashboard"); diff --git a/store/account.ts b/store/account.ts deleted file mode 100644 index c1da399..0000000 --- a/store/account.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { create } from "zustand"; -import { AccountSettings } from "@/types/global"; - -type ResponseObject = { - ok: boolean; - data: Omit | object | string; -}; - -type AccountStore = { - account: AccountSettings; - setAccount: (id: number) => void; - updateAccount: (user: AccountSettings) => Promise; -}; - -const useAccountStore = create()((set) => ({ - account: {} as AccountSettings, - setAccount: async (id) => { - const response = await fetch(`/api/v1/users/${id}`); - - const data = await response.json(); - - if (response.ok) set({ account: { ...data.response } }); - }, - updateAccount: async (user) => { - const response = await fetch(`/api/v1/users/${user.id}`, { - method: "PUT", - body: JSON.stringify(user), - headers: { - "Content-Type": "application/json", - }, - }); - - const data = await response.json(); - - if (response.ok) set({ account: { ...data.response } }); - - return { ok: response.ok, data: data.response }; - }, -})); - -export default useAccountStore;