From 2df4aad077e8fec4e4dfa1bdb9f5de17f4295c3d Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 10 Jun 2023 02:01:14 +0330 Subject: [PATCH] tab-seperated modals + eslint fix + much more bug fixed and improvements --- .eslintrc.json | 5 +- README.md | 4 +- components/ClickAwayHandler.tsx | 6 +- components/CollectionCard.tsx | 20 +- components/Dashboard/CollectionItem.tsx | 2 +- components/Dashboard/LinkItem.tsx | 11 +- .../InputSelect/CollectionSelection.tsx | 2 +- components/InputSelect/TagSelection.tsx | 2 +- components/LinkCard.tsx | 11 +- components/Loader.tsx | 2 +- components/Modal/ChangePassword.tsx | 71 ------- .../Modal/Collection/CollectionInfo.tsx | 15 +- .../Modal/Collection/DeleteCollection.tsx | 65 +++++-- .../Modal/Collection/TeamManagement.tsx | 36 ++-- components/Modal/Collection/index.tsx | 102 +++++++++++ components/Modal/User/ChangePassword.tsx | 90 +++++++++ components/Modal/User/PrivacySettings.tsx | 102 +++++++++++ .../ProfileSettings.tsx} | 173 +++++------------- components/Modal/User/index.tsx | 88 +++++++++ components/Modal/index.tsx | 8 +- components/Navbar.tsx | 12 +- components/ProfilePhoto.tsx | 26 ++- components/PublicPage/LinkCard.tsx | 4 +- components/Sidebar.tsx | 2 +- .../useInitialData.tsx | 2 +- hooks/useRedirect.tsx | 2 +- layouts/AuthRedirect.tsx | 6 +- layouts/MainLayout.tsx | 2 +- lib/api/archive.ts | 8 +- .../collections/deleteCollection.ts | 5 +- .../controllers/collections/getCollections.ts | 2 +- .../controllers/collections/postCollection.ts | 2 +- .../collections/updateCollection.ts | 2 +- lib/api/controllers/links/deleteLink.ts | 2 +- lib/api/controllers/links/getLinks.ts | 2 +- lib/api/controllers/links/postLink.ts | 2 +- lib/api/controllers/links/updateLink.ts | 2 +- lib/api/controllers/public/getCollection.ts | 2 +- lib/api/controllers/tags/getTags.ts | 2 +- lib/api/controllers/users/getUsers.ts | 2 +- lib/api/controllers/users/updateUser.ts | 60 ++++-- lib/api/getPermission.ts | 7 +- lib/api/getTitle.ts | 4 +- lib/client/avatarExists.ts | 4 + lib/client/getPublicUserDataByEmail.ts | 2 +- package.json | 1 + pages/api/archives/[...params].ts | 2 +- pages/api/auth/register.ts | 2 +- pages/api/avatar/[id].ts | 2 +- pages/api/public/routes/collections.ts | 5 +- pages/api/routes/collections/index.ts | 5 +- pages/api/routes/links/index.ts | 2 +- pages/api/routes/tags/index.ts | 2 +- pages/api/routes/users/index.ts | 2 +- pages/collections/[id].tsx | 35 ++-- pages/collections/index.tsx | 8 +- pages/dashboard.tsx | 4 +- pages/login.tsx | 5 +- pages/public/collections/[id].tsx | 2 +- pages/register.tsx | 2 +- pages/search/[query].tsx | 13 +- pages/tags/[id].tsx | 2 +- styles/globals.css | 2 +- yarn.lock | 9 +- 64 files changed, 713 insertions(+), 373 deletions(-) delete mode 100644 components/Modal/ChangePassword.tsx create mode 100644 components/Modal/Collection/index.tsx create mode 100644 components/Modal/User/ChangePassword.tsx create mode 100644 components/Modal/User/PrivacySettings.tsx rename components/Modal/{UserSettings.tsx => User/ProfileSettings.tsx} (54%) create mode 100644 components/Modal/User/index.tsx rename lib/client/getInitialData.ts => hooks/useInitialData.tsx (93%) create mode 100644 lib/client/avatarExists.ts diff --git a/.eslintrc.json b/.eslintrc.json index bffb357..09937b6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "next/core-web-vitals" + "extends": "next/core-web-vitals", + "rules": { + "react-hooks/exhaustive-deps": "off" + } } diff --git a/README.md b/README.md index 71e0956..ffc92a9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# LinkWarden - -## A place for your useful links. +# Linkwarden Rebuilding things from ground up... diff --git a/components/ClickAwayHandler.tsx b/components/ClickAwayHandler.tsx index d577e2f..88ff23b 100644 --- a/components/ClickAwayHandler.tsx +++ b/components/ClickAwayHandler.tsx @@ -26,7 +26,11 @@ function useOutsideAlerter( }, [ref, onClickOutside]); } -export default function ({ children, onClickOutside, className }: Props) { +export default function ClickAwayHandler({ + children, + onClickOutside, + className, +}: Props) { const wrapperRef = useRef(null); useOutsideAlerter(wrapperRef, onClickOutside); diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index 9abd8a2..fed3b1f 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -6,12 +6,10 @@ import useLinkStore from "@/store/links"; import Dropdown from "./Dropdown"; import { useState } from "react"; import Modal from "@/components/Modal"; -import CollectionInfo from "@/components/Modal/Collection/CollectionInfo"; -import DeleteCollection from "@/components/Modal/Collection/DeleteCollection"; +import CollectionModal from "@/components/Modal/Collection"; import ProfilePhoto from "./ProfilePhoto"; -import TeamManagement from "./Modal/Collection/TeamManagement"; -export default function ({ +export default function CollectionCard({ collection, }: { collection: CollectionIncludingMembers; @@ -126,7 +124,7 @@ export default function ({ ) : null} {editCollectionModal ? ( - - ) : null} {deleteCollectionModal ? ( - ) : null} diff --git a/components/Dashboard/CollectionItem.tsx b/components/Dashboard/CollectionItem.tsx index 1827639..4e64e88 100644 --- a/components/Dashboard/CollectionItem.tsx +++ b/components/Dashboard/CollectionItem.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { CollectionIncludingMembers } from "@/types/global"; import useLinkStore from "@/store/links"; -export default function ({ +export default function CollectionItem({ collection, }: { collection: CollectionIncludingMembers; diff --git a/components/Dashboard/LinkItem.tsx b/components/Dashboard/LinkItem.tsx index b3ff801..152178c 100644 --- a/components/Dashboard/LinkItem.tsx +++ b/components/Dashboard/LinkItem.tsx @@ -19,7 +19,7 @@ type Props = { count: number; }; -export default function ({ link, count }: Props) { +export default function LinkItem({ link, count }: Props) { const [expandDropdown, setExpandDropdown] = useState(false); const [editModal, setEditModal] = useState(false); @@ -102,7 +102,12 @@ export default function ({ link, count }: Props) {

{formattedDate}

- +

{url.host}

([]); diff --git a/components/LinkCard.tsx b/components/LinkCard.tsx index 916a568..6d53b04 100644 --- a/components/LinkCard.tsx +++ b/components/LinkCard.tsx @@ -23,7 +23,7 @@ type Props = { count: number; }; -export default function ({ link, count }: Props) { +export default function LinkCard({ link, count }: Props) { const [expandDropdown, setExpandDropdown] = useState(false); const [editModal, setEditModal] = useState(false); @@ -126,7 +126,12 @@ export default function ({ link, count }: Props) {

{formattedDate}

-
+

{url.host}

Loading...

diff --git a/components/Modal/ChangePassword.tsx b/components/Modal/ChangePassword.tsx deleted file mode 100644 index 89635e1..0000000 --- a/components/Modal/ChangePassword.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState } from "react"; -import { AccountSettings } from "@/types/global"; - -type Props = { - togglePasswordFormModal: Function; - user: AccountSettings; - setPasswordForm: Function; -}; - -export default function ChangePassword({ - togglePasswordFormModal, - user, - setPasswordForm, -}: Props) { - const [oldPassword, setOldPassword] = useState(""); - const [newPassword1, setNewPassword1] = useState(""); - const [newPassword2, setNewPassword2] = useState(""); - - const submit = async () => { - if (oldPassword !== "" && newPassword1 !== "" && newPassword2 !== "") { - if (newPassword1 === newPassword2) { - setPasswordForm(oldPassword, newPassword1); - togglePasswordFormModal(); - } else { - console.log("Passwords do not match."); - } - } else { - console.log("Please fill out all the fields."); - } - }; - - return ( -
-
-

Change Password

- -

Old Password

- - setOldPassword(e.target.value)} - type="text" - className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" - /> -

New Password

- - setNewPassword1(e.target.value)} - type="text" - className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" - /> -

Re-enter New Password

- - setNewPassword2(e.target.value)} - type="text" - className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" - /> - -
- Save -
-
-
- ); -} diff --git a/components/Modal/Collection/CollectionInfo.tsx b/components/Modal/Collection/CollectionInfo.tsx index 66546c8..dfc43a2 100644 --- a/components/Modal/Collection/CollectionInfo.tsx +++ b/components/Modal/Collection/CollectionInfo.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { Dispatch, SetStateAction } from "react"; import { faFolder, faPenToSquare, @@ -13,18 +13,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; type Props = { toggleCollectionModal: Function; - activeCollection: CollectionIncludingMembers; + setCollection: Dispatch>; + collection: CollectionIncludingMembers; method: "CREATE" | "UPDATE"; }; export default function CollectionInfo({ toggleCollectionModal, - activeCollection, + setCollection, + collection, method, }: Props) { - const [collection, setCollection] = - useState(activeCollection); - const { updateCollection, addCollection } = useCollectionStore(); const submit = async () => { @@ -41,10 +40,6 @@ export default function CollectionInfo({ return (
-

- {method === "CREATE" ? "Add" : "Edit"} Collection -

-

diff --git a/components/Modal/Collection/DeleteCollection.tsx b/components/Modal/Collection/DeleteCollection.tsx index ef7c815..408bf1b 100644 --- a/components/Modal/Collection/DeleteCollection.tsx +++ b/components/Modal/Collection/DeleteCollection.tsx @@ -21,7 +21,7 @@ export default function DeleteCollection({ const router = useRouter(); const submit = async () => { - if (!collection.id) return null; + if (!collection.id || collection.name !== inputField) return null; const response = await removeCollection(collection.id); if (response) { @@ -31,23 +31,56 @@ export default function DeleteCollection({ }; return ( -

-

Delete Collection

+
+

Warning!

-

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

+
+
+

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

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

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

    +
    +
    - 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" - /> +
    +

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

    + + setInputField(e.target.value)} + type="text" + placeholder={`Type "${collection.name}" Here.`} + className="w-72 sm:w-96 rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" + /> +
    >; + collection: CollectionIncludingMembers; + method: "CREATE" | "UPDATE"; }; export default function TeamManagement({ toggleCollectionModal, - activeCollection, + setCollection, + collection, + method, }: Props) { - const [collection, setCollection] = - useState(activeCollection); - const currentURL = new URL(document.URL); const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`; @@ -39,7 +41,7 @@ export default function TeamManagement({ }, }); - const { updateCollection } = useCollectionStore(); + const { addCollection, updateCollection } = useCollectionStore(); const session = useSession(); @@ -65,17 +67,17 @@ export default function TeamManagement({ const submit = async () => { if (!collection) return null; - const response = await updateCollection(collection); + let response = null; + + if (method === "CREATE") response = await addCollection(collection); + else if (method === "UPDATE") response = await updateCollection(collection); + else console.log("Unknown method."); if (response) toggleCollectionModal(); }; return (
    -

    - Sharing & Collaboration -

    -

    Make Public

    - {collection?.members[0]?.user ? ( + + {collection?.members[0]?.user && ( <>

    (All Members have Read access to this collection.)

    -
    {collection.members .sort((a, b) => (a.userId as number) - (b.userId as number)) @@ -297,12 +299,12 @@ export default function TeamManagement({ })}
    - ) : null} + )}
    diff --git a/components/Modal/Collection/index.tsx b/components/Modal/Collection/index.tsx new file mode 100644 index 0000000..993b526 --- /dev/null +++ b/components/Modal/Collection/index.tsx @@ -0,0 +1,102 @@ +import { Tab } from "@headlessui/react"; +import CollectionInfo from "./CollectionInfo"; +import { CollectionIncludingMembers } from "@/types/global"; +import TeamManagement from "./TeamManagement"; +import { useState } from "react"; +import DeleteCollection from "./DeleteCollection"; + +type Props = { + toggleCollectionModal: Function; + activeCollection: CollectionIncludingMembers; + method: "CREATE" | "UPDATE"; + className?: string; + defaultIndex?: number; +}; + +export default function CollectionModal({ + className, + defaultIndex, + toggleCollectionModal, + activeCollection, + method, +}: Props) { + const [collection, setCollection] = + useState(activeCollection); + + return ( +
    + +

    + {method === "CREATE" && "Add"} Collection{" "} + {method === "UPDATE" && "Settings"} +

    + + {method === "UPDATE" && ( + <> + + selected + ? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" + : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" + } + > + Collection Info + + + selected + ? "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" + } + > + Share & Collaborate + + + selected + ? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" + : "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" + } + > + Delete Collection + + + )} + + + + + + + {method === "UPDATE" && ( + <> + + + + + + + + )} + +
    +
    + ); +} diff --git a/components/Modal/User/ChangePassword.tsx b/components/Modal/User/ChangePassword.tsx new file mode 100644 index 0000000..0f0be11 --- /dev/null +++ b/components/Modal/User/ChangePassword.tsx @@ -0,0 +1,90 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { AccountSettings } from "@/types/global"; +import useAccountStore from "@/store/account"; +import { useSession } from "next-auth/react"; +import { faPenToSquare } from "@fortawesome/free-regular-svg-icons"; +import SubmitButton from "@/components/SubmitButton"; + +type Props = { + togglePasswordFormModal: Function; + setUser: Dispatch>; + user: AccountSettings; +}; + +export default function ChangePassword({ + togglePasswordFormModal, + setUser, + user, +}: Props) { + const [oldPassword, setOldPassword] = useState(""); + const [newPassword, setNewPassword1] = useState(""); + const [newPassword2, setNewPassword2] = useState(""); + + const { account, updateAccount } = useAccountStore(); + const { update } = useSession(); + + useEffect(() => { + if ( + !(oldPassword == "" || newPassword == "" || newPassword2 == "") && + newPassword === newPassword2 + ) { + setUser({ ...user, oldPassword, newPassword }); + } + }, [oldPassword, newPassword, newPassword2]); + + const submit = async () => { + if (oldPassword == "" || newPassword == "" || newPassword2 == "") { + console.log("Please fill all the fields."); + } else if (newPassword === newPassword2) { + const response = await updateAccount({ + ...user, + }); + + if (user.email !== account.email || user.name !== account.name) + update({ email: user.email, name: user.name }); + + if (response) { + setUser({ ...user, oldPassword: undefined, newPassword: undefined }); + togglePasswordFormModal(); + } + } else { + console.log("Passwords do not match."); + } + }; + + return ( +
    +

    Old Password

    + + setOldPassword(e.target.value)} + type="password" + className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" + /> +

    New Password

    + + setNewPassword1(e.target.value)} + type="password" + className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" + /> +

    Re-enter New Password

    + + setNewPassword2(e.target.value)} + type="password" + className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100" + /> + + +
    + ); +} diff --git a/components/Modal/User/PrivacySettings.tsx b/components/Modal/User/PrivacySettings.tsx new file mode 100644 index 0000000..74544cf --- /dev/null +++ b/components/Modal/User/PrivacySettings.tsx @@ -0,0 +1,102 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import Checkbox from "../../Checkbox"; +import useAccountStore from "@/store/account"; +import { AccountSettings } from "@/types/global"; +import { useSession } from "next-auth/react"; +import { faPenToSquare } from "@fortawesome/free-regular-svg-icons"; +import SubmitButton from "../../SubmitButton"; + +type Props = { + toggleSettingsModal: Function; + setUser: Dispatch>; + user: AccountSettings; +}; + +export default function PrivacySettings({ + toggleSettingsModal, + setUser, + user, +}: Props) { + const { update } = useSession(); + const { account, updateAccount } = useAccountStore(); + + const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState( + user.whitelistedUsers.join(", ") + ); + + useEffect(() => { + setUser({ + ...user, + whitelistedUsers: stringToArray(whitelistedUsersTextbox), + }); + }, [whitelistedUsersTextbox]); + + useEffect(() => { + setUser({ ...user, oldPassword: undefined, newPassword: undefined }); + }, []); + + const stringToArray = (str: string) => { + const stringWithoutSpaces = str.replace(/\s+/g, ""); + + const wordsArray = stringWithoutSpaces.split(","); + + return wordsArray; + }; + + const submit = async () => { + const response = await updateAccount({ + ...user, + }); + + setUser({ ...user, oldPassword: undefined, newPassword: undefined }); + + if (user.email !== account.email || user.name !== account.name) + update({ email: user.email, name: user.name }); + + if (response) toggleSettingsModal(); + }; + + return ( +
    +
    +

    Profile Visibility

    + + setUser({ ...user, isPrivate: !user.isPrivate })} + /> + +

    + This will limit who can find and add you to other Collections. +

    + + {user.isPrivate && ( +
    +

    Whitelisted Users

    +

    + Please provide the Email addresses of the users you wish to grant + visibility to your profile. Separated by comma. +

    +