tab-seperated modals + eslint fix + much more bug fixed and improvements

This commit is contained in:
Daniel 2023-06-10 02:01:14 +03:30
parent dcdef77387
commit 2df4aad077
64 changed files with 713 additions and 373 deletions

View File

@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/exhaustive-deps": "off"
}
}

View File

@ -1,5 +1,3 @@
# LinkWarden
## A place for your useful links.
# Linkwarden
Rebuilding things from ground up...

View File

@ -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);

View File

@ -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 ? (
<Modal toggleModal={toggleEditCollectionModal}>
<CollectionInfo
<CollectionModal
toggleCollectionModal={toggleEditCollectionModal}
activeCollection={collection}
method="UPDATE"
@ -135,17 +133,21 @@ export default function ({
) : null}
{collectionMembersModal ? (
<Modal toggleModal={toggleCollectionMembersModal}>
<TeamManagement
<CollectionModal
defaultIndex={1}
toggleCollectionModal={toggleCollectionMembersModal}
activeCollection={collection}
method="UPDATE"
/>
</Modal>
) : null}
{deleteCollectionModal ? (
<Modal toggleModal={toggleDeleteCollectionModal}>
<DeleteCollection
collection={collection}
toggleDeleteCollectionModal={toggleDeleteCollectionModal}
<CollectionModal
defaultIndex={2}
activeCollection={collection}
toggleCollectionModal={toggleDeleteCollectionModal}
method="UPDATE"
/>
</Modal>
) : null}

View File

@ -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;

View File

@ -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) {
</div>
<div className="flex gap-2 items-center flex-wrap">
<p className="text-gray-500">{formattedDate}</p>
<a href={link.url} target="_blank" className="group/url">
<a
href={link.url}
target="_blank"
rel="noreferrer"
className="group/url"
>
<div className="text-gray-500 font-bold flex items-center gap-1">
<p>{url.host}</p>
<FontAwesomeIcon
@ -134,6 +139,7 @@ export default function ({ link, count }: Props) {
link.screenshotPath
)}`}
target="_blank"
rel="noreferrer"
title="Screenshot"
>
<FontAwesomeIcon
@ -146,6 +152,7 @@ export default function ({ link, count }: Props) {
link.pdfPath
)}`}
target="_blank"
rel="noreferrer"
title="PDF"
>
<FontAwesomeIcon

View File

@ -15,7 +15,7 @@ type Props = {
| undefined;
};
export default function ({ onChange, defaultValue }: Props) {
export default function CollectionSelection({ onChange, defaultValue }: Props) {
const { collections } = useCollectionStore();
const router = useRouter();

View File

@ -12,7 +12,7 @@ type Props = {
}[];
};
export default function ({ onChange, defaultValue }: Props) {
export default function TagSelection({ onChange, defaultValue }: Props) {
const { tags } = useTagStore();
const [options, setOptions] = useState<Options[]>([]);

View File

@ -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) {
</div>
<div className="flex gap-2 items-center flex-wrap">
<p className="text-gray-500">{formattedDate}</p>
<a href={link.url} target="_blank" className="group/url">
<a
href={link.url}
target="_blank"
rel="noreferrer"
className="group/url"
>
<div className="text-sky-400 font-bold flex items-center gap-1">
<p>{url.host}</p>
<FontAwesomeIcon
@ -158,6 +163,7 @@ export default function ({ link, count }: Props) {
link.screenshotPath
)}`}
target="_blank"
rel="noreferrer"
title="Screenshot"
>
<FontAwesomeIcon
@ -170,6 +176,7 @@ export default function ({ link, count }: Props) {
link.pdfPath
)}`}
target="_blank"
rel="noreferrer"
title="PDF"
>
<FontAwesomeIcon

View File

@ -1,4 +1,4 @@
export default function () {
export default function Loading() {
return (
<div>
<p>Loading...</p>

View File

@ -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 (
<div className="sm:w-[33rem] w-72">
<div className="max-w-sm mx-auto flex flex-col gap-3">
<p className="text-xl text-sky-500 mb-2 text-center">Change Password</p>
<p className="text-sm text-sky-500">Old Password</p>
<input
value={oldPassword}
onChange={(e) => 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"
/>
<p className="text-sm text-sky-500">New Password</p>
<input
value={newPassword1}
onChange={(e) => 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"
/>
<p className="text-sm text-sky-500">Re-enter New Password</p>
<input
value={newPassword2}
onChange={(e) => 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"
/>
<div
className="mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none duration-100 bg-sky-500 hover:bg-sky-400 cursor-pointer"
onClick={submit}
>
Save
</div>
</div>
</div>
);
}

View File

@ -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<SetStateAction<CollectionIncludingMembers>>;
collection: CollectionIncludingMembers;
method: "CREATE" | "UPDATE";
};
export default function CollectionInfo({
toggleCollectionModal,
activeCollection,
setCollection,
collection,
method,
}: Props) {
const [collection, setCollection] =
useState<CollectionIncludingMembers>(activeCollection);
const { updateCollection, addCollection } = useCollectionStore();
const submit = async () => {
@ -41,10 +40,6 @@ export default function CollectionInfo({
return (
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
<p className="text-xl text-sky-500 mb-2 text-center">
{method === "CREATE" ? "Add" : "Edit"} Collection
</p>
<div className="flex flex-col sm:flex-row gap-3">
<div className="w-full">
<p className="text-sm text-sky-500 mb-2">

View File

@ -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,13 +31,45 @@ export default function DeleteCollection({
};
return (
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
<p className="text-xl text-sky-500 mb-2 text-center">Delete Collection</p>
<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>
<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>
<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:
To confirm, type &quot;
<span className="font-bold text-sky-500">{collection.name}</span>
&quot; in the box below:
</p>
<input
@ -46,8 +78,9 @@ export default function DeleteCollection({
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"
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 ${

View File

@ -1,8 +1,9 @@
import React, { useState } from "react";
import { Dispatch, SetStateAction, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faClose,
faPenToSquare,
faPlus,
faUserPlus,
} from "@fortawesome/free-solid-svg-icons";
import useCollectionStore from "@/store/collections";
@ -15,16 +16,17 @@ import ProfilePhoto from "@/components/ProfilePhoto";
type Props = {
toggleCollectionModal: Function;
activeCollection: CollectionIncludingMembers;
setCollection: Dispatch<SetStateAction<CollectionIncludingMembers>>;
collection: CollectionIncludingMembers;
method: "CREATE" | "UPDATE";
};
export default function TeamManagement({
toggleCollectionModal,
activeCollection,
setCollection,
collection,
method,
}: Props) {
const [collection, setCollection] =
useState<CollectionIncludingMembers>(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 (
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
<p className="text-xl text-sky-500 mb-2 text-center w-5/6 mx-auto">
Sharing & Collaboration
</p>
<p className="text-sm text-sky-500">Make Public</p>
<Checkbox
@ -153,12 +155,12 @@ export default function TeamManagement({
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
</div>
</div>
{collection?.members[0]?.user ? (
{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">
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
@ -297,12 +299,12 @@ export default function TeamManagement({
})}
</div>
</>
) : null}
)}
<SubmitButton
onClick={submit}
label="Edit Collection"
icon={faPenToSquare}
label={method === "CREATE" ? "Add Collection" : "Edit Collection"}
icon={method === "CREATE" ? faPlus : faPenToSquare}
className="mx-auto mt-2"
/>
</div>

View File

@ -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<CollectionIncludingMembers>(activeCollection);
return (
<div className={className}>
<Tab.Group defaultIndex={defaultIndex}>
<p
className={`text-xl text-sky-500 text-center ${
method === "UPDATE" && "mb-5"
}`}
>
{method === "CREATE" && "Add"} Collection{" "}
{method === "UPDATE" && "Settings"}
</p>
<Tab.List className="flex justify-center flex-col sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-600">
{method === "UPDATE" && (
<>
<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
? "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
</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.List>
<Tab.Panels>
<Tab.Panel>
<CollectionInfo
toggleCollectionModal={toggleCollectionModal}
setCollection={setCollection}
collection={collection}
method={method}
/>
</Tab.Panel>
{method === "UPDATE" && (
<>
<Tab.Panel>
<TeamManagement
toggleCollectionModal={toggleCollectionModal}
setCollection={setCollection}
collection={collection}
method={method}
/>
</Tab.Panel>
<Tab.Panel>
<DeleteCollection
toggleDeleteCollectionModal={toggleCollectionModal}
collection={collection}
/>
</Tab.Panel>
</>
)}
</Tab.Panels>
</Tab.Group>
</div>
);
}

View File

@ -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<SetStateAction<AccountSettings>>;
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 (
<div className="mx-auto flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
<p className="text-sm text-sky-500">Old Password</p>
<input
value={oldPassword}
onChange={(e) => 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"
/>
<p className="text-sm text-sky-500">New Password</p>
<input
value={newPassword}
onChange={(e) => 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"
/>
<p className="text-sm text-sky-500">Re-enter New Password</p>
<input
value={newPassword2}
onChange={(e) => 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"
/>
<SubmitButton
onClick={submit}
label="Apply Settings"
icon={faPenToSquare}
className="mx-auto mt-2"
/>
</div>
);
}

View File

@ -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<SetStateAction<AccountSettings>>;
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 (
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
<div>
<p className="text-sm text-sky-500 mb-2">Profile Visibility</p>
<Checkbox
label="Make profile private"
state={user.isPrivate}
className="text-sm sm:text-base"
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
/>
<p className="text-gray-500 text-sm">
This will limit who can find and add you to other Collections.
</p>
{user.isPrivate && (
<div>
<p className="text-sm text-sky-500 my-2">Whitelisted Users</p>
<p className="text-gray-500 text-sm mb-3">
Please provide the Email addresses of the users you wish to grant
visibility to your profile. Separated by comma.
</p>
<textarea
className="w-full resize-none border rounded-md duration-100 bg-white p-2 outline-none border-sky-100 focus:border-sky-500"
placeholder="Your profile is hidden from everyone right now..."
value={whitelistedUsersTextbox}
onChange={(e) => {
setWhiteListedUsersTextbox(e.target.value);
}}
/>
</div>
)}
</div>
<SubmitButton
onClick={submit}
label="Apply Settings"
icon={faPenToSquare}
className="mx-auto mt-2"
/>
</div>
);
}

View File

@ -1,57 +1,28 @@
import { useEffect, useState } from "react";
import { Dispatch, SetStateAction, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser, faClose } from "@fortawesome/free-solid-svg-icons";
import Checkbox from "../Checkbox";
import { faClose } from "@fortawesome/free-solid-svg-icons";
import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global";
import { useSession } from "next-auth/react";
import { resizeImage } from "@/lib/client/resizeImage";
import Modal from ".";
import ChangePassword from "./ChangePassword";
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import SubmitButton from "../SubmitButton";
import ProfilePhoto from "../ProfilePhoto";
import SubmitButton from "../../SubmitButton";
import ProfilePhoto from "../../ProfilePhoto";
type Props = {
toggleSettingsModal: Function;
setUser: Dispatch<SetStateAction<AccountSettings>>;
user: AccountSettings;
};
export default function UserSettings({ toggleSettingsModal }: Props) {
export default function ProfileSettings({
toggleSettingsModal,
setUser,
user,
}: Props) {
const { update } = useSession();
const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>({
...account,
});
const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState(
user.whitelistedUsers.join(", ")
);
const [passwordFormModal, setPasswordFormModal] = useState(false);
const togglePasswordFormModal = () => {
setPasswordFormModal(!passwordFormModal);
};
const setPasswordForm = (oldPassword?: string, newPassword?: string) => {
setUser({ ...user, oldPassword, newPassword });
};
useEffect(() => {
setUser({
...user,
whitelistedUsers: stringToArray(whitelistedUsersTextbox),
});
}, [whitelistedUsersTextbox]);
const stringToArray = (str: string) => {
const stringWithoutSpaces = str.replace(/\s+/g, "");
const wordsArray = stringWithoutSpaces.split(",");
return wordsArray;
};
const handleImageUpload = async (e: any) => {
const file: File = e.target.files[0];
@ -79,12 +50,16 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
}
};
useEffect(() => {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
}, []);
const submit = async () => {
const response = await updateAccount({
...user,
});
setPasswordForm(undefined, undefined);
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
if (user.email !== account.email || user.name !== account.name)
update({ email: user.email, name: user.name });
@ -93,54 +68,10 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
};
return (
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
<p className="text-xl text-sky-500 mb-2 text-center">Settings</p>
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
<div className="flex flex-col gap-3">
<div>
<p className="text-sm text-sky-500 mb-2">Display Name</p>
<input
type="text"
value={user.name}
onChange={(e) => setUser({ ...user, name: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
<div>
<p className="text-sm text-sky-500 mb-2">Email</p>
<input
type="text"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
<div>
<p className="text-sm text-sky-500 mb-2">Password</p>
<div className="w-fit" onClick={togglePasswordFormModal}>
<div className="border border-sky-100 rounded-md bg-white px-2 py-1 text-center select-none cursor-pointer text-sky-900 duration-100 hover:border-sky-500">
Change Password
</div>
</div>
{user.newPassword && user.oldPassword ? (
<p className="text-gray-500 text-sm mt-2">
Password modified. Please click{" "}
<span className=" whitespace-nowrap">"Apply Settings"</span> to
apply the changes..
</p>
) : null}
</div>
</div>
<div className="sm:row-span-2 sm:justify-self-center mb-3">
<p className="text-sm text-sky-500 mb-2 sm:text-center">
Profile Photo
</p>
<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">
<ProfilePhoto
src={user.profilePic}
@ -179,6 +110,28 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
</div>
</div>
</div>
<div className="flex flex-col gap-3">
<div>
<p className="text-sm text-sky-500 mb-2">Display Name</p>
<input
type="text"
value={user.name}
onChange={(e) => setUser({ ...user, name: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
<div>
<p className="text-sm text-sky-500 mb-2">Email</p>
<input
type="text"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
</div>
</div>
{/* <hr /> TODO: Export functionality
@ -190,56 +143,12 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
Export Data
</div>
</div> */}
<hr />
<p className="text-sm text-sky-500 mb-2">Profile Visibility</p>
<Checkbox
label="Make profile private"
state={user.isPrivate}
className="text-sm sm:text-base"
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
/>
<p className="text-gray-500 text-sm">
This will limit who can find and add you to other Collections.
</p>
{user.isPrivate ? (
<div>
<p className="text-sm text-sky-500 mb-2">Whitelisted Users</p>
<p className="text-gray-500 text-sm mb-3">
Please provide the Email addresses of the users you wish to grant
visibility to your profile. Separated by comma.
</p>
<textarea
className="w-full resize-none border rounded-md duration-100 bg-white p-2 outline-none border-sky-100 focus:border-sky-500"
placeholder="Your profile is hidden from everyone right now..."
value={whitelistedUsersTextbox}
onChange={(e) => {
setWhiteListedUsersTextbox(e.target.value);
}}
/>
</div>
) : null}
<SubmitButton
onClick={submit}
label="Apply Settings"
icon={faPenToSquare}
className="mx-auto mt-2"
/>
{passwordFormModal ? (
<Modal toggleModal={togglePasswordFormModal}>
<ChangePassword
user={user}
togglePasswordFormModal={togglePasswordFormModal}
setPasswordForm={setPasswordForm}
/>
</Modal>
) : null}
</div>
);
}

View File

@ -0,0 +1,88 @@
import { Tab } from "@headlessui/react";
import { AccountSettings } from "@/types/global";
import { useState } from "react";
import ChangePassword from "./ChangePassword";
import ProfileSettings from "./ProfileSettings";
import PrivacySettings from "./PrivacySettings";
type Props = {
toggleSettingsModal: Function;
activeUser: AccountSettings;
className?: string;
defaultIndex?: number;
};
export default function UserModal({
className,
defaultIndex,
toggleSettingsModal,
activeUser,
}: Props) {
const [user, setUser] = useState<AccountSettings>(activeUser);
return (
<div className={className}>
<Tab.Group defaultIndex={defaultIndex}>
<p className="text-xl text-sky-500 mb-5 text-center">
Account Settings
</p>
<Tab.List className="flex justify-center flex-col sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-600">
<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"
}
>
Profile Settings
</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"
}
>
Privacy Settings
</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"
}
>
Password
</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<ProfileSettings
toggleSettingsModal={toggleSettingsModal}
setUser={setUser}
user={user}
/>
</Tab.Panel>
<Tab.Panel>
<PrivacySettings
toggleSettingsModal={toggleSettingsModal}
setUser={setUser}
user={user}
/>
</Tab.Panel>
<Tab.Panel>
<ChangePassword
togglePasswordFormModal={toggleSettingsModal}
setUser={setUser}
user={user}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
);
}

View File

@ -6,12 +6,16 @@ import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
type Props = {
toggleModal: Function;
children: ReactNode;
className?: string;
};
export default function ({ toggleModal, children }: Props) {
export default function Modal({ toggleModal, className, children }: Props) {
return (
<div className="overflow-y-auto py-2 fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 flex items-center fade-in z-30">
<ClickAwayHandler onClickOutside={toggleModal} className="w-fit m-auto">
<ClickAwayHandler
onClickOutside={toggleModal}
className={`w-fit m-auto h-[50rem] ${className}`}
>
<div className="slide-up relative border-sky-100 rounded-2xl border-solid border shadow-lg p-5 bg-white">
<div
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}

View File

@ -2,7 +2,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { signOut } from "next-auth/react";
import {
faPlus,
faCircleUser,
faChevronDown,
faBars,
} from "@fortawesome/free-solid-svg-icons";
@ -14,11 +13,11 @@ import ClickAwayHandler from "@/components/ClickAwayHandler";
import Sidebar from "@/components/Sidebar";
import { useRouter } from "next/router";
import Search from "@/components/Search";
import UserSettings from "./Modal/UserSettings";
import UserModal from "@/components/Modal/User";
import useAccountStore from "@/store/account";
import ProfilePhoto from "./ProfilePhoto";
import ProfilePhoto from "@/components/ProfilePhoto";
export default function () {
export default function Navbar() {
const { account } = useAccountStore();
const [profileDropdown, setProfileDropdown] = useState(false);
@ -131,7 +130,10 @@ export default function () {
{settingsModal ? (
<Modal toggleModal={toggleSettingsModal}>
<UserSettings toggleSettingsModal={toggleSettingsModal} />
<UserModal
toggleSettingsModal={toggleSettingsModal}
activeUser={account}
/>
</Modal>
) : null}

View File

@ -1,33 +1,41 @@
import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser } from "@fortawesome/free-solid-svg-icons";
import Image from "next/image";
import avatarExists from "@/lib/client/avatarExists";
type Props = {
src: string;
className?: string;
emptyImage?: boolean;
};
export default function ProfilePhoto({ src, className }: Props) {
const [error, setError] = useState(false);
export default function ProfilePhoto({ src, className, emptyImage }: Props) {
const [error, setError] = useState<boolean>(emptyImage || true);
const checkAvatarExistence = async () => {
const canPass = await avatarExists(src);
setError(!canPass);
};
useEffect(() => {
setError(false);
if (src) checkAvatarExistence();
}, [src]);
return error || !src ? (
return error ? (
<div
className={`bg-sky-500 text-white h-10 w-10 shadow rounded-full border-[3px] border-slate-200 flex items-center justify-center ${className}`}
>
<FontAwesomeIcon icon={faUser} className="w-1/2 h-1/2" />
</div>
) : (
<img
alt=""
<Image
alt="Avatar"
src={src}
height={112}
width={112}
className={`h-10 w-10 shadow rounded-full border-[3px] border-slate-200 ${className}`}
onError={() => {
setError(true);
}}
/>
);
}

View File

@ -11,7 +11,7 @@ type Props = {
count: number;
};
export default function ({ link, count }: Props) {
export default function LinkCard({ link, count }: Props) {
const url = new URL(link.url);
const formattedDate = new Date(
link.createdAt as unknown as string
@ -22,7 +22,7 @@ export default function ({ link, count }: Props) {
});
return (
<a href={link.url} target="_blank" className="rounded-3xl">
<a href={link.url} target="_blank" rel="noreferrer" className="rounded-3xl">
<div className="bg-gradient-to-tr from-slate-200 from-10% to-gray-50 via-20% shadow-md sm:hover:shadow-none duration-100 rounded-3xl cursor-pointer p-5 flex items-start relative gap-5 sm:gap-10 group/item">
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}

View File

@ -12,7 +12,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
export default function ({ className }: { className?: string }) {
export default function Sidebar({ className }: { className?: string }) {
const { collections } = useCollectionStore();
const { tags } = useTagStore();

View File

@ -5,7 +5,7 @@ import useTagStore from "@/store/tags";
import useLinkStore from "@/store/links";
import useAccountStore from "@/store/account";
export default function () {
export default function useInitialData() {
const { status, data } = useSession();
const { setCollections } = useCollectionStore();
const { setTags } = useTagStore();

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
export default function () {
export default function useRedirect() {
const router = useRouter();
const { status } = useSession();
const [redirect, setRedirect] = useState(true);

View File

@ -3,18 +3,18 @@ import { useSession } from "next-auth/react";
import Loader from "../components/Loader";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import getInitialData from "@/lib/client/getInitialData";
import useInitialData from "@/hooks/useInitialData";
interface Props {
children: ReactNode;
}
export default function ({ children }: Props) {
export default function AuthRedirect({ children }: Props) {
const router = useRouter();
const { status } = useSession();
const [redirect, setRedirect] = useState(true);
getInitialData();
useInitialData();
useEffect(() => {
if (!router.pathname.startsWith("/public")) {

View File

@ -10,7 +10,7 @@ interface Props {
children: ReactNode;
}
export default function ({ children }: Props) {
export default function MainLayout({ children }: Props) {
const { status } = useSession();
const router = useRouter();
const redirect = useRedirect();

View File

@ -4,7 +4,11 @@ import puppeteer from "puppeteer-extra";
import AdblockerPlugin from "puppeteer-extra-plugin-adblocker";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
export default async (url: string, collectionId: number, linkId: number) => {
export default async function archive(
url: string,
collectionId: number,
linkId: number
) {
const archivePath = `data/archives/${collectionId}/${linkId}`;
const browser = await puppeteer.launch();
@ -38,7 +42,7 @@ export default async (url: string, collectionId: number, linkId: number) => {
console.log(err);
await browser.close();
}
};
}
const autoScroll = async (page: Page) => {
await page.evaluate(async () => {

View File

@ -2,7 +2,10 @@ import { prisma } from "@/lib/api/db";
import getPermission from "@/lib/api/getPermission";
import fs from "fs";
export default async function (collection: { id: number }, userId: number) {
export default async function deleteCollection(
collection: { id: number },
userId: number
) {
if (!collection.id)
return { response: "Please choose a valid collection.", status: 401 };

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
export default async function (userId: number) {
export default async function getCollection(userId: number) {
const collections = await prisma.collection.findMany({
where: {
OR: [

View File

@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
import { CollectionIncludingMembers } from "@/types/global";
import { existsSync, mkdirSync } from "fs";
export default async function (
export default async function postCollection(
collection: CollectionIncludingMembers,
userId: number
) {

View File

@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
import { CollectionIncludingMembers } from "@/types/global";
import getPermission from "@/lib/api/getPermission";
export default async function (
export default async function updateCollection(
collection: CollectionIncludingMembers,
userId: number
) {

View File

@ -4,7 +4,7 @@ import fs from "fs";
import { Link, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
export default async function (
export default async function deleteLink(
link: LinkIncludingCollectionAndTags,
userId: number
) {

View File

@ -1,5 +1,5 @@
import { prisma } from "@/lib/api/db";
export default async function (userId: number) {
export default async function getLink(userId: number) {
const links = await prisma.link.findMany({
where: {
collection: {

View File

@ -7,7 +7,7 @@ import AES from "crypto-js/aes";
import getPermission from "@/lib/api/getPermission";
import { existsSync, mkdirSync } from "fs";
export default async function (
export default async function postLink(
link: LinkIncludingCollectionAndTags,
userId: number
) {

View File

@ -3,7 +3,7 @@ import { LinkIncludingCollectionAndTags } from "@/types/global";
import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
export default async function (
export default async function updateLink(
link: LinkIncludingCollectionAndTags,
userId: number
) {

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
export default async function (collectionId: number) {
export default async function getCollection(collectionId: number) {
let data;
const collection = await prisma.collection.findFirst({

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
export default async function (userId: number) {
export default async function getTags(userId: number) {
// remove empty tags
await prisma.tag.deleteMany({
where: {

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
export default async function (
export default async function getUser(
lookupEmail: string,
isSelf: boolean,
userEmail: string

View File

@ -4,7 +4,40 @@ import fs from "fs";
import path from "path";
import bcrypt from "bcrypt";
export default async function (user: AccountSettings, userId: number) {
export default async function updateUser(
user: AccountSettings,
userId: number
) {
// Password Settings
if (user.newPassword && user.oldPassword) {
const targetUser = await prisma.user.findUnique({
where: {
id: user.id,
},
});
if (
targetUser &&
bcrypt.compareSync(user.oldPassword, targetUser.password)
) {
const saltRounds = 10;
const newHashedPassword = bcrypt.hashSync(user.newPassword, saltRounds);
await prisma.user.update({
where: {
id: userId,
},
data: {
password: newHashedPassword,
},
});
} else {
return { response: "Old password is incorrect.", status: 400 };
}
}
// Avatar Settings
const profilePic = user.profilePic;
if (profilePic.startsWith("data:image/jpeg;base64")) {
@ -25,6 +58,10 @@ export default async function (user: AccountSettings, userId: number) {
}
} else {
console.log("A file larger than 1.5MB was uploaded.");
return {
response: "A file larger than 1.5MB was uploaded.",
status: 400,
};
}
} else if (profilePic == "") {
fs.unlink(`data/uploads/avatar/${userId}.jpg`, (err) => {
@ -32,6 +69,8 @@ export default async function (user: AccountSettings, userId: number) {
});
}
// Other settings
const updatedUser = await prisma.user.update({
where: {
id: userId,
@ -44,25 +83,6 @@ export default async function (user: AccountSettings, userId: number) {
},
});
if (user.newPassword && user.oldPassword) {
const saltRounds = 10;
if (bcrypt.compareSync(user.oldPassword, updatedUser.password)) {
const newHashedPassword = bcrypt.hashSync(user.newPassword, saltRounds);
await prisma.user.update({
where: {
id: userId,
},
data: {
password: newHashedPassword,
},
});
} else {
return { response: "Passwords do not match.", status: 403 };
}
}
const { password, ...userInfo } = updatedUser;
const response: Omit<AccountSettings, "password"> = {

View File

@ -1,6 +1,9 @@
import { prisma } from "@/lib/api/db";
export default async (userId: number, collectionId: number) => {
export default async function getPermission(
userId: number,
collectionId: number
) {
const check = await prisma.collection.findFirst({
where: {
AND: {
@ -12,4 +15,4 @@ export default async (userId: number, collectionId: number) => {
});
return check;
};
}

View File

@ -1,4 +1,4 @@
export default async (url: string) => {
export default async function getTitle(url: string) {
const response = await fetch(url);
const text = await response.text();
@ -6,4 +6,4 @@ export default async (url: string) => {
let match = text.match(/<title.*>([^<]*)<\/title>/);
if (match) return match[1];
else return "";
};
}

View File

@ -0,0 +1,4 @@
export default async function avatarExists(fileUrl: string): Promise<boolean> {
const response = await fetch(fileUrl, { method: "HEAD" });
return !(response.headers.get("content-type") === "text/plain");
}

View File

@ -1,4 +1,4 @@
export default async function (email: string) {
export default async function getPublicUserDataByEmail(email: string) {
const response = await fetch(`/api/routes/users?email=${email}`);
const data = await response.json();

View File

@ -17,6 +17,7 @@
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.15",
"@next/font": "13.1.6",
"@prisma/client": "^4.9.0",
"@types/crypto-js": "^4.1.1",

View File

@ -7,7 +7,7 @@ import path from "path";
import fs from "fs";
import getPermission from "@/lib/api/getPermission";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (!req.query.params)
return res.status(401).json({ response: "Invalid parameters." });

View File

@ -12,7 +12,7 @@ interface User {
password: string;
}
export default async function (
export default async function Index(
req: NextApiRequest,
res: NextApiResponse<Data>
) {

View File

@ -5,7 +5,7 @@ import { prisma } from "@/lib/api/db";
import path from "path";
import fs from "fs";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
const userId = session?.user.id;

View File

@ -1,7 +1,10 @@
import getCollection from "@/lib/api/controllers/public/getCollection";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
const collectionId = Number(req.query.collectionId);
if (!collectionId) {

View File

@ -6,7 +6,10 @@ import postCollection from "@/lib/api/controllers/collections/postCollection";
import updateCollection from "@/lib/api/controllers/collections/updateCollection";
import deleteCollection from "@/lib/api/controllers/collections/deleteCollection";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) {

View File

@ -6,7 +6,7 @@ import postLink from "@/lib/api/controllers/links/postLink";
import deleteLink from "@/lib/api/controllers/links/deleteLink";
import updateLink from "@/lib/api/controllers/links/updateLink";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function links(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) {

View File

@ -3,7 +3,7 @@ import { getServerSession } from "next-auth/next";
import { authOptions } from "pages/api/auth/[...nextauth]";
import getTags from "@/lib/api/controllers/tags/getTags";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) {

View File

@ -4,7 +4,7 @@ import { authOptions } from "pages/api/auth/[...nextauth]";
import getUsers from "@/lib/api/controllers/users/getUsers";
import updateUser from "@/lib/api/controllers/users/updateUser";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function users(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.email) {

View File

@ -2,8 +2,7 @@ import Dropdown from "@/components/Dropdown";
import LinkCard from "@/components/LinkCard";
import Modal from "@/components/Modal";
import LinkModal from "@/components/Modal/LinkModal";
import CollectionInfo from "@/components/Modal/Collection/CollectionInfo";
import DeleteCollection from "@/components/Modal/Collection/DeleteCollection";
import CollectionModal from "@/components/Modal/Collection";
import useCollectionStore from "@/store/collections";
import useLinkStore from "@/store/links";
import { CollectionIncludingMembers } from "@/types/global";
@ -18,10 +17,9 @@ import { ChangeEvent, useEffect, useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import { useSession } from "next-auth/react";
import ProfilePhoto from "@/components/ProfilePhoto";
import TeamManagement from "@/components/Modal/Collection/TeamManagement";
import SortLinkDropdown from "@/components/SortLinkDropdown";
export default function () {
export default function Index() {
const router = useRouter();
const { links } = useLinkStore();
@ -250,8 +248,11 @@ export default function () {
) : null}
{collectionInfoModal && activeCollection ? (
<Modal toggleModal={toggleCollectionInfoModal}>
<CollectionInfo
<Modal
toggleModal={toggleCollectionInfoModal}
className="h-[35rem]"
>
<CollectionModal
toggleCollectionModal={toggleCollectionInfoModal}
activeCollection={activeCollection}
method="UPDATE"
@ -260,19 +261,29 @@ export default function () {
) : null}
{collectionMembersModal && activeCollection ? (
<Modal toggleModal={toggleCollectionMembersModal}>
<TeamManagement
<Modal
toggleModal={toggleCollectionMembersModal}
className="h-[35rem]"
>
<CollectionModal
defaultIndex={1}
toggleCollectionModal={toggleCollectionMembersModal}
activeCollection={activeCollection}
method="UPDATE"
/>
</Modal>
) : null}
{deleteCollectionModal && activeCollection ? (
<Modal toggleModal={toggleDeleteCollectionModal}>
<DeleteCollection
collection={activeCollection}
toggleDeleteCollectionModal={toggleDeleteCollectionModal}
<Modal
toggleModal={toggleDeleteCollectionModal}
className="h-[35rem]"
>
<CollectionModal
defaultIndex={2}
toggleCollectionModal={toggleDeleteCollectionModal}
activeCollection={activeCollection}
method="UPDATE"
/>
</Modal>
) : null}

View File

@ -13,10 +13,10 @@ import Modal from "@/components/Modal";
import MainLayout from "@/layouts/MainLayout";
import ClickAwayHandler from "@/components/ClickAwayHandler";
import RadioButton from "@/components/RadioButton";
import CollectionInfo from "@/components/Modal/Collection/CollectionInfo";
import CollectionModal from "@/components/Modal/Collection";
import { useSession } from "next-auth/react";
export default function () {
export default function Collections() {
const { collections } = useCollectionStore();
const [expandDropdown, setExpandDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
@ -212,8 +212,8 @@ export default function () {
</div>
{collectionModal ? (
<Modal toggleModal={toggleCollectionModal}>
<CollectionInfo
<Modal toggleModal={toggleCollectionModal} className="h-[35rem]">
<CollectionModal
activeCollection={{
name: "",
description: "",

View File

@ -9,7 +9,7 @@ import Link from "next/link";
import CollectionItem from "@/components/Dashboard/CollectionItem";
import { useEffect, useState } from "react";
export default function () {
export default function Dashboard() {
const { collections } = useCollectionStore();
const { links } = useLinkStore();
const { tags } = useTagStore();
@ -29,7 +29,7 @@ export default function () {
);
// console.log(links.length);
}, [collections]);
}, [collections, links]);
return (
// ml-80

View File

@ -1,6 +1,5 @@
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
interface FormData {
@ -8,9 +7,7 @@ interface FormData {
password: string;
}
export default function () {
const router = useRouter();
export default function Login() {
const [form, setForm] = useState<FormData>({
email: "",
password: "",

View File

@ -46,7 +46,7 @@ export default function PublicCollections() {
</div>
<div className="flex flex-col gap-5 my-8">
{data?.links.map((e, i) => {
{data?.links?.map((e, i) => {
return <LinkCard key={i} link={e} count={i} />;
})}
</div>

View File

@ -8,7 +8,7 @@ interface FormData {
password: string;
}
export default function () {
export default function Register() {
const router = useRouter();
const [form, setForm] = useState<FormData>({

View File

@ -42,18 +42,17 @@ export default function Links() {
setSortBy(event.target.value);
};
const { name, url, title, collection, tags } = searchFilter;
useEffect(() => {
const linksArray = [
...links.filter((link) => {
if (
(name && link.name.toLowerCase().includes(routeQuery)) ||
(url && link.url.toLowerCase().includes(routeQuery)) ||
(title && link.title.toLowerCase().includes(routeQuery)) ||
(collection &&
(searchFilter.name && link.name.toLowerCase().includes(routeQuery)) ||
(searchFilter.url && link.url.toLowerCase().includes(routeQuery)) ||
(searchFilter.title &&
link.title.toLowerCase().includes(routeQuery)) ||
(searchFilter.collection &&
link.collection.name.toLowerCase().includes(routeQuery)) ||
(tags &&
(searchFilter.tags &&
link.tags.some((tag) =>
tag.name.toLowerCase().includes(routeQuery)
))

View File

@ -9,7 +9,7 @@ import { Tag } from "@prisma/client";
import useTagStore from "@/store/tags";
import SortLinkDropdown from "@/components/SortLinkDropdown";
export default function () {
export default function Index() {
const router = useRouter();
const { links } = useLinkStore();

View File

@ -85,7 +85,7 @@
/* For react-colorful */
.color-picker .react-colorful {
width: 7.5rem;
width: 100%;
height: 7.5rem;
}
.color-picker .react-colorful__hue {

View File

@ -248,6 +248,13 @@
dependencies:
prop-types "^15.8.1"
"@headlessui/react@^1.7.15":
version "1.7.15"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.15.tgz#53ef6ae132af81b8f188414767b6e79ebf8dc73f"
integrity sha512-OTO0XtoRQ6JPB1cKNFYBZv2Q0JMqMGNhYP1CjPvcJvjz8YGokz8oAj89HIYZGN0gZzn/4kk9iUpmMF4Q21Gsqw==
dependencies:
client-only "^0.0.1"
"@humanwhocodes/config-array@^0.11.8":
version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@ -1064,7 +1071,7 @@ chromium-bidi@0.4.5:
dependencies:
mitt "3.0.0"
client-only@0.0.1:
client-only@0.0.1, client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==