diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index f3d4522..7e685ce 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -7,6 +7,7 @@ import { useState } from "react"; import ProfilePhoto from "./ProfilePhoto"; import { faCalendarDays } from "@fortawesome/free-regular-svg-icons"; import useModalStore from "@/store/modals"; +import usePermissions from "@/hooks/usePermissions"; type Props = { collection: CollectionIncludingMembersAndLinkCount; @@ -27,6 +28,8 @@ export default function CollectionCard({ collection, className }: Props) { const [expandDropdown, setExpandDropdown] = useState(false); + const permissions = usePermissions(collection.id as number); + return (
); }) @@ -72,7 +75,7 @@ export default function CollectionCard({ collection, className }: Props) {
- {collection._count.links} + {collection._count && collection._count.links}
@@ -91,6 +94,7 @@ export default function CollectionCard({ collection, className }: Props) { modal: "COLLECTION", state: true, method: "UPDATE", + isOwner: permissions === true, active: collection, }); setExpandDropdown(false); @@ -103,6 +107,7 @@ export default function CollectionCard({ collection, className }: Props) { modal: "COLLECTION", state: true, method: "UPDATE", + isOwner: permissions === true, active: collection, defaultIndex: 1, }); @@ -116,6 +121,7 @@ export default function CollectionCard({ collection, className }: Props) { modal: "COLLECTION", state: true, method: "UPDATE", + isOwner: permissions === true, active: collection, defaultIndex: 2, }); diff --git a/components/LinkCard.tsx b/components/LinkCard.tsx index c7cd323..ed3e65f 100644 --- a/components/LinkCard.tsx +++ b/components/LinkCard.tsx @@ -1,7 +1,6 @@ import { CollectionIncludingMembersAndLinkCount, LinkIncludingShortenedCollectionAndTags, - Member, } from "@/types/global"; import { faFolder, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -61,7 +60,7 @@ export default function LinkCard({ link, count, className }: Props) { return (
{permissions && (
{ - if (!collection.id || collection.name !== inputField) return null; + if (permissions === true) if (collection.name !== inputField) return null; - const response = await removeCollection(collection.id); + const response = await removeCollection(collection.id as number); if (response) { toggleDeleteCollectionModal(); router.push("/collections"); } }; + const permissions = usePermissions(collection.id as number); + return (
-

Warning!

+ {permissions === true ? ( + <> +

Warning!

-
-
-

- Please note that deleting the collection will permanently remove all - its contents, including the following: -

-
-
  • - Links: All links within the collection will be permanently - deleted. -
  • -
  • - Tags: All tags associated with the collection will be removed. -
  • -
  • - Screenshots/PDFs: Any screenshots or PDFs attached to links within - this collection will be permanently deleted. -
  • -
  • - Members: Any members who have been granted access to the - collection will lose their permissions and no longer be able to - view or interact with the content. -
  • +
    +
    +

    + Please note that deleting the collection will permanently remove + all its contents, including the following: +

    +
    +
  • + Links: All links within the collection will be permanently + deleted. +
  • +
  • + Tags: All tags associated with the collection will be removed. +
  • +
  • + Screenshots/PDFs: Any screenshots or PDFs attached to links + within this collection will be permanently deleted. +
  • +
  • + Members: Any members who have been granted access to the + collection will lose their permissions and no longer be able + to view or interact with the content. +
  • +
    +

    + Please double-check that you have backed up any essential data + and have informed the relevant members about this action. +

    +
    -

    - Please double-check that you have backed up any essential data and - have informed the relevant members about this action. -

    -
    -
    -
    -

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

    +

    + 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 outline-none focus:border-sky-500 duration-100" + /> +
    + + ) : ( +

    + Click the button below to leave the current collection:

    - - 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 outline-none focus:border-sky-500 duration-100" - /> -
    + )}
    - - Delete Collection + + {permissions === true ? "Delete" : "Leave"} Collection
    ); diff --git a/components/Modal/Collection/TeamManagement.tsx b/components/Modal/Collection/TeamManagement.tsx index 7fa79d5..02a3efd 100644 --- a/components/Modal/Collection/TeamManagement.tsx +++ b/components/Modal/Collection/TeamManagement.tsx @@ -13,6 +13,7 @@ import addMemberToCollection from "@/lib/client/addMemberToCollection"; import Checkbox from "../../Checkbox"; import SubmitButton from "@/components/SubmitButton"; import ProfilePhoto from "@/components/ProfilePhoto"; +import usePermissions from "@/hooks/usePermissions"; type Props = { toggleCollectionModal: Function; @@ -29,6 +30,8 @@ export default function TeamManagement({ collection, method, }: Props) { + const permissions = usePermissions(collection.id as number); + const currentURL = new URL(document.URL); const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`; @@ -80,19 +83,23 @@ export default function TeamManagement({ return (
    -

    Make Public

    + {permissions === true && ( + <> +

    Make Public

    - - setCollection({ ...collection, isPublic: !collection.isPublic }) - } - /> + + setCollection({ ...collection, isPublic: !collection.isPublic }) + } + /> -

    - This will let Anyone to view this collection. -

    +

    + This will let Anyone to view this collection. +

    + + )} {collection.isPublic ? (
    @@ -116,54 +123,58 @@ export default function TeamManagement({
    ) : null} -
    + {permissions !== true && collection.isPublic &&
    } -

    Member Management

    + {permissions === true && ( + <> +

    Member Management

    -
    - { - setMember({ - ...member, - user: { ...member.user, email: e.target.value }, - }); - }} - onKeyDown={(e) => - e.key === "Enter" && - addMemberToCollection( - session.data?.user.email as string, - member.user.email, - collection, - setMemberState - ) - } - type="text" - placeholder="Email" - className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" - /> +
    + { + setMember({ + ...member, + user: { ...member.user, email: e.target.value }, + }); + }} + onKeyDown={(e) => + e.key === "Enter" && + addMemberToCollection( + session.data?.user.email as string, + member.user.email, + collection, + setMemberState + ) + } + type="text" + placeholder="Email" + className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" + /> -
    - addMemberToCollection( - session.data?.user.email as string, - member.user.email, - collection, - setMemberState - ) - } - className="flex items-center justify-center bg-sky-500 hover:bg-sky-400 duration-100 text-white w-12 h-12 p-3 rounded-md cursor-pointer" - > - -
    -
    +
    + addMemberToCollection( + session.data?.user.email as string, + member.user.email, + collection, + setMemberState + ) + } + className="flex items-center justify-center bg-sky-500 hover:bg-sky-400 duration-100 text-white w-12 h-12 p-3 rounded-md cursor-pointer" + > + +
    +
    + + )} {collection?.members[0]?.user && ( <>

    (All Members have Read access to this collection.)

    -
    +
    {collection.members .sort((a, b) => (a.userId as number) - (b.userId as number)) .map((e, i) => { @@ -172,24 +183,29 @@ export default function TeamManagement({ key={i} className="relative border p-2 rounded-md border-sky-100 flex flex-col sm:flex-row sm:items-center gap-2 justify-between" > - { - const updatedMembers = collection.members.filter( - (member) => { - return member.user.email !== e.user.email; - } - ); - setCollection({ - ...collection, - members: updatedMembers, - }); - }} - /> + {permissions === true && ( + { + const updatedMembers = collection.members.filter( + (member) => { + return member.user.email !== e.user.email; + } + ); + setCollection({ + ...collection, + members: updatedMembers, + }); + }} + /> + )}
    - +

    {e.user.name} @@ -197,104 +213,161 @@ export default function TeamManagement({

    {e.user.email}

    -
    +
    -

    +

    Permissions

    -

    - (Click to toggle.) + {permissions === true && ( +

    + (Click to toggle.) +

    + )} +
    + + {permissions !== true && + !e.canCreate && + !e.canUpdate && + !e.canDelete ? ( +

    + Has no permissions.

    -
    - -
    - +
    + )}
    ); @@ -303,12 +376,14 @@ export default function TeamManagement({ )} - + {permissions === true && ( + + )}
    ); } diff --git a/components/Modal/Collection/index.tsx b/components/Modal/Collection/index.tsx index 10e38df..e3d5f8a 100644 --- a/components/Modal/Collection/index.tsx +++ b/components/Modal/Collection/index.tsx @@ -10,6 +10,7 @@ type Props = toggleCollectionModal: Function; activeCollection: CollectionIncludingMembersAndLinkCount; method: "UPDATE"; + isOwner: boolean; className?: string; defaultIndex?: number; } @@ -17,6 +18,7 @@ type Props = toggleCollectionModal: Function; activeCollection?: CollectionIncludingMembersAndLinkCount; method: "CREATE"; + isOwner: boolean; className?: string; defaultIndex?: number; }; @@ -25,6 +27,7 @@ export default function CollectionModal({ className, defaultIndex, toggleCollectionModal, + isOwner, activeCollection, method, }: Props) { @@ -48,6 +51,17 @@ export default function CollectionModal({ {method === "UPDATE" && ( <> + {isOwner && ( + + selected + ? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" + : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" + } + > + Collection Info + + )} selected @@ -55,7 +69,7 @@ export default function CollectionModal({ : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" } > - Collection Info + {isOwner ? "Share & Collaborate" : "View Team"} @@ -64,29 +78,22 @@ export default function CollectionModal({ : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" } > - Share & Collaborate - - - selected - ? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" - : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" - } - > - Delete Collection + {isOwner ? "Delete Collection" : "Leave Collection"} )} - - - + {(isOwner || method === "CREATE") && ( + + + + )} {method === "UPDATE" && ( <> diff --git a/components/Modal/User/ProfileSettings.tsx b/components/Modal/User/ProfileSettings.tsx index 030a4a6..3aec628 100644 --- a/components/Modal/User/ProfileSettings.tsx +++ b/components/Modal/User/ProfileSettings.tsx @@ -77,10 +77,10 @@ export default function ProfileSettings({

    Profile Photo

    -
    +
    {profileStatus && ( diff --git a/components/ModalManagement.tsx b/components/ModalManagement.tsx index 9c8b772..b8953f8 100644 --- a/components/ModalManagement.tsx +++ b/components/ModalManagement.tsx @@ -40,6 +40,7 @@ export default function ModalManagement() {

    @@ -43,7 +43,7 @@ export default function ProfilePhoto({ src={src} height={112} width={112} - className={`h-10 w-10 shadow rounded-full aspect-square border-[3px] border-slate-200 ${className}`} + className={`h-10 w-10 shadow rounded-full aspect-square border border-slate-200 ${className}`} /> ); } diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index a2fb4e4..746897b 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -1,7 +1,7 @@ import useAccountStore from "@/store/account"; import useCollectionStore from "@/store/collections"; -import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; -import React, { useEffect, useState } from "react"; +import { Member } from "@/types/global"; +import { useEffect, useState } from "react"; export default function usePermissions(collectionId: number) { const { collections } = useCollectionStore(); @@ -26,7 +26,7 @@ export default function usePermissions(collectionId: number) { setPermissions(account.id === collection.ownerId || getPermission); } - }, [collections]); + }, [account, collections, collectionId]); return permissions; } diff --git a/lib/api/controllers/collections/deleteCollection.ts b/lib/api/controllers/collections/deleteCollection.ts index 7fcd2d2..d45308a 100644 --- a/lib/api/controllers/collections/deleteCollection.ts +++ b/lib/api/controllers/collections/deleteCollection.ts @@ -1,24 +1,45 @@ import { prisma } from "@/lib/api/db"; import getPermission from "@/lib/api/getPermission"; +import { UsersAndCollections } from "@prisma/client"; import fs from "fs"; export default async function deleteCollection( collection: { id: number }, userId: number ) { - if (!collection.id) + const collectionId = collection.id; + + if (!collectionId) return { response: "Please choose a valid collection.", status: 401 }; - const collectionIsAccessible = await getPermission(userId, collection.id); + const collectionIsAccessible = await getPermission(userId, collectionId); - if (!(collectionIsAccessible?.ownerId === userId)) + const memberHasAccess = collectionIsAccessible?.members.some( + (e: UsersAndCollections) => e.userId === userId + ); + + if (collectionIsAccessible?.ownerId !== userId && memberHasAccess) { + // Remove relation/Leave collection + const deletedUsersAndCollectionsRelation = + await prisma.usersAndCollections.delete({ + where: { + userId_collectionId: { + userId: userId, + collectionId: collectionId, + }, + }, + }); + + return { response: deletedUsersAndCollectionsRelation, status: 200 }; + } else 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, + id: collectionId, }, }, }); @@ -26,13 +47,13 @@ export default async function deleteCollection( await prisma.link.deleteMany({ where: { collection: { - id: collection.id, + id: collectionId, }, }, }); try { - fs.rmdirSync(`data/archives/${collection.id}`, { recursive: true }); + fs.rmdirSync(`data/archives/${collectionId}`, { recursive: true }); } catch (error) { console.log( "Collection's archive directory wasn't deleted most likely because it didn't exist..." @@ -41,7 +62,7 @@ export default async function deleteCollection( return await prisma.collection.delete({ where: { - id: collection.id, + id: collectionId, }, }); }); diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index 16a29aa..72b338a 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -17,6 +17,7 @@ import ProfilePhoto from "@/components/ProfilePhoto"; import SortDropdown from "@/components/SortDropdown"; import useModalStore from "@/store/modals"; import useLinks from "@/hooks/useLinks"; +import usePermissions from "@/hooks/usePermissions"; export default function Index() { const { setModal } = useModalStore(); @@ -35,6 +36,8 @@ export default function Index() { const [activeCollection, setActiveCollection] = useState(); + const permissions = usePermissions(activeCollection?.id as number); + useLinks({ collectionId: Number(router.query.id), sort: sortBy }); useEffect(() => { @@ -71,13 +74,13 @@ export default function Index() { >
    - activeCollection && setModal({ modal: "COLLECTION", state: true, method: "UPDATE", + isOwner: permissions === true, active: activeCollection, - defaultIndex: 1, + defaultIndex: permissions === true ? 1 : 0, }) } className="flex justify-center sm:justify-end items-center w-fit mx-auto sm:mr-0 sm:ml-auto group cursor-pointer" @@ -87,10 +90,7 @@ export default function Index() { activeCollection.members[0] && "mr-1" }`} > - {activeCollection.ownerId === data?.user.id - ? "Manage" - : "View"}{" "} - Team + {permissions === true ? "Manage" : "View"} Team
    {activeCollection?.members .sort((a, b) => (a.userId as number) - (b.userId as number)) @@ -99,7 +99,7 @@ export default function Index() { ); }) @@ -155,54 +155,68 @@ export default function Index() { {expandDropdown ? ( { + setModal({ + modal: "LINK", + state: true, + method: "CREATE", + }); + setExpandDropdown(false); + }, + } + : undefined, + permissions === true + ? { + name: "Edit Collection Info", + onClick: () => { + activeCollection && + setModal({ + modal: "COLLECTION", + state: true, + method: "UPDATE", + isOwner: permissions === true, + active: activeCollection, + }); + setExpandDropdown(false); + }, + } + : undefined, { - name: "Add Link Here", - onClick: () => { - setModal({ - modal: "LINK", - state: true, - method: "CREATE", - }); - setExpandDropdown(false); - }, - }, - { - name: "Edit Collection Info", - onClick: () => { - activeCollection && - setModal({ - modal: "COLLECTION", - state: true, - method: "UPDATE", - active: activeCollection, - }); - setExpandDropdown(false); - }, - }, - { - name: "Share/Collaborate", + name: + permissions === true + ? "Share/Collaborate" + : "View Team", onClick: () => { activeCollection && setModal({ modal: "COLLECTION", state: true, method: "UPDATE", + isOwner: permissions === true, active: activeCollection, defaultIndex: 1, }); setExpandDropdown(false); }, }, + { - name: "Delete Collection", + name: + permissions === true + ? "Delete Collection" + : "Leave Collection", onClick: () => { activeCollection && setModal({ modal: "COLLECTION", state: true, method: "UPDATE", + isOwner: permissions === true, active: activeCollection, - defaultIndex: 2, + defaultIndex: permissions === true ? 2 : 1, }); setExpandDropdown(false); }, diff --git a/store/modals.ts b/store/modals.ts index 31f35ff..68e3203 100644 --- a/store/modals.ts +++ b/store/modals.ts @@ -30,6 +30,7 @@ type Modal = modal: "COLLECTION"; state: boolean; method: "UPDATE"; + isOwner: boolean; active: CollectionIncludingMembersAndLinkCount; defaultIndex?: number; } @@ -37,6 +38,7 @@ type Modal = modal: "COLLECTION"; state: boolean; method: "CREATE"; + isOwner: boolean; active?: CollectionIncludingMembersAndLinkCount; defaultIndex?: number; }