managed how collections are viewed by members

This commit is contained in:
Daniel 2023-06-22 18:05:02 +03:30
parent 51c5615fea
commit 3abea1d1b7
13 changed files with 432 additions and 288 deletions

View File

@ -7,6 +7,7 @@ import { useState } from "react";
import ProfilePhoto from "./ProfilePhoto"; import ProfilePhoto from "./ProfilePhoto";
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons"; import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
import usePermissions from "@/hooks/usePermissions";
type Props = { type Props = {
collection: CollectionIncludingMembersAndLinkCount; collection: CollectionIncludingMembersAndLinkCount;
@ -27,6 +28,8 @@ export default function CollectionCard({ collection, className }: Props) {
const [expandDropdown, setExpandDropdown] = useState(false); const [expandDropdown, setExpandDropdown] = useState(false);
const permissions = usePermissions(collection.id as number);
return ( return (
<div <div
className={`bg-gradient-to-tr from-sky-100 from-10% via-gray-100 via-20% to-white to-100% self-stretch min-h-[12rem] rounded-2xl shadow duration-100 hover:shadow-none group relative ${className}`} className={`bg-gradient-to-tr from-sky-100 from-10% via-gray-100 via-20% to-white to-100% self-stretch min-h-[12rem] rounded-2xl shadow duration-100 hover:shadow-none group relative ${className}`}
@ -58,7 +61,7 @@ export default function CollectionCard({ collection, className }: Props) {
<ProfilePhoto <ProfilePhoto
key={i} key={i}
src={`/api/avatar/${e.userId}`} src={`/api/avatar/${e.userId}`}
className="-mr-3" className="-mr-3 border-[3px]"
/> />
); );
}) })
@ -72,7 +75,7 @@ export default function CollectionCard({ collection, className }: Props) {
<div className="text-right w-40"> <div className="text-right w-40">
<div className="text-sky-500 font-bold text-sm flex justify-end gap-1 items-center"> <div className="text-sky-500 font-bold text-sm flex justify-end gap-1 items-center">
<FontAwesomeIcon icon={faLink} className="w-5 h-5 text-sky-600" /> <FontAwesomeIcon icon={faLink} className="w-5 h-5 text-sky-600" />
{collection._count.links} {collection._count && collection._count.links}
</div> </div>
<div className="flex items-center justify-end gap-1 text-gray-600"> <div className="flex items-center justify-end gap-1 text-gray-600">
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" /> <FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
@ -91,6 +94,7 @@ export default function CollectionCard({ collection, className }: Props) {
modal: "COLLECTION", modal: "COLLECTION",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true,
active: collection, active: collection,
}); });
setExpandDropdown(false); setExpandDropdown(false);
@ -103,6 +107,7 @@ export default function CollectionCard({ collection, className }: Props) {
modal: "COLLECTION", modal: "COLLECTION",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true,
active: collection, active: collection,
defaultIndex: 1, defaultIndex: 1,
}); });
@ -116,6 +121,7 @@ export default function CollectionCard({ collection, className }: Props) {
modal: "COLLECTION", modal: "COLLECTION",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true,
active: collection, active: collection,
defaultIndex: 2, defaultIndex: 2,
}); });

View File

@ -1,7 +1,6 @@
import { import {
CollectionIncludingMembersAndLinkCount, CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags, LinkIncludingShortenedCollectionAndTags,
Member,
} from "@/types/global"; } from "@/types/global";
import { faFolder, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { faFolder, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -61,7 +60,7 @@ export default function LinkCard({ link, count, className }: Props) {
return ( return (
<div <div
className={`bg-gradient-to-tr from-slate-200 from-10% to-gray-50 via-20% shadow hover:shadow-none cursor-pointer duration-100 p-5 rounded-2xl relative group ${className}`} className={`bg-gradient-to-tr from-slate-200 from-10% to-gray-50 via-20% shadow hover:shadow-none cursor-pointer duration-100 rounded-2xl relative group ${className}`}
> >
{permissions && ( {permissions && (
<div <div
@ -87,7 +86,7 @@ export default function LinkCard({ link, count, className }: Props) {
active: link, active: link,
}); });
}} }}
className="flex items-start gap-5 sm:gap-10 h-full w-full" className="flex items-start gap-5 sm:gap-10 h-full w-full p-5"
> >
<Image <Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`} src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}

View File

@ -1,9 +1,13 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-solid-svg-icons"; import {
faRightFromBracket,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import usePermissions from "@/hooks/usePermissions";
type Props = { type Props = {
toggleDeleteCollectionModal: Function; toggleDeleteCollectionModal: Function;
@ -21,24 +25,28 @@ export default function DeleteCollection({
const router = useRouter(); const router = useRouter();
const submit = async () => { const submit = async () => {
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) { if (response) {
toggleDeleteCollectionModal(); toggleDeleteCollectionModal();
router.push("/collections"); router.push("/collections");
} }
}; };
const permissions = usePermissions(collection.id as number);
return ( return (
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80"> <div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
{permissions === true ? (
<>
<p className="text-red-500 font-bold text-center">Warning!</p> <p className="text-red-500 font-bold text-center">Warning!</p>
<div className="max-h-[20rem] overflow-y-auto"> <div className="max-h-[20rem] overflow-y-auto">
<div className="text-gray-500"> <div className="text-gray-500">
<p> <p>
Please note that deleting the collection will permanently remove all Please note that deleting the collection will permanently remove
its contents, including the following: all its contents, including the following:
</p> </p>
<div className="p-3"> <div className="p-3">
<li className="list-inside"> <li className="list-inside">
@ -49,18 +57,18 @@ export default function DeleteCollection({
Tags: All tags associated with the collection will be removed. Tags: All tags associated with the collection will be removed.
</li> </li>
<li className="list-inside"> <li className="list-inside">
Screenshots/PDFs: Any screenshots or PDFs attached to links within Screenshots/PDFs: Any screenshots or PDFs attached to links
this collection will be permanently deleted. within this collection will be permanently deleted.
</li> </li>
<li className="list-inside"> <li className="list-inside">
Members: Any members who have been granted access to the Members: Any members who have been granted access to the
collection will lose their permissions and no longer be able to collection will lose their permissions and no longer be able
view or interact with the content. to view or interact with the content.
</li> </li>
</div> </div>
<p> <p>
Please double-check that you have backed up any essential data and Please double-check that you have backed up any essential data
have informed the relevant members about this action. and have informed the relevant members about this action.
</p> </p>
</div> </div>
</div> </div>
@ -81,17 +89,28 @@ export default function DeleteCollection({
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" 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"
/> />
</div> </div>
</>
) : (
<p className="text-gray-500">
Click the button below to leave the current collection:
</p>
)}
<div <div
className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${ className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${
inputField === collection.name permissions === true
? inputField === collection.name
? "bg-red-500 hover:bg-red-400 cursor-pointer" ? "bg-red-500 hover:bg-red-400 cursor-pointer"
: "cursor-not-allowed bg-red-300" : "cursor-not-allowed bg-red-300"
: "bg-red-500 hover:bg-red-400 cursor-pointer"
}`} }`}
onClick={submit} onClick={submit}
> >
<FontAwesomeIcon icon={faTrashCan} className="h-5" /> <FontAwesomeIcon
Delete Collection icon={permissions === true ? faTrashCan : faRightFromBracket}
className="h-5"
/>
{permissions === true ? "Delete" : "Leave"} Collection
</div> </div>
</div> </div>
); );

View File

@ -13,6 +13,7 @@ import addMemberToCollection from "@/lib/client/addMemberToCollection";
import Checkbox from "../../Checkbox"; import Checkbox from "../../Checkbox";
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import ProfilePhoto from "@/components/ProfilePhoto"; import ProfilePhoto from "@/components/ProfilePhoto";
import usePermissions from "@/hooks/usePermissions";
type Props = { type Props = {
toggleCollectionModal: Function; toggleCollectionModal: Function;
@ -29,6 +30,8 @@ export default function TeamManagement({
collection, collection,
method, method,
}: Props) { }: Props) {
const permissions = usePermissions(collection.id as number);
const currentURL = new URL(document.URL); const currentURL = new URL(document.URL);
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`; const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
@ -80,6 +83,8 @@ export default function TeamManagement({
return ( return (
<div className="flex flex-col gap-3 sm:w-[35rem] w-80"> <div className="flex flex-col gap-3 sm:w-[35rem] w-80">
{permissions === true && (
<>
<p className="text-sm text-sky-500">Make Public</p> <p className="text-sm text-sky-500">Make Public</p>
<Checkbox <Checkbox
@ -93,6 +98,8 @@ export default function TeamManagement({
<p className="text-gray-500 text-sm"> <p className="text-gray-500 text-sm">
This will let <b>Anyone</b> to view this collection. This will let <b>Anyone</b> to view this collection.
</p> </p>
</>
)}
{collection.isPublic ? ( {collection.isPublic ? (
<div> <div>
@ -116,8 +123,10 @@ export default function TeamManagement({
</div> </div>
) : null} ) : null}
<hr /> {permissions !== true && collection.isPublic && <hr />}
{permissions === true && (
<>
<p className="text-sm text-sky-500">Member Management</p> <p className="text-sm text-sky-500">Member Management</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -157,13 +166,15 @@ export default function TeamManagement({
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" /> <FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
</div> </div>
</div> </div>
</>
)}
{collection?.members[0]?.user && ( {collection?.members[0]?.user && (
<> <>
<p className="text-center text-gray-500 text-xs sm:text-sm"> <p className="text-center text-gray-500 text-xs sm:text-sm">
(All Members have <b>Read</b> access to this collection.) (All Members have <b>Read</b> access to this collection.)
</p> </p>
<div className="max-h-[20rem] overflow-auto flex flex-col gap-3 rounded-md shadow-inner"> <div className="max-h-[20rem] overflow-auto flex flex-col gap-3 rounded-md">
{collection.members {collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number)) .sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => { .map((e, i) => {
@ -172,6 +183,7 @@ export default function TeamManagement({
key={i} 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" className="relative border p-2 rounded-md border-sky-100 flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
> >
{permissions === true && (
<FontAwesomeIcon <FontAwesomeIcon
icon={faClose} icon={faClose}
className="absolute right-2 top-2 text-gray-500 h-4 hover:text-red-500 duration-100 cursor-pointer" className="absolute right-2 top-2 text-gray-500 h-4 hover:text-red-500 duration-100 cursor-pointer"
@ -188,8 +200,12 @@ export default function TeamManagement({
}); });
}} }}
/> />
)}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ProfilePhoto src={`/api/avatar/${e.userId}`} /> <ProfilePhoto
src={`/api/avatar/${e.userId}`}
className="border-[3px]"
/>
<div> <div>
<p className="text-sm font-bold text-sky-500"> <p className="text-sm font-bold text-sky-500">
{e.user.name} {e.user.name}
@ -197,24 +213,45 @@ export default function TeamManagement({
<p className="text-sky-900">{e.user.email}</p> <p className="text-sky-900">{e.user.email}</p>
</div> </div>
</div> </div>
<div className="flex sm:block items-center gap-5"> <div className="flex sm:block items-center gap-5 min-w-[10rem]">
<div> <div>
<p className="font-bold text-sm text-sky-500"> <p
className={`font-bold text-sm text-sky-500 ${
permissions === true ? "" : "mb-2"
}`}
>
Permissions Permissions
</p> </p>
{permissions === true && (
<p className="text-xs text-gray-500 mb-2"> <p className="text-xs text-gray-500 mb-2">
(Click to toggle.) (Click to toggle.)
</p> </p>
)}
</div> </div>
{permissions !== true &&
!e.canCreate &&
!e.canUpdate &&
!e.canDelete ? (
<p className="text-sm text-gray-500">
Has no permissions.
</p>
) : (
<div> <div>
<label className="cursor-pointer mr-1"> <label
className={
permissions === true
? "cursor-pointer mr-1"
: "mr-1"
}
>
<input <input
type="checkbox" type="checkbox"
id="canCreate" id="canCreate"
className="peer sr-only" className="peer sr-only"
checked={e.canCreate} checked={e.canCreate}
onChange={() => { onChange={() => {
if (permissions === true) {
const updatedMembers = collection.members.map( const updatedMembers = collection.members.map(
(member) => { (member) => {
if (member.user.email === e.user.email) { if (member.user.email === e.user.email) {
@ -230,20 +267,34 @@ export default function TeamManagement({
...collection, ...collection,
members: updatedMembers, members: updatedMembers,
}); });
}
}} }}
/> />
<span className="text-sky-900 peer-checked:bg-sky-500 text-sm hover:bg-slate-200 duration-75 peer-checked:text-white rounded p-1 select-none"> <span
className={`text-sky-900 peer-checked:bg-sky-500 text-sm ${
permissions === true
? "hover:bg-slate-200 duration-75"
: ""
} peer-checked:text-white rounded p-1 select-none`}
>
Create Create
</span> </span>
</label> </label>
<label className="cursor-pointer mr-1"> <label
className={
permissions === true
? "cursor-pointer mr-1"
: "mr-1"
}
>
<input <input
type="checkbox" type="checkbox"
id="canUpdate" id="canUpdate"
className="peer sr-only" className="peer sr-only"
checked={e.canUpdate} checked={e.canUpdate}
onChange={() => { onChange={() => {
if (permissions === true) {
const updatedMembers = collection.members.map( const updatedMembers = collection.members.map(
(member) => { (member) => {
if (member.user.email === e.user.email) { if (member.user.email === e.user.email) {
@ -259,20 +310,34 @@ export default function TeamManagement({
...collection, ...collection,
members: updatedMembers, members: updatedMembers,
}); });
}
}} }}
/> />
<span className="text-sky-900 peer-checked:bg-sky-500 text-sm hover:bg-slate-200 duration-75 peer-checked:text-white rounded p-1 select-none"> <span
className={`text-sky-900 peer-checked:bg-sky-500 text-sm ${
permissions === true
? "hover:bg-slate-200 duration-75"
: ""
} peer-checked:text-white rounded p-1 select-none`}
>
Update Update
</span> </span>
</label> </label>
<label className="cursor-pointer mr-1"> <label
className={
permissions === true
? "cursor-pointer mr-1"
: "mr-1"
}
>
<input <input
type="checkbox" type="checkbox"
id="canDelete" id="canDelete"
className="peer sr-only" className="peer sr-only"
checked={e.canDelete} checked={e.canDelete}
onChange={() => { onChange={() => {
if (permissions === true) {
const updatedMembers = collection.members.map( const updatedMembers = collection.members.map(
(member) => { (member) => {
if (member.user.email === e.user.email) { if (member.user.email === e.user.email) {
@ -288,13 +353,21 @@ export default function TeamManagement({
...collection, ...collection,
members: updatedMembers, members: updatedMembers,
}); });
}
}} }}
/> />
<span className="text-sky-900 peer-checked:bg-sky-500 text-sm hover:bg-slate-200 duration-75 peer-checked:text-white rounded p-1 select-none"> <span
className={`text-sky-900 peer-checked:bg-sky-500 text-sm ${
permissions === true
? "hover:bg-slate-200 duration-75"
: ""
} peer-checked:text-white rounded p-1 select-none`}
>
Delete Delete
</span> </span>
</label> </label>
</div> </div>
)}
</div> </div>
</div> </div>
); );
@ -303,12 +376,14 @@ export default function TeamManagement({
</> </>
)} )}
{permissions === true && (
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
label={method === "CREATE" ? "Add" : "Save"} label={method === "CREATE" ? "Add" : "Save"}
icon={method === "CREATE" ? faPlus : faPenToSquare} icon={method === "CREATE" ? faPlus : faPenToSquare}
className="mx-auto mt-2" className="mx-auto mt-2"
/> />
)}
</div> </div>
); );
} }

View File

@ -10,6 +10,7 @@ type Props =
toggleCollectionModal: Function; toggleCollectionModal: Function;
activeCollection: CollectionIncludingMembersAndLinkCount; activeCollection: CollectionIncludingMembersAndLinkCount;
method: "UPDATE"; method: "UPDATE";
isOwner: boolean;
className?: string; className?: string;
defaultIndex?: number; defaultIndex?: number;
} }
@ -17,6 +18,7 @@ type Props =
toggleCollectionModal: Function; toggleCollectionModal: Function;
activeCollection?: CollectionIncludingMembersAndLinkCount; activeCollection?: CollectionIncludingMembersAndLinkCount;
method: "CREATE"; method: "CREATE";
isOwner: boolean;
className?: string; className?: string;
defaultIndex?: number; defaultIndex?: number;
}; };
@ -25,6 +27,7 @@ export default function CollectionModal({
className, className,
defaultIndex, defaultIndex,
toggleCollectionModal, toggleCollectionModal,
isOwner,
activeCollection, activeCollection,
method, method,
}: Props) { }: Props) {
@ -48,6 +51,7 @@ export default function CollectionModal({
<Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-600"> <Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-600">
{method === "UPDATE" && ( {method === "UPDATE" && (
<> <>
{isOwner && (
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
@ -57,6 +61,7 @@ export default function CollectionModal({
> >
Collection Info Collection Info
</Tab> </Tab>
)}
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
@ -64,7 +69,7 @@ export default function CollectionModal({
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none"
} }
> >
Share & Collaborate {isOwner ? "Share & Collaborate" : "View Team"}
</Tab> </Tab>
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
@ -73,12 +78,13 @@ export default function CollectionModal({
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none"
} }
> >
Delete Collection {isOwner ? "Delete Collection" : "Leave Collection"}
</Tab> </Tab>
</> </>
)} )}
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
{(isOwner || method === "CREATE") && (
<Tab.Panel> <Tab.Panel>
<CollectionInfo <CollectionInfo
toggleCollectionModal={toggleCollectionModal} toggleCollectionModal={toggleCollectionModal}
@ -87,6 +93,7 @@ export default function CollectionModal({
method={method} method={method}
/> />
</Tab.Panel> </Tab.Panel>
)}
{method === "UPDATE" && ( {method === "UPDATE" && (
<> <>

View File

@ -77,10 +77,10 @@ export default function ProfileSettings({
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto"> <div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
<div className="sm:row-span-2 sm:justify-self-center mx-auto mb-3"> <div className="sm:row-span-2 sm:justify-self-center mx-auto mb-3">
<p className="text-sm text-sky-500 mb-2 text-center">Profile Photo</p> <p className="text-sm text-sky-500 mb-2 text-center">Profile Photo</p>
<div className="w-28 h-28 flex items-center justify-center border border-sky-100 rounded-full relative"> <div className="w-28 h-28 flex items-center justify-center rounded-full relative">
<ProfilePhoto <ProfilePhoto
src={user.profilePic} src={user.profilePic}
className="h-auto aspect-square w-28 border-[1px]" className="h-auto w-28"
status={handleProfileStatus} status={handleProfileStatus}
/> />
{profileStatus && ( {profileStatus && (

View File

@ -40,6 +40,7 @@ export default function ModalManagement() {
<CollectionModal <CollectionModal
toggleCollectionModal={toggleModal} toggleCollectionModal={toggleModal}
method={modal.method} method={modal.method}
isOwner={modal.isOwner}
defaultIndex={modal.defaultIndex} defaultIndex={modal.defaultIndex}
activeCollection={ activeCollection={
modal.active as CollectionIncludingMembersAndLinkCount modal.active as CollectionIncludingMembersAndLinkCount

View File

@ -69,7 +69,7 @@ export default function Navbar() {
> >
<ProfilePhoto <ProfilePhoto
src={account.profilePic} src={account.profilePic}
className="sm:group-hover:h-8 sm:group-hover:w-8 duration-100" className="sm:group-hover:h-8 sm:group-hover:w-8 duration-100 border-[3px]"
/> />
<p <p
id="profile-dropdown" id="profile-dropdown"

View File

@ -33,7 +33,7 @@ export default function ProfilePhoto({
return error || !src ? ( return error || !src ? (
<div <div
className={`bg-sky-500 text-white h-10 w-10 aspect-square shadow rounded-full border-[3px] border-slate-200 flex items-center justify-center ${className}`} className={`bg-sky-500 text-white h-10 w-10 aspect-square shadow rounded-full border border-slate-200 flex items-center justify-center ${className}`}
> >
<FontAwesomeIcon icon={faUser} className="w-1/2 h-1/2 aspect-square" /> <FontAwesomeIcon icon={faUser} className="w-1/2 h-1/2 aspect-square" />
</div> </div>
@ -43,7 +43,7 @@ export default function ProfilePhoto({
src={src} src={src}
height={112} height={112}
width={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}`}
/> />
); );
} }

View File

@ -1,7 +1,7 @@
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import { Member } from "@/types/global";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function usePermissions(collectionId: number) { export default function usePermissions(collectionId: number) {
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();
@ -26,7 +26,7 @@ export default function usePermissions(collectionId: number) {
setPermissions(account.id === collection.ownerId || getPermission); setPermissions(account.id === collection.ownerId || getPermission);
} }
}, [collections]); }, [account, collections, collectionId]);
return permissions; return permissions;
} }

View File

@ -1,24 +1,45 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";
import { UsersAndCollections } from "@prisma/client";
import fs from "fs"; import fs from "fs";
export default async function deleteCollection( export default async function deleteCollection(
collection: { id: number }, collection: { id: number },
userId: number userId: number
) { ) {
if (!collection.id) const collectionId = collection.id;
if (!collectionId)
return { response: "Please choose a valid collection.", status: 401 }; 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 }; return { response: "Collection is not accessible.", status: 401 };
}
const deletedCollection = await prisma.$transaction(async () => { const deletedCollection = await prisma.$transaction(async () => {
await prisma.usersAndCollections.deleteMany({ await prisma.usersAndCollections.deleteMany({
where: { where: {
collection: { collection: {
id: collection.id, id: collectionId,
}, },
}, },
}); });
@ -26,13 +47,13 @@ export default async function deleteCollection(
await prisma.link.deleteMany({ await prisma.link.deleteMany({
where: { where: {
collection: { collection: {
id: collection.id, id: collectionId,
}, },
}, },
}); });
try { try {
fs.rmdirSync(`data/archives/${collection.id}`, { recursive: true }); fs.rmdirSync(`data/archives/${collectionId}`, { recursive: true });
} catch (error) { } catch (error) {
console.log( console.log(
"Collection's archive directory wasn't deleted most likely because it didn't exist..." "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({ return await prisma.collection.delete({
where: { where: {
id: collection.id, id: collectionId,
}, },
}); });
}); });

View File

@ -17,6 +17,7 @@ import ProfilePhoto from "@/components/ProfilePhoto";
import SortDropdown from "@/components/SortDropdown"; import SortDropdown from "@/components/SortDropdown";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
import useLinks from "@/hooks/useLinks"; import useLinks from "@/hooks/useLinks";
import usePermissions from "@/hooks/usePermissions";
export default function Index() { export default function Index() {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@ -35,6 +36,8 @@ export default function Index() {
const [activeCollection, setActiveCollection] = const [activeCollection, setActiveCollection] =
useState<CollectionIncludingMembersAndLinkCount>(); useState<CollectionIncludingMembersAndLinkCount>();
const permissions = usePermissions(activeCollection?.id as number);
useLinks({ collectionId: Number(router.query.id), sort: sortBy }); useLinks({ collectionId: Number(router.query.id), sort: sortBy });
useEffect(() => { useEffect(() => {
@ -71,13 +74,13 @@ export default function Index() {
> >
<div <div
onClick={() => onClick={() =>
activeCollection &&
setModal({ setModal({
modal: "COLLECTION", modal: "COLLECTION",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true,
active: activeCollection, 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" 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.members[0] && "mr-1"
}`} }`}
> >
{activeCollection.ownerId === data?.user.id {permissions === true ? "Manage" : "View"} Team
? "Manage"
: "View"}{" "}
Team
</div> </div>
{activeCollection?.members {activeCollection?.members
.sort((a, b) => (a.userId as number) - (b.userId as number)) .sort((a, b) => (a.userId as number) - (b.userId as number))
@ -99,7 +99,7 @@ export default function Index() {
<ProfilePhoto <ProfilePhoto
key={i} key={i}
src={`/api/avatar/${e.userId}`} src={`/api/avatar/${e.userId}`}
className="-mr-3 duration-100" className="-mr-3 duration-100 border-[3px]"
/> />
); );
}) })
@ -155,7 +155,8 @@ export default function Index() {
{expandDropdown ? ( {expandDropdown ? (
<Dropdown <Dropdown
items={[ items={[
{ permissions === true || permissions?.canCreate
? {
name: "Add Link Here", name: "Add Link Here",
onClick: () => { onClick: () => {
setModal({ setModal({
@ -165,8 +166,10 @@ export default function Index() {
}); });
setExpandDropdown(false); setExpandDropdown(false);
}, },
}, }
{ : undefined,
permissions === true
? {
name: "Edit Collection Info", name: "Edit Collection Info",
onClick: () => { onClick: () => {
activeCollection && activeCollection &&
@ -174,35 +177,46 @@ export default function Index() {
modal: "COLLECTION", modal: "COLLECTION",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true,
active: activeCollection, active: activeCollection,
}); });
setExpandDropdown(false); setExpandDropdown(false);
}, },
}, }
: undefined,
{ {
name: "Share/Collaborate", name:
permissions === true
? "Share/Collaborate"
: "View Team",
onClick: () => { onClick: () => {
activeCollection && activeCollection &&
setModal({ setModal({
modal: "COLLECTION", modal: "COLLECTION",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true,
active: activeCollection, active: activeCollection,
defaultIndex: 1, defaultIndex: 1,
}); });
setExpandDropdown(false); setExpandDropdown(false);
}, },
}, },
{ {
name: "Delete Collection", name:
permissions === true
? "Delete Collection"
: "Leave Collection",
onClick: () => { onClick: () => {
activeCollection && activeCollection &&
setModal({ setModal({
modal: "COLLECTION", modal: "COLLECTION",
state: true, state: true,
method: "UPDATE", method: "UPDATE",
isOwner: permissions === true,
active: activeCollection, active: activeCollection,
defaultIndex: 2, defaultIndex: permissions === true ? 2 : 1,
}); });
setExpandDropdown(false); setExpandDropdown(false);
}, },

View File

@ -30,6 +30,7 @@ type Modal =
modal: "COLLECTION"; modal: "COLLECTION";
state: boolean; state: boolean;
method: "UPDATE"; method: "UPDATE";
isOwner: boolean;
active: CollectionIncludingMembersAndLinkCount; active: CollectionIncludingMembersAndLinkCount;
defaultIndex?: number; defaultIndex?: number;
} }
@ -37,6 +38,7 @@ type Modal =
modal: "COLLECTION"; modal: "COLLECTION";
state: boolean; state: boolean;
method: "CREATE"; method: "CREATE";
isOwner: boolean;
active?: CollectionIncludingMembersAndLinkCount; active?: CollectionIncludingMembersAndLinkCount;
defaultIndex?: number; defaultIndex?: number;
} }