feat: added delete link functionality

This commit is contained in:
Daniel 2023-03-23 18:55:17 +03:30
parent bcb467ea02
commit 2e3ec53d2a
8 changed files with 110 additions and 24 deletions

View File

@ -16,7 +16,7 @@ function useOutsideAlerter(
ref.current &&
!ref.current.contains(event.target as HTMLInputElement)
) {
onClickOutside();
onClickOutside(event);
}
}
document.addEventListener("mouseup", handleClickOutside);

View File

@ -19,16 +19,18 @@ export default function ({ onClickOutside, className, items }: Props) {
return (
<ClickAwayHandler
onClickOutside={onClickOutside}
className={`${className} border border-sky-100 shadow mb-5 bg-gray-50 p-2 rounded flex flex-col gap-1`}
className={`${className} border border-sky-100 shadow mb-5 bg-gray-50 rounded flex flex-col `}
>
{items.map((e, i) => {
const inner = (
<div className="flex items-center gap-2 p-2 rounded cursor-pointer hover:bg-white hover:outline outline-sky-100 outline-1 duration-100">
<div className="cursor-pointer hover:bg-white hover:outline outline-sky-100 outline-1 duration-100">
<div className="flex items-center gap-2 p-2 rounded hover:opacity-60 duration-100">
{React.cloneElement(e.icon, {
className: "text-sky-500 w-5 h-5",
})}
<p className="text-sky-900">{e.name}</p>
</div>
</div>
);
return e.href ? (

View File

@ -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 ({
</div>
<p className="text-sky-400 text-sm font-medium">{link.title}</p>
<div className="flex gap-3 items-center flex-wrap my-3">
<div className="flex items-center gap-1 cursor-pointer hover:opacity-80 duration-100">
<div className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100">
<FontAwesomeIcon icon={faFolder} className="w-4 text-sky-300" />
<p className="text-sky-900">{link.collection.name}</p>
</div>
@ -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"
/>
<div>
<p className="text-center text-sky-400 text-sm font-bold">
@ -142,9 +146,13 @@ export default function ({
{
name: "Delete",
icon: <FontAwesomeIcon icon={faTrashCan} />,
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}

View File

@ -53,9 +53,13 @@ export default function () {
<div
className="flex gap-2 items-center mb-5 p-3 w-fit text-gray-600 cursor-pointer hover:outline outline-sky-100 outline-1 hover:bg-gray-50 rounded duration-100"
onClick={() => setProfileDropdown(!profileDropdown)}
id="profile-dropdown"
>
<FontAwesomeIcon icon={faCircleUser} className="h-5" />
<div className="flex items-center gap-1">
<FontAwesomeIcon
icon={faCircleUser}
className="h-5 pointer-events-none"
/>
<div className="flex items-center gap-1 pointer-events-none">
<p className="font-bold">{user?.name}</p>
<FontAwesomeIcon icon={faChevronDown} className="h-3" />
</div>
@ -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}

View File

@ -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,
});
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -6,7 +6,7 @@ type LinkStore = {
setLinks: () => void;
addLink: (linkName: NewLink) => Promise<boolean>;
updateLink: (link: ExtendedLink) => void;
removeLink: (linkId: number) => void;
removeLink: (link: ExtendedLink) => void;
};
const useLinkStore = create<LinkStore>()((set) => ({
@ -40,10 +40,25 @@ const useLinkStore = create<LinkStore>()((set) => ({
set((state) => ({
links: state.links.map((c) => (c.id === link.id ? link : c)),
})),
removeLink: (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((c) => c.id !== linkId),
links: state.links.filter((e) => e.id !== link.id),
}));
console.log(data);
return response.ok;
},
}));