managed how collections are viewed by members
This commit is contained in:
parent
51c5615fea
commit
3abea1d1b7
|
@ -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 (
|
||||
<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}`}
|
||||
|
@ -58,7 +61,7 @@ export default function CollectionCard({ collection, className }: Props) {
|
|||
<ProfilePhoto
|
||||
key={i}
|
||||
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-sky-500 font-bold text-sm flex justify-end gap-1 items-center">
|
||||
<FontAwesomeIcon icon={faLink} className="w-5 h-5 text-sky-600" />
|
||||
{collection._count.links}
|
||||
{collection._count && collection._count.links}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1 text-gray-600">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
<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 && (
|
||||
<div
|
||||
|
@ -87,7 +86,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
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
|
||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import React, { useState } from "react";
|
||||
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 useCollectionStore from "@/store/collections";
|
||||
import { useRouter } from "next/router";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
|
||||
type Props = {
|
||||
toggleDeleteCollectionModal: Function;
|
||||
|
@ -21,77 +25,92 @@ export default function DeleteCollection({
|
|||
const router = useRouter();
|
||||
|
||||
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) {
|
||||
toggleDeleteCollectionModal();
|
||||
router.push("/collections");
|
||||
}
|
||||
};
|
||||
|
||||
const permissions = usePermissions(collection.id as number);
|
||||
|
||||
return (
|
||||
<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="text-gray-500">
|
||||
<p>
|
||||
Please note that deleting the collection will permanently remove all
|
||||
its contents, including the following:
|
||||
</p>
|
||||
<div className="p-3">
|
||||
<li className="list-inside">
|
||||
Links: All links within the collection will be permanently
|
||||
deleted.
|
||||
</li>
|
||||
<li className="list-inside">
|
||||
Tags: All tags associated with the collection will be removed.
|
||||
</li>
|
||||
<li className="list-inside">
|
||||
Screenshots/PDFs: Any screenshots or PDFs attached to links within
|
||||
this collection will be permanently deleted.
|
||||
</li>
|
||||
<li className="list-inside">
|
||||
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.
|
||||
</li>
|
||||
<div className="max-h-[20rem] overflow-y-auto">
|
||||
<div className="text-gray-500">
|
||||
<p>
|
||||
Please note that deleting the collection will permanently remove
|
||||
all its contents, including the following:
|
||||
</p>
|
||||
<div className="p-3">
|
||||
<li className="list-inside">
|
||||
Links: All links within the collection will be permanently
|
||||
deleted.
|
||||
</li>
|
||||
<li className="list-inside">
|
||||
Tags: All tags associated with the collection will be removed.
|
||||
</li>
|
||||
<li className="list-inside">
|
||||
Screenshots/PDFs: Any screenshots or PDFs attached to links
|
||||
within this collection will be permanently deleted.
|
||||
</li>
|
||||
<li className="list-inside">
|
||||
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.
|
||||
</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>
|
||||
<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">
|
||||
<p className="text-sky-900 select-none text-center">
|
||||
To confirm, type "
|
||||
<span className="font-bold text-sky-500">{collection.name}</span>
|
||||
" in the box below:
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sky-900 select-none text-center">
|
||||
To confirm, type "
|
||||
<span className="font-bold text-sky-500">{collection.name}</span>
|
||||
" 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>
|
||||
|
||||
<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
|
||||
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
|
||||
? "bg-red-500 hover:bg-red-400 cursor-pointer"
|
||||
: "cursor-not-allowed bg-red-300"
|
||||
permissions === true
|
||||
? inputField === collection.name
|
||||
? "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}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} className="h-5" />
|
||||
Delete Collection
|
||||
<FontAwesomeIcon
|
||||
icon={permissions === true ? faTrashCan : faRightFromBracket}
|
||||
className="h-5"
|
||||
/>
|
||||
{permissions === true ? "Delete" : "Leave"} Collection
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<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
|
||||
label="Make this a public collection."
|
||||
state={collection.isPublic}
|
||||
onClick={() =>
|
||||
setCollection({ ...collection, isPublic: !collection.isPublic })
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Make this a public collection."
|
||||
state={collection.isPublic}
|
||||
onClick={() =>
|
||||
setCollection({ ...collection, isPublic: !collection.isPublic })
|
||||
}
|
||||
/>
|
||||
|
||||
<p className="text-gray-500 text-sm">
|
||||
This will let <b>Anyone</b> to view this collection.
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
This will let <b>Anyone</b> to view this collection.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{collection.isPublic ? (
|
||||
<div>
|
||||
|
@ -116,54 +123,58 @@ export default function TeamManagement({
|
|||
</div>
|
||||
) : 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">
|
||||
<input
|
||||
value={member.user.email}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
value={member.user.email}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
|
||||
<div
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{collection?.members[0]?.user && (
|
||||
<>
|
||||
<p className="text-center text-gray-500 text-xs sm:text-sm">
|
||||
(All Members have <b>Read</b> access to this collection.)
|
||||
</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
|
||||
.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"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faClose}
|
||||
className="absolute right-2 top-2 text-gray-500 h-4 hover:text-red-500 duration-100 cursor-pointer"
|
||||
title="Remove Member"
|
||||
onClick={() => {
|
||||
const updatedMembers = collection.members.filter(
|
||||
(member) => {
|
||||
return member.user.email !== e.user.email;
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{permissions === true && (
|
||||
<FontAwesomeIcon
|
||||
icon={faClose}
|
||||
className="absolute right-2 top-2 text-gray-500 h-4 hover:text-red-500 duration-100 cursor-pointer"
|
||||
title="Remove Member"
|
||||
onClick={() => {
|
||||
const updatedMembers = collection.members.filter(
|
||||
(member) => {
|
||||
return member.user.email !== e.user.email;
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<ProfilePhoto src={`/api/avatar/${e.userId}`} />
|
||||
<ProfilePhoto
|
||||
src={`/api/avatar/${e.userId}`}
|
||||
className="border-[3px]"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-sky-500">
|
||||
{e.user.name}
|
||||
|
@ -197,104 +213,161 @@ export default function TeamManagement({
|
|||
<p className="text-sky-900">{e.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex sm:block items-center gap-5">
|
||||
<div className="flex sm:block items-center gap-5 min-w-[10rem]">
|
||||
<div>
|
||||
<p className="font-bold text-sm text-sky-500">
|
||||
<p
|
||||
className={`font-bold text-sm text-sky-500 ${
|
||||
permissions === true ? "" : "mb-2"
|
||||
}`}
|
||||
>
|
||||
Permissions
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
(Click to toggle.)
|
||||
{permissions === true && (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="cursor-pointer mr-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="canCreate"
|
||||
className="peer sr-only"
|
||||
checked={e.canCreate}
|
||||
onChange={() => {
|
||||
const updatedMembers = collection.members.map(
|
||||
(member) => {
|
||||
if (member.user.email === e.user.email) {
|
||||
return {
|
||||
...member,
|
||||
canCreate: !e.canCreate,
|
||||
};
|
||||
}
|
||||
return member;
|
||||
) : (
|
||||
<div>
|
||||
<label
|
||||
className={
|
||||
permissions === true
|
||||
? "cursor-pointer mr-1"
|
||||
: "mr-1"
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="canCreate"
|
||||
className="peer sr-only"
|
||||
checked={e.canCreate}
|
||||
onChange={() => {
|
||||
if (permissions === true) {
|
||||
const updatedMembers = collection.members.map(
|
||||
(member) => {
|
||||
if (member.user.email === e.user.email) {
|
||||
return {
|
||||
...member,
|
||||
canCreate: !e.canCreate,
|
||||
};
|
||||
}
|
||||
return member;
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
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">
|
||||
Create
|
||||
</span>
|
||||
</label>
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="cursor-pointer mr-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="canUpdate"
|
||||
className="peer sr-only"
|
||||
checked={e.canUpdate}
|
||||
onChange={() => {
|
||||
const updatedMembers = collection.members.map(
|
||||
(member) => {
|
||||
if (member.user.email === e.user.email) {
|
||||
return {
|
||||
...member,
|
||||
canUpdate: !e.canUpdate,
|
||||
};
|
||||
}
|
||||
return member;
|
||||
<label
|
||||
className={
|
||||
permissions === true
|
||||
? "cursor-pointer mr-1"
|
||||
: "mr-1"
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="canUpdate"
|
||||
className="peer sr-only"
|
||||
checked={e.canUpdate}
|
||||
onChange={() => {
|
||||
if (permissions === true) {
|
||||
const updatedMembers = collection.members.map(
|
||||
(member) => {
|
||||
if (member.user.email === e.user.email) {
|
||||
return {
|
||||
...member,
|
||||
canUpdate: !e.canUpdate,
|
||||
};
|
||||
}
|
||||
return member;
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
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">
|
||||
Update
|
||||
</span>
|
||||
</label>
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="cursor-pointer mr-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="canDelete"
|
||||
className="peer sr-only"
|
||||
checked={e.canDelete}
|
||||
onChange={() => {
|
||||
const updatedMembers = collection.members.map(
|
||||
(member) => {
|
||||
if (member.user.email === e.user.email) {
|
||||
return {
|
||||
...member,
|
||||
canDelete: !e.canDelete,
|
||||
};
|
||||
}
|
||||
return member;
|
||||
<label
|
||||
className={
|
||||
permissions === true
|
||||
? "cursor-pointer mr-1"
|
||||
: "mr-1"
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="canDelete"
|
||||
className="peer sr-only"
|
||||
checked={e.canDelete}
|
||||
onChange={() => {
|
||||
if (permissions === true) {
|
||||
const updatedMembers = collection.members.map(
|
||||
(member) => {
|
||||
if (member.user.email === e.user.email) {
|
||||
return {
|
||||
...member,
|
||||
canDelete: !e.canDelete,
|
||||
};
|
||||
}
|
||||
return member;
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
}
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
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">
|
||||
Delete
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -303,12 +376,14 @@ export default function TeamManagement({
|
|||
</>
|
||||
)}
|
||||
|
||||
<SubmitButton
|
||||
onClick={submit}
|
||||
label={method === "CREATE" ? "Add" : "Save"}
|
||||
icon={method === "CREATE" ? faPlus : faPenToSquare}
|
||||
className="mx-auto mt-2"
|
||||
/>
|
||||
{permissions === true && (
|
||||
<SubmitButton
|
||||
onClick={submit}
|
||||
label={method === "CREATE" ? "Add" : "Save"}
|
||||
icon={method === "CREATE" ? faPlus : faPenToSquare}
|
||||
className="mx-auto mt-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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({
|
|||
<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" && (
|
||||
<>
|
||||
{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
|
||||
className={({ selected }) =>
|
||||
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"}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
|
@ -64,29 +78,22 @@ export default function CollectionModal({
|
|||
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none"
|
||||
}
|
||||
>
|
||||
Share & Collaborate
|
||||
</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
|
||||
{isOwner ? "Delete Collection" : "Leave Collection"}
|
||||
</Tab>
|
||||
</>
|
||||
)}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<CollectionInfo
|
||||
toggleCollectionModal={toggleCollectionModal}
|
||||
setCollection={setCollection}
|
||||
collection={collection}
|
||||
method={method}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
{(isOwner || method === "CREATE") && (
|
||||
<Tab.Panel>
|
||||
<CollectionInfo
|
||||
toggleCollectionModal={toggleCollectionModal}
|
||||
setCollection={setCollection}
|
||||
collection={collection}
|
||||
method={method}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
)}
|
||||
|
||||
{method === "UPDATE" && (
|
||||
<>
|
||||
|
|
|
@ -77,10 +77,10 @@ export default function ProfileSettings({
|
|||
<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">
|
||||
<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
|
||||
src={user.profilePic}
|
||||
className="h-auto aspect-square w-28 border-[1px]"
|
||||
className="h-auto w-28"
|
||||
status={handleProfileStatus}
|
||||
/>
|
||||
{profileStatus && (
|
||||
|
|
|
@ -40,6 +40,7 @@ export default function ModalManagement() {
|
|||
<CollectionModal
|
||||
toggleCollectionModal={toggleModal}
|
||||
method={modal.method}
|
||||
isOwner={modal.isOwner}
|
||||
defaultIndex={modal.defaultIndex}
|
||||
activeCollection={
|
||||
modal.active as CollectionIncludingMembersAndLinkCount
|
||||
|
|
|
@ -69,7 +69,7 @@ export default function Navbar() {
|
|||
>
|
||||
<ProfilePhoto
|
||||
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
|
||||
id="profile-dropdown"
|
||||
|
|
|
@ -33,7 +33,7 @@ export default function ProfilePhoto({
|
|||
|
||||
return error || !src ? (
|
||||
<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" />
|
||||
</div>
|
||||
|
@ -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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<CollectionIncludingMembersAndLinkCount>();
|
||||
|
||||
const permissions = usePermissions(activeCollection?.id as number);
|
||||
|
||||
useLinks({ collectionId: Number(router.query.id), sort: sortBy });
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -71,13 +74,13 @@ export default function Index() {
|
|||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
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
|
||||
</div>
|
||||
{activeCollection?.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
|
@ -99,7 +99,7 @@ export default function Index() {
|
|||
<ProfilePhoto
|
||||
key={i}
|
||||
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 ? (
|
||||
<Dropdown
|
||||
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",
|
||||
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);
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Ŝarĝante…
Reference in New Issue