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
|
# Linkwarden
|
||||||
|
|
||||||
## A place for your useful links.
|
|
||||||
|
|
||||||
Rebuilding things from ground up...
|
Rebuilding things from ground up...
|
||||||
|
|
|
@ -26,7 +26,11 @@ function useOutsideAlerter(
|
||||||
}, [ref, onClickOutside]);
|
}, [ref, onClickOutside]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ({ children, onClickOutside, className }: Props) {
|
export default function ClickAwayHandler({
|
||||||
|
children,
|
||||||
|
onClickOutside,
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
const wrapperRef = useRef(null);
|
const wrapperRef = useRef(null);
|
||||||
useOutsideAlerter(wrapperRef, onClickOutside);
|
useOutsideAlerter(wrapperRef, onClickOutside);
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,10 @@ import useLinkStore from "@/store/links";
|
||||||
import Dropdown from "./Dropdown";
|
import Dropdown from "./Dropdown";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import CollectionInfo from "@/components/Modal/Collection/CollectionInfo";
|
import CollectionModal from "@/components/Modal/Collection";
|
||||||
import DeleteCollection from "@/components/Modal/Collection/DeleteCollection";
|
|
||||||
import ProfilePhoto from "./ProfilePhoto";
|
import ProfilePhoto from "./ProfilePhoto";
|
||||||
import TeamManagement from "./Modal/Collection/TeamManagement";
|
|
||||||
|
|
||||||
export default function ({
|
export default function CollectionCard({
|
||||||
collection,
|
collection,
|
||||||
}: {
|
}: {
|
||||||
collection: CollectionIncludingMembers;
|
collection: CollectionIncludingMembers;
|
||||||
|
@ -126,7 +124,7 @@ export default function ({
|
||||||
) : null}
|
) : null}
|
||||||
{editCollectionModal ? (
|
{editCollectionModal ? (
|
||||||
<Modal toggleModal={toggleEditCollectionModal}>
|
<Modal toggleModal={toggleEditCollectionModal}>
|
||||||
<CollectionInfo
|
<CollectionModal
|
||||||
toggleCollectionModal={toggleEditCollectionModal}
|
toggleCollectionModal={toggleEditCollectionModal}
|
||||||
activeCollection={collection}
|
activeCollection={collection}
|
||||||
method="UPDATE"
|
method="UPDATE"
|
||||||
|
@ -135,17 +133,21 @@ export default function ({
|
||||||
) : null}
|
) : null}
|
||||||
{collectionMembersModal ? (
|
{collectionMembersModal ? (
|
||||||
<Modal toggleModal={toggleCollectionMembersModal}>
|
<Modal toggleModal={toggleCollectionMembersModal}>
|
||||||
<TeamManagement
|
<CollectionModal
|
||||||
|
defaultIndex={1}
|
||||||
toggleCollectionModal={toggleCollectionMembersModal}
|
toggleCollectionModal={toggleCollectionMembersModal}
|
||||||
activeCollection={collection}
|
activeCollection={collection}
|
||||||
|
method="UPDATE"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : null}
|
) : null}
|
||||||
{deleteCollectionModal ? (
|
{deleteCollectionModal ? (
|
||||||
<Modal toggleModal={toggleDeleteCollectionModal}>
|
<Modal toggleModal={toggleDeleteCollectionModal}>
|
||||||
<DeleteCollection
|
<CollectionModal
|
||||||
collection={collection}
|
defaultIndex={2}
|
||||||
toggleDeleteCollectionModal={toggleDeleteCollectionModal}
|
activeCollection={collection}
|
||||||
|
toggleCollectionModal={toggleDeleteCollectionModal}
|
||||||
|
method="UPDATE"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
||||||
import { CollectionIncludingMembers } from "@/types/global";
|
import { CollectionIncludingMembers } from "@/types/global";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
|
|
||||||
export default function ({
|
export default function CollectionItem({
|
||||||
collection,
|
collection,
|
||||||
}: {
|
}: {
|
||||||
collection: CollectionIncludingMembers;
|
collection: CollectionIncludingMembers;
|
||||||
|
|
|
@ -19,7 +19,7 @@ type Props = {
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ({ link, count }: Props) {
|
export default function LinkItem({ link, count }: Props) {
|
||||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||||
const [editModal, setEditModal] = useState(false);
|
const [editModal, setEditModal] = useState(false);
|
||||||
|
|
||||||
|
@ -102,7 +102,12 @@ export default function ({ link, count }: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center flex-wrap">
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
<p className="text-gray-500">{formattedDate}</p>
|
<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">
|
<div className="text-gray-500 font-bold flex items-center gap-1">
|
||||||
<p>{url.host}</p>
|
<p>{url.host}</p>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -134,6 +139,7 @@ export default function ({ link, count }: Props) {
|
||||||
link.screenshotPath
|
link.screenshotPath
|
||||||
)}`}
|
)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
title="Screenshot"
|
title="Screenshot"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -146,6 +152,7 @@ export default function ({ link, count }: Props) {
|
||||||
link.pdfPath
|
link.pdfPath
|
||||||
)}`}
|
)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
title="PDF"
|
title="PDF"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
|
|
@ -15,7 +15,7 @@ type Props = {
|
||||||
| undefined;
|
| undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ({ onChange, defaultValue }: Props) {
|
export default function CollectionSelection({ onChange, defaultValue }: Props) {
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
const router = useRouter();
|
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 { tags } = useTagStore();
|
||||||
|
|
||||||
const [options, setOptions] = useState<Options[]>([]);
|
const [options, setOptions] = useState<Options[]>([]);
|
||||||
|
|
|
@ -23,7 +23,7 @@ type Props = {
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ({ link, count }: Props) {
|
export default function LinkCard({ link, count }: Props) {
|
||||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||||
const [editModal, setEditModal] = useState(false);
|
const [editModal, setEditModal] = useState(false);
|
||||||
|
|
||||||
|
@ -126,7 +126,12 @@ export default function ({ link, count }: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center flex-wrap">
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
<p className="text-gray-500">{formattedDate}</p>
|
<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">
|
<div className="text-sky-400 font-bold flex items-center gap-1">
|
||||||
<p>{url.host}</p>
|
<p>{url.host}</p>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -158,6 +163,7 @@ export default function ({ link, count }: Props) {
|
||||||
link.screenshotPath
|
link.screenshotPath
|
||||||
)}`}
|
)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
title="Screenshot"
|
title="Screenshot"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -170,6 +176,7 @@ export default function ({ link, count }: Props) {
|
||||||
link.pdfPath
|
link.pdfPath
|
||||||
)}`}
|
)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
title="PDF"
|
title="PDF"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function () {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Loading...</p>
|
<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 {
|
import {
|
||||||
faFolder,
|
faFolder,
|
||||||
faPenToSquare,
|
faPenToSquare,
|
||||||
|
@ -13,18 +13,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
toggleCollectionModal: Function;
|
toggleCollectionModal: Function;
|
||||||
activeCollection: CollectionIncludingMembers;
|
setCollection: Dispatch<SetStateAction<CollectionIncludingMembers>>;
|
||||||
|
collection: CollectionIncludingMembers;
|
||||||
method: "CREATE" | "UPDATE";
|
method: "CREATE" | "UPDATE";
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CollectionInfo({
|
export default function CollectionInfo({
|
||||||
toggleCollectionModal,
|
toggleCollectionModal,
|
||||||
activeCollection,
|
setCollection,
|
||||||
|
collection,
|
||||||
method,
|
method,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [collection, setCollection] =
|
|
||||||
useState<CollectionIncludingMembers>(activeCollection);
|
|
||||||
|
|
||||||
const { updateCollection, addCollection } = useCollectionStore();
|
const { updateCollection, addCollection } = useCollectionStore();
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
|
@ -41,10 +40,6 @@ export default function CollectionInfo({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||||
<p className="text-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="flex flex-col sm:flex-row gap-3">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="text-sm text-sky-500 mb-2">
|
<p className="text-sm text-sky-500 mb-2">
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default function DeleteCollection({
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!collection.id) return null;
|
if (!collection.id || collection.name !== inputField) return null;
|
||||||
|
|
||||||
const response = await removeCollection(collection.id);
|
const response = await removeCollection(collection.id);
|
||||||
if (response) {
|
if (response) {
|
||||||
|
@ -31,23 +31,56 @@ export default function DeleteCollection({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
|
||||||
<p className="text-xl text-sky-500 mb-2 text-center">Delete Collection</p>
|
<p className="text-red-500 font-bold text-center">Warning!</p>
|
||||||
|
|
||||||
<p className="text-sky-900 select-none text-center">
|
<div className="max-h-[20rem] overflow-y-auto">
|
||||||
To confirm, type "
|
<div className="text-gray-500">
|
||||||
<span className="font-bold text-sky-500">{collection.name}</span>" in
|
<p>
|
||||||
the box below:
|
Please note that deleting the collection will permanently remove all
|
||||||
</p>
|
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>
|
||||||
|
|
||||||
<input
|
<div className="flex flex-col gap-3">
|
||||||
autoFocus
|
<p className="text-sky-900 select-none text-center">
|
||||||
value={inputField}
|
To confirm, type "
|
||||||
onChange={(e) => setInputField(e.target.value)}
|
<span className="font-bold text-sky-500">{collection.name}</span>
|
||||||
type="text"
|
" in the box below:
|
||||||
placeholder={`Type "${collection.name}" Here.`}
|
</p>
|
||||||
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"
|
|
||||||
/>
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={inputField}
|
||||||
|
onChange={(e) => setInputField(e.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder={`Type "${collection.name}" Here.`}
|
||||||
|
className="w-72 sm:w-96 rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${
|
className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
faClose,
|
faClose,
|
||||||
faPenToSquare,
|
faPenToSquare,
|
||||||
|
faPlus,
|
||||||
faUserPlus,
|
faUserPlus,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
|
@ -15,16 +16,17 @@ import ProfilePhoto from "@/components/ProfilePhoto";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
toggleCollectionModal: Function;
|
toggleCollectionModal: Function;
|
||||||
activeCollection: CollectionIncludingMembers;
|
setCollection: Dispatch<SetStateAction<CollectionIncludingMembers>>;
|
||||||
|
collection: CollectionIncludingMembers;
|
||||||
|
method: "CREATE" | "UPDATE";
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TeamManagement({
|
export default function TeamManagement({
|
||||||
toggleCollectionModal,
|
toggleCollectionModal,
|
||||||
activeCollection,
|
setCollection,
|
||||||
|
collection,
|
||||||
|
method,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [collection, setCollection] =
|
|
||||||
useState<CollectionIncludingMembers>(activeCollection);
|
|
||||||
|
|
||||||
const currentURL = new URL(document.URL);
|
const currentURL = new URL(document.URL);
|
||||||
|
|
||||||
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
|
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
|
||||||
|
@ -39,7 +41,7 @@ export default function TeamManagement({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { updateCollection } = useCollectionStore();
|
const { addCollection, updateCollection } = useCollectionStore();
|
||||||
|
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
|
@ -65,17 +67,17 @@ export default function TeamManagement({
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!collection) return null;
|
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();
|
if (response) toggleCollectionModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||||
<p className="text-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>
|
<p className="text-sm text-sky-500">Make Public</p>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -153,12 +155,12 @@ export default function TeamManagement({
|
||||||
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{collection?.members[0]?.user ? (
|
|
||||||
|
{collection?.members[0]?.user && (
|
||||||
<>
|
<>
|
||||||
<p className="text-center text-gray-500 text-xs sm:text-sm">
|
<p className="text-center text-gray-500 text-xs sm:text-sm">
|
||||||
(All Members have <b>Read</b> access to this collection.)
|
(All Members have <b>Read</b> access to this collection.)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="max-h-[20rem] overflow-auto flex flex-col gap-3 rounded-md shadow-inner">
|
<div className="max-h-[20rem] overflow-auto flex flex-col gap-3 rounded-md shadow-inner">
|
||||||
{collection.members
|
{collection.members
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||||
|
@ -297,12 +299,12 @@ export default function TeamManagement({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
label="Edit Collection"
|
label={method === "CREATE" ? "Add Collection" : "Edit Collection"}
|
||||||
icon={faPenToSquare}
|
icon={method === "CREATE" ? faPlus : faPenToSquare}
|
||||||
className="mx-auto mt-2"
|
className="mx-auto mt-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faUser, faClose } from "@fortawesome/free-solid-svg-icons";
|
import { faClose } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Checkbox from "../Checkbox";
|
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
import { AccountSettings } from "@/types/global";
|
import { AccountSettings } from "@/types/global";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { resizeImage } from "@/lib/client/resizeImage";
|
import { resizeImage } from "@/lib/client/resizeImage";
|
||||||
import Modal from ".";
|
|
||||||
import ChangePassword from "./ChangePassword";
|
|
||||||
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
|
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
|
||||||
import SubmitButton from "../SubmitButton";
|
import SubmitButton from "../../SubmitButton";
|
||||||
import ProfilePhoto from "../ProfilePhoto";
|
import ProfilePhoto from "../../ProfilePhoto";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
toggleSettingsModal: Function;
|
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 { update } = useSession();
|
||||||
const { account, updateAccount } = useAccountStore();
|
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 handleImageUpload = async (e: any) => {
|
||||||
const file: File = e.target.files[0];
|
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 submit = async () => {
|
||||||
const response = await updateAccount({
|
const response = await updateAccount({
|
||||||
...user,
|
...user,
|
||||||
});
|
});
|
||||||
|
|
||||||
setPasswordForm(undefined, undefined);
|
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
|
||||||
|
|
||||||
if (user.email !== account.email || user.name !== account.name)
|
if (user.email !== account.email || user.name !== account.name)
|
||||||
update({ email: user.email, name: user.name });
|
update({ email: user.email, name: user.name });
|
||||||
|
@ -93,54 +68,10 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
|
||||||
<p className="text-xl text-sky-500 mb-2 text-center">Settings</p>
|
|
||||||
|
|
||||||
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
|
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="sm:row-span-2 sm:justify-self-center mx-auto mb-3">
|
||||||
<div>
|
<p className="text-sm text-sky-500 mb-2 text-center">Profile Photo</p>
|
||||||
<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="w-28 h-28 flex items-center justify-center border border-sky-100 rounded-full relative">
|
<div className="w-28 h-28 flex items-center justify-center border border-sky-100 rounded-full relative">
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
src={user.profilePic}
|
src={user.profilePic}
|
||||||
|
@ -179,6 +110,28 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* <hr /> TODO: Export functionality
|
{/* <hr /> TODO: Export functionality
|
||||||
|
@ -190,56 +143,12 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
|
||||||
Export Data
|
Export Data
|
||||||
</div>
|
</div>
|
||||||
</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
|
<SubmitButton
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
label="Apply Settings"
|
label="Apply Settings"
|
||||||
icon={faPenToSquare}
|
icon={faPenToSquare}
|
||||||
className="mx-auto mt-2"
|
className="mx-auto mt-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{passwordFormModal ? (
|
|
||||||
<Modal toggleModal={togglePasswordFormModal}>
|
|
||||||
<ChangePassword
|
|
||||||
user={user}
|
|
||||||
togglePasswordFormModal={togglePasswordFormModal}
|
|
||||||
setPasswordForm={setPasswordForm}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</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 = {
|
type Props = {
|
||||||
toggleModal: Function;
|
toggleModal: Function;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ({ toggleModal, children }: Props) {
|
export default function Modal({ toggleModal, className, children }: Props) {
|
||||||
return (
|
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">
|
<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 className="slide-up relative border-sky-100 rounded-2xl border-solid border shadow-lg p-5 bg-white">
|
||||||
<div
|
<div
|
||||||
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import {
|
import {
|
||||||
faPlus,
|
faPlus,
|
||||||
faCircleUser,
|
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faBars,
|
faBars,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
@ -14,11 +13,11 @@ import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Search from "@/components/Search";
|
import Search from "@/components/Search";
|
||||||
import UserSettings from "./Modal/UserSettings";
|
import UserModal from "@/components/Modal/User";
|
||||||
import useAccountStore from "@/store/account";
|
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 { account } = useAccountStore();
|
||||||
|
|
||||||
const [profileDropdown, setProfileDropdown] = useState(false);
|
const [profileDropdown, setProfileDropdown] = useState(false);
|
||||||
|
@ -131,7 +130,10 @@ export default function () {
|
||||||
|
|
||||||
{settingsModal ? (
|
{settingsModal ? (
|
||||||
<Modal toggleModal={toggleSettingsModal}>
|
<Modal toggleModal={toggleSettingsModal}>
|
||||||
<UserSettings toggleSettingsModal={toggleSettingsModal} />
|
<UserModal
|
||||||
|
toggleSettingsModal={toggleSettingsModal}
|
||||||
|
activeUser={account}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,41 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faUser } from "@fortawesome/free-solid-svg-icons";
|
import { faUser } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import Image from "next/image";
|
||||||
|
import avatarExists from "@/lib/client/avatarExists";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
src: string;
|
src: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
emptyImage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProfilePhoto({ src, className }: Props) {
|
export default function ProfilePhoto({ src, className, emptyImage }: Props) {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState<boolean>(emptyImage || true);
|
||||||
|
|
||||||
|
const checkAvatarExistence = async () => {
|
||||||
|
const canPass = await avatarExists(src);
|
||||||
|
|
||||||
|
setError(!canPass);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(false);
|
if (src) checkAvatarExistence();
|
||||||
}, [src]);
|
}, [src]);
|
||||||
|
|
||||||
return error || !src ? (
|
return error ? (
|
||||||
<div
|
<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}`}
|
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" />
|
<FontAwesomeIcon icon={faUser} className="w-1/2 h-1/2" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<Image
|
||||||
alt=""
|
alt="Avatar"
|
||||||
src={src}
|
src={src}
|
||||||
|
height={112}
|
||||||
|
width={112}
|
||||||
className={`h-10 w-10 shadow rounded-full border-[3px] border-slate-200 ${className}`}
|
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;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ({ link, count }: Props) {
|
export default function LinkCard({ link, count }: Props) {
|
||||||
const url = new URL(link.url);
|
const url = new URL(link.url);
|
||||||
const formattedDate = new Date(
|
const formattedDate = new Date(
|
||||||
link.createdAt as unknown as string
|
link.createdAt as unknown as string
|
||||||
|
@ -22,7 +22,7 @@ export default function ({ link, count }: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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">
|
<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
|
<Image
|
||||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function ({ className }: { className?: string }) {
|
export default function Sidebar({ className }: { className?: string }) {
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
const { tags } = useTagStore();
|
const { tags } = useTagStore();
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import useTagStore from "@/store/tags";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
|
|
||||||
export default function () {
|
export default function useInitialData() {
|
||||||
const { status, data } = useSession();
|
const { status, data } = useSession();
|
||||||
const { setCollections } = useCollectionStore();
|
const { setCollections } = useCollectionStore();
|
||||||
const { setTags } = useTagStore();
|
const { setTags } = useTagStore();
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export default function () {
|
export default function useRedirect() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
const [redirect, setRedirect] = useState(true);
|
const [redirect, setRedirect] = useState(true);
|
||||||
|
|
|
@ -3,18 +3,18 @@ import { useSession } from "next-auth/react";
|
||||||
import Loader from "../components/Loader";
|
import Loader from "../components/Loader";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import getInitialData from "@/lib/client/getInitialData";
|
import useInitialData from "@/hooks/useInitialData";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ({ children }: Props) {
|
export default function AuthRedirect({ children }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
const [redirect, setRedirect] = useState(true);
|
const [redirect, setRedirect] = useState(true);
|
||||||
|
|
||||||
getInitialData();
|
useInitialData();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.pathname.startsWith("/public")) {
|
if (!router.pathname.startsWith("/public")) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ({ children }: Props) {
|
export default function MainLayout({ children }: Props) {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const redirect = useRedirect();
|
const redirect = useRedirect();
|
||||||
|
|
|
@ -4,7 +4,11 @@ import puppeteer from "puppeteer-extra";
|
||||||
import AdblockerPlugin from "puppeteer-extra-plugin-adblocker";
|
import AdblockerPlugin from "puppeteer-extra-plugin-adblocker";
|
||||||
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
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 archivePath = `data/archives/${collectionId}/${linkId}`;
|
||||||
|
|
||||||
const browser = await puppeteer.launch();
|
const browser = await puppeteer.launch();
|
||||||
|
@ -38,7 +42,7 @@ export default async (url: string, collectionId: number, linkId: number) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const autoScroll = async (page: Page) => {
|
const autoScroll = async (page: Page) => {
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
|
|
|
@ -2,7 +2,10 @@ import { prisma } from "@/lib/api/db";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
import fs from "fs";
|
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)
|
if (!collection.id)
|
||||||
return { response: "Please choose a valid collection.", status: 401 };
|
return { response: "Please choose a valid collection.", status: 401 };
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
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({
|
const collections = await prisma.collection.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
|
||||||
import { CollectionIncludingMembers } from "@/types/global";
|
import { CollectionIncludingMembers } from "@/types/global";
|
||||||
import { existsSync, mkdirSync } from "fs";
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
|
||||||
export default async function (
|
export default async function postCollection(
|
||||||
collection: CollectionIncludingMembers,
|
collection: CollectionIncludingMembers,
|
||||||
userId: number
|
userId: number
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
|
||||||
import { CollectionIncludingMembers } from "@/types/global";
|
import { CollectionIncludingMembers } from "@/types/global";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
|
|
||||||
export default async function (
|
export default async function updateCollection(
|
||||||
collection: CollectionIncludingMembers,
|
collection: CollectionIncludingMembers,
|
||||||
userId: number
|
userId: number
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import fs from "fs";
|
||||||
import { Link, UsersAndCollections } from "@prisma/client";
|
import { Link, UsersAndCollections } from "@prisma/client";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
|
|
||||||
export default async function (
|
export default async function deleteLink(
|
||||||
link: LinkIncludingCollectionAndTags,
|
link: LinkIncludingCollectionAndTags,
|
||||||
userId: number
|
userId: number
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
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({
|
const links = await prisma.link.findMany({
|
||||||
where: {
|
where: {
|
||||||
collection: {
|
collection: {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import AES from "crypto-js/aes";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
import { existsSync, mkdirSync } from "fs";
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
|
||||||
export default async function (
|
export default async function postLink(
|
||||||
link: LinkIncludingCollectionAndTags,
|
link: LinkIncludingCollectionAndTags,
|
||||||
userId: number
|
userId: number
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { LinkIncludingCollectionAndTags } from "@/types/global";
|
||||||
import { UsersAndCollections } from "@prisma/client";
|
import { UsersAndCollections } from "@prisma/client";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
|
|
||||||
export default async function (
|
export default async function updateLink(
|
||||||
link: LinkIncludingCollectionAndTags,
|
link: LinkIncludingCollectionAndTags,
|
||||||
userId: number
|
userId: number
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
export default async function (collectionId: number) {
|
export default async function getCollection(collectionId: number) {
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
const collection = await prisma.collection.findFirst({
|
const collection = await prisma.collection.findFirst({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
export default async function (userId: number) {
|
export default async function getTags(userId: number) {
|
||||||
// remove empty tags
|
// remove empty tags
|
||||||
await prisma.tag.deleteMany({
|
await prisma.tag.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
export default async function (
|
export default async function getUser(
|
||||||
lookupEmail: string,
|
lookupEmail: string,
|
||||||
isSelf: boolean,
|
isSelf: boolean,
|
||||||
userEmail: string
|
userEmail: string
|
||||||
|
|
|
@ -4,7 +4,40 @@ import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import bcrypt from "bcrypt";
|
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;
|
const profilePic = user.profilePic;
|
||||||
|
|
||||||
if (profilePic.startsWith("data:image/jpeg;base64")) {
|
if (profilePic.startsWith("data:image/jpeg;base64")) {
|
||||||
|
@ -25,6 +58,10 @@ export default async function (user: AccountSettings, userId: number) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("A file larger than 1.5MB was uploaded.");
|
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 == "") {
|
} else if (profilePic == "") {
|
||||||
fs.unlink(`data/uploads/avatar/${userId}.jpg`, (err) => {
|
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({
|
const updatedUser = await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
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 { password, ...userInfo } = updatedUser;
|
||||||
|
|
||||||
const response: Omit<AccountSettings, "password"> = {
|
const response: Omit<AccountSettings, "password"> = {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
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({
|
const check = await prisma.collection.findFirst({
|
||||||
where: {
|
where: {
|
||||||
AND: {
|
AND: {
|
||||||
|
@ -12,4 +15,4 @@ export default async (userId: number, collectionId: number) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return check;
|
return check;
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default async (url: string) => {
|
export default async function getTitle(url: string) {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
|
@ -6,4 +6,4 @@ export default async (url: string) => {
|
||||||
let match = text.match(/<title.*>([^<]*)<\/title>/);
|
let match = text.match(/<title.*>([^<]*)<\/title>/);
|
||||||
if (match) return match[1];
|
if (match) return match[1];
|
||||||
else return "";
|
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 response = await fetch(`/api/routes/users?email=${email}`);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@headlessui/react": "^1.7.15",
|
||||||
"@next/font": "13.1.6",
|
"@next/font": "13.1.6",
|
||||||
"@prisma/client": "^4.9.0",
|
"@prisma/client": "^4.9.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
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)
|
if (!req.query.params)
|
||||||
return res.status(401).json({ response: "Invalid parameters." });
|
return res.status(401).json({ response: "Invalid parameters." });
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface User {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function (
|
export default async function Index(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<Data>
|
res: NextApiResponse<Data>
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { prisma } from "@/lib/api/db";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
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 session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
const userId = session?.user.id;
|
const userId = session?.user.id;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import getCollection from "@/lib/api/controllers/public/getCollection";
|
import getCollection from "@/lib/api/controllers/public/getCollection";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
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);
|
const collectionId = Number(req.query.collectionId);
|
||||||
|
|
||||||
if (!collectionId) {
|
if (!collectionId) {
|
||||||
|
|
|
@ -6,7 +6,10 @@ import postCollection from "@/lib/api/controllers/collections/postCollection";
|
||||||
import updateCollection from "@/lib/api/controllers/collections/updateCollection";
|
import updateCollection from "@/lib/api/controllers/collections/updateCollection";
|
||||||
import deleteCollection from "@/lib/api/controllers/collections/deleteCollection";
|
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);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
if (!session?.user?.email) {
|
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 deleteLink from "@/lib/api/controllers/links/deleteLink";
|
||||||
import updateLink from "@/lib/api/controllers/links/updateLink";
|
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);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
if (!session?.user?.email) {
|
if (!session?.user?.email) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { getServerSession } from "next-auth/next";
|
||||||
import { authOptions } from "pages/api/auth/[...nextauth]";
|
import { authOptions } from "pages/api/auth/[...nextauth]";
|
||||||
import getTags from "@/lib/api/controllers/tags/getTags";
|
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);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
if (!session?.user?.email) {
|
if (!session?.user?.email) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { authOptions } from "pages/api/auth/[...nextauth]";
|
||||||
import getUsers from "@/lib/api/controllers/users/getUsers";
|
import getUsers from "@/lib/api/controllers/users/getUsers";
|
||||||
import updateUser from "@/lib/api/controllers/users/updateUser";
|
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);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
if (!session?.user?.email) {
|
if (!session?.user?.email) {
|
||||||
|
|
|
@ -2,8 +2,7 @@ import Dropdown from "@/components/Dropdown";
|
||||||
import LinkCard from "@/components/LinkCard";
|
import LinkCard from "@/components/LinkCard";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import LinkModal from "@/components/Modal/LinkModal";
|
import LinkModal from "@/components/Modal/LinkModal";
|
||||||
import CollectionInfo from "@/components/Modal/Collection/CollectionInfo";
|
import CollectionModal from "@/components/Modal/Collection";
|
||||||
import DeleteCollection from "@/components/Modal/Collection/DeleteCollection";
|
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { CollectionIncludingMembers } from "@/types/global";
|
import { CollectionIncludingMembers } from "@/types/global";
|
||||||
|
@ -18,10 +17,9 @@ import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||||
import TeamManagement from "@/components/Modal/Collection/TeamManagement";
|
|
||||||
import SortLinkDropdown from "@/components/SortLinkDropdown";
|
import SortLinkDropdown from "@/components/SortLinkDropdown";
|
||||||
|
|
||||||
export default function () {
|
export default function Index() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
|
@ -250,8 +248,11 @@ export default function () {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{collectionInfoModal && activeCollection ? (
|
{collectionInfoModal && activeCollection ? (
|
||||||
<Modal toggleModal={toggleCollectionInfoModal}>
|
<Modal
|
||||||
<CollectionInfo
|
toggleModal={toggleCollectionInfoModal}
|
||||||
|
className="h-[35rem]"
|
||||||
|
>
|
||||||
|
<CollectionModal
|
||||||
toggleCollectionModal={toggleCollectionInfoModal}
|
toggleCollectionModal={toggleCollectionInfoModal}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
method="UPDATE"
|
method="UPDATE"
|
||||||
|
@ -260,19 +261,29 @@ export default function () {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{collectionMembersModal && activeCollection ? (
|
{collectionMembersModal && activeCollection ? (
|
||||||
<Modal toggleModal={toggleCollectionMembersModal}>
|
<Modal
|
||||||
<TeamManagement
|
toggleModal={toggleCollectionMembersModal}
|
||||||
|
className="h-[35rem]"
|
||||||
|
>
|
||||||
|
<CollectionModal
|
||||||
|
defaultIndex={1}
|
||||||
toggleCollectionModal={toggleCollectionMembersModal}
|
toggleCollectionModal={toggleCollectionMembersModal}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
|
method="UPDATE"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{deleteCollectionModal && activeCollection ? (
|
{deleteCollectionModal && activeCollection ? (
|
||||||
<Modal toggleModal={toggleDeleteCollectionModal}>
|
<Modal
|
||||||
<DeleteCollection
|
toggleModal={toggleDeleteCollectionModal}
|
||||||
collection={activeCollection}
|
className="h-[35rem]"
|
||||||
toggleDeleteCollectionModal={toggleDeleteCollectionModal}
|
>
|
||||||
|
<CollectionModal
|
||||||
|
defaultIndex={2}
|
||||||
|
toggleCollectionModal={toggleDeleteCollectionModal}
|
||||||
|
activeCollection={activeCollection}
|
||||||
|
method="UPDATE"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -13,10 +13,10 @@ import Modal from "@/components/Modal";
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
import RadioButton from "@/components/RadioButton";
|
import RadioButton from "@/components/RadioButton";
|
||||||
import CollectionInfo from "@/components/Modal/Collection/CollectionInfo";
|
import CollectionModal from "@/components/Modal/Collection";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
export default function () {
|
export default function Collections() {
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
const [sortDropdown, setSortDropdown] = useState(false);
|
||||||
|
@ -212,8 +212,8 @@ export default function () {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{collectionModal ? (
|
{collectionModal ? (
|
||||||
<Modal toggleModal={toggleCollectionModal}>
|
<Modal toggleModal={toggleCollectionModal} className="h-[35rem]">
|
||||||
<CollectionInfo
|
<CollectionModal
|
||||||
activeCollection={{
|
activeCollection={{
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Link from "next/link";
|
||||||
import CollectionItem from "@/components/Dashboard/CollectionItem";
|
import CollectionItem from "@/components/Dashboard/CollectionItem";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function () {
|
export default function Dashboard() {
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
const { tags } = useTagStore();
|
const { tags } = useTagStore();
|
||||||
|
@ -29,7 +29,7 @@ export default function () {
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log(links.length);
|
// console.log(links.length);
|
||||||
}, [collections]);
|
}, [collections, links]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// ml-80
|
// ml-80
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
|
@ -8,9 +7,7 @@ interface FormData {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function Login() {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [form, setForm] = useState<FormData>({
|
const [form, setForm] = useState<FormData>({
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default function PublicCollections() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5 my-8">
|
<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} />;
|
return <LinkCard key={i} link={e} count={i} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface FormData {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function Register() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [form, setForm] = useState<FormData>({
|
const [form, setForm] = useState<FormData>({
|
||||||
|
|
|
@ -42,18 +42,17 @@ export default function Links() {
|
||||||
setSortBy(event.target.value);
|
setSortBy(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { name, url, title, collection, tags } = searchFilter;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const linksArray = [
|
const linksArray = [
|
||||||
...links.filter((link) => {
|
...links.filter((link) => {
|
||||||
if (
|
if (
|
||||||
(name && link.name.toLowerCase().includes(routeQuery)) ||
|
(searchFilter.name && link.name.toLowerCase().includes(routeQuery)) ||
|
||||||
(url && link.url.toLowerCase().includes(routeQuery)) ||
|
(searchFilter.url && link.url.toLowerCase().includes(routeQuery)) ||
|
||||||
(title && link.title.toLowerCase().includes(routeQuery)) ||
|
(searchFilter.title &&
|
||||||
(collection &&
|
link.title.toLowerCase().includes(routeQuery)) ||
|
||||||
|
(searchFilter.collection &&
|
||||||
link.collection.name.toLowerCase().includes(routeQuery)) ||
|
link.collection.name.toLowerCase().includes(routeQuery)) ||
|
||||||
(tags &&
|
(searchFilter.tags &&
|
||||||
link.tags.some((tag) =>
|
link.tags.some((tag) =>
|
||||||
tag.name.toLowerCase().includes(routeQuery)
|
tag.name.toLowerCase().includes(routeQuery)
|
||||||
))
|
))
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Tag } from "@prisma/client";
|
||||||
import useTagStore from "@/store/tags";
|
import useTagStore from "@/store/tags";
|
||||||
import SortLinkDropdown from "@/components/SortLinkDropdown";
|
import SortLinkDropdown from "@/components/SortLinkDropdown";
|
||||||
|
|
||||||
export default function () {
|
export default function Index() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
|
|
||||||
/* For react-colorful */
|
/* For react-colorful */
|
||||||
.color-picker .react-colorful {
|
.color-picker .react-colorful {
|
||||||
width: 7.5rem;
|
width: 100%;
|
||||||
height: 7.5rem;
|
height: 7.5rem;
|
||||||
}
|
}
|
||||||
.color-picker .react-colorful__hue {
|
.color-picker .react-colorful__hue {
|
||||||
|
|
|
@ -248,6 +248,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.8.1"
|
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":
|
"@humanwhocodes/config-array@^0.11.8":
|
||||||
version "0.11.8"
|
version "0.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
|
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:
|
dependencies:
|
||||||
mitt "3.0.0"
|
mitt "3.0.0"
|
||||||
|
|
||||||
client-only@0.0.1:
|
client-only@0.0.1, client-only@^0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
Ŝarĝante…
Reference in New Issue