From 80f366cd7beeae748e594d5f87ba3c3b977cb1e3 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 13 Aug 2024 00:08:57 -0400 Subject: [PATCH] refactored link state management + a lot of other changes... --- components/CollectionCard.tsx | 2 +- components/CollectionListing.tsx | 4 +- .../InputSelect/CollectionSelection.tsx | 2 +- components/InputSelect/TagSelection.tsx | 2 +- components/LinkListOptions.tsx | 123 ++--- components/LinkViews/Layouts/CardView.tsx | 23 +- components/LinkViews/LinkCard.tsx | 15 +- .../LinkViews/LinkComponents/LinkActions.tsx | 40 +- .../LinkComponents/LinkCollection.tsx | 33 +- components/LinkViews/LinkList.tsx | 9 +- components/LinkViews/LinkMasonry.tsx | 12 +- components/LinkViews/Links.tsx | 226 ++++++++ .../ModalContent/BulkDeleteLinksModal.tsx | 26 +- components/ModalContent/DeleteLinkModal.tsx | 34 +- .../EditCollectionSharingModal.tsx | 2 +- components/ModalContent/EditLinkModal.tsx | 21 +- components/ModalContent/NewLinkModal.tsx | 25 +- .../ModalContent/PreservedFormatsModal.tsx | 12 +- components/ModalContent/UploadFileModal.tsx | 25 +- components/PreserverdFormatRow.tsx | 11 +- components/ProfileDropdown.tsx | 2 +- components/ReadableView.tsx | 13 +- components/Sidebar.tsx | 2 +- components/SortDropdown.tsx | 9 +- components/ViewDropdown.tsx | 6 +- hooks/store/collections.tsx | 3 - hooks/store/dashboardData.tsx | 16 + hooks/store/links.tsx | 499 ++++++++++++++++++ hooks/store/publicLinks.tsx | 93 ++++ hooks/store/tags.tsx | 1 - hooks/store/tokens.tsx | 1 - hooks/store/user.tsx | 2 +- hooks/useCollectivePermissions.ts | 4 +- hooks/useInitialData.tsx | 2 +- hooks/useLinks.tsx | 103 ---- hooks/usePermissions.tsx | 4 +- layouts/AuthRedirect.tsx | 2 +- .../controllers/dashboard/getDashboardData.ts | 16 +- lib/api/controllers/links/getLinks.ts | 4 +- package.json | 1 + pages/_app.tsx | 17 +- pages/admin.tsx | 2 +- pages/collections/[id].tsx | 54 +- pages/collections/index.tsx | 6 +- pages/dashboard.tsx | 70 ++- pages/links/index.tsx | 47 +- pages/links/pinned.tsx | 46 +- pages/preserved/[id].tsx | 11 +- pages/public/collections/[id].tsx | 59 +-- pages/public/preserved/[id].tsx | 12 +- pages/search.tsx | 53 +- pages/settings/access-tokens.tsx | 2 +- pages/subscribe.tsx | 2 +- pages/tags/[id].tsx | 42 +- store/links.ts | 246 +-------- store/localSettings.ts | 14 +- types/global.ts | 3 +- yarn.lock | 5 + 58 files changed, 1302 insertions(+), 819 deletions(-) create mode 100644 components/LinkViews/Links.tsx create mode 100644 hooks/store/dashboardData.tsx create mode 100644 hooks/store/links.tsx create mode 100644 hooks/store/publicLinks.tsx delete mode 100644 hooks/useLinks.tsx diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index 9a47f6e..aaf177e 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -20,7 +20,7 @@ type Props = { export default function CollectionCard({ collection, className }: Props) { const { t } = useTranslation(); const { settings } = useLocalSettingsStore(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const formattedDate = new Date(collection.createdAt as string).toLocaleString( "en-US", diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index df85572..f118184 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -25,9 +25,9 @@ interface ExtendedTreeItem extends TreeItem { const CollectionListing = () => { const { t } = useTranslation(); const updateCollection = useUpdateCollection(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const updateUser = useUpdateUser(); const router = useRouter(); diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 24700c2..81bef39 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -24,7 +24,7 @@ export default function CollectionSelection({ showDefaultValue = true, creatable = true, }: Props) { - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); diff --git a/components/InputSelect/TagSelection.tsx b/components/InputSelect/TagSelection.tsx index c9d3c27..efd246b 100644 --- a/components/InputSelect/TagSelection.tsx +++ b/components/InputSelect/TagSelection.tsx @@ -13,7 +13,7 @@ type Props = { }; export default function TagSelection({ onChange, defaultValue }: Props) { - const { data: tags } = useTags(); + const { data: tags = [] } = useTags(); const [options, setOptions] = useState([]); diff --git a/components/LinkListOptions.tsx b/components/LinkListOptions.tsx index 842363e..6ec063d 100644 --- a/components/LinkListOptions.tsx +++ b/components/LinkListOptions.tsx @@ -5,17 +5,17 @@ import ViewDropdown from "./ViewDropdown"; import { TFunction } from "i18next"; import BulkDeleteLinksModal from "./ModalContent/BulkDeleteLinksModal"; import BulkEditLinksModal from "./ModalContent/BulkEditLinksModal"; -import toast from "react-hot-toast"; import useCollectivePermissions from "@/hooks/useCollectivePermissions"; import { useRouter } from "next/router"; import useLinkStore from "@/store/links"; -import { Sort } from "@/types/global"; +import { Sort, ViewMode } from "@/types/global"; +import { useBulkDeleteLinks, useLinks } from "@/hooks/store/links"; type Props = { children: React.ReactNode; t: TFunction<"translation", undefined>; - viewMode: string; - setViewMode: Dispatch>; + viewMode: ViewMode; + setViewMode: Dispatch>; searchFilter?: { name: boolean; url: boolean; @@ -48,8 +48,11 @@ const LinkListOptions = ({ editMode, setEditMode, }: Props) => { - const { links, selectedLinks, setSelectedLinks, deleteLinksById } = - useLinkStore(); + const { selectedLinks, setSelectedLinks } = useLinkStore(); + + const deleteLinksById = useBulkDeleteLinks(); + + const { links } = useLinks(); const router = useRouter(); @@ -73,23 +76,14 @@ const LinkListOptions = ({ }; const bulkDeleteLinks = async () => { - const load = toast.loading(t("deleting_selections")); - - const response = await deleteLinksById( - selectedLinks.map((link) => link.id as number) + await deleteLinksById.mutateAsync( + selectedLinks.map((link) => link.id as number), + { + onSuccess: () => { + setSelectedLinks([]); + }, + } ); - - toast.dismiss(load); - - if (response.ok) { - toast.success( - selectedLinks.length === 1 - ? t("link_deleted") - : t("links_deleted", { count: selectedLinks.length }) - ); - } else { - toast.error(response.data as string); - } }; return ( @@ -99,57 +93,64 @@ const LinkListOptions = ({
- {links.length > 0 && editMode !== undefined && setEditMode && ( -
{ - setEditMode(!editMode); - setSelectedLinks([]); - }} - className={`btn btn-square btn-sm btn-ghost ${ - editMode - ? "bg-primary/20 hover:bg-primary/20" - : "hover:bg-neutral/20" - }`} - > - -
- )} + {links && + links.length > 0 && + editMode !== undefined && + setEditMode && ( +
{ + setEditMode(!editMode); + setSelectedLinks([]); + }} + className={`btn btn-square btn-sm btn-ghost ${ + editMode + ? "bg-primary/20 hover:bg-primary/20" + : "hover:bg-neutral/20" + }`} + > + +
+ )} {searchFilter && setSearchFilter && ( )} - + { + setSortBy(value); + }} + t={t} + />
- {editMode && links.length > 0 && ( + {links && editMode && links.length > 0 && (
- {links.length > 0 && ( -
- handleSelectAll()} - checked={ - selectedLinks.length === links.length && links.length > 0 - } - /> - {selectedLinks.length > 0 ? ( - - {selectedLinks.length === 1 - ? t("link_selected") - : t("links_selected", { count: selectedLinks.length })} - - ) : ( - {t("nothing_selected")} - )} -
- )} +
+ handleSelectAll()} + checked={ + selectedLinks.length === links.length && links.length > 0 + } + /> + {selectedLinks.length > 0 ? ( + + {selectedLinks.length === 1 + ? t("link_selected") + : t("links_selected", { count: selectedLinks.length })} + + ) : ( + {t("nothing_selected")} + )} +
diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index b1af6a4..ae52ef8 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -46,7 +46,7 @@ export default function EditCollectionSharingModal({ } }; - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const permissions = usePermissions(collection.id as number); const currentURL = new URL(document.URL); diff --git a/components/ModalContent/EditLinkModal.tsx b/components/ModalContent/EditLinkModal.tsx index 1d873c7..7613c25 100644 --- a/components/ModalContent/EditLinkModal.tsx +++ b/components/ModalContent/EditLinkModal.tsx @@ -3,12 +3,11 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; -import toast from "react-hot-toast"; import Link from "next/link"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; +import { useUpdateLink } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -27,9 +26,10 @@ export default function EditLinkModal({ onClose, activeLink }: Props) { console.log(error); } - const { updateLink } = useLinkStore(); const [submitLoader, setSubmitLoader] = useState(false); + const updateLink = useUpdateLink(); + const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; setLink({ @@ -50,19 +50,14 @@ export default function EditLinkModal({ onClose, activeLink }: Props) { const submit = async () => { if (!submitLoader) { setSubmitLoader(true); - const load = toast.loading(t("updating")); - let response = await updateLink(link); - toast.dismiss(load); - if (response.ok) { - toast.success(t("updated")); - onClose(); - } else { - toast.error(response.data as string); - } + await updateLink.mutateAsync(link, { + onSuccess: () => { + onClose(); + }, + }); setSubmitLoader(false); - return response; } }; diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 16c8489..05350a5 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -3,14 +3,13 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { useSession } from "next-auth/react"; import { useRouter } from "next/router"; -import toast from "react-hot-toast"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useAddLink } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -39,11 +38,13 @@ export default function NewLinkModal({ onClose }: Props) { const [link, setLink] = useState(initial); - const { addLink } = useLinkStore(); + + const addLink = useAddLink(); + const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; @@ -86,15 +87,13 @@ export default function NewLinkModal({ onClose }: Props) { const submit = async () => { if (!submitLoader) { setSubmitLoader(true); - const load = toast.loading(t("creating_link")); - const response = await addLink(link); - toast.dismiss(load); - if (response.ok) { - toast.success(t("link_created")); - onClose(); - } else { - toast.error(response.data as string); - } + + await addLink.mutateAsync(link, { + onSuccess: () => { + onClose(); + }, + }); + setSubmitLoader(false); } }; diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 3162013..3d2e29d 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags, ArchivedFormat, @@ -20,6 +19,7 @@ import getPublicUserData from "@/lib/client/getPublicUserData"; import { useTranslation } from "next-i18next"; import { BeatLoader } from "react-spinners"; import { useUser } from "@/hooks/store/user"; +import { useGetLink } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -29,8 +29,8 @@ type Props = { export default function PreservedFormatsModal({ onClose, activeLink }: Props) { const { t } = useTranslation(); const session = useSession(); - const { getLink } = useLinkStore(); - const { data: user } = useUser(); + const getLink = useGetLink(); + const { data: user = {} } = useUser(); const [link, setLink] = useState(activeLink); const router = useRouter(); @@ -98,7 +98,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { useEffect(() => { (async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -108,7 +108,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { if (!isReady()) { interval = setInterval(async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -137,7 +137,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { toast.dismiss(load); if (response.ok) { - const newLink = await getLink(link?.id as number); + const newLink = await getLink.mutateAsync(link?.id as number); setLink( (newLink as any).response as LinkIncludingShortenedCollectionAndTags ); diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 74a8d90..2e43d56 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -3,7 +3,6 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags, ArchivedFormat, @@ -14,6 +13,7 @@ import toast from "react-hot-toast"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUploadFile } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -45,11 +45,11 @@ export default function UploadFileModal({ onClose }: Props) { useState(initial); const [file, setFile] = useState(); - const { uploadFile } = useLinkStore(); + const uploadFile = useUploadFile(); const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; @@ -115,20 +115,17 @@ export default function UploadFileModal({ onClose }: Props) { // } setSubmitLoader(true); - const load = toast.loading(t("creating")); - const response = await uploadFile(link, file); - - toast.dismiss(load); - if (response.ok) { - toast.success(t("created_success")); - onClose(); - } else { - toast.error(response.data as string); - } + await uploadFile.mutateAsync( + { link, file }, + { + onSuccess: () => { + onClose(); + }, + } + ); setSubmitLoader(false); - return response; } }; diff --git a/components/PreserverdFormatRow.tsx b/components/PreserverdFormatRow.tsx index ce0f21d..2e18e39 100644 --- a/components/PreserverdFormatRow.tsx +++ b/components/PreserverdFormatRow.tsx @@ -1,13 +1,11 @@ import React, { useEffect, useState } from "react"; -import useLinkStore from "@/store/links"; import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; -import toast from "react-hot-toast"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useSession } from "next-auth/react"; +import { useGetLink } from "@/hooks/store/links"; type Props = { name: string; @@ -24,8 +22,7 @@ export default function PreservedFormatRow({ activeLink, downloadable, }: Props) { - const session = useSession(); - const { getLink } = useLinkStore(); + const getLink = useGetLink(); const [link, setLink] = useState(activeLink); @@ -36,7 +33,7 @@ export default function PreservedFormatRow({ useEffect(() => { (async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -45,7 +42,7 @@ export default function PreservedFormatRow({ let interval: any; if (link?.image === "pending" || link?.pdf === "pending") { interval = setInterval(async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index 4927517..d32893d 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -9,7 +9,7 @@ import { useUser } from "@/hooks/store/user"; export default function ProfileDropdown() { const { t } = useTranslation(); const { settings, updateSettings } = useLocalSettingsStore(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1); diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index da73b01..801e9c1 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -1,7 +1,6 @@ import unescapeString from "@/lib/client/unescapeString"; import { readabilityAvailable } from "@/lib/shared/getArchiveValidity"; import isValidUrl from "@/lib/shared/isValidUrl"; -import useLinkStore from "@/store/links"; import { ArchivedFormat, CollectionIncludingMembersAndLinkCount, @@ -16,6 +15,7 @@ import React, { useEffect, useMemo, useState } from "react"; import LinkActions from "./LinkViews/LinkComponents/LinkActions"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useGetLink } from "@/hooks/store/links"; type LinkContent = { title: string; @@ -45,8 +45,8 @@ export default function ReadableView({ link }: Props) { const router = useRouter(); - const { getLink } = useLinkStore(); - const { data: collections } = useCollections(); + const getLink = useGetLink(); + const { data: collections = [] } = useCollections(); const collection = useMemo(() => { return collections.find( @@ -73,7 +73,7 @@ export default function ReadableView({ link }: Props) { }, [link]); useEffect(() => { - if (link) getLink(link?.id as number); + if (link) getLink.mutateAsync(link?.id as number); let interval: any; if ( @@ -87,7 +87,10 @@ export default function ReadableView({ link }: Props) { !link?.readable || !link?.monolith) ) { - interval = setInterval(() => getLink(link.id as number), 5000); + interval = setInterval( + () => getLink.mutateAsync(link.id as number), + 5000 + ); } else { if (interval) { clearInterval(interval); diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 540d067..dc15c61 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -24,7 +24,7 @@ export default function Sidebar({ className }: { className?: string }) { const { data: collections } = useCollections(); - const { data: tags } = useTags(); + const { data: tags = [] } = useTags(); const [active, setActive] = useState(""); const router = useRouter(); diff --git a/components/SortDropdown.tsx b/components/SortDropdown.tsx index 002dac5..809d4c3 100644 --- a/components/SortDropdown.tsx +++ b/components/SortDropdown.tsx @@ -1,7 +1,8 @@ -import React, { Dispatch, SetStateAction } from "react"; +import React, { Dispatch, SetStateAction, useEffect } from "react"; import { Sort } from "@/types/global"; import { dropdownTriggerer } from "@/lib/client/utils"; import { TFunction } from "i18next"; +import useLocalSettingsStore from "@/store/localSettings"; type Props = { sortBy: Sort; @@ -10,6 +11,12 @@ type Props = { }; export default function SortDropdown({ sortBy, setSort, t }: Props) { + const { updateSettings } = useLocalSettingsStore(); + + useEffect(() => { + updateSettings({ sortBy }); + }, [sortBy]); + return (
>; + viewMode: ViewMode; + setViewMode: Dispatch>; }; export default function ViewDropdown({ viewMode, setViewMode }: Props) { @@ -19,7 +19,7 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) { }; useEffect(() => { - updateSettings({ viewMode: viewMode as ViewMode }); + updateSettings({ viewMode }); }, [viewMode]); return ( diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx index 932bb51..4d0a93f 100644 --- a/hooks/store/collections.tsx +++ b/hooks/store/collections.tsx @@ -11,7 +11,6 @@ const useCollections = () => { const data = await response.json(); return data.response; }, - initialData: [], }); }; @@ -42,8 +41,6 @@ const useCreateCollection = () => { onSuccess: (data) => { toast.success(t("created")); return queryClient.setQueryData(["collections"], (oldData: any) => { - console.log([...oldData, data]); - return [...oldData, data]; }); }, diff --git a/hooks/store/dashboardData.tsx b/hooks/store/dashboardData.tsx new file mode 100644 index 0000000..358b035 --- /dev/null +++ b/hooks/store/dashboardData.tsx @@ -0,0 +1,16 @@ +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import { useQuery } from "@tanstack/react-query"; + +const useDashboardData = () => { + return useQuery({ + queryKey: ["dashboardData"], + queryFn: async (): Promise => { + const response = await fetch("/api/v1/dashboard"); + const data = await response.json(); + + return data.response; + }, + }); +}; + +export { useDashboardData }; diff --git a/hooks/store/links.tsx b/hooks/store/links.tsx new file mode 100644 index 0000000..93b9b75 --- /dev/null +++ b/hooks/store/links.tsx @@ -0,0 +1,499 @@ +import { + InfiniteData, + useInfiniteQuery, + UseInfiniteQueryResult, + useQueryClient, + useMutation, +} from "@tanstack/react-query"; +import { useEffect, useMemo } from "react"; +import { + ArchivedFormat, + LinkIncludingShortenedCollectionAndTags, + LinkRequestQuery, +} from "@/types/global"; +import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; + +const useLinks = (params: LinkRequestQuery = {}) => { + const router = useRouter(); + + const queryParamsObject = { + sort: params.sort ?? Number(window.localStorage.getItem("sortBy")) ?? 0, + collectionId: + params.collectionId ?? router.pathname === "/collections/[id]" + ? router.query.id + : undefined, + tagId: + params.tagId ?? router.pathname === "/tags/[id]" + ? router.query.id + : undefined, + pinnedOnly: + params.pinnedOnly ?? router.pathname === "/links/pinned" + ? true + : undefined, + searchQueryString: params.searchQueryString, + searchByName: params.searchByName, + searchByUrl: params.searchByUrl, + searchByDescription: params.searchByDescription, + searchByTextContent: params.searchByTextContent, + searchByTags: params.searchByTags, + } as LinkRequestQuery; + + const queryString = buildQueryString(queryParamsObject); + + const { data, ...rest } = useFetchLinks(queryString); + + const links = useMemo(() => { + return data?.pages.reduce((acc, page) => { + return [...acc, ...page]; + }, []); + }, [data]); + + return { + links, + data: { ...data, ...rest }, + } as { + links: LinkIncludingShortenedCollectionAndTags[]; + data: UseInfiniteQueryResult, Error>; + }; +}; + +const useFetchLinks = (params: string) => { + return useInfiniteQuery({ + queryKey: ["links", { params }], + queryFn: async (params) => { + const response = await fetch( + "/api/v1/links?cursor=" + + params.pageParam + + ((params.queryKey[1] as any).params + ? "&" + (params.queryKey[1] as any).params + : "") + ); + const data = await response.json(); + + return data.response; + }, + initialPageParam: 0, + refetchOnWindowFocus: false, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPage.at(-1).id; + }, + }); +}; + +const buildQueryString = (params: LinkRequestQuery) => { + return Object.keys(params) + .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + params[key as keyof LinkRequestQuery] as string + )}` + ) + .join("&"); +}; + +const useAddLink = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => { + const load = toast.loading(t("creating_link")); + + const response = await fetch("/api/v1/links", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(link), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("link_created")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return [data, ...oldData]; + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)], + pageParams: oldData?.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useUpdateLink = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => { + const load = toast.loading(t("updating")); + + const response = await fetch(`/api/v1/links/${link.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(link), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("updated")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.map((e: any) => (e.id === data.id ? data : e)); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useDeleteLink = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id: number) => { + const load = toast.loading(t("deleting")); + + const response = await fetch(`/api/v1/links/${id}`, { + method: "DELETE", + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("deleted")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.filter((e: any) => e.id !== data.id); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].filter((e: any) => e.id !== data.id), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useGetLink = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id: number) => { + const response = await fetch(`/api/v1/links/${id}`); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.map((e: any) => (e.id === data.id ? data : e)); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + }); +}; + +const useBulkDeleteLinks = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (linkIds: number[]) => { + const load = toast.loading(t("deleting")); + + const response = await fetch("/api/v1/links", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ linkIds }), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return linkIds; + }, + onSuccess: (data) => { + toast.success(t("deleted")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.filter((e: any) => !data.includes(e.id)); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].filter((e: any) => !data.includes(e.id)), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useUploadFile = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ link, file }: any) => { + let fileType: ArchivedFormat | null = null; + let linkType: "url" | "image" | "pdf" | null = null; + + if (file?.type === "image/jpg" || file.type === "image/jpeg") { + fileType = ArchivedFormat.jpeg; + linkType = "image"; + } else if (file.type === "image/png") { + fileType = ArchivedFormat.png; + linkType = "image"; + } else if (file.type === "application/pdf") { + fileType = ArchivedFormat.pdf; + linkType = "pdf"; + } else { + return { ok: false, data: "Invalid file type." }; + } + + const load = toast.loading(t("creating")); + + const response = await fetch("/api/v1/links", { + body: JSON.stringify({ + ...link, + type: linkType, + name: link.name ? link.name : file.name, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + if (response.ok) { + const formBody = new FormData(); + file && formBody.append("file", file); + + await fetch( + `/api/v1/archives/${(data as any).response.id}?format=${fileType}`, + { + body: formBody, + method: "POST", + } + ); + } + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("created_success")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return [data, ...oldData]; + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)], + pageParams: oldData?.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useBulkEditLinks = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + links, + newData, + removePreviousTags, + }: { + links: LinkIncludingShortenedCollectionAndTags[]; + newData: Pick< + LinkIncludingShortenedCollectionAndTags, + "tags" | "collectionId" + >; + removePreviousTags: boolean; + }) => { + const load = toast.loading(t("updating")); + + const response = await fetch("/api/v1/links", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ links, newData, removePreviousTags }), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("updated")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.map((e: any) => + data.find((d: any) => d.id === e.id) ? data : e + ); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].map((e: any) => + data.find((d: any) => d.id === e.id) ? data : e + ), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { + useLinks, + useAddLink, + useUpdateLink, + useDeleteLink, + useBulkDeleteLinks, + useUploadFile, + useGetLink, + useBulkEditLinks, +}; diff --git a/hooks/store/publicLinks.tsx b/hooks/store/publicLinks.tsx new file mode 100644 index 0000000..86c7961 --- /dev/null +++ b/hooks/store/publicLinks.tsx @@ -0,0 +1,93 @@ +import { + InfiniteData, + useInfiniteQuery, + UseInfiniteQueryResult, +} from "@tanstack/react-query"; +import { useMemo } from "react"; +import { + LinkIncludingShortenedCollectionAndTags, + LinkRequestQuery, +} from "@/types/global"; +import { useRouter } from "next/router"; + +const usePublicLinks = (params: LinkRequestQuery = {}) => { + const router = useRouter(); + + const queryParamsObject = { + sort: params.sort ?? Number(window.localStorage.getItem("sortBy")) ?? 0, + collectionId: params.collectionId ?? router.query.id, + tagId: + params.tagId ?? router.pathname === "/tags/[id]" + ? router.query.id + : undefined, + pinnedOnly: + params.pinnedOnly ?? router.pathname === "/links/pinned" + ? true + : undefined, + searchQueryString: params.searchQueryString, + searchByName: params.searchByName, + searchByUrl: params.searchByUrl, + searchByDescription: params.searchByDescription, + searchByTextContent: params.searchByTextContent, + searchByTags: params.searchByTags, + } as LinkRequestQuery; + + const queryString = buildQueryString(queryParamsObject); + + const { data, ...rest } = useFetchLinks(queryString); + + const links = useMemo(() => { + return data?.pages.reduce((acc, page) => { + return [...acc, ...page]; + }, []); + }, [data]); + + return { + links, + data: { ...data, ...rest }, + } as { + links: LinkIncludingShortenedCollectionAndTags[]; + data: UseInfiniteQueryResult, Error>; + }; +}; + +const useFetchLinks = (params: string) => { + return useInfiniteQuery({ + queryKey: ["links", { params }], + queryFn: async (params) => { + const response = await fetch( + "/api/v1/public/collections/links?cursor=" + + params.pageParam + + ((params.queryKey[1] as any).params + ? "&" + (params.queryKey[1] as any).params + : "") + ); + + const data = await response.json(); + + return data.response; + }, + initialPageParam: 0, + refetchOnWindowFocus: false, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPage.at(-1).id; + }, + }); +}; + +const buildQueryString = (params: LinkRequestQuery) => { + return Object.keys(params) + .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + params[key as keyof LinkRequestQuery] as string + )}` + ) + .join("&"); +}; + +export { usePublicLinks }; diff --git a/hooks/store/tags.tsx b/hooks/store/tags.tsx index df51829..c9738fe 100644 --- a/hooks/store/tags.tsx +++ b/hooks/store/tags.tsx @@ -13,7 +13,6 @@ const useTags = () => { const data = await response.json(); return data.response; }, - initialData: [], }); }; diff --git a/hooks/store/tokens.tsx b/hooks/store/tokens.tsx index 20bef18..18bcd9d 100644 --- a/hooks/store/tokens.tsx +++ b/hooks/store/tokens.tsx @@ -14,7 +14,6 @@ const useTokens = () => { const data = await response.json(); return data.response as AccessToken[]; }, - initialData: [], }); }; diff --git a/hooks/store/user.tsx b/hooks/store/user.tsx index ca1c433..a470cc5 100644 --- a/hooks/store/user.tsx +++ b/hooks/store/user.tsx @@ -19,7 +19,7 @@ const useUser = () => { return data.response; }, enabled: !!userId, - initialData: {}, + placeholderData: {}, }); }; diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts index b79cd4d..0d2e5fb 100644 --- a/hooks/useCollectivePermissions.ts +++ b/hooks/useCollectivePermissions.ts @@ -4,9 +4,9 @@ import { useCollections } from "./store/collections"; import { useUser } from "./store/user"; export default function useCollectivePermissions(collectionIds: number[]) { - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index 73e0096..04cf247 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -6,7 +6,7 @@ import { useUser } from "./store/user"; export default function useInitialData() { const { status, data } = useSession(); // const { setLinks } = useLinkStore(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const { setSettings } = useLocalSettingsStore(); useEffect(() => { diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx deleted file mode 100644 index bec2d49..0000000 --- a/hooks/useLinks.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { LinkRequestQuery } from "@/types/global"; -import { useEffect, useState } from "react"; -import useDetectPageBottom from "./useDetectPageBottom"; -import { useRouter } from "next/router"; -import useLinkStore from "@/store/links"; - -export default function useLinks( - { - sort, - collectionId, - tagId, - pinnedOnly, - searchQueryString, - searchByName, - searchByUrl, - searchByDescription, - searchByTags, - searchByTextContent, - }: LinkRequestQuery = { sort: 0 } -) { - const { links, setLinks, resetLinks, selectedLinks, setSelectedLinks } = - useLinkStore(); - const router = useRouter(); - - const [isLoading, setIsLoading] = useState(true); - - const { reachedBottom, setReachedBottom } = useDetectPageBottom(); - - const getLinks = async (isInitialCall: boolean, cursor?: number) => { - const params = { - sort, - cursor, - collectionId, - tagId, - pinnedOnly, - searchQueryString, - searchByName, - searchByUrl, - searchByDescription, - searchByTags, - searchByTextContent, - }; - - const buildQueryString = (params: LinkRequestQuery) => { - return Object.keys(params) - .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) - .map( - (key) => - `${encodeURIComponent(key)}=${encodeURIComponent( - params[key as keyof LinkRequestQuery] as string - )}` - ) - .join("&"); - }; - - let queryString = buildQueryString(params); - - let basePath; - - if (router.pathname === "/dashboard") basePath = "/api/v1/dashboard"; - else if (router.pathname.startsWith("/public/collections/[id]")) { - queryString = queryString + "&collectionId=" + router.query.id; - basePath = "/api/v1/public/collections/links"; - } else basePath = "/api/v1/links"; - - setIsLoading(true); - - const response = await fetch(`${basePath}?${queryString}`); - - const data = await response.json(); - - setIsLoading(false); - - if (response.ok) setLinks(data.response, isInitialCall); - }; - - useEffect(() => { - // Save the selected links before resetting the links - // and then restore the selected links after resetting the links - const previouslySelected = selectedLinks; - resetLinks(); - - setSelectedLinks(previouslySelected); - getLinks(true); - }, [ - router, - sort, - searchQueryString, - searchByName, - searchByUrl, - searchByDescription, - searchByTextContent, - searchByTags, - ]); - - useEffect(() => { - if (reachedBottom) getLinks(false, links?.at(-1)?.id); - - setReachedBottom(false); - }, [reachedBottom]); - - return { isLoading }; -} diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index 7625b49..c672c30 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -4,9 +4,9 @@ import { useCollections } from "./store/collections"; import { useUser } from "./store/user"; export default function usePermissions(collectionId: number) { - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 0d07aee..c3a6a3d 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -14,7 +14,7 @@ export default function AuthRedirect({ children }: Props) { const router = useRouter(); const { status } = useSession(); const [shouldRenderChildren, setShouldRenderChildren] = useState(false); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); useInitialData(); diff --git a/lib/api/controllers/dashboard/getDashboardData.ts b/lib/api/controllers/dashboard/getDashboardData.ts index 3b0f751..bb2f9e4 100644 --- a/lib/api/controllers/dashboard/getDashboardData.ts +++ b/lib/api/controllers/dashboard/getDashboardData.ts @@ -5,7 +5,7 @@ export default async function getDashboardData( userId: number, query: LinkRequestQuery ) { - let order: any; + let order: any = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" }; @@ -42,7 +42,7 @@ export default async function getDashboardData( select: { id: true }, }, }, - orderBy: order || { id: "desc" }, + orderBy: order, }); const recentlyAddedLinks = await prisma.link.findMany({ @@ -67,10 +67,18 @@ export default async function getDashboardData( select: { id: true }, }, }, - orderBy: order || { id: "desc" }, + orderBy: order, }); - const links = [...recentlyAddedLinks, ...pinnedLinks].sort( + const combinedLinks = [...recentlyAddedLinks, ...pinnedLinks]; + + const uniqueLinks = Array.from( + combinedLinks + .reduce((map, item) => map.set(item.id, item), new Map()) + .values() + ); + + const links = uniqueLinks.sort( (a, b) => (new Date(b.id) as any) - (new Date(a.id) as any) ); diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index 561c919..a6b1dab 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -5,7 +5,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) { const POSTGRES_IS_ENABLED = process.env.DATABASE_URL?.startsWith("postgresql"); - let order: any; + let order: any = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" }; @@ -146,7 +146,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) { select: { id: true }, }, }, - orderBy: order || { id: "desc" }, + orderBy: order, }); return { response: links, status: 200 }; diff --git a/package.json b/package.json index f766af5..f25c334 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "react-hot-toast": "^2.4.1", "react-i18next": "^14.1.2", "react-image-file-resizer": "^0.4.8", + "react-intersection-observer": "^9.13.0", "react-masonry-css": "^1.0.16", "react-select": "^5.7.4", "react-spinners": "^0.13.8", diff --git a/pages/_app.tsx b/pages/_app.tsx index 45f3657..dc6603a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -14,7 +14,13 @@ import { appWithTranslation } from "next-i18next"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 30, + }, + }, +}); function App({ Component, @@ -105,12 +111,3 @@ function App({ } export default appWithTranslation(App); - -// function GetData({ children }: { children: React.ReactNode }) { -// const status = useInitialData(); -// return typeof window !== "undefined" && status !== "loading" ? ( -// children -// ) : ( -// <> -// ); -// } diff --git a/pages/admin.tsx b/pages/admin.tsx index 72d6f2b..df62ffe 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -21,7 +21,7 @@ type UserModal = { export default function Admin() { const { t } = useTranslation(); - const { data: users } = useUsers(); + const { data: users = [] } = useUsers(); const [searchQuery, setSearchQuery] = useState(""); const [filteredUsers, setFilteredUsers] = useState(); diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index f226d2d..c96f807 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -1,4 +1,3 @@ -import useLinkStore from "@/store/links"; import { CollectionIncludingMembersAndLinkCount, Sort, @@ -8,7 +7,6 @@ import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; import ProfilePhoto from "@/components/ProfilePhoto"; -import useLinks from "@/hooks/useLinks"; import usePermissions from "@/hooks/usePermissions"; import NoLinksFound from "@/components/NoLinksFound"; import useLocalSettingsStore from "@/store/localSettings"; @@ -16,16 +14,15 @@ import getPublicUserData from "@/lib/client/getPublicUserData"; import EditCollectionModal from "@/components/ModalContent/EditCollectionModal"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; import DeleteCollectionModal from "@/components/ModalContent/DeleteCollectionModal"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import { dropdownTriggerer } from "@/lib/client/utils"; import NewCollectionModal from "@/components/ModalContent/NewCollectionModal"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; 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/user"; +import { useLinks } from "@/hooks/store/links"; +import Links from "@/components/LinkViews/Links"; export default function Index() { const { t } = useTranslation(); @@ -33,25 +30,29 @@ export default function Index() { const router = useRouter(); - const { links } = useLinkStore(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); + + const { links, data } = useLinks({ + sort: sortBy, + collectionId: Number(router.query.id), + }); const [activeCollection, setActiveCollection] = useState(); const permissions = usePermissions(activeCollection?.id as number); - useLinks({ collectionId: Number(router.query.id), sort: sortBy }); - useEffect(() => { setActiveCollection( collections.find((e) => e.id === Number(router.query.id)) ); }, [router, collections]); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const [collectionOwner, setCollectionOwner] = useState({ id: null as unknown as number, @@ -97,19 +98,10 @@ export default function Index() { if (editMode) return setEditMode(false); }, [router]); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
- {links.some((e) => e.collectionId === Number(router.query.id)) ? ( - e.collection.id === activeCollection?.id - )} - /> - ) : ( - - )} + + {!data.isLoading && !links[0] && }
{activeCollection && ( <> diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 0bf2c7e..051d02a 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -13,8 +13,10 @@ import { useCollections } from "@/hooks/store/collections"; export default function Collections() { const { t } = useTranslation(); - const { data: collections } = useCollections(); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const { data: collections = [] } = useCollections(); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); const [sortedCollections, setSortedCollections] = useState(collections); const { data } = useSession(); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index a609c6b..d6f540d 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -1,7 +1,5 @@ -import useLinkStore from "@/store/links"; import MainLayout from "@/layouts/MainLayout"; import { useEffect, useState } from "react"; -import useLinks from "@/hooks/useLinks"; import Link from "next/link"; import useWindowDimensions from "@/hooks/useWindowDimensions"; import React from "react"; @@ -10,28 +8,25 @@ import { MigrationFormat, MigrationRequest, ViewMode } from "@/types/global"; import DashboardItem from "@/components/DashboardItem"; import NewLinkModal from "@/components/ModalContent/NewLinkModal"; import PageHeader from "@/components/PageHeader"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import ViewDropdown from "@/components/ViewDropdown"; import { dropdownTriggerer } from "@/lib/client/utils"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useTags } from "@/hooks/store/tags"; +import { useDashboardData } from "@/hooks/store/dashboardData"; +import Links from "@/components/LinkViews/Links"; export default function Dashboard() { const { t } = useTranslation(); - const { data: collections } = useCollections(); - const { links } = useLinkStore(); - const { data: tags } = useTags(); + const { data: collections = [] } = useCollections(); + const dashboardData = useDashboardData(); + const { data: tags = [] } = useTags(); const [numberOfLinks, setNumberOfLinks] = useState(0); const [showLinks, setShowLinks] = useState(3); - useLinks({ pinnedOnly: true, sort: 0 }); - useEffect(() => { setNumberOfLinks( collections.reduce( @@ -81,7 +76,7 @@ export default function Dashboard() { body: JSON.stringify(body), }); - const data = await response.json(); + await response.json(); toast.dismiss(load); @@ -99,20 +94,10 @@ export default function Dashboard() { const [newLinkModal, setNewLinkModal] = useState(false); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - // [ViewMode.Grid]: , - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -171,12 +156,30 @@ export default function Dashboard() {
- {links[0] ? ( + {dashboardData.isLoading ? (
- + +
+ ) : dashboardData.data && + dashboardData.data[0] && + !dashboardData.isLoading ? ( +
+
) : (
@@ -300,12 +303,21 @@ export default function Dashboard() { style={{ flex: "1 1 auto" }} className="flex flex-col 2xl:flex-row items-start 2xl:gap-2" > - {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( + {dashboardData.isLoading ? (
- +
+ ) : dashboardData.data?.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( +
+ e.pinnedBy && e.pinnedBy[0]) .slice(0, showLinks)} + layout={viewMode} />
) : ( diff --git a/pages/links/index.tsx b/pages/links/index.tsx index 1580324..7b1e539 100644 --- a/pages/links/index.tsx +++ b/pages/links/index.tsx @@ -1,29 +1,28 @@ import NoLinksFound from "@/components/NoLinksFound"; -import useLinks from "@/hooks/useLinks"; -// import { useLinks } from "@/hooks/store/links"; +import { useLinks } from "@/hooks/store/links"; import MainLayout from "@/layouts/MainLayout"; -import useLinkStore from "@/store/links"; import React, { useEffect, useState } from "react"; import PageHeader from "@/components/PageHeader"; import { Sort, ViewMode } from "@/types/global"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import { useRouter } from "next/router"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import LinkListOptions from "@/components/LinkListOptions"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; +import Links from "@/components/LinkViews/Links"; -export default function Links() { +export default function Index() { const { t } = useTranslation(); - // const { data: links } = useLinks(); - const { links } = useLinkStore(); - - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); + + const { links, data } = useLinks({ + sort: sortBy, + }); const router = useRouter(); @@ -33,17 +32,6 @@ export default function Links() { if (editMode) return setEditMode(false); }, [router]); - useLinks({ sort: sortBy }); - - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -63,9 +51,14 @@ export default function Links() { /> - {links[0] ? ( - - ) : ( + + {!data.isLoading && !links[0] && ( )}
diff --git a/pages/links/pinned.tsx b/pages/links/pinned.tsx index 60d75d8..3f59c44 100644 --- a/pages/links/pinned.tsx +++ b/pages/links/pinned.tsx @@ -1,45 +1,32 @@ -import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; -import useLinkStore from "@/store/links"; import React, { useEffect, useState } from "react"; import PageHeader from "@/components/PageHeader"; import { Sort, ViewMode } from "@/types/global"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import { useRouter } from "next/router"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; +import { useLinks } from "@/hooks/store/links"; +import Links from "@/components/LinkViews/Links"; export default function PinnedLinks() { const { t } = useTranslation(); - const { links } = useLinkStore(); - - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card + ); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst ); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); - useLinks({ sort: sortBy, pinnedOnly: true }); + const { links, data } = useLinks({ + sort: sortBy, + pinnedOnly: true, + }); const router = useRouter(); const [editMode, setEditMode] = useState(false); - useEffect(() => { - if (editMode) return setEditMode(false); - }, [router]); - - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -59,9 +46,14 @@ export default function PinnedLinks() { /> - {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( - - ) : ( + + {!data.isLoading && !links[0] && (
(); @@ -18,7 +20,7 @@ export default function Index() { useEffect(() => { const fetchLink = async () => { if (router.query.id) { - await getLink(Number(router.query.id)); + await getLink.mutateAsync(Number(router.query.id)); } }; @@ -26,7 +28,8 @@ export default function Index() { }, []); useEffect(() => { - if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id))); + if (links && links[0]) + setLink(links.find((e) => e.id === Number(router.query.id))); }, [links]); return ( diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 632b831..2700796 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -8,8 +8,6 @@ import { import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import Head from "next/head"; -import useLinks from "@/hooks/useLinks"; -import useLinkStore from "@/store/links"; import ProfilePhoto from "@/components/ProfilePhoto"; import ToggleDarkMode from "@/components/ToggleDarkMode"; import getPublicUserData from "@/lib/client/getPublicUserData"; @@ -18,21 +16,19 @@ import Link from "next/link"; import useLocalSettingsStore from "@/store/localSettings"; import SearchBar from "@/components/SearchBar"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; import { useCollections } from "@/hooks/store/collections"; +import { usePublicLinks } from "@/hooks/store/publicLinks"; +import Links from "@/components/LinkViews/Links"; export default function PublicCollections() { const { t } = useTranslation(); - const { links } = useLinkStore(); const { settings } = useLocalSettingsStore(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); @@ -54,9 +50,11 @@ export default function PublicCollections() { textContent: false, }); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); - useLinks({ + const { links, data } = usePublicLinks({ sort: sortBy, searchQueryString: router.query.q ? decodeURIComponent(router.query.q as string) @@ -91,19 +89,10 @@ export default function PublicCollections() { const [editCollectionSharingModal, setEditCollectionSharingModal] = useState(false); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return collection ? (
- {links[0] ? ( - e.collectionId === Number(router.query.id)) - .map((e, i) => { - const linkWithCollectionData = { - ...e, - collection: collection, // Append collection data - }; - return linkWithCollectionData; - })} - /> - ) : ( -

{t("collection_is_empty")}

- )} + { + const linkWithCollectionData = { + ...e, + collection: collection, // Append collection data + }; + return linkWithCollectionData; + }) as any + } + layout={viewMode} + placeholderCount={1} + useData={data} + /> + {!data.isLoading && !links[0] &&

{t("collection_is_empty")}

} {/*

List created with Linkwarden. diff --git a/pages/public/preserved/[id].tsx b/pages/public/preserved/[id].tsx index fdb332a..2f415ae 100644 --- a/pages/public/preserved/[id].tsx +++ b/pages/public/preserved/[id].tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import useLinkStore from "@/store/links"; import { useRouter } from "next/router"; import { ArchivedFormat, @@ -7,20 +6,20 @@ import { } from "@/types/global"; import ReadableView from "@/components/ReadableView"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useGetLink, useLinks } from "@/hooks/store/links"; export default function Index() { - const { links, getLink } = useLinkStore(); + const { links } = useLinks(); + const getLink = useGetLink(); const [link, setLink] = useState(); const router = useRouter(); - let isPublic = router.pathname.startsWith("/public") ? true : false; - useEffect(() => { const fetchLink = async () => { if (router.query.id) { - await getLink(Number(router.query.id), isPublic); + await getLink.mutateAsync(Number(router.query.id)); } }; @@ -28,7 +27,8 @@ export default function Index() { }, []); useEffect(() => { - if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id))); + if (links && links[0]) + setLink(links.find((e) => e.id === Number(router.query.id))); }, [links]); return ( diff --git a/pages/search.tsx b/pages/search.tsx index d075cd8..e628268 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,23 +1,17 @@ -import useLinks from "@/hooks/useLinks"; +import { useLinks } from "@/hooks/store/links"; import MainLayout from "@/layouts/MainLayout"; -import useLinkStore from "@/store/links"; import { Sort, ViewMode } from "@/types/global"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import PageHeader from "@/components/PageHeader"; -import { GridLoader } from "react-spinners"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import LinkListOptions from "@/components/LinkListOptions"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; +import Links from "@/components/LinkViews/Links"; export default function Search() { const { t } = useTranslation(); - const { links } = useLinkStore(); - const router = useRouter(); const [searchFilter, setSearchFilter] = useState({ @@ -28,11 +22,13 @@ export default function Search() { textContent: false, }); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); const [editMode, setEditMode] = useState(false); @@ -40,7 +36,17 @@ export default function Search() { if (editMode) return setEditMode(false); }, [router]); - const { isLoading } = useLinks({ + // const { isLoading } = useLink({ + // sort: sortBy, + // searchQueryString: decodeURIComponent(router.query.q as string), + // searchByName: searchFilter.name, + // searchByUrl: searchFilter.url, + // searchByDescription: searchFilter.description, + // searchByTextContent: searchFilter.textContent, + // searchByTags: searchFilter.tags, + // }); + + const { links, data } = useLinks({ sort: sortBy, searchQueryString: decodeURIComponent(router.query.q as string), searchByName: searchFilter.name, @@ -50,15 +56,6 @@ export default function Search() { searchByTags: searchFilter.tags, }); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (

@@ -76,7 +73,9 @@ export default function Search() { - {!isLoading && !links[0] ? ( + {/* { + !isLoading && + !links[0] ? (

{t("nothing_found")}

) : links[0] ? ( ) - )} + )} */} + +
); diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx index 91d1761..9e1cfd6 100644 --- a/pages/settings/access-tokens.tsx +++ b/pages/settings/access-tokens.tsx @@ -18,7 +18,7 @@ export default function AccessTokens() { setRevokeTokenModal(true); }; - const { data: tokens } = useTokens(); + const { data: tokens = [] } = useTokens(); return ( diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index fe8bfd2..555798c 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -20,7 +20,7 @@ export default function Subscribe() { const router = useRouter(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); useEffect(() => { const hasInactiveSubscription = diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 92fffe7..e71fe84 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -1,31 +1,28 @@ -import useLinkStore from "@/store/links"; import { useRouter } from "next/router"; import { FormEvent, useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; import { Sort, TagIncludingLinkCount, ViewMode } from "@/types/global"; -import useLinks from "@/hooks/useLinks"; -import { toast } from "react-hot-toast"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; +import { useLinks } from "@/hooks/store/links"; import { dropdownTriggerer } from "@/lib/client/utils"; import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal"; import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; import { useRemoveTag, useTags, useUpdateTag } from "@/hooks/store/tags"; +import Links from "@/components/LinkViews/Links"; export default function Index() { const { t } = useTranslation(); const router = useRouter(); - const { links } = useLinkStore(); - const { data: tags } = useTags(); + const { data: tags = [] } = useTags(); const updateTag = useUpdateTag(); const removeTag = useRemoveTag(); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); const [renameTag, setRenameTag] = useState(false); const [newTagName, setNewTagName] = useState(); @@ -40,7 +37,10 @@ export default function Index() { if (editMode) return setEditMode(false); }, [router]); - useLinks({ tagId: Number(router.query.id), sort: sortBy }); + const { links, data } = useLinks({ + sort: sortBy, + tagId: Number(router.query.id), + }); useEffect(() => { const tag = tags.find((e: any) => e.id === Number(router.query.id)); @@ -98,19 +98,10 @@ export default function Index() { setRenameTag(false); }; - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -210,11 +201,12 @@ export default function Index() {
- - e.tags.some((e) => e.id === Number(router.query.id)) - )} + links={links} + layout={viewMode} + placeholderCount={1} + useData={data} />
{bulkDeleteLinksModal && ( diff --git a/store/links.ts b/store/links.ts index 9339b6e..18fbe15 100644 --- a/store/links.ts +++ b/store/links.ts @@ -1,8 +1,5 @@ import { create } from "zustand"; -import { - ArchivedFormat, - LinkIncludingShortenedCollectionAndTags, -} from "@/types/global"; +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; type ResponseObject = { ok: boolean; @@ -10,24 +7,8 @@ type ResponseObject = { }; type LinkStore = { - links: LinkIncludingShortenedCollectionAndTags[]; selectedLinks: LinkIncludingShortenedCollectionAndTags[]; - setLinks: ( - data: LinkIncludingShortenedCollectionAndTags[], - isInitialCall: boolean - ) => void; setSelectedLinks: (links: LinkIncludingShortenedCollectionAndTags[]) => void; - addLink: ( - body: LinkIncludingShortenedCollectionAndTags - ) => Promise; - uploadFile: ( - link: LinkIncludingShortenedCollectionAndTags, - file: File - ) => Promise; - getLink: (linkId: number, publicRoute?: boolean) => Promise; - updateLink: ( - link: LinkIncludingShortenedCollectionAndTags - ) => Promise; updateLinks: ( links: LinkIncludingShortenedCollectionAndTags[], removePreviousTags: boolean, @@ -36,180 +17,11 @@ type LinkStore = { "tags" | "collectionId" > ) => Promise; - removeLink: (linkId: number) => Promise; - deleteLinksById: (linkIds: number[]) => Promise; - resetLinks: () => void; }; const useLinkStore = create()((set) => ({ - links: [], selectedLinks: [], - setLinks: async (data, isInitialCall) => { - isInitialCall && - set(() => ({ - links: [], - })); - set((state) => ({ - // Filter duplicate links by id - links: [...state.links, ...data].reduce( - (links: LinkIncludingShortenedCollectionAndTags[], item) => { - if (!links.some((link) => link.id === item.id)) { - links.push(item); - } - return links; - }, - [] - ), - })); - }, setSelectedLinks: (links) => set({ selectedLinks: links }), - addLink: async (body) => { - const response = await fetch("/api/v1/links", { - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: [data.response, ...state.links], - })); - } - - return { ok: response.ok, data: data.response }; - }, - uploadFile: async (link, file) => { - let fileType: ArchivedFormat | null = null; - let linkType: "url" | "image" | "pdf" | null = null; - - if (file?.type === "image/jpg" || file.type === "image/jpeg") { - fileType = ArchivedFormat.jpeg; - linkType = "image"; - } else if (file.type === "image/png") { - fileType = ArchivedFormat.png; - linkType = "image"; - } else if (file.type === "application/pdf") { - fileType = ArchivedFormat.pdf; - linkType = "pdf"; - } else { - return { ok: false, data: "Invalid file type." }; - } - - const response = await fetch("/api/v1/links", { - body: JSON.stringify({ - ...link, - type: linkType, - name: link.name ? link.name : file.name, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); - - const data = await response.json(); - - const createdLink: LinkIncludingShortenedCollectionAndTags = data.response; - - console.log(data); - - if (response.ok) { - const formBody = new FormData(); - file && formBody.append("file", file); - - await fetch( - `/api/v1/archives/${(data as any).response.id}?format=${fileType}`, - { - body: formBody, - method: "POST", - } - ); - - // get file extension - const extension = file.name.split(".").pop() || ""; - - set((state) => ({ - links: [ - { - ...createdLink, - image: - linkType === "image" - ? `archives/${createdLink.collectionId}/${ - createdLink.id + extension - }` - : null, - pdf: - linkType === "pdf" - ? `archives/${createdLink.collectionId}/${ - createdLink.id + ".pdf" - }` - : null, - }, - ...state.links, - ], - })); - } - - return { ok: response.ok, data: data.response }; - }, - getLink: async (linkId, publicRoute) => { - const path = publicRoute - ? `/api/v1/public/links/${linkId}` - : `/api/v1/links/${linkId}`; - - const response = await fetch(path); - - const data = await response.json(); - - if (response.ok) { - set((state) => { - const linkExists = state.links.some( - (link) => link.id === data.response.id - ); - - if (linkExists) { - return { - links: state.links.map((e) => - e.id === data.response.id ? data.response : e - ), - }; - } else { - return { - links: [...state.links, data.response], - }; - } - }); - - return data; - } - - return { ok: response.ok, data: data.response }; - }, - updateLink: async (link) => { - const response = await fetch(`/api/v1/links/${link.id}`, { - body: JSON.stringify(link), - headers: { - "Content-Type": "application/json", - }, - method: "PUT", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: state.links.map((e) => - e.id === data.response.id ? data.response : e - ), - })); - } - - return { ok: response.ok, data: data.response }; - }, updateLinks: async (links, removePreviousTags, newData) => { const response = await fetch("/api/v1/links", { body: JSON.stringify({ links, removePreviousTags, newData }), @@ -222,65 +34,11 @@ const useLinkStore = create()((set) => ({ const data = await response.json(); if (response.ok) { - set((state) => ({ - links: state.links.map((e) => - links.some((link) => link.id === e.id) - ? { - ...e, - collectionId: newData.collectionId ?? e.collectionId, - collection: { - ...e.collection, - id: newData.collectionId ?? e.collection.id, - }, - tags: removePreviousTags - ? [...(newData.tags ?? [])] - : [...e.tags, ...(newData.tags ?? [])], - } - : e - ), - })); + // Update the selected links with the new data } return { ok: response.ok, data: data.response }; }, - removeLink: async (linkId) => { - const response = await fetch(`/api/v1/links/${linkId}`, { - headers: { - "Content-Type": "application/json", - }, - method: "DELETE", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: state.links.filter((e) => e.id !== linkId), - })); - } - - return { ok: response.ok, data: data.response }; - }, - deleteLinksById: async (linkIds: number[]) => { - const response = await fetch("/api/v1/links", { - body: JSON.stringify({ linkIds }), - headers: { - "Content-Type": "application/json", - }, - method: "DELETE", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: state.links.filter((e) => !linkIds.includes(e.id as number)), - })); - } - - return { ok: response.ok, data: data.response }; - }, - resetLinks: () => set({ links: [] }), })); export default useLinkStore; diff --git a/store/localSettings.ts b/store/localSettings.ts index 6c79d6b..864fa28 100644 --- a/store/localSettings.ts +++ b/store/localSettings.ts @@ -1,8 +1,10 @@ +import { Sort } from "@/types/global"; import { create } from "zustand"; type LocalSettings = { theme?: string; viewMode?: string; + sortBy?: Sort; }; type LocalSettingsStore = { @@ -15,10 +17,11 @@ const useLocalSettingsStore = create((set) => ({ settings: { theme: "", viewMode: "", + sortBy: Sort.DateNewestFirst, }, updateSettings: async (newSettings) => { if ( - newSettings.theme && + newSettings.theme !== undefined && newSettings.theme !== localStorage.getItem("theme") ) { localStorage.setItem("theme", newSettings.theme); @@ -29,7 +32,7 @@ const useLocalSettingsStore = create((set) => ({ } if ( - newSettings.viewMode && + newSettings.viewMode !== undefined && newSettings.viewMode !== localStorage.getItem("viewMode") ) { localStorage.setItem("viewMode", newSettings.viewMode); @@ -37,6 +40,13 @@ const useLocalSettingsStore = create((set) => ({ // const localTheme = localStorage.getItem("viewMode") || ""; } + if ( + newSettings.sortBy !== undefined && + newSettings.sortBy !== Number(localStorage.getItem("sortBy")) + ) { + localStorage.setItem("sortBy", newSettings.sortBy.toString()); + } + set((state) => ({ settings: { ...state.settings, ...newSettings } })); }, setSettings: async () => { diff --git a/types/global.ts b/types/global.ts index 90c9aef..1cbbde8 100644 --- a/types/global.ts +++ b/types/global.ts @@ -67,7 +67,6 @@ export interface PublicCollectionIncludingLinks extends Collection { export enum ViewMode { Card = "card", - Grid = "grid", List = "list", Masonry = "masonry", } @@ -82,7 +81,7 @@ export enum Sort { } export type LinkRequestQuery = { - sort: Sort; + sort?: Sort; cursor?: number; collectionId?: number; tagId?: number; diff --git a/yarn.lock b/yarn.lock index 174222e..1aecfae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5274,6 +5274,11 @@ react-image-file-resizer@^0.4.8: resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af" integrity sha512-Ue7CfKnSlsfJ//SKzxNMz8avDgDSpWQDOnTKOp/GNRFJv4dO9L5YGHNEnj40peWkXXAK2OK0eRIoXhOYpUzUTQ== +react-intersection-observer@^9.13.0: + version "9.13.0" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz#ee10827954cf6ccc204d027f8400a6ddb8df163a" + integrity sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"