diff --git a/components/SortDropdown.tsx b/components/SortDropdown.tsx
index 6ae3a36..dedac7f 100644
--- a/components/SortDropdown.tsx
+++ b/components/SortDropdown.tsx
@@ -25,6 +25,18 @@ export default function SortDropdown({
>
Sort by
+ setSort(Sort.DateNewestFirst)}
+ />
+
+ setSort(Sort.DateOldestFirst)}
+ />
+
setSort(Sort.DescriptionZA)}
/>
-
- setSort(Sort.DateNewestFirst)}
- />
-
- setSort(Sort.DateOldestFirst)}
- />
);
diff --git a/hooks/useDetectPageBottom.tsx b/hooks/useDetectPageBottom.tsx
new file mode 100644
index 0000000..518620e
--- /dev/null
+++ b/hooks/useDetectPageBottom.tsx
@@ -0,0 +1,25 @@
+import { useState, useEffect } from "react";
+
+const useDetectPageBottom = () => {
+ const [reachedBottom, setReachedBottom] = useState
(false);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const offsetHeight = document.documentElement.offsetHeight;
+ const innerHeight = window.innerHeight;
+ const scrollTop = document.documentElement.scrollTop;
+
+ const hasReachedBottom = offsetHeight - (innerHeight + scrollTop) <= 100;
+
+ setReachedBottom(hasReachedBottom);
+ };
+
+ window.addEventListener("scroll", handleScroll);
+
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, []);
+
+ return reachedBottom;
+};
+
+export default useDetectPageBottom;
diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx
index 9a1298f..626e1a5 100644
--- a/hooks/useInitialData.tsx
+++ b/hooks/useInitialData.tsx
@@ -9,14 +9,14 @@ export default function useInitialData() {
const { status, data } = useSession();
const { setCollections } = useCollectionStore();
const { setTags } = useTagStore();
- const { setLinks } = useLinkStore();
+ // const { setLinks } = useLinkStore();
const { setAccount } = useAccountStore();
useEffect(() => {
if (status === "authenticated") {
setCollections();
setTags();
- setLinks();
+ // setLinks();
setAccount(data.user.email as string);
}
}, [status]);
diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx
new file mode 100644
index 0000000..de52f09
--- /dev/null
+++ b/hooks/useLinks.tsx
@@ -0,0 +1,58 @@
+import { LinkRequestQuery } from "@/types/global";
+import { useEffect } from "react";
+import useDetectPageBottom from "./useDetectPageBottom";
+import { useRouter } from "next/router";
+import useLinkStore from "@/store/links";
+
+export default function useLinks({
+ sort,
+ searchFilter,
+ searchQuery,
+ pinnedOnly,
+ collectionId,
+ tagId,
+}: Omit = {}) {
+ const { links, setLinks, resetLinks } = useLinkStore();
+ const router = useRouter();
+
+ const hasReachedBottom = useDetectPageBottom();
+
+ const getLinks = async (isInitialCall: boolean, cursor?: number) => {
+ const response = await fetch(
+ `/api/routes/links?cursor=${cursor}${
+ (sort ? "&sort=" + sort : "") +
+ (searchQuery && searchFilter
+ ? "&searchQuery=" +
+ searchQuery +
+ "&searchFilter=" +
+ searchFilter.name +
+ "-" +
+ searchFilter.url +
+ "-" +
+ searchFilter.description +
+ "-" +
+ searchFilter.collection +
+ "-" +
+ searchFilter.tags
+ : "") +
+ (collectionId ? "&collectionId=" + collectionId : "") +
+ (tagId ? "&tagId=" + tagId : "") +
+ (pinnedOnly ? "&pinnedOnly=" + pinnedOnly : "")
+ }`
+ );
+
+ const data = await response.json();
+
+ if (response.ok) setLinks(data.response, isInitialCall);
+ };
+
+ useEffect(() => {
+ resetLinks();
+
+ getLinks(true);
+ }, [router, sort, searchFilter]);
+
+ useEffect(() => {
+ if (hasReachedBottom) getLinks(false, links?.at(-1)?.id);
+ }, [hasReachedBottom]);
+}
diff --git a/hooks/useSort.tsx b/hooks/useSort.tsx
index 38893e5..337c523 100644
--- a/hooks/useSort.tsx
+++ b/hooks/useSort.tsx
@@ -1,12 +1,12 @@
import {
CollectionIncludingMembers,
- LinkIncludingCollectionAndTags,
+ LinkIncludingShortenedCollectionAndTags,
Sort,
} from "@/types/global";
import { SetStateAction, useEffect } from "react";
type Props<
- T extends CollectionIncludingMembers | LinkIncludingCollectionAndTags
+ T extends CollectionIncludingMembers | LinkIncludingShortenedCollectionAndTags
> = {
sortBy: Sort;
@@ -15,7 +15,7 @@ type Props<
};
export default function useSort<
- T extends CollectionIncludingMembers | LinkIncludingCollectionAndTags
+ T extends CollectionIncludingMembers | LinkIncludingShortenedCollectionAndTags
>({ sortBy, data, setData }: Props) {
useEffect(() => {
const dataArray = [...data];
diff --git a/lib/api/controllers/links/deleteLink.ts b/lib/api/controllers/links/deleteLink.ts
index 4d43387..7849c1e 100644
--- a/lib/api/controllers/links/deleteLink.ts
+++ b/lib/api/controllers/links/deleteLink.ts
@@ -1,11 +1,11 @@
import { prisma } from "@/lib/api/db";
-import { LinkIncludingCollectionAndTags } from "@/types/global";
+import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import fs from "fs";
import { Link, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
export default async function deleteLink(
- link: LinkIncludingCollectionAndTags,
+ link: LinkIncludingShortenedCollectionAndTags,
userId: number
) {
if (!link || !link.collectionId)
diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts
index 3d10f73..0a4a613 100644
--- a/lib/api/controllers/links/getLinks.ts
+++ b/lib/api/controllers/links/getLinks.ts
@@ -1,31 +1,229 @@
import { prisma } from "@/lib/api/db";
-export default async function getLink(userId: number) {
- const links = await prisma.link.findMany({
- where: {
- collection: {
- OR: [
- {
- ownerId: userId,
- },
- {
- members: {
- some: {
- userId,
+import { LinkRequestQuery, LinkSearchFilter, Sort } from "@/types/global";
+
+export default async function getLink(userId: number, query: LinkRequestQuery) {
+ query.sort = Number(query.sort) || 0;
+ query.pinnedOnly = query.pinnedOnly
+ ? JSON.parse(query.pinnedOnly as unknown as string)
+ : undefined;
+
+ if (query.searchFilter) {
+ const filterParams = (query.searchFilter as unknown as string).split("-");
+
+ query.searchFilter = {} as LinkSearchFilter;
+
+ query.searchFilter.name = JSON.parse(filterParams[0]);
+ query.searchFilter.url = JSON.parse(filterParams[1]);
+ query.searchFilter.description = JSON.parse(filterParams[2]);
+ query.searchFilter.collection = JSON.parse(filterParams[3]);
+ query.searchFilter.tags = JSON.parse(filterParams[4]);
+ }
+
+ console.log(query.searchFilter);
+
+ // Sorting logic
+ let order: any;
+ if (query.sort === Sort.DateNewestFirst)
+ order = {
+ createdAt: "desc",
+ };
+ else if (query.sort === Sort.DateOldestFirst)
+ order = {
+ createdAt: "asc",
+ };
+ else if (query.sort === Sort.NameAZ)
+ order = {
+ name: "asc",
+ };
+ else if (query.sort === Sort.NameZA)
+ order = {
+ name: "desc",
+ };
+ else if (query.sort === Sort.DescriptionAZ)
+ order = {
+ name: "asc",
+ };
+ else if (query.sort === Sort.DescriptionZA)
+ order = {
+ name: "desc",
+ };
+
+ const links =
+ // Searching logic
+ query.searchFilter && query.searchQuery
+ ? await prisma.link.findMany({
+ take: Number(process.env.PAGINATION_TAKE_COUNT),
+ skip: query.cursor !== "undefined" ? 1 : undefined,
+ cursor:
+ query.cursor !== "undefined"
+ ? {
+ id: Number(query.cursor),
+ }
+ : undefined,
+ where: {
+ OR: [
+ {
+ name: {
+ contains: query.searchFilter?.name
+ ? query.searchQuery
+ : undefined,
+ mode: "insensitive",
+ },
},
+ {
+ url: {
+ contains: query.searchFilter?.url
+ ? query.searchQuery
+ : undefined,
+ mode: "insensitive",
+ },
+ },
+ {
+ description: {
+ contains: query.searchFilter?.description
+ ? query.searchQuery
+ : undefined,
+ mode: "insensitive",
+ },
+ },
+ {
+ collection: {
+ name: {
+ contains: query.searchFilter?.collection
+ ? query.searchQuery
+ : undefined,
+ mode: "insensitive",
+ },
+ OR: [
+ {
+ ownerId: userId,
+ },
+ {
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ ],
+ },
+ },
+ {
+ tags: {
+ // If tagId was defined, search by tag
+ some: {
+ name: {
+ contains: query.searchFilter?.tags
+ ? query.searchQuery
+ : undefined,
+ mode: "insensitive",
+ },
+ OR: [
+ { ownerId: userId }, // Tags owned by the user
+ {
+ links: {
+ some: {
+ name: {
+ contains: query.searchFilter?.tags
+ ? query.searchQuery
+ : undefined,
+ mode: "insensitive",
+ },
+ collection: {
+ members: {
+ some: {
+ userId, // Tags from collections where the user is a member
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ include: {
+ tags: true,
+ collection: true,
+ pinnedBy: {
+ where: { id: userId },
+ select: { id: true },
},
},
- ],
- },
- },
- include: {
- tags: true,
- collection: true,
- pinnedBy: {
- where: { id: userId },
- select: { id: true },
- },
- },
- });
+ orderBy: order || undefined,
+ })
+ : // If not searching
+ await prisma.link.findMany({
+ take: Number(process.env.PAGINATION_TAKE_COUNT),
+ skip: query.cursor !== "undefined" ? 1 : undefined,
+ cursor:
+ query.cursor !== "undefined"
+ ? {
+ id: Number(query.cursor),
+ }
+ : undefined,
+ where: {
+ pinnedBy: query.pinnedOnly ? { some: { id: userId } } : undefined,
+ collection: {
+ id: query.collectionId && Number(query.collectionId), // If collectionId was defined, search by collection
+
+ OR: [
+ {
+ ownerId: userId,
+ },
+ {
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ ],
+ },
+ tags: {
+ some: query.tagId // If tagId was defined, search by tag
+ ? {
+ id: Number(query.tagId),
+ OR: [
+ { ownerId: userId }, // Tags owned by the user
+ {
+ links: {
+ some: {
+ collection: {
+ members: {
+ some: {
+ userId, // Tags from collections where the user is a member
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ }
+ : undefined,
+ },
+ },
+ include: {
+ tags: true,
+ collection: {
+ select: {
+ id: true,
+ ownerId: true,
+ name: true,
+ color: true,
+ },
+ },
+ pinnedBy: {
+ where: { id: userId },
+ select: { id: true },
+ },
+ },
+ orderBy: order || undefined,
+ });
return { response: links, status: 200 };
}
diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts
index 37d2ad1..51480d0 100644
--- a/lib/api/controllers/links/postLink.ts
+++ b/lib/api/controllers/links/postLink.ts
@@ -1,5 +1,5 @@
import { prisma } from "@/lib/api/db";
-import { LinkIncludingCollectionAndTags } from "@/types/global";
+import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import getTitle from "../../getTitle";
import archive from "../../archive";
import { Link, UsersAndCollections } from "@prisma/client";
@@ -7,7 +7,7 @@ import getPermission from "@/lib/api/getPermission";
import { existsSync, mkdirSync } from "fs";
export default async function postLink(
- link: LinkIncludingCollectionAndTags,
+ link: LinkIncludingShortenedCollectionAndTags,
userId: number
) {
link.collection.name = link.collection.name.trim();
diff --git a/lib/api/controllers/links/updateLink.ts b/lib/api/controllers/links/updateLink.ts
index 4e8f765..56e03f0 100644
--- a/lib/api/controllers/links/updateLink.ts
+++ b/lib/api/controllers/links/updateLink.ts
@@ -1,10 +1,10 @@
import { prisma } from "@/lib/api/db";
-import { LinkIncludingCollectionAndTags } from "@/types/global";
+import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
export default async function updateLink(
- link: LinkIncludingCollectionAndTags,
+ link: LinkIncludingShortenedCollectionAndTags,
userId: number
) {
if (!link) return { response: "Please choose a valid link.", status: 401 };
diff --git a/lib/api/controllers/public/getCollection.ts b/lib/api/controllers/public/getCollection.ts
index b68610a..ce883fd 100644
--- a/lib/api/controllers/public/getCollection.ts
+++ b/lib/api/controllers/public/getCollection.ts
@@ -1,35 +1,40 @@
import { prisma } from "@/lib/api/db";
+import { PublicLinkRequestQuery } from "@/types/global";
-export default async function getCollection(collectionId: number) {
+export default async function getCollection(query: PublicLinkRequestQuery) {
let data;
const collection = await prisma.collection.findFirst({
where: {
- id: collectionId,
+ id: Number(query.collectionId),
isPublic: true,
},
- include: {
- links: {
- select: {
- id: true,
- name: true,
- url: true,
- description: true,
- collectionId: true,
- createdAt: true,
- },
- },
- },
});
if (collection) {
- const user = await prisma.user.findUnique({
+ const links = await prisma.link.findMany({
+ take: Number(process.env.PAGINATION_TAKE_COUNT),
+ skip: query.cursor !== "undefined" ? 1 : undefined,
+ cursor:
+ query.cursor !== "undefined"
+ ? {
+ id: Number(query.cursor),
+ }
+ : undefined,
where: {
- id: collection.ownerId,
+ collection: {
+ id: Number(query.collectionId),
+ },
+ },
+ include: {
+ tags: true,
+ },
+ orderBy: {
+ createdAt: "desc",
},
});
- data = { ownerName: user?.name, ...collection };
+ data = { ...collection, links: [...links] };
return { response: data, status: 200 };
} else {
diff --git a/lib/client/getPublicCollectionData.ts b/lib/client/getPublicCollectionData.ts
index 18e932a..7aad89f 100644
--- a/lib/client/getPublicCollectionData.ts
+++ b/lib/client/getPublicCollectionData.ts
@@ -1,14 +1,26 @@
+import { PublicCollectionIncludingLinks } from "@/types/global";
+import { Dispatch, SetStateAction } from "react";
+
const getPublicCollectionData = async (
collectionId: string,
- setData?: Function
+ prevData: PublicCollectionIncludingLinks,
+ setData: Dispatch>
) => {
const res = await fetch(
- "/api/public/routes/collections/?collectionId=" + collectionId
+ "/api/public/routes/collections?collectionId=" +
+ collectionId +
+ "&cursor=" +
+ prevData?.links?.at(-1)?.id
);
const data = await res.json();
- if (setData) setData(data.response);
+ prevData
+ ? setData({
+ ...data.response,
+ links: [...prevData.links, ...data.response.links],
+ })
+ : setData(data.response);
return data;
};
diff --git a/pages/api/public/routes/collections.ts b/pages/api/public/routes/collections.ts
index 86c2b3e..57d8adf 100644
--- a/pages/api/public/routes/collections.ts
+++ b/pages/api/public/routes/collections.ts
@@ -1,20 +1,21 @@
import getCollection from "@/lib/api/controllers/public/getCollection";
+import { PublicLinkRequestQuery } from "@/types/global";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
- const collectionId = Number(req.query.collectionId);
+ const query: PublicLinkRequestQuery = req.query;
- if (!collectionId) {
+ if (!query) {
return res
.status(401)
.json({ response: "Please choose a valid collection." });
}
if (req.method === "GET") {
- const collection = await getCollection(collectionId);
+ const collection = await getCollection(query);
return res
.status(collection.status)
.json({ response: collection.response });
diff --git a/pages/api/routes/links/index.ts b/pages/api/routes/links/index.ts
index 949118d..eff2be7 100644
--- a/pages/api/routes/links/index.ts
+++ b/pages/api/routes/links/index.ts
@@ -14,7 +14,7 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
}
if (req.method === "GET") {
- const links = await getLinks(session.user.id);
+ const links = await getLinks(session.user.id, req.query);
return res.status(links.status).json({ response: links.response });
} else if (req.method === "POST") {
const newlink = await postLink(req.body, session.user.id);
diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx
index 95c3f96..b6659d9 100644
--- a/pages/collections/[id].tsx
+++ b/pages/collections/[id].tsx
@@ -16,7 +16,7 @@ import { useSession } from "next-auth/react";
import ProfilePhoto from "@/components/ProfilePhoto";
import SortDropdown from "@/components/SortDropdown";
import useModalStore from "@/store/modals";
-import useSort from "@/hooks/useSort";
+import useLinks from "@/hooks/useLinks";
export default function Index() {
const { setModal } = useModalStore();
@@ -30,14 +30,12 @@ export default function Index() {
const [expandDropdown, setExpandDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
- const [sortBy, setSortBy] = useState(Sort.NameAZ);
+ const [sortBy, setSortBy] = useState(Sort.DateNewestFirst);
const [activeCollection, setActiveCollection] =
useState();
- const [sortedLinks, setSortedLinks] = useState(links);
-
- useSort({ sortBy, setData: setSortedLinks, data: links });
+ useLinks({ collectionId: Number(router.query.id), sort: sortBy });
useEffect(() => {
setActiveCollection(
@@ -223,7 +221,7 @@ export default function Index() {