tab-seperated modals + eslint fix + much more bug fixed and improvements
This commit is contained in:
parent
dcdef77387
commit
2df4aad077
|
@ -1,3 +1,6 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# LinkWarden
|
||||
|
||||
## A place for your useful links.
|
||||
# Linkwarden
|
||||
|
||||
Rebuilding things from ground up...
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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[]>([]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function () {
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div>
|
||||
<p>Loading...</p>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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 "
|
||||
<span className="font-bold text-sky-500">{collection.name}</span>
|
||||
" 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 ${
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>}
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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`}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
|
@ -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);
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"> = {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 "";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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." });
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ interface User {
|
|||
password: string;
|
||||
}
|
||||
|
||||
export default async function (
|
||||
export default async function Index(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data>
|
||||
) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: "",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: "",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -8,7 +8,7 @@ interface FormData {
|
|||
password: string;
|
||||
}
|
||||
|
||||
export default function () {
|
||||
export default function Register() {
|
||||
const router = useRouter();
|
||||
|
||||
const [form, setForm] = useState<FormData>({
|
||||
|
|
|
@ -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)
|
||||
))
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
|
||||
/* For react-colorful */
|
||||
.color-picker .react-colorful {
|
||||
width: 7.5rem;
|
||||
width: 100%;
|
||||
height: 7.5rem;
|
||||
}
|
||||
.color-picker .react-colorful__hue {
|
||||
|
|
|
@ -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==
|
||||
|
|
Ŝarĝante…
Reference in New Issue