diff --git a/.env.sample b/.env.sample index db674ce..bdcd182 100644 --- a/.env.sample +++ b/.env.sample @@ -1,4 +1,4 @@ -AES_SECRET=very_sensitive_secret1 NEXTAUTH_SECRET=very_sensitive_secret2 DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden -NEXTAUTH_URL=http://localhost:3000 \ No newline at end of file +NEXTAUTH_URL=http://localhost:3000 +PAGINATION_TAKE_COUNT=20 \ No newline at end of file diff --git a/components/LinkCard.tsx b/components/LinkCard.tsx index 278f4e7..275f392 100644 --- a/components/LinkCard.tsx +++ b/components/LinkCard.tsx @@ -1,6 +1,6 @@ import { CollectionIncludingMembers, - LinkIncludingCollectionAndTags, + LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; import { faFolder, @@ -20,7 +20,7 @@ import useAccountStore from "@/store/account"; import useModalStore from "@/store/modals"; type Props = { - link: LinkIncludingCollectionAndTags; + link: LinkIncludingShortenedCollectionAndTags; count: number; className?: string; }; @@ -146,7 +146,7 @@ export default function LinkCard({ link, count, className }: Props) { className="group/url" >
-

{url.host}

+

{url.host}

( + const [link, setLink] = useState( method === "UPDATE" ? activeLink : { diff --git a/components/ModalManagement.tsx b/components/ModalManagement.tsx index bad08cb..a674bec 100644 --- a/components/ModalManagement.tsx +++ b/components/ModalManagement.tsx @@ -4,7 +4,7 @@ import LinkModal from "./Modal/LinkModal"; import { AccountSettings, CollectionIncludingMembers, - LinkIncludingCollectionAndTags, + LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; import CollectionModal from "./Modal/Collection"; import UserModal from "./Modal/User"; @@ -22,7 +22,7 @@ export default function ModalManagement() { ); diff --git a/components/PublicPage/LinkCard.tsx b/components/PublicPage/LinkCard.tsx index 70bb112..6e37a45 100644 --- a/components/PublicPage/LinkCard.tsx +++ b/components/PublicPage/LinkCard.tsx @@ -4,10 +4,14 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Image from "next/image"; -import { Link as LinkType } from "@prisma/client"; +import { Link as LinkType, Tag } from "@prisma/client"; + +interface LinksIncludingTags extends LinkType { + tags: Tag[]; +} type Props = { - link: LinkType; + link: LinksIncludingTags; count: number; }; @@ -58,7 +62,18 @@ export default function LinkCard({ link, count }: Props) {

{link.description}

- +
+
+ {link.tags.map((e, i) => ( +

+ {e.name} +

+ ))} +
+

{formattedDate}

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() {
- {sortedLinks.map((e, i) => { + {links.map((e, i) => { return ; })}
diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index a5c1e8f..ccf4cb2 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -20,7 +20,7 @@ export default function Collections() { const { collections } = useCollectionStore(); const [expandDropdown, setExpandDropdown] = useState(false); const [sortDropdown, setSortDropdown] = useState(false); - const [sortBy, setSortBy] = useState(Sort.NameAZ); + const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); const [sortedCollections, setSortedCollections] = useState(collections); const session = useSession(); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 021e226..140f8ed 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -12,6 +12,7 @@ import Link from "next/link"; import CollectionCard from "@/components/CollectionCard"; import { Disclosure, Transition } from "@headlessui/react"; import { useEffect, useState } from "react"; +import useLinks from "@/hooks/useLinks"; export default function Dashboard() { const { collections } = useCollectionStore(); @@ -34,6 +35,8 @@ export default function Dashboard() { return storedValue ? storedValue === "true" : true; }); + useLinks({ pinnedOnly: true }); + useEffect(() => { localStorage.setItem( "tagPinDisclosure", @@ -131,11 +134,9 @@ export default function Dashboard() { leaveTo="transform opacity-0 -translate-y-3" > - {links - .filter((e) => e.pinnedBy && e.pinnedBy[0]) - .map((e, i) => ( - - ))} + {links.map((e, i) => ( + + ))}
diff --git a/pages/links.tsx b/pages/links.tsx index 9ebcc58..c8cae0e 100644 --- a/pages/links.tsx +++ b/pages/links.tsx @@ -1,6 +1,6 @@ import LinkCard from "@/components/LinkCard"; import SortDropdown from "@/components/SortDropdown"; -import useSort from "@/hooks/useSort"; +import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; import useLinkStore from "@/store/links"; import { Sort } from "@/types/global"; @@ -12,10 +12,9 @@ export default function Links() { const { links } = useLinkStore(); const [sortDropdown, setSortDropdown] = useState(false); - const [sortBy, setSortBy] = useState(Sort.NameAZ); - const [sortedLinks, setSortedLinks] = useState(links); + const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); - useSort({ sortBy, setData: setSortedLinks, data: links }); + useLinks({ sort: sortBy }); return ( @@ -54,7 +53,7 @@ export default function Links() {
- {sortedLinks.map((e, i) => { + {links.map((e, i) => { return ; })}
diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 1b5d322..d2d45be 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -1,4 +1,5 @@ import LinkCard from "@/components/PublicPage/LinkCard"; +import useDetectPageBottom from "@/hooks/useDetectPageBottom"; import getPublicCollectionData from "@/lib/client/getPublicCollectionData"; import { PublicCollectionIncludingLinks } from "@/types/global"; import { useRouter } from "next/router"; @@ -6,6 +7,7 @@ import React, { useEffect, useState } from "react"; export default function PublicCollections() { const router = useRouter(); + const hasReachedBottom = useDetectPageBottom(); const [data, setData] = useState(); @@ -13,7 +15,8 @@ export default function PublicCollections() { if (router.query.id) { getPublicCollectionData( router.query.id as string, - (e: PublicCollectionIncludingLinks) => setData(e) + data as PublicCollectionIncludingLinks, + setData ); } @@ -27,6 +30,16 @@ export default function PublicCollections() { // ); }, []); + useEffect(() => { + if (hasReachedBottom && router.query.id) { + getPublicCollectionData( + router.query.id as string, + data as PublicCollectionIncludingLinks, + setData + ); + } + }, [hasReachedBottom]); + return data ? (
- {data.ownerName && ( -

{"By " + data.ownerName}

- )} -

{data.description}

diff --git a/pages/search/[query].tsx b/pages/search/[query].tsx index 98c1e7b..cb2c202 100644 --- a/pages/search/[query].tsx +++ b/pages/search/[query].tsx @@ -1,33 +1,21 @@ import FilterSearchDropdown from "@/components/FilterSearchDropdown"; import LinkCard from "@/components/LinkCard"; import SortDropdown from "@/components/SortDropdown"; -import useSort from "@/hooks/useSort"; +import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; import useLinkStore from "@/store/links"; -import { Sort } from "@/types/global"; +import { LinkSearchFilter, Sort } from "@/types/global"; import { faFilter, faSearch, faSort } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; - -type SearchFilter = { - name: boolean; - url: boolean; - description: boolean; - collection: boolean; - tags: boolean; -}; +import { useState } from "react"; export default function Links() { const { links } = useLinkStore(); const router = useRouter(); - const routeQuery = decodeURIComponent( - router.query.query as string - ).toLowerCase(); - - const [searchFilter, setSearchFilter] = useState({ + const [searchFilter, setSearchFilter] = useState({ name: true, url: true, description: true, @@ -37,32 +25,13 @@ export default function Links() { const [filterDropdown, setFilterDropdown] = useState(false); const [sortDropdown, setSortDropdown] = useState(false); - const [sortBy, setSortBy] = useState(Sort.NameAZ); + const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); - const [filteredLinks, setFilteredLinks] = useState(links); - const [sortedLinks, setSortedLinks] = useState(filteredLinks); - - useSort({ sortBy, setData: setSortedLinks, data: links }); - - useEffect(() => { - setFilteredLinks([ - ...sortedLinks.filter((link) => { - if ( - (searchFilter.name && link.name.toLowerCase().includes(routeQuery)) || - (searchFilter.url && link.url.toLowerCase().includes(routeQuery)) || - (searchFilter.description && - link.description.toLowerCase().includes(routeQuery)) || - (searchFilter.collection && - link.collection.name.toLowerCase().includes(routeQuery)) || - (searchFilter.tags && - link.tags.some((tag) => - tag.name.toLowerCase().includes(routeQuery) - )) - ) - return true; - }), - ]); - }, [searchFilter, sortedLinks, router]); + useLinks({ + searchFilter: searchFilter, + searchQuery: router.query.query as string, + sort: sortBy, + }); return ( @@ -126,8 +95,8 @@ export default function Links() {
- {filteredLinks[0] ? ( - filteredLinks.map((e, i) => { + {links[0] ? ( + links.map((e, i) => { return ; }) ) : ( diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 0cf913a..c85040f 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -9,7 +9,7 @@ import { Tag } from "@prisma/client"; import useTagStore from "@/store/tags"; import SortDropdown from "@/components/SortDropdown"; import { Sort } from "@/types/global"; -import useSort from "@/hooks/useSort"; +import useLinks from "@/hooks/useLinks"; export default function Index() { const router = useRouter(); @@ -18,13 +18,11 @@ export default function Index() { const { tags } = useTagStore(); const [sortDropdown, setSortDropdown] = useState(false); - const [sortBy, setSortBy] = useState(Sort.NameAZ); + const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); const [activeTag, setActiveTag] = useState(); - const [sortedLinks, setSortedLinks] = useState(links); - - useSort({ sortBy, setData: setSortedLinks, data: links }); + useLinks({ tagId: Number(router.query.id), sort: sortBy }); useEffect(() => { setActiveTag(tags.find((e) => e.id === Number(router.query.id))); @@ -69,7 +67,7 @@ export default function Index() {
- {sortedLinks.map((e, i) => { + {links.map((e, i) => { return ; })}
diff --git a/store/collections.ts b/store/collections.ts index 707133a..c88828f 100644 --- a/store/collections.ts +++ b/store/collections.ts @@ -1,7 +1,6 @@ import { create } from "zustand"; import { CollectionIncludingMembers } from "@/types/global"; import useTagStore from "./tags"; -import useLinkStore from "./links"; type CollectionStore = { collections: CollectionIncludingMembers[]; @@ -80,7 +79,6 @@ const useCollectionStore = create()((set) => ({ collections: state.collections.filter((e) => e.id !== id), })); useTagStore.getState().setTags(); - useLinkStore.getState().setLinks(); } return response.ok; diff --git a/store/links.ts b/store/links.ts index 6c75303..610c440 100644 --- a/store/links.ts +++ b/store/links.ts @@ -1,24 +1,34 @@ import { create } from "zustand"; -import { LinkIncludingCollectionAndTags } from "@/types/global"; +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import useTagStore from "./tags"; import useCollectionStore from "./collections"; type LinkStore = { - links: LinkIncludingCollectionAndTags[]; - setLinks: () => void; - addLink: (body: LinkIncludingCollectionAndTags) => Promise; - updateLink: (link: LinkIncludingCollectionAndTags) => Promise; - removeLink: (link: LinkIncludingCollectionAndTags) => Promise; + links: LinkIncludingShortenedCollectionAndTags[]; + setLinks: ( + data: LinkIncludingShortenedCollectionAndTags[], + isInitialCall: boolean + ) => void; + addLink: (body: LinkIncludingShortenedCollectionAndTags) => Promise; + updateLink: ( + link: LinkIncludingShortenedCollectionAndTags + ) => Promise; + removeLink: ( + link: LinkIncludingShortenedCollectionAndTags + ) => Promise; + resetLinks: () => void; }; const useLinkStore = create()((set) => ({ links: [], - setLinks: async () => { - const response = await fetch("/api/routes/links"); - - const data = await response.json(); - - if (response.ok) set({ links: data.response }); + setLinks: async (data, isInitialCall) => { + isInitialCall && + set(() => ({ + links: [], + })); + set((state) => ({ + links: [...state.links, ...data], + })); }, addLink: async (body) => { const response = await fetch("/api/routes/links", { @@ -35,7 +45,7 @@ const useLinkStore = create()((set) => ({ if (response.ok) { set((state) => ({ - links: [...state.links, data.response], + links: [data.response, ...state.links], })); useTagStore.getState().setTags(); useCollectionStore.getState().setCollections(); @@ -90,6 +100,7 @@ const useLinkStore = create()((set) => ({ return response.ok; }, + resetLinks: () => set({ links: [] }), })); export default useLinkStore; diff --git a/store/modals.ts b/store/modals.ts index c88c845..d54ab94 100644 --- a/store/modals.ts +++ b/store/modals.ts @@ -1,7 +1,7 @@ import { AccountSettings, CollectionIncludingMembers, - LinkIncludingCollectionAndTags, + LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; import { create } from "zustand"; @@ -16,13 +16,13 @@ type Modal = modal: "LINK"; state: boolean; method: "CREATE"; - active?: LinkIncludingCollectionAndTags; + active?: LinkIncludingShortenedCollectionAndTags; } | { modal: "LINK"; state: boolean; method: "UPDATE"; - active: LinkIncludingCollectionAndTags; + active: LinkIncludingShortenedCollectionAndTags; } | { modal: "COLLECTION"; diff --git a/types/global.ts b/types/global.ts index a0b9b96..8c0672a 100644 --- a/types/global.ts +++ b/types/global.ts @@ -3,7 +3,7 @@ import { Collection, Link, Tag, User } from "@prisma/client"; type OptionalExcluding = Partial & Pick; -export interface LinkIncludingCollectionAndTags +export interface LinkIncludingShortenedCollectionAndTags extends Omit { id?: number; createdAt?: string; @@ -12,7 +12,10 @@ export interface LinkIncludingCollectionAndTags pinnedBy?: { id: number; }[]; - collection: OptionalExcluding; + collection: OptionalExcluding< + Pick, + "name" | "ownerId" + >; } export interface Member { @@ -37,17 +40,42 @@ export interface AccountSettings extends User { newPassword?: string; } -export interface PublicCollectionIncludingLinks - extends Omit { - ownerName?: string; - links: Link[]; +interface LinksIncludingTags extends Link { + tags: Tag[]; +} + +export interface PublicCollectionIncludingLinks extends Collection { + links: LinksIncludingTags[]; } export enum Sort { + DateNewestFirst, + DateOldestFirst, NameAZ, NameZA, DescriptionAZ, DescriptionZA, - DateNewestFirst, - DateOldestFirst, } + +export type LinkSearchFilter = { + name: boolean; + url: boolean; + description: boolean; + collection: boolean; + tags: boolean; +}; + +export type LinkRequestQuery = { + cursor?: string; + collectionId?: number; + tagId?: number; + sort?: Sort; + searchFilter?: LinkSearchFilter; + searchQuery?: string; + pinnedOnly?: boolean; +}; + +export type PublicLinkRequestQuery = { + cursor?: string; + collectionId?: number; +};