diff --git a/components/PublicPage/LinkCard.tsx b/components/PublicPage/LinkCard.tsx index 3be1972..21a24d2 100644 --- a/components/PublicPage/LinkCard.tsx +++ b/components/PublicPage/LinkCard.tsx @@ -4,9 +4,10 @@ import Image from "next/image"; import { Link as LinkType, Tag } from "@prisma/client"; import isValidUrl from "@/lib/client/isValidUrl"; import unescapeString from "@/lib/client/unescapeString"; +import { TagIncludingLinkCount } from "@/types/global"; interface LinksIncludingTags extends LinkType { - tags: Tag[]; + tags: TagIncludingLinkCount[]; } type Props = { diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 366c28c..78a31a2 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -176,6 +176,9 @@ export default function Sidebar({ className }: { className?: string }) { className="w-4 h-4 drop-shadow text-gray-500 dark:text-gray-300" /> ) : undefined} +
+ {e._count?.links} +
); @@ -235,6 +238,9 @@ export default function Sidebar({ className }: { className?: string }) {

{e.name}

+
+ {e._count?.links} +
); diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx index 60f43db..77e4dce 100644 --- a/hooks/useLinks.tsx +++ b/hooks/useLinks.tsx @@ -50,13 +50,17 @@ export default function useLinks( .join("&"); }; - const queryString = buildQueryString(params); + let queryString = buildQueryString(params); - const response = await fetch( - `/api/v1/${ - router.asPath === "/dashboard" ? "dashboard" : "links" - }?${queryString}` - ); + 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"; + + const response = await fetch(`${basePath}?${queryString}`); const data = await response.json(); diff --git a/lib/api/controllers/public/collections/getPublicCollection.ts b/lib/api/controllers/public/collections/getPublicCollection.ts new file mode 100644 index 0000000..6c5de3a --- /dev/null +++ b/lib/api/controllers/public/collections/getPublicCollection.ts @@ -0,0 +1,32 @@ +import { prisma } from "@/lib/api/db"; + +export default async function getPublicCollection(id: number) { + const collection = await prisma.collection.findFirst({ + where: { + id, + isPublic: true, + }, + include: { + members: { + include: { + user: { + select: { + username: true, + name: true, + image: true, + }, + }, + }, + }, + _count: { + select: { links: true }, + }, + }, + }); + + if (collection) { + return { response: collection, status: 200 }; + } else { + return { response: "Collection not found.", status: 400 }; + } +} diff --git a/lib/api/controllers/public/getCollection.ts b/lib/api/controllers/public/getCollection.ts deleted file mode 100644 index a9a2753..0000000 --- a/lib/api/controllers/public/getCollection.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { prisma } from "@/lib/api/db"; -import { PublicLinkRequestQuery } from "@/types/global"; - -export default async function getCollection(body: string) { - const query: PublicLinkRequestQuery = JSON.parse(decodeURIComponent(body)); - console.log(query); - - let data; - - const collection = await prisma.collection.findFirst({ - where: { - id: query.collectionId, - isPublic: true, - }, - }); - - if (collection) { - const links = await prisma.link.findMany({ - take: Number(process.env.PAGINATION_TAKE_COUNT) || 20, - skip: query.cursor ? 1 : undefined, - cursor: query.cursor - ? { - id: query.cursor, - } - : undefined, - where: { - collection: { - id: query.collectionId, - }, - }, - include: { - tags: true, - }, - orderBy: { - createdAt: "desc", - }, - }); - - data = { ...collection, links: [...links] }; - - return { response: data, status: 200 }; - } else { - return { response: "Collection not found...", status: 400 }; - } -} diff --git a/lib/api/controllers/public/links/getPublicLinksUnderCollection.ts b/lib/api/controllers/public/links/getPublicLinksUnderCollection.ts new file mode 100644 index 0000000..f4113b6 --- /dev/null +++ b/lib/api/controllers/public/links/getPublicLinksUnderCollection.ts @@ -0,0 +1,88 @@ +import { prisma } from "@/lib/api/db"; +import { LinkRequestQuery, Sort } from "@/types/global"; + +export default async function getLink( + query: Omit +) { + const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); + + 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 = { description: "asc" }; + else if (query.sort === Sort.DescriptionZA) order = { description: "desc" }; + + const searchConditions = []; + + if (query.searchQueryString) { + if (query.searchByName) { + searchConditions.push({ + name: { + contains: query.searchQueryString, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }); + } + + if (query.searchByUrl) { + searchConditions.push({ + url: { + contains: query.searchQueryString, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }); + } + + if (query.searchByDescription) { + searchConditions.push({ + description: { + contains: query.searchQueryString, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }); + } + + if (query.searchByTextContent) { + searchConditions.push({ + textContent: { + contains: query.searchQueryString, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }); + } + + if (query.searchByTags) { + searchConditions.push({ + tags: { + some: { + name: { + contains: query.searchQueryString, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }, + }, + }); + } + } + + const links = await prisma.link.findMany({ + take: Number(process.env.PAGINATION_TAKE_COUNT) || 20, + skip: query.cursor ? 1 : undefined, + cursor: query.cursor ? { id: query.cursor } : undefined, + where: { + collection: { + id: query.collectionId, + isPublic: true, + }, + [query.searchQueryString ? "OR" : "AND"]: [...searchConditions], + }, + include: { + tags: true, + }, + orderBy: order || { createdAt: "desc" }, + }); + + return { response: links, status: 200 }; +} diff --git a/lib/api/controllers/public/users/getPublicUserById.ts b/lib/api/controllers/public/users/getPublicUser.ts similarity index 96% rename from lib/api/controllers/public/users/getPublicUserById.ts rename to lib/api/controllers/public/users/getPublicUser.ts index 8f7ea48..fff10a5 100644 --- a/lib/api/controllers/public/users/getPublicUserById.ts +++ b/lib/api/controllers/public/users/getPublicUser.ts @@ -1,6 +1,6 @@ import { prisma } from "@/lib/api/db"; -export default async function getPublicUserById( +export default async function getPublicUser( targetId: number | string, isId: boolean, requestingId?: number diff --git a/lib/api/controllers/tags/getTags.ts b/lib/api/controllers/tags/getTags.ts index e1b007f..85a8d17 100644 --- a/lib/api/controllers/tags/getTags.ts +++ b/lib/api/controllers/tags/getTags.ts @@ -30,6 +30,11 @@ export default async function getTags(userId: number) { }, ], }, + include: { + _count: { + select: { links: true }, + }, + }, // orderBy: { // links: { // _count: "desc", diff --git a/lib/client/getPublicCollectionData.ts b/lib/client/getPublicCollectionData.ts index b86c173..d7452c4 100644 --- a/lib/client/getPublicCollectionData.ts +++ b/lib/client/getPublicCollectionData.ts @@ -1,33 +1,19 @@ -import { - PublicCollectionIncludingLinks, - PublicLinkRequestQuery, -} from "@/types/global"; +import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { Dispatch, SetStateAction } from "react"; const getPublicCollectionData = async ( collectionId: number, - prevData: PublicCollectionIncludingLinks, - setData: Dispatch> + setData: Dispatch< + SetStateAction + > ) => { - const requestBody: PublicLinkRequestQuery = { - cursor: prevData?.links?.at(-1)?.id, - collectionId, - }; - - const encodedData = encodeURIComponent(JSON.stringify(requestBody)); - - const res = await fetch( - "/api/v1/public/collections?body=" + encodeURIComponent(encodedData) - ); + const res = await fetch("/api/v1/public/collections/" + collectionId); const data = await res.json(); - prevData - ? setData({ - ...data.response, - links: [...prevData.links, ...data.response.links], - }) - : setData(data.response); + console.log(data); + + setData(data.response); return data; }; diff --git a/pages/api/v1/public/collections.ts b/pages/api/v1/public/collections/[id].ts similarity index 66% rename from pages/api/v1/public/collections.ts rename to pages/api/v1/public/collections/[id].ts index 5f6b808..c9b2fde 100644 --- a/pages/api/v1/public/collections.ts +++ b/pages/api/v1/public/collections/[id].ts @@ -1,18 +1,18 @@ -import getCollection from "@/lib/api/controllers/public/getCollection"; +import getPublicCollection from "@/lib/api/controllers/public/collections/getPublicCollection"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function collections( req: NextApiRequest, res: NextApiResponse ) { - if (!req?.query?.body) { + if (!req?.query?.id) { return res .status(401) .json({ response: "Please choose a valid collection." }); } if (req.method === "GET") { - const collection = await getCollection(req?.query?.body as string); + const collection = await getPublicCollection(Number(req?.query?.id)); return res .status(collection.status) .json({ response: collection.response }); diff --git a/pages/api/v1/public/collections/links/[id].ts b/pages/api/v1/public/collections/links/[id].ts new file mode 100644 index 0000000..e69de29 diff --git a/pages/api/v1/public/collections/links/index.ts b/pages/api/v1/public/collections/links/index.ts new file mode 100644 index 0000000..dd55179 --- /dev/null +++ b/pages/api/v1/public/collections/links/index.ts @@ -0,0 +1,41 @@ +import getPublicLinksUnderCollection from "@/lib/api/controllers/public/links/getPublicLinksUnderCollection"; +import { LinkRequestQuery } from "@/types/global"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function collections( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === "GET") { + // Convert the type of the request query to "LinkRequestQuery" + const convertedData: Omit = { + sort: Number(req.query.sort as string), + cursor: req.query.cursor ? Number(req.query.cursor as string) : undefined, + collectionId: req.query.collectionId + ? Number(req.query.collectionId as string) + : undefined, + pinnedOnly: req.query.pinnedOnly + ? req.query.pinnedOnly === "true" + : undefined, + searchQueryString: req.query.searchQueryString + ? (req.query.searchQueryString as string) + : undefined, + searchByName: req.query.searchByName === "true" ? true : undefined, + searchByUrl: req.query.searchByUrl === "true" ? true : undefined, + searchByDescription: + req.query.searchByDescription === "true" ? true : undefined, + searchByTextContent: + req.query.searchByTextContent === "true" ? true : undefined, + searchByTags: req.query.searchByTags === "true" ? true : undefined, + }; + + if (!convertedData.collectionId) { + return res + .status(400) + .json({ response: "Please choose a valid collection." }); + } + + const links = await getPublicLinksUnderCollection(convertedData); + return res.status(links.status).json({ response: links.response }); + } +} diff --git a/pages/api/v1/public/users/[id].ts b/pages/api/v1/public/users/[id].ts index 5126740..f5b66ed 100644 --- a/pages/api/v1/public/users/[id].ts +++ b/pages/api/v1/public/users/[id].ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import getPublicUserById from "@/lib/api/controllers/public/users/getPublicUserById"; +import getPublicUser from "@/lib/api/controllers/public/users/getPublicUser"; import { getToken } from "next-auth/jwt"; export default async function users(req: NextApiRequest, res: NextApiResponse) { @@ -12,7 +12,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) { const isId = lookupId.split("").every((e) => Number.isInteger(parseInt(e))); if (req.method === "GET") { - const users = await getPublicUserById(lookupId, isId, requestingId); + const users = await getPublicUser(lookupId, isId, requestingId); return res.status(users.status).json({ response: users.response }); } } diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 80d03f2..01fab2c 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -1,12 +1,13 @@ "use client"; import LinkCard from "@/components/PublicPage/LinkCard"; -import useDetectPageBottom from "@/hooks/useDetectPageBottom"; import getPublicCollectionData from "@/lib/client/getPublicCollectionData"; -import { PublicCollectionIncludingLinks } from "@/types/global"; +import { CollectionIncludingMembersAndLinkCount, Sort } from "@/types/global"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { motion, Variants } from "framer-motion"; import Head from "next/head"; +import useLinks from "@/hooks/useLinks"; +import useLinkStore from "@/store/links"; const cardVariants: Variants = { offscreen: { @@ -23,20 +24,42 @@ const cardVariants: Variants = { }; export default function PublicCollections() { - const router = useRouter(); - const { reachedBottom, setReachedBottom } = useDetectPageBottom(); + const { links } = useLinkStore(); - const [data, setData] = useState(); + const router = useRouter(); + + const [searchFilter, setSearchFilter] = useState({ + name: true, + url: true, + description: true, + textContent: true, + tags: true, + }); + + const [filterDropdown, setFilterDropdown] = useState(false); + const [sortDropdown, setSortDropdown] = useState(false); + const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + + useLinks({ + sort: sortBy, + searchQueryString: router.query.q + ? decodeURIComponent(router.query.q as string) + : undefined, + searchByName: searchFilter.name, + searchByUrl: searchFilter.url, + searchByDescription: searchFilter.description, + searchByTextContent: searchFilter.textContent, + searchByTags: searchFilter.tags, + }); + + const [collection, setCollection] = + useState(); document.body.style.background = "white"; useEffect(() => { if (router.query.id) { - getPublicCollectionData( - Number(router.query.id), - data as PublicCollectionIncludingLinks, - setData - ); + getPublicCollectionData(Number(router.query.id), setCollection); } // document @@ -49,26 +72,14 @@ export default function PublicCollections() { // ); }, []); - useEffect(() => { - if (reachedBottom && router.query.id) { - getPublicCollectionData( - Number(router.query.id), - data as PublicCollectionIncludingLinks, - setData - ); - } - - setReachedBottom(false); - }, [reachedBottom]); - - return data ? ( + return collection ? (
- {data ? ( + {collection ? ( - {data.name} | Linkwarden + {collection.name} | Linkwarden @@ -76,31 +87,33 @@ export default function PublicCollections() {
-

{data.name}

+

{collection.name}

- {data.description && ( + {collection.description && ( <>
-

{data.description}

+

{collection.description}

)}
- {data?.links?.map((e, i) => { - return ( - - - + {links + ?.filter((e) => e.collectionId === Number(router.query.id)) + .map((e, i) => { + return ( + + + + - - ); - })} + ); + })}
{/*

diff --git a/pages/search/index.tsx b/pages/search.tsx similarity index 100% rename from pages/search/index.tsx rename to pages/search.tsx diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 466c3bb..5859c2a 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -11,10 +11,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useRouter } from "next/router"; import { FormEvent, useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; -import { Tag } from "@prisma/client"; import useTagStore from "@/store/tags"; import SortDropdown from "@/components/SortDropdown"; -import { Sort } from "@/types/global"; +import { Sort, TagIncludingLinkCount } from "@/types/global"; import useLinks from "@/hooks/useLinks"; import Dropdown from "@/components/Dropdown"; import { toast } from "react-hot-toast"; @@ -33,7 +32,7 @@ export default function Index() { const [renameTag, setRenameTag] = useState(false); const [newTagName, setNewTagName] = useState(); - const [activeTag, setActiveTag] = useState(); + const [activeTag, setActiveTag] = useState(); useLinks({ tagId: Number(router.query.id), sort: sortBy }); diff --git a/store/tags.ts b/store/tags.ts index a27b859..6778152 100644 --- a/store/tags.ts +++ b/store/tags.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { Tag } from "@prisma/client"; +import { TagIncludingLinkCount } from "@/types/global"; type ResponseObject = { ok: boolean; @@ -7,9 +7,9 @@ type ResponseObject = { }; type TagStore = { - tags: Tag[]; + tags: TagIncludingLinkCount[]; setTags: () => void; - updateTag: (tag: Tag) => Promise; + updateTag: (tag: TagIncludingLinkCount) => Promise; removeTag: (tagId: number) => Promise; }; diff --git a/types/global.ts b/types/global.ts index 26058cf..2af3766 100644 --- a/types/global.ts +++ b/types/global.ts @@ -37,6 +37,10 @@ export interface CollectionIncludingMembersAndLinkCount members: Member[]; } +export interface TagIncludingLinkCount extends Tag { + _count?: { links: number }; +} + export interface AccountSettings extends User { newPassword?: string; whitelistedUsers: string[];