feat: added delete link functionality
This commit is contained in:
parent
bcb467ea02
commit
2e3ec53d2a
|
@ -16,7 +16,7 @@ function useOutsideAlerter(
|
||||||
ref.current &&
|
ref.current &&
|
||||||
!ref.current.contains(event.target as HTMLInputElement)
|
!ref.current.contains(event.target as HTMLInputElement)
|
||||||
) {
|
) {
|
||||||
onClickOutside();
|
onClickOutside(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener("mouseup", handleClickOutside);
|
document.addEventListener("mouseup", handleClickOutside);
|
||||||
|
|
|
@ -19,16 +19,18 @@ export default function ({ onClickOutside, className, items }: Props) {
|
||||||
return (
|
return (
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
onClickOutside={onClickOutside}
|
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) => {
|
{items.map((e, i) => {
|
||||||
const inner = (
|
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, {
|
{React.cloneElement(e.icon, {
|
||||||
className: "text-sky-500 w-5 h-5",
|
className: "text-sky-500 w-5 h-5",
|
||||||
})}
|
})}
|
||||||
<p className="text-sky-900">{e.name}</p>
|
<p className="text-sky-900">{e.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return e.href ? (
|
return e.href ? (
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Dropdown from "./Dropdown";
|
import Dropdown from "./Dropdown";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
|
||||||
export default function ({
|
export default function ({
|
||||||
link,
|
link,
|
||||||
|
@ -24,6 +25,8 @@ export default function ({
|
||||||
const [editDropdown, setEditDropdown] = useState(false);
|
const [editDropdown, setEditDropdown] = useState(false);
|
||||||
const [archiveLabel, setArchiveLabel] = useState("Archived Formats");
|
const [archiveLabel, setArchiveLabel] = useState("Archived Formats");
|
||||||
|
|
||||||
|
const { removeLink } = useLinkStore();
|
||||||
|
|
||||||
const shortendURL = new URL(link.url).host.toLowerCase();
|
const shortendURL = new URL(link.url).host.toLowerCase();
|
||||||
const formattedDate = new Date(link.createdAt).toLocaleString("en-US", {
|
const formattedDate = new Date(link.createdAt).toLocaleString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
@ -52,7 +55,7 @@ export default function ({
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sky-400 text-sm font-medium">{link.title}</p>
|
<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 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" />
|
<FontAwesomeIcon icon={faFolder} className="w-4 text-sky-300" />
|
||||||
<p className="text-sky-900">{link.collection.name}</p>
|
<p className="text-sky-900">{link.collection.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,6 +89,7 @@ export default function ({
|
||||||
icon={faEllipsis}
|
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"
|
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)}
|
onClick={() => setEditDropdown(!editDropdown)}
|
||||||
|
id="edit-dropdown"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-center text-sky-400 text-sm font-bold">
|
<p className="text-center text-sky-400 text-sm font-bold">
|
||||||
|
@ -142,9 +146,13 @@ export default function ({
|
||||||
{
|
{
|
||||||
name: "Delete",
|
name: "Delete",
|
||||||
icon: <FontAwesomeIcon icon={faTrashCan} />,
|
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"
|
className="absolute top-10 right-0"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -53,9 +53,13 @@ export default function () {
|
||||||
<div
|
<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"
|
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)}
|
onClick={() => setProfileDropdown(!profileDropdown)}
|
||||||
|
id="profile-dropdown"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCircleUser} className="h-5" />
|
<FontAwesomeIcon
|
||||||
<div className="flex items-center gap-1">
|
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>
|
<p className="font-bold">{user?.name}</p>
|
||||||
<FontAwesomeIcon icon={faChevronDown} className="h-3" />
|
<FontAwesomeIcon icon={faChevronDown} className="h-3" />
|
||||||
</div>
|
</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"
|
className="absolute top-12 left-0"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -33,12 +33,16 @@ export default async function (req: NextApiRequest, res: NextApiResponse) {
|
||||||
const decryptedPath = AES.decrypt(encryptedPath, AES_SECRET).toString(enc);
|
const decryptedPath = AES.decrypt(encryptedPath, AES_SECRET).toString(enc);
|
||||||
|
|
||||||
const filePath = path.join(process.cwd(), decryptedPath);
|
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"))
|
if (!fs.existsSync(filePath))
|
||||||
res.setHeader("Content-Type", "application/pdf");
|
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.send(file);
|
||||||
|
|
||||||
return res.status(200).send(file);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { getServerSession } from "next-auth/next";
|
||||||
import { authOptions } from "pages/api/auth/[...nextauth]";
|
import { authOptions } from "pages/api/auth/[...nextauth]";
|
||||||
import getLinks from "@/lib/api/controllers/links/getLinks";
|
import getLinks from "@/lib/api/controllers/links/getLinks";
|
||||||
import postLink from "@/lib/api/controllers/links/postLink";
|
import postLink from "@/lib/api/controllers/links/postLink";
|
||||||
|
import deleteLink from "@/lib/api/controllers/links/deleteLink";
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
response: object[] | string;
|
response: object[] | string;
|
||||||
|
@ -19,5 +20,6 @@ export default async function (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET") return await getLinks(req, res, session);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ type LinkStore = {
|
||||||
setLinks: () => void;
|
setLinks: () => void;
|
||||||
addLink: (linkName: NewLink) => Promise<boolean>;
|
addLink: (linkName: NewLink) => Promise<boolean>;
|
||||||
updateLink: (link: ExtendedLink) => void;
|
updateLink: (link: ExtendedLink) => void;
|
||||||
removeLink: (linkId: number) => void;
|
removeLink: (link: ExtendedLink) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useLinkStore = create<LinkStore>()((set) => ({
|
const useLinkStore = create<LinkStore>()((set) => ({
|
||||||
|
@ -40,10 +40,25 @@ const useLinkStore = create<LinkStore>()((set) => ({
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
links: state.links.map((c) => (c.id === link.id ? link : c)),
|
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) => ({
|
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;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
Ŝarĝante…
Reference in New Issue