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,77 +25,92 @@ 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">
<p className="text-red-500 font-bold text-center">Warning!</p> {permissions === true ? (
<>
<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">
Links: All links within the collection will be permanently Links: All links within the collection will be permanently
deleted. deleted.
</li> </li>
<li className="list-inside"> <li className="list-inside">
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>
<p>
Please double-check that you have backed up any essential data
and have informed the relevant members about this action.
</p>
</div>
</div> </div>
<p>
Please double-check that you have backed up any essential data and
have informed the relevant members about this action.
</p>
</div>
</div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<p className="text-sky-900 select-none text-center"> <p className="text-sky-900 select-none text-center">
To confirm, type &quot; To confirm, type &quot;
<span className="font-bold text-sky-500">{collection.name}</span> <span className="font-bold text-sky-500">{collection.name}</span>
&quot; in the box below: &quot; in the box below:
</p>
<input
autoFocus
value={inputField}
onChange={(e) => 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"
/>
</div>
</>
) : (
<p className="text-gray-500">
Click the button below to leave the current collection:
</p> </p>
)}
<input
autoFocus
value={inputField}
onChange={(e) => 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"
/>
</div>
<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
? "bg-red-500 hover:bg-red-400 cursor-pointer" ? inputField === collection.name
: "cursor-not-allowed bg-red-300" ? "bg-red-500 hover:bg-red-400 cursor-pointer"
: "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,19 +83,23 @@ 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">
<p className="text-sm text-sky-500">Make Public</p> {permissions === true && (
<>
<p className="text-sm text-sky-500">Make Public</p>
<Checkbox <Checkbox
label="Make this a public collection." label="Make this a public collection."
state={collection.isPublic} state={collection.isPublic}
onClick={() => onClick={() =>
setCollection({ ...collection, isPublic: !collection.isPublic }) setCollection({ ...collection, isPublic: !collection.isPublic })
} }
/> />
<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,54 +123,58 @@ export default function TeamManagement({
</div> </div>
) : null} ) : null}
<hr /> {permissions !== true && collection.isPublic && <hr />}
<p className="text-sm text-sky-500">Member Management</p> {permissions === true && (
<>
<p className="text-sm text-sky-500">Member Management</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
value={member.user.email} value={member.user.email}
onChange={(e) => { onChange={(e) => {
setMember({ setMember({
...member, ...member,
user: { ...member.user, email: e.target.value }, user: { ...member.user, email: e.target.value },
}); });
}} }}
onKeyDown={(e) => onKeyDown={(e) =>
e.key === "Enter" && e.key === "Enter" &&
addMemberToCollection( addMemberToCollection(
session.data?.user.email as string, session.data?.user.email as string,
member.user.email, member.user.email,
collection, collection,
setMemberState setMemberState
) )
} }
type="text" type="text"
placeholder="Email" placeholder="Email"
className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/> />
<div <div
onClick={() => onClick={() =>
addMemberToCollection( addMemberToCollection(
session.data?.user.email as string, session.data?.user.email as string,
member.user.email, member.user.email,
collection, collection,
setMemberState 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" 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"
> >
<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,24 +183,29 @@ 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"
> >
<FontAwesomeIcon {permissions === true && (
icon={faClose} <FontAwesomeIcon
className="absolute right-2 top-2 text-gray-500 h-4 hover:text-red-500 duration-100 cursor-pointer" icon={faClose}
title="Remove Member" className="absolute right-2 top-2 text-gray-500 h-4 hover:text-red-500 duration-100 cursor-pointer"
onClick={() => { title="Remove Member"
const updatedMembers = collection.members.filter( onClick={() => {
(member) => { const updatedMembers = collection.members.filter(
return member.user.email !== e.user.email; (member) => {
} return member.user.email !== e.user.email;
); }
setCollection({ );
...collection, setCollection({
members: updatedMembers, ...collection,
}); members: updatedMembers,
}} });
/> }}
/>
)}
<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,104 +213,161 @@ 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>
<p className="text-xs text-gray-500 mb-2"> {permissions === true && (
(Click to toggle.) <p className="text-xs text-gray-500 mb-2">
(Click to toggle.)
</p>
)}
</div>
{permissions !== true &&
!e.canCreate &&
!e.canUpdate &&
!e.canDelete ? (
<p className="text-sm text-gray-500">
Has no permissions.
</p> </p>
</div> ) : (
<div>
<div> <label
<label className="cursor-pointer mr-1"> className={
<input permissions === true
type="checkbox" ? "cursor-pointer mr-1"
id="canCreate" : "mr-1"
className="peer sr-only" }
checked={e.canCreate} >
onChange={() => { <input
const updatedMembers = collection.members.map( type="checkbox"
(member) => { id="canCreate"
if (member.user.email === e.user.email) { className="peer sr-only"
return { checked={e.canCreate}
...member, onChange={() => {
canCreate: !e.canCreate, if (permissions === true) {
}; const updatedMembers = collection.members.map(
} (member) => {
return member; if (member.user.email === e.user.email) {
return {
...member,
canCreate: !e.canCreate,
};
}
return member;
}
);
setCollection({
...collection,
members: updatedMembers,
});
} }
); }}
setCollection({ />
...collection, <span
members: updatedMembers, className={`text-sky-900 peer-checked:bg-sky-500 text-sm ${
}); permissions === true
}} ? "hover:bg-slate-200 duration-75"
/> : ""
<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"> } peer-checked:text-white rounded p-1 select-none`}
Create >
</span> Create
</label> </span>
</label>
<label className="cursor-pointer mr-1"> <label
<input className={
type="checkbox" permissions === true
id="canUpdate" ? "cursor-pointer mr-1"
className="peer sr-only" : "mr-1"
checked={e.canUpdate} }
onChange={() => { >
const updatedMembers = collection.members.map( <input
(member) => { type="checkbox"
if (member.user.email === e.user.email) { id="canUpdate"
return { className="peer sr-only"
...member, checked={e.canUpdate}
canUpdate: !e.canUpdate, onChange={() => {
}; if (permissions === true) {
} const updatedMembers = collection.members.map(
return member; (member) => {
if (member.user.email === e.user.email) {
return {
...member,
canUpdate: !e.canUpdate,
};
}
return member;
}
);
setCollection({
...collection,
members: updatedMembers,
});
} }
); }}
setCollection({ />
...collection, <span
members: updatedMembers, className={`text-sky-900 peer-checked:bg-sky-500 text-sm ${
}); permissions === true
}} ? "hover:bg-slate-200 duration-75"
/> : ""
<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"> } peer-checked:text-white rounded p-1 select-none`}
Update >
</span> Update
</label> </span>
</label>
<label className="cursor-pointer mr-1"> <label
<input className={
type="checkbox" permissions === true
id="canDelete" ? "cursor-pointer mr-1"
className="peer sr-only" : "mr-1"
checked={e.canDelete} }
onChange={() => { >
const updatedMembers = collection.members.map( <input
(member) => { type="checkbox"
if (member.user.email === e.user.email) { id="canDelete"
return { className="peer sr-only"
...member, checked={e.canDelete}
canDelete: !e.canDelete, onChange={() => {
}; if (permissions === true) {
} const updatedMembers = collection.members.map(
return member; (member) => {
if (member.user.email === e.user.email) {
return {
...member,
canDelete: !e.canDelete,
};
}
return member;
}
);
setCollection({
...collection,
members: updatedMembers,
});
} }
); }}
setCollection({ />
...collection, <span
members: updatedMembers, className={`text-sky-900 peer-checked:bg-sky-500 text-sm ${
}); permissions === true
}} ? "hover:bg-slate-200 duration-75"
/> : ""
<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"> } peer-checked:text-white rounded p-1 select-none`}
Delete >
</span> Delete
</label> </span>
</div> </label>
</div>
)}
</div> </div>
</div> </div>
); );
@ -303,12 +376,14 @@ export default function TeamManagement({
</> </>
)} )}
<SubmitButton {permissions === true && (
onClick={submit} <SubmitButton
label={method === "CREATE" ? "Add" : "Save"} onClick={submit}
icon={method === "CREATE" ? faPlus : faPenToSquare} label={method === "CREATE" ? "Add" : "Save"}
className="mx-auto mt-2" icon={method === "CREATE" ? faPlus : faPenToSquare}
/> 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,17 @@ 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
className={({ selected }) =>
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
</Tab>
)}
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
@ -55,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"
} }
> >
Collection Info {isOwner ? "Share & Collaborate" : "View Team"}
</Tab> </Tab>
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
@ -64,29 +78,22 @@ 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 ? "Delete Collection" : "Leave Collection"}
</Tab>
<Tab
className={({ selected }) =>
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
</Tab> </Tab>
</> </>
)} )}
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel> {(isOwner || method === "CREATE") && (
<CollectionInfo <Tab.Panel>
toggleCollectionModal={toggleCollectionModal} <CollectionInfo
setCollection={setCollection} toggleCollectionModal={toggleCollectionModal}
collection={collection} setCollection={setCollection}
method={method} collection={collection}
/> 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,54 +155,68 @@ export default function Index() {
{expandDropdown ? ( {expandDropdown ? (
<Dropdown <Dropdown
items={[ items={[
permissions === true || permissions?.canCreate
? {
name: "Add Link Here",
onClick: () => {
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", name:
onClick: () => { permissions === true
setModal({ ? "Share/Collaborate"
modal: "LINK", : "View Team",
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",
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;
} }