From 4252b795860c720ebd8db2a14bff671d5da64631 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 23 Oct 2023 10:45:48 -0400 Subject: [PATCH] added recent links to dashboard --- components/LinkCard.tsx | 2 +- components/Navbar.tsx | 7 +- components/NoLinksFound.tsx | 2 +- hooks/useInitialData.tsx | 1 - hooks/useLinks.tsx | 50 ++++- hooks/useWindowDimensions.tsx | 25 +++ layouts/MainLayout.tsx | 2 +- layouts/SettingsLayout.tsx | 7 +- .../controllers/dashboard/getDashboardData.ts | 80 +++++++ lib/api/controllers/links/getLinks.ts | 24 +- pages/api/v1/dashboard/index.ts | 27 +++ pages/api/v1/links/index.ts | 24 +- pages/collections/[id].tsx | 2 +- pages/dashboard.tsx | 212 +++++++++++++++++- pages/links.tsx | 4 +- pages/search/[query].tsx | 11 +- pages/settings/account.tsx | 6 +- store/links.ts | 12 +- types/global.ts | 16 +- 19 files changed, 461 insertions(+), 53 deletions(-) create mode 100644 hooks/useWindowDimensions.tsx create mode 100644 lib/api/controllers/dashboard/getDashboardData.ts create mode 100644 pages/api/v1/dashboard/index.ts diff --git a/components/LinkCard.tsx b/components/LinkCard.tsx index 86637c0..04903fa 100644 --- a/components/LinkCard.tsx +++ b/components/LinkCard.tsx @@ -240,7 +240,7 @@ export default function LinkCard({ link, count, className }: Props) { if (target.id !== "expand-dropdown" + link.id) setExpandDropdown(false); }} - className="absolute top-12 right-5 w-fit" + className="absolute top-12 right-5 w-40" /> ) : null} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 158dae2..9f8984f 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -11,6 +11,7 @@ import useAccountStore from "@/store/account"; import ProfilePhoto from "@/components/ProfilePhoto"; import useModalStore from "@/store/modals"; import { useTheme } from "next-themes"; +import useWindowDimensions from "@/hooks/useWindowDimensions"; export default function Navbar() { const { setModal } = useModalStore(); @@ -33,7 +34,11 @@ export default function Navbar() { const [sidebar, setSidebar] = useState(false); - window.addEventListener("resize", () => setSidebar(false)); + const { width } = useWindowDimensions(); + + useEffect(() => { + setSidebar(false); + }, [width]); useEffect(() => { setSidebar(false); diff --git a/components/NoLinksFound.tsx b/components/NoLinksFound.tsx index 97b8dce..0d61f6e 100644 --- a/components/NoLinksFound.tsx +++ b/components/NoLinksFound.tsx @@ -11,7 +11,7 @@ export default function NoLinksFound({ text }: Props) { const { setModal } = useModalStore(); return ( -
+

{text || "You haven't created any Links Here"}

diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index a0bd1c4..c6b1ea3 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -2,7 +2,6 @@ import useCollectionStore from "@/store/collections"; import { useEffect } from "react"; import { useSession } from "next-auth/react"; import useTagStore from "@/store/tags"; -import useLinkStore from "@/store/links"; import useAccountStore from "@/store/account"; export default function useInitialData() { diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx index 7f8d84e..56024b0 100644 --- a/hooks/useLinks.tsx +++ b/hooks/useLinks.tsx @@ -7,11 +7,14 @@ import useLinkStore from "@/store/links"; export default function useLinks( { sort, - searchFilter, - searchQuery, - pinnedOnly, collectionId, tagId, + pinnedOnly, + searchQueryString, + searchByName, + searchByUrl, + searchByDescription, + searchByTags, }: LinkRequestQuery = { sort: 0 } ) { const { links, setLinks, resetLinks } = useLinkStore(); @@ -20,20 +23,37 @@ export default function useLinks( const { reachedBottom, setReachedBottom } = useDetectPageBottom(); const getLinks = async (isInitialCall: boolean, cursor?: number) => { - const requestBody: LinkRequestQuery = { - cursor, + const params = { sort, - searchFilter, - searchQuery, - pinnedOnly, + cursor, collectionId, tagId, + pinnedOnly, + searchQueryString, + searchByName, + searchByUrl, + searchByDescription, + searchByTags, }; - const encodedData = encodeURIComponent(JSON.stringify(requestBody)); + 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 queryString = buildQueryString(params); const response = await fetch( - `/api/v1/links?body=${encodeURIComponent(encodedData)}` + `/api/v1/${ + router.asPath === "/dashboard" ? "dashboard" : "links" + }?${queryString}` ); const data = await response.json(); @@ -45,7 +65,15 @@ export default function useLinks( resetLinks(); getLinks(true); - }, [router, sort, searchFilter]); + }, [ + router, + sort, + searchQueryString, + searchByName, + searchByUrl, + searchByDescription, + searchByTags, + ]); useEffect(() => { if (reachedBottom) getLinks(false, links?.at(-1)?.id); diff --git a/hooks/useWindowDimensions.tsx b/hooks/useWindowDimensions.tsx new file mode 100644 index 0000000..bea7246 --- /dev/null +++ b/hooks/useWindowDimensions.tsx @@ -0,0 +1,25 @@ +import React, { useState, useEffect } from "react"; + +export default function useWindowDimensions() { + const [dimensions, setDimensions] = useState({ + height: window.innerHeight, + width: window.innerWidth, + }); + + useEffect(() => { + function handleResize() { + setDimensions({ + height: window.innerHeight, + width: window.innerWidth, + }); + } + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + return dimensions; +} diff --git a/layouts/MainLayout.tsx b/layouts/MainLayout.tsx index 48ecbb8..f3f17cb 100644 --- a/layouts/MainLayout.tsx +++ b/layouts/MainLayout.tsx @@ -26,7 +26,7 @@ export default function MainLayout({ children }: Props) {
-
+
{children}
diff --git a/layouts/SettingsLayout.tsx b/layouts/SettingsLayout.tsx index bb0d6cd..0989ceb 100644 --- a/layouts/SettingsLayout.tsx +++ b/layouts/SettingsLayout.tsx @@ -7,6 +7,7 @@ import ClickAwayHandler from "@/components/ClickAwayHandler"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import Link from "next/link"; +import useWindowDimensions from "@/hooks/useWindowDimensions"; interface Props { children: ReactNode; @@ -25,7 +26,11 @@ export default function SettingsLayout({ children }: Props) { const [sidebar, setSidebar] = useState(false); - window.addEventListener("resize", () => setSidebar(false)); + const { width } = useWindowDimensions(); + + useEffect(() => { + setSidebar(false); + }, [width]); useEffect(() => { setSidebar(false); diff --git a/lib/api/controllers/dashboard/getDashboardData.ts b/lib/api/controllers/dashboard/getDashboardData.ts new file mode 100644 index 0000000..7425a66 --- /dev/null +++ b/lib/api/controllers/dashboard/getDashboardData.ts @@ -0,0 +1,80 @@ +import { prisma } from "@/lib/api/db"; +import { LinkRequestQuery, Sort } from "@/types/global"; + +export default async function getDashboardData( + userId: number, + query: LinkRequestQuery +) { + 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 pinnedLinks = 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: { + AND: [ + { + collection: { + OR: [ + { ownerId: userId }, + { + members: { + some: { userId }, + }, + }, + ], + }, + }, + { + pinnedBy: { some: { id: userId } }, + }, + ], + }, + include: { + tags: true, + collection: true, + pinnedBy: { + where: { id: userId }, + select: { id: true }, + }, + }, + orderBy: order || { createdAt: "desc" }, + }); + + const recentlyAddedLinks = await prisma.link.findMany({ + take: 6, + where: { + collection: { + OR: [ + { ownerId: userId }, + { + members: { + some: { userId }, + }, + }, + ], + }, + }, + include: { + tags: true, + collection: true, + pinnedBy: { + where: { id: userId }, + select: { id: true }, + }, + }, + orderBy: order || { createdAt: "desc" }, + }); + + const links = [...recentlyAddedLinks, ...pinnedLinks].sort( + (a, b) => (new Date(b.createdAt) as any) - (new Date(a.createdAt) as any) + ); + + return { response: links, status: 200 }; +} diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index ac0d057..fd0f257 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -1,9 +1,7 @@ import { prisma } from "@/lib/api/db"; import { LinkRequestQuery, Sort } from "@/types/global"; -export default async function getLink(userId: number, body: string) { - const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); - +export default async function getLink(userId: number, query: LinkRequestQuery) { const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); let order: any; @@ -16,40 +14,40 @@ export default async function getLink(userId: number, body: string) { const searchConditions = []; - if (query.searchQuery) { - if (query.searchFilter?.name) { + if (query.searchQueryString) { + if (query.searchByName) { searchConditions.push({ name: { - contains: query.searchQuery, + contains: query.searchQueryString, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, }, }); } - if (query.searchFilter?.url) { + if (query.searchByUrl) { searchConditions.push({ url: { - contains: query.searchQuery, + contains: query.searchQueryString, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, }, }); } - if (query.searchFilter?.description) { + if (query.searchByDescription) { searchConditions.push({ description: { - contains: query.searchQuery, + contains: query.searchQueryString, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, }, }); } - if (query.searchFilter?.tags) { + if (query.searchByTags) { searchConditions.push({ tags: { some: { name: { - contains: query.searchQuery, + contains: query.searchQueryString, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, }, OR: [ @@ -117,7 +115,7 @@ export default async function getLink(userId: number, body: string) { OR: [ ...tagCondition, { - [query.searchQuery ? "OR" : "AND"]: [ + [query.searchQueryString ? "OR" : "AND"]: [ { pinnedBy: query.pinnedOnly ? { some: { id: userId } } diff --git a/pages/api/v1/dashboard/index.ts b/pages/api/v1/dashboard/index.ts new file mode 100644 index 0000000..5c92e00 --- /dev/null +++ b/pages/api/v1/dashboard/index.ts @@ -0,0 +1,27 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; +import { LinkRequestQuery } from "@/types/global"; +import getDashboardData from "@/lib/api/controllers/dashboard/getDashboardData"; + +export default async function links(req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession(req, res, authOptions); + + if (!session?.user?.id) { + return res.status(401).json({ response: "You must be logged in." }); + } else if (session?.user?.isSubscriber === false) + res.status(401).json({ + response: + "You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.", + }); + + if (req.method === "GET") { + const convertedData: LinkRequestQuery = { + sort: Number(req.query.sort as string), + cursor: req.query.cursor ? Number(req.query.cursor as string) : undefined, + }; + + const links = await getDashboardData(session.user.id, convertedData); + return res.status(links.status).json({ response: links.response }); + } +} diff --git a/pages/api/v1/links/index.ts b/pages/api/v1/links/index.ts index 6eab9af..c878083 100644 --- a/pages/api/v1/links/index.ts +++ b/pages/api/v1/links/index.ts @@ -3,6 +3,7 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import getLinks from "@/lib/api/controllers/links/getLinks"; import postLink from "@/lib/api/controllers/links/postLink"; +import { LinkRequestQuery } from "@/types/global"; export default async function links(req: NextApiRequest, res: NextApiResponse) { const session = await getServerSession(req, res, authOptions); @@ -16,7 +17,28 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) { }); if (req.method === "GET") { - const links = await getLinks(session.user.id, req?.query?.body as string); + // Convert the type of the request query to "LinkRequestQuery" + const convertedData: LinkRequestQuery = { + 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, + tagId: req.query.tagId ? Number(req.query.tagId 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, + searchByTags: req.query.searchByTags === "true" ? true : undefined, + }; + + const links = await getLinks(session.user.id, convertedData); 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 1c19ef0..8e0bc06 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -52,7 +52,7 @@ export default function Index() { return ( -
+
(() => { const storedValue = typeof window !== "undefined" && @@ -45,6 +60,61 @@ export default function Dashboard() { ); }, [linkPinDisclosure]); + const handleNumberOfRecents = () => { + if (window.innerWidth > 1550) { + setShowRecents(6); + } else if (window.innerWidth > 1295) { + setShowRecents(4); + } else setShowRecents(3); + }; + + const { width } = useWindowDimensions(); + + useEffect(() => { + handleNumberOfRecents(); + }, [width]); + + const [importDropdown, setImportDropdown] = useState(false); + + const importBookmarks = async (e: any, format: MigrationFormat) => { + const file: File = e.target.files[0]; + + if (file) { + var reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = async function (e) { + const load = toast.loading("Importing..."); + + const request: string = e.target?.result as string; + + const body: MigrationRequest = { + format, + data: request, + }; + + const response = await fetch("/api/v1/migration", { + method: "POST", + body: JSON.stringify(body), + }); + + const data = await response.json(); + + toast.dismiss(load); + + toast.success("Imported the Bookmarks! Reloading the page..."); + + setImportDropdown(false); + + setTimeout(() => { + location.reload(); + }, 2000); + }; + reader.onerror = function (e) { + console.log("Error:", e); + }; + } + }; + return (
@@ -89,6 +159,146 @@ export default function Dashboard() {
+
+
+ +

+ Recently Added Links +

+
+ + View All + + +
+ +
+ {links[0] ? ( +
+
+ {links.slice(0, showRecents).map((e, i) => ( + + ))} +
+
+ ) : ( +
+

+ View Your Recently Added Links Here! +

+

+ This section will view your latest added Links across every + Collections you have access to. +

+ +
+
{ + setModal({ + modal: "LINK", + state: true, + method: "CREATE", + }); + }} + className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100 group" + > + + + Create New Link + +
+ +
+
setImportDropdown(!importDropdown)} + id="import-dropdown" + className="flex gap-2 select-none text-sm cursor-pointer p-2 px-3 rounded-md border dark:hover:border-sky-600 text-black border-black dark:text-white dark:border-white hover:border-sky-500 hover:dark:border-sky-500 hover:text-sky-500 hover:dark:text-sky-500 duration-100 group" + > + + + Import Your Bookmarks + +
+ {importDropdown ? ( + { + const target = e.target as HTMLInputElement; + if (target.id !== "import-dropdown") + setImportDropdown(false); + }} + className={`absolute text-black dark:text-white top-10 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`} + > +
+ + +
+
+ ) : null} +
+
+
+ )} +
+
{links diff --git a/pages/links.tsx b/pages/links.tsx index 4429079..9925193 100644 --- a/pages/links.tsx +++ b/pages/links.tsx @@ -19,7 +19,7 @@ export default function Links() { return ( -
+
) : ( - + )}
diff --git a/pages/search/[query].tsx b/pages/search/[query].tsx index 0ed78b2..ad04ed7 100644 --- a/pages/search/[query].tsx +++ b/pages/search/[query].tsx @@ -4,7 +4,7 @@ import SortDropdown from "@/components/SortDropdown"; import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; import useLinkStore from "@/store/links"; -import { LinkSearchFilter, Sort } from "@/types/global"; +import { 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"; @@ -15,7 +15,7 @@ export default function Links() { const router = useRouter(); - const [searchFilter, setSearchFilter] = useState({ + const [searchFilter, setSearchFilter] = useState({ name: true, url: true, description: true, @@ -27,9 +27,12 @@ export default function Links() { const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); useLinks({ - searchFilter: searchFilter, - searchQuery: router.query.query as string, sort: sortBy, + searchQueryString: router.query.query as string, + searchByName: searchFilter.name, + searchByUrl: searchFilter.url, + searchByDescription: searchFilter.description, + searchByTags: searchFilter.tags, }); return ( diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 1ee8c2a..5850efa 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faClose, faPenToSquare } from "@fortawesome/free-solid-svg-icons"; +import { faClose } from "@fortawesome/free-solid-svg-icons"; import useAccountStore from "@/store/account"; import { AccountSettings } from "@/types/global"; import { toast } from "react-hot-toast"; @@ -269,7 +269,7 @@ export default function Account() { Import your data from other platforms.

setImportDropdown(true)} + onClick={() => setImportDropdown(!importDropdown)} className="w-fit relative" id="import-dropdown" > @@ -286,7 +286,7 @@ export default function Account() { if (target.id !== "import-dropdown") setImportDropdown(false); }} - className={`absolute top-7 left-0 w-48 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`} + className={`absolute top-7 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`} >