From 2e3ec53d2a3785f8c828b8f9441ef4c2c6301f90 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Mar 2023 18:55:17 +0330 Subject: [PATCH] feat: added delete link functionality --- components/ClickAwayHandler.tsx | 2 +- components/Dropdown.tsx | 14 ++++---- components/LinkList.tsx | 12 +++++-- components/Sidebar/index.tsx | 13 +++++-- lib/api/controllers/links/deleteLink.ts | 48 +++++++++++++++++++++++++ pages/api/archives/[...params].ts | 16 +++++---- pages/api/routes/links/index.ts | 4 ++- store/links.ts | 25 ++++++++++--- 8 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 lib/api/controllers/links/deleteLink.ts diff --git a/components/ClickAwayHandler.tsx b/components/ClickAwayHandler.tsx index d69edca..fa5191a 100644 --- a/components/ClickAwayHandler.tsx +++ b/components/ClickAwayHandler.tsx @@ -16,7 +16,7 @@ function useOutsideAlerter( ref.current && !ref.current.contains(event.target as HTMLInputElement) ) { - onClickOutside(); + onClickOutside(event); } } document.addEventListener("mouseup", handleClickOutside); diff --git a/components/Dropdown.tsx b/components/Dropdown.tsx index 6490027..e6d165a 100644 --- a/components/Dropdown.tsx +++ b/components/Dropdown.tsx @@ -19,15 +19,17 @@ export default function ({ onClickOutside, className, items }: Props) { return ( {items.map((e, i) => { const inner = ( -
- {React.cloneElement(e.icon, { - className: "text-sky-500 w-5 h-5", - })} -

{e.name}

+
+
+ {React.cloneElement(e.icon, { + className: "text-sky-500 w-5 h-5", + })} +

{e.name}

+
); diff --git a/components/LinkList.tsx b/components/LinkList.tsx index 11d3cac..48d7209 100644 --- a/components/LinkList.tsx +++ b/components/LinkList.tsx @@ -13,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useState } from "react"; import Image from "next/image"; import Dropdown from "./Dropdown"; +import useLinkStore from "@/store/links"; export default function ({ link, @@ -24,6 +25,8 @@ export default function ({ const [editDropdown, setEditDropdown] = useState(false); const [archiveLabel, setArchiveLabel] = useState("Archived Formats"); + const { removeLink } = useLinkStore(); + const shortendURL = new URL(link.url).host.toLowerCase(); const formattedDate = new Date(link.createdAt).toLocaleString("en-US", { year: "numeric", @@ -52,7 +55,7 @@ export default function ({

{link.title}

-
+

{link.collection.name}

@@ -86,6 +89,7 @@ export default function ({ icon={faEllipsis} className="w-6 h-6 text-gray-500 rounded cursor-pointer hover:bg-white hover:outline outline-sky-100 outline-1 duration-100 p-2" onClick={() => setEditDropdown(!editDropdown)} + id="edit-dropdown" />

@@ -142,9 +146,13 @@ export default function ({ { name: "Delete", icon: , + onClick: () => removeLink(link), }, ]} - onClickOutside={() => setEditDropdown(!editDropdown)} + onClickOutside={(e: Event) => { + const target = e.target as HTMLInputElement; + if (target.id !== "edit-dropdown") setEditDropdown(false); + }} className="absolute top-10 right-0" /> ) : null} diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx index c1ba807..8004545 100644 --- a/components/Sidebar/index.tsx +++ b/components/Sidebar/index.tsx @@ -53,9 +53,13 @@ export default function () {

setProfileDropdown(!profileDropdown)} + id="profile-dropdown" > - -
+ +

{user?.name}

@@ -76,7 +80,10 @@ export default function () { }, }, ]} - onClickOutside={() => setProfileDropdown(!profileDropdown)} + onClickOutside={(e: Event) => { + const target = e.target as HTMLInputElement; + if (target.id !== "profile-dropdown") setProfileDropdown(false); + }} className="absolute top-12 left-0" /> ) : null} diff --git a/lib/api/controllers/links/deleteLink.ts b/lib/api/controllers/links/deleteLink.ts new file mode 100644 index 0000000..05ae85d --- /dev/null +++ b/lib/api/controllers/links/deleteLink.ts @@ -0,0 +1,48 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { prisma } from "@/lib/api/db"; +import { Session } from "next-auth"; +import { ExtendedLink, NewLink } from "@/types/global"; +import { existsSync, mkdirSync } from "fs"; +import getTitle from "../../getTitle"; +import archive from "../../archive"; +import { Link, UsersAndCollections } from "@prisma/client"; +import AES from "crypto-js/aes"; +import hasAccessToCollection from "@/lib/api/hasAccessToCollection"; + +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + session: Session +) { + if (!session?.user?.email) { + return res.status(401).json({ response: "You must be logged in." }); + } + + const link: ExtendedLink = req?.body; + + if (!link) { + return res.status(401).json({ response: "Please choose a valid link." }); + } + + const collectionIsAccessible = await hasAccessToCollection( + session.user.id, + link.collectionId + ); + + const memberHasAccess = collectionIsAccessible?.members.some( + (e: UsersAndCollections) => e.userId === session.user.id && e.canDelete + ); + + if (!(collectionIsAccessible?.ownerId === session.user.id || memberHasAccess)) + return res.status(401).json({ response: "Collection is not accessible." }); + + const deleteLink: Link = await prisma.link.delete({ + where: { + id: link.id, + }, + }); + + return res.status(200).json({ + response: deleteLink, + }); +} diff --git a/pages/api/archives/[...params].ts b/pages/api/archives/[...params].ts index 5ec56b3..64dc43a 100644 --- a/pages/api/archives/[...params].ts +++ b/pages/api/archives/[...params].ts @@ -33,12 +33,16 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { const decryptedPath = AES.decrypt(encryptedPath, AES_SECRET).toString(enc); const filePath = path.join(process.cwd(), decryptedPath); - const file = fs.readFileSync(filePath); + const file = fs.existsSync(filePath) + ? fs.readFileSync(filePath) + : "File not found, it's possible that the file you're looking for either doesn't exist or hasn't been created yet."; - if (filePath.endsWith(".pdf")) - res.setHeader("Content-Type", "application/pdf"); + if (!fs.existsSync(filePath)) + res.setHeader("Content-Type", "text/plain").status(404); + else if (filePath.endsWith(".pdf")) + res.setHeader("Content-Type", "application/pdf").status(200); + else if (filePath.endsWith(".png")) + res.setHeader("Content-Type", "image/png").status(200); - if (filePath.endsWith(".png")) res.setHeader("Content-Type", "image/png"); - - return res.status(200).send(file); + return res.send(file); } diff --git a/pages/api/routes/links/index.ts b/pages/api/routes/links/index.ts index 1ded56f..7f8cb26 100644 --- a/pages/api/routes/links/index.ts +++ b/pages/api/routes/links/index.ts @@ -3,6 +3,7 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "pages/api/auth/[...nextauth]"; import getLinks from "@/lib/api/controllers/links/getLinks"; import postLink from "@/lib/api/controllers/links/postLink"; +import deleteLink from "@/lib/api/controllers/links/deleteLink"; type Data = { response: object[] | string; @@ -19,5 +20,6 @@ export default async function ( } if (req.method === "GET") return await getLinks(req, res, session); - if (req.method === "POST") return await postLink(req, res, session); + else if (req.method === "POST") return await postLink(req, res, session); + else if (req.method === "DELETE") return await deleteLink(req, res, session); } diff --git a/store/links.ts b/store/links.ts index 6eb05d1..a9ba598 100644 --- a/store/links.ts +++ b/store/links.ts @@ -6,7 +6,7 @@ type LinkStore = { setLinks: () => void; addLink: (linkName: NewLink) => Promise; updateLink: (link: ExtendedLink) => void; - removeLink: (linkId: number) => void; + removeLink: (link: ExtendedLink) => void; }; const useLinkStore = create()((set) => ({ @@ -40,10 +40,25 @@ const useLinkStore = create()((set) => ({ set((state) => ({ links: state.links.map((c) => (c.id === link.id ? link : c)), })), - removeLink: (linkId) => { - set((state) => ({ - links: state.links.filter((c) => c.id !== linkId), - })); + removeLink: async (link) => { + const response = await fetch("/api/routes/links", { + body: JSON.stringify(link), + headers: { + "Content-Type": "application/json", + }, + method: "DELETE", + }); + + const data = await response.json(); + + if (response.ok) + set((state) => ({ + links: state.links.filter((e) => e.id !== link.id), + })); + + console.log(data); + + return response.ok; }, }));