From 8b7a660766ba3c8fa45d448d29e8b74f8200e5c2 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 29 Apr 2023 00:40:29 +0330 Subject: [PATCH] feat: added edit + delete collection functionality --- components/Modal/AddCollection.tsx | 4 +- components/Modal/AddLink.tsx | 4 +- components/Modal/DeleteCollection.tsx | 67 +++++++++++++++++++ components/Modal/EditCollection.tsx | 31 ++++----- components/Modal/EditLink.tsx | 4 +- .../collections/deleteCollection.ts | 48 +++++++++++++ .../collections/updateCollection.ts | 48 +++++++++++++ lib/api/controllers/tags/getTags.ts | 2 +- lib/api/controllers/users/getUsers.ts | 4 +- lib/client/getPublicUserDataByEmail.ts | 12 ++++ pages/api/routes/collections/index.ts | 8 +++ pages/api/routes/links/index.ts | 10 +-- pages/collections/[id].tsx | 34 +++++++--- store/collections.ts | 61 +++++++++++++---- 14 files changed, 285 insertions(+), 52 deletions(-) create mode 100644 components/Modal/DeleteCollection.tsx create mode 100644 lib/api/controllers/collections/deleteCollection.ts create mode 100644 lib/api/controllers/collections/updateCollection.ts create mode 100644 lib/client/getPublicUserDataByEmail.ts diff --git a/components/Modal/AddCollection.tsx b/components/Modal/AddCollection.tsx index 239a235..ef60264 100644 --- a/components/Modal/AddCollection.tsx +++ b/components/Modal/AddCollection.tsx @@ -27,7 +27,7 @@ export default function AddCollection({ toggleCollectionModal }: Props) { const session = useSession(); - const submitCollection = async () => { + const submit = async () => { console.log(newCollection); const response = await addCollection(newCollection as NewCollection); @@ -249,7 +249,7 @@ export default function AddCollection({ toggleCollectionModal }: Props) {
Add Collection diff --git a/components/Modal/AddLink.tsx b/components/Modal/AddLink.tsx index b5fce22..7e6941d 100644 --- a/components/Modal/AddLink.tsx +++ b/components/Modal/AddLink.tsx @@ -67,7 +67,7 @@ export default function AddLink({ toggleLinkModal }: Props) { }); }; - const submitLink = async () => { + const submit = async () => { console.log(newLink); const response = await addLink(newLink as NewLink); @@ -119,7 +119,7 @@ export default function AddLink({ toggleLinkModal }: Props) {
Add Link diff --git a/components/Modal/DeleteCollection.tsx b/components/Modal/DeleteCollection.tsx new file mode 100644 index 0000000..64483b7 --- /dev/null +++ b/components/Modal/DeleteCollection.tsx @@ -0,0 +1,67 @@ +// Copyright (C) 2022-present Daniel31x13 +// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with this program. If not, see . + +import React, { useState } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons"; +import { ExtendedCollection } from "@/types/global"; +import useCollectionStore from "@/store/collections"; +import { useRouter } from "next/router"; + +type Props = { + toggleCollectionModal: Function; + collection: ExtendedCollection; +}; + +export default function AddCollection({ + toggleCollectionModal, + collection, +}: Props) { + const [inputField, setInputField] = useState(""); + + const { removeCollection } = useCollectionStore(); + + const router = useRouter(); + + const submit = async () => { + const response = await removeCollection(collection.id); + if (response) { + toggleCollectionModal(); + router.push("/collections"); + } + }; + + return ( +
+

Delete Collection

+ +

+ To confirm, type " + {collection.name}" in + the box below: +

+ + setInputField(e.target.value)} + type="text" + placeholder={`Type "${collection.name}" Here.`} + className=" w-72 sm:w-96 rounded-md p-3 mx-auto border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100" + /> + +
+ + Delete Collection +
+
+ ); +} diff --git a/components/Modal/EditCollection.tsx b/components/Modal/EditCollection.tsx index 2049622..444ec7e 100644 --- a/components/Modal/EditCollection.tsx +++ b/components/Modal/EditCollection.tsx @@ -5,10 +5,11 @@ import React, { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faClose, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faClose, faPenToSquare } from "@fortawesome/free-solid-svg-icons"; import useCollectionStore from "@/store/collections"; -import { ExtendedCollection, NewCollection } from "@/types/global"; +import { ExtendedCollection } from "@/types/global"; import { useSession } from "next-auth/react"; +import getPublicUserDataByEmail from "@/lib/client/getPublicUserDataByEmail"; type Props = { toggleCollectionModal: Function; @@ -24,24 +25,18 @@ export default function EditCollection({ const [memberEmail, setMemberEmail] = useState(""); - // const { addCollection } = useCollectionStore(); + const { updateCollection } = useCollectionStore(); const session = useSession(); - const submitCollection = async () => { + const submit = async () => { console.log(activeCollection); - // const response = await addCollection(newCollection as NewCollection); + const response = await updateCollection( + activeCollection as ExtendedCollection + ); - // if (response) toggleCollectionModal(); - }; - - const getUserByEmail = async (email: string) => { - const response = await fetch(`/api/routes/users?email=${email}`); - - const data = await response.json(); - - return data.response; + if (response) toggleCollectionModal(); }; return ( @@ -103,7 +98,7 @@ export default function EditCollection({ memberEmail.trim() !== ownerEmail ) { // Lookup, get data/err, list ... - const user = await getUserByEmail(memberEmail.trim()); + const user = await getPublicUserDataByEmail(memberEmail.trim()); if (user.email) { const newMember = { @@ -257,10 +252,10 @@ export default function EditCollection({
- - Add Collection + + Edit Collection
); diff --git a/components/Modal/EditLink.tsx b/components/Modal/EditLink.tsx index fe9027d..a59d289 100644 --- a/components/Modal/EditLink.tsx +++ b/components/Modal/EditLink.tsx @@ -40,7 +40,7 @@ export default function EditLink({ toggleLinkModal, link }: Props) { }); }; - const submitLink = async () => { + const submit = async () => { updateLink(currentLink); toggleLinkModal(); }; @@ -87,7 +87,7 @@ export default function EditLink({ toggleLinkModal, link }: Props) {
Edit Link diff --git a/lib/api/controllers/collections/deleteCollection.ts b/lib/api/controllers/collections/deleteCollection.ts new file mode 100644 index 0000000..97d20e6 --- /dev/null +++ b/lib/api/controllers/collections/deleteCollection.ts @@ -0,0 +1,48 @@ +// Copyright (C) 2022-present Daniel31x13 +// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with this program. If not, see . + +import { prisma } from "@/lib/api/db"; +import getPermission from "@/lib/api/getPermission"; +import fs from "fs"; + +export default async function (collection: { id: number }, userId: number) { + console.log(collection.id); + + if (!collection.id) + return { response: "Please choose a valid collection.", status: 401 }; + + const collectionIsAccessible = await getPermission(userId, collection.id); + + if (!(collectionIsAccessible?.ownerId === userId)) + return { response: "Collection is not accessible.", status: 401 }; + + const deletedCollection = await prisma.$transaction(async () => { + await prisma.usersAndCollections.deleteMany({ + where: { + collection: { + id: collection.id, + }, + }, + }); + + await prisma.link.deleteMany({ + where: { + collection: { + id: collection.id, + }, + }, + }); + + fs.rmdirSync(`data/archives/${collection.id}`, { recursive: true }); + + return await prisma.collection.delete({ + where: { + id: collection.id, + }, + }); + }); + + return { response: deletedCollection, status: 200 }; +} diff --git a/lib/api/controllers/collections/updateCollection.ts b/lib/api/controllers/collections/updateCollection.ts new file mode 100644 index 0000000..4f8519a --- /dev/null +++ b/lib/api/controllers/collections/updateCollection.ts @@ -0,0 +1,48 @@ +// Copyright (C) 2022-present Daniel31x13 +// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with this program. If not, see . + +import { prisma } from "@/lib/api/db"; +import { ExtendedCollection } from "@/types/global"; +import getPermission from "@/lib/api/getPermission"; + +export default async function (collection: ExtendedCollection, userId: number) { + if (!collection) + return { response: "Please choose a valid collection.", status: 401 }; + + const collectionIsAccessible = await getPermission(userId, collection.id); + + if (!(collectionIsAccessible?.ownerId === userId)) + return { response: "Collection is not accessible.", status: 401 }; + + const updatedCollection = await prisma.$transaction(async () => { + await prisma.usersAndCollections.deleteMany({ + where: { + collection: { + id: collection.id, + }, + }, + }); + + return await prisma.collection.update({ + where: { + id: collection.id, + }, + data: { + name: collection.name, + description: collection.description, + members: { + create: collection.members.map((e) => ({ + user: { connect: { email: e.user.email } }, + canCreate: e.canCreate, + canUpdate: e.canUpdate, + canDelete: e.canDelete, + })), + }, + }, + }); + }); + + return { response: updatedCollection, status: 200 }; +} diff --git a/lib/api/controllers/tags/getTags.ts b/lib/api/controllers/tags/getTags.ts index 3ca0183..7e73dea 100644 --- a/lib/api/controllers/tags/getTags.ts +++ b/lib/api/controllers/tags/getTags.ts @@ -6,7 +6,7 @@ import { prisma } from "@/lib/api/db"; export default async function (userId: number) { - // tag cleanup + // remove empty tags await prisma.tag.deleteMany({ where: { links: { diff --git a/lib/api/controllers/users/getUsers.ts b/lib/api/controllers/users/getUsers.ts index 1da4aee..ae119e7 100644 --- a/lib/api/controllers/users/getUsers.ts +++ b/lib/api/controllers/users/getUsers.ts @@ -19,5 +19,7 @@ export default async function (email: string) { createdAt: user?.createdAt, }; - return { response: unsensitiveUserInfo || null, status: 200 }; + const statusCode = user?.id ? 200 : 404; + + return { response: unsensitiveUserInfo || null, status: statusCode }; } diff --git a/lib/client/getPublicUserDataByEmail.ts b/lib/client/getPublicUserDataByEmail.ts new file mode 100644 index 0000000..a91fa07 --- /dev/null +++ b/lib/client/getPublicUserDataByEmail.ts @@ -0,0 +1,12 @@ +// Copyright (C) 2022-present Daniel31x13 +// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with this program. If not, see . + +export default async function (email: string) { + const response = await fetch(`/api/routes/users?email=${email}`); + + const data = await response.json(); + + return data.response; +} diff --git a/pages/api/routes/collections/index.ts b/pages/api/routes/collections/index.ts index e4ba78d..096e7e7 100644 --- a/pages/api/routes/collections/index.ts +++ b/pages/api/routes/collections/index.ts @@ -8,6 +8,8 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "pages/api/auth/[...nextauth]"; import getCollections from "@/lib/api/controllers/collections/getCollections"; import postCollection from "@/lib/api/controllers/collections/postCollection"; +import updateCollection from "@/lib/api/controllers/collections/updateCollection"; +import deleteCollection from "@/lib/api/controllers/collections/deleteCollection"; export default async function (req: NextApiRequest, res: NextApiResponse) { const session = await getServerSession(req, res, authOptions); @@ -26,5 +28,11 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { return res .status(newCollection.status) .json({ response: newCollection.response }); + } else if (req.method === "PUT") { + const updated = await updateCollection(req.body, session.user.id); + return res.status(updated.status).json({ response: updated.response }); + } else if (req.method === "DELETE") { + const deleted = await deleteCollection(req.body, session.user.id); + return res.status(deleted.status).json({ response: deleted.response }); } } diff --git a/pages/api/routes/links/index.ts b/pages/api/routes/links/index.ts index 811c545..979b308 100644 --- a/pages/api/routes/links/index.ts +++ b/pages/api/routes/links/index.ts @@ -26,15 +26,15 @@ export default async function (req: NextApiRequest, res: NextApiResponse) { return res.status(newlink.status).json({ response: newlink.response, }); - } else if (req.method === "DELETE") { - const deleted = await deleteLink(req.body, session.user.id); - return res.status(deleted.status).json({ - response: deleted.response, - }); } else if (req.method === "PUT") { const updated = await updateLink(req.body, session.user.id); return res.status(updated.status).json({ response: updated.response, }); + } else if (req.method === "DELETE") { + const deleted = await deleteLink(req.body, session.user.id); + return res.status(deleted.status).json({ + response: deleted.response, + }); } } diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index d6c5653..1376e70 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -8,6 +8,7 @@ import LinkList from "@/components/LinkList"; import Modal from "@/components/Modal"; import AddLink from "@/components/Modal/AddLink"; import EditCollection from "@/components/Modal/EditCollection"; +import DeleteCollection from "@/components/Modal/DeleteCollection"; import useCollectionStore from "@/store/collections"; import useLinkStore from "@/store/links"; import { ExtendedCollection, ExtendedLink } from "@/types/global"; @@ -19,7 +20,6 @@ import { faTrashCan, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Collection } from "@prisma/client"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -31,7 +31,8 @@ export default function () { const [expandDropdown, setExpandDropdown] = useState(false); const [linkModal, setLinkModal] = useState(false); - const [collectionModal, setCollectionModal] = useState(false); + const [editCollectionModal, setEditCollectionModal] = useState(false); + const [deleteCollectionModal, setDeleteCollectionModal] = useState(false); const [activeCollection, setActiveCollection] = useState(); const [linksByCollection, setLinksByCollection] = @@ -41,8 +42,12 @@ export default function () { setLinkModal(!linkModal); }; - const toggleCollectionModal = () => { - setCollectionModal(!collectionModal); + const toggleEditCollectionModal = () => { + setEditCollectionModal(!editCollectionModal); + }; + + const toggleDeleteCollectionModal = () => { + setDeleteCollectionModal(!deleteCollectionModal); }; useEffect(() => { @@ -90,13 +95,17 @@ export default function () { name: "Edit Collection", icon: , onClick: () => { - toggleCollectionModal(); + toggleEditCollectionModal(); setExpandDropdown(false); }, }, { name: "Delete Collection", icon: , + onClick: () => { + toggleDeleteCollectionModal(); + setExpandDropdown(false); + }, }, ]} onClickOutside={(e: Event) => { @@ -113,14 +122,23 @@ export default function () { ) : null} - {collectionModal && activeCollection ? ( - + {editCollectionModal && activeCollection ? ( + ) : null} + + {deleteCollectionModal && activeCollection ? ( + + + + ) : null}
{linksByCollection.map((e, i) => { diff --git a/store/collections.ts b/store/collections.ts index ee39c9c..c180e6d 100644 --- a/store/collections.ts +++ b/store/collections.ts @@ -4,15 +4,15 @@ // You should have received a copy of the GNU General Public License along with this program. If not, see . import { create } from "zustand"; -import { Collection } from "@prisma/client"; import { ExtendedCollection, NewCollection } from "@/types/global"; +import useTagStore from "./tags"; type CollectionStore = { collections: ExtendedCollection[]; setCollections: () => void; addCollection: (body: NewCollection) => Promise; - // updateCollection: (collection: Collection) => void; - removeCollection: (collectionId: number) => void; + updateCollection: (collection: ExtendedCollection) => Promise; + removeCollection: (collectionId: number) => Promise; }; const useCollectionStore = create()((set) => ({ @@ -44,16 +44,51 @@ const useCollectionStore = create()((set) => ({ return response.ok; }, - // updateCollection: (collection) => - // set((state) => ({ - // collections: state.collections.map((c) => - // c.id === collection.id ? collection : c - // ), - // })), - removeCollection: (collectionId) => { - set((state) => ({ - collections: state.collections.filter((c) => c.id !== collectionId), - })); + updateCollection: async (collection) => { + const response = await fetch("/api/routes/collections", { + body: JSON.stringify(collection), + headers: { + "Content-Type": "application/json", + }, + method: "PUT", + }); + + const data = await response.json(); + + console.log(data); + + if (response.ok) + set((state) => ({ + collections: state.collections.map((e) => + e.id === collection.id ? collection : e + ), + })); + + return response.ok; + }, + removeCollection: async (id) => { + const response = await fetch("/api/routes/collections", { + body: JSON.stringify({ id }), + headers: { + "Content-Type": "application/json", + }, + method: "DELETE", + }); + + console.log(id); + + const data = await response.json(); + + console.log(data); + + if (response.ok) { + set((state) => ({ + collections: state.collections.filter((e) => e.id !== id), + })); + useTagStore.getState().setTags(); + } + + return response.ok; }, }));