added toasts popups + improved login/signup page + many more changes and improvements

This commit is contained in:
Daniel 2023-06-27 02:03:40 +03:30
parent 0ddd9079bf
commit f1bd48be83
28 changed files with 464 additions and 199 deletions

View File

@ -13,6 +13,7 @@ import useAccountStore from "@/store/account";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons"; import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import { toast } from "react-hot-toast";
type Props = { type Props = {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
@ -48,6 +49,35 @@ export default function LinkCard({ link, count, className }: Props) {
const { removeLink, updateLink } = useLinkStore(); const { removeLink, updateLink } = useLinkStore();
const pinLink = async () => {
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
const load = toast.loading("Applying...");
setExpandDropdown(false);
const response = await updateLink({
...link,
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
});
toast.dismiss(load);
response.ok &&
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
};
const deleteLink = async () => {
const load = toast.loading("Deleting...");
const response = await removeLink(link);
toast.dismiss(load);
response.ok && toast.success(`Link Deleted.`);
setExpandDropdown(false);
};
const url = new URL(link.url); const url = new URL(link.url);
const formattedDate = new Date(link.createdAt as string).toLocaleString( const formattedDate = new Date(link.createdAt as string).toLocaleString(
"en-US", "en-US",
@ -97,7 +127,7 @@ export default function LinkCard({ link, count, className }: Props) {
width={64} width={64}
height={64} height={64}
alt="" alt=""
className="blur-sm absolute w-16 group-hover:scale-50 group-hover:blur-none group-hover:opacity-100 duration-100 rounded-md bottom-5 right-5 opacity-60 select-none" className="blur-sm absolute w-16 group-hover:opacity-80 duration-100 rounded-md bottom-5 right-5 opacity-60 select-none"
draggable="false" draggable="false"
onError={(e) => { onError={(e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
@ -141,16 +171,7 @@ export default function LinkCard({ link, count, className }: Props) {
link?.pinnedBy && link.pinnedBy[0] link?.pinnedBy && link.pinnedBy[0]
? "Unpin" ? "Unpin"
: "Pin to Dashboard", : "Pin to Dashboard",
onClick: () => { onClick: pinLink,
updateLink({
...link,
pinnedBy:
link?.pinnedBy && link.pinnedBy[0]
? undefined
: [{ id: account.id }],
});
setExpandDropdown(false);
},
} }
: undefined, : undefined,
permissions === true || permissions?.canUpdate permissions === true || permissions?.canUpdate
@ -173,10 +194,7 @@ export default function LinkCard({ link, count, className }: Props) {
permissions === true || permissions?.canDelete permissions === true || permissions?.canDelete
? { ? {
name: "Delete", name: "Delete",
onClick: () => { onClick: deleteLink,
removeLink(link);
setExpandDropdown(false);
},
} }
: undefined, : undefined,
]} ]}

View File

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import { import {
faFolder, faFolder,
faPenToSquare, faPenToSquare,
@ -10,6 +10,7 @@ import RequiredBadge from "../../RequiredBadge";
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import { HexColorPicker } from "react-colorful"; import { HexColorPicker } from "react-colorful";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { toast } from "react-hot-toast";
type Props = { type Props = {
toggleCollectionModal: Function; toggleCollectionModal: Function;
@ -26,18 +27,33 @@ export default function CollectionInfo({
collection, collection,
method, method,
}: Props) { }: Props) {
const [submitLoader, setSubmitLoader] = useState(false);
const { updateCollection, addCollection } = useCollectionStore(); const { updateCollection, addCollection } = useCollectionStore();
const submit = async () => { const submit = async () => {
if (!collection) return null; if (!collection) return null;
let response = null; setSubmitLoader(true);
const load = toast.loading(
method === "UPDATE" ? "Applying..." : "Creating..."
);
let response;
if (method === "CREATE") response = await addCollection(collection); if (method === "CREATE") response = await addCollection(collection);
else if (method === "UPDATE") response = await updateCollection(collection); else response = await updateCollection(collection);
else console.log("Unknown method.");
if (response) toggleCollectionModal(); toast.dismiss(load);
if (response.ok) {
toast.success(
`Collection ${method === "UPDATE" ? "Saved!" : "Created!"}`
);
toggleCollectionModal();
} else toast.error(response.data as string);
setSubmitLoader(false);
}; };
return ( return (
@ -102,6 +118,7 @@ export default function CollectionInfo({
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
loading={submitLoader}
label={method === "CREATE" ? "Add" : "Save"} label={method === "CREATE" ? "Add" : "Save"}
icon={method === "CREATE" ? faPlus : faPenToSquare} icon={method === "CREATE" ? faPlus : faPenToSquare}
className="mx-auto mt-2" className="mx-auto mt-2"

View File

@ -8,6 +8,7 @@ import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import { toast } from "react-hot-toast";
type Props = { type Props = {
toggleDeleteCollectionModal: Function; toggleDeleteCollectionModal: Function;
@ -27,8 +28,14 @@ export default function DeleteCollection({
const submit = async () => { const submit = async () => {
if (permissions === true) if (collection.name !== inputField) return null; if (permissions === true) if (collection.name !== inputField) return null;
const load = toast.loading("Deleting...");
const response = await removeCollection(collection.id as number); const response = await removeCollection(collection.id as number);
if (response) {
toast.dismiss(load);
if (response.ok) {
toast.success("Collection Deleted.");
toggleDeleteCollectionModal(); toggleDeleteCollectionModal();
router.push("/collections"); router.push("/collections");
} }

View File

@ -14,6 +14,7 @@ import Checkbox from "../../Checkbox";
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import ProfilePhoto from "@/components/ProfilePhoto"; import ProfilePhoto from "@/components/ProfilePhoto";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import { toast } from "react-hot-toast";
type Props = { type Props = {
toggleCollectionModal: Function; toggleCollectionModal: Function;
@ -69,16 +70,30 @@ export default function TeamManagement({
}); });
}; };
const [submitLoader, setSubmitLoader] = useState(false);
const submit = async () => { const submit = async () => {
if (!collection) return null; if (!collection) return null;
let response = null; setSubmitLoader(true);
const load = toast.loading(
method === "UPDATE" ? "Applying..." : "Creating..."
);
let response;
if (method === "CREATE") response = await addCollection(collection); if (method === "CREATE") response = await addCollection(collection);
else if (method === "UPDATE") response = await updateCollection(collection); else response = await updateCollection(collection);
else console.log("Unknown method.");
if (response) toggleCollectionModal(); toast.dismiss(load);
if (response.ok) {
toast.success("Collection Saved!");
toggleCollectionModal();
} else toast.error(response.data as string);
setSubmitLoader(false);
}; };
return ( return (
@ -111,7 +126,7 @@ export default function TeamManagement({
try { try {
navigator.clipboard navigator.clipboard
.writeText(publicCollectionURL) .writeText(publicCollectionURL)
.then(() => console.log("Copied!")); .then(() => toast.success("Copied!"));
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@ -379,6 +394,7 @@ export default function TeamManagement({
{permissions === true && ( {permissions === true && (
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
loading={submitLoader}
label={method === "CREATE" ? "Add" : "Save"} label={method === "CREATE" ? "Add" : "Save"}
icon={method === "CREATE" ? faPlus : faPenToSquare} icon={method === "CREATE" ? faPlus : faPenToSquare}
className="mx-auto mt-2" className="mx-auto mt-2"

View File

@ -10,6 +10,7 @@ import { useSession } from "next-auth/react";
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import SubmitButton from "../../SubmitButton"; import SubmitButton from "../../SubmitButton";
import { toast } from "react-hot-toast";
type Props = type Props =
| { | {
@ -28,6 +29,8 @@ export default function EditLink({
method, method,
activeLink, activeLink,
}: Props) { }: Props) {
const [submitLoader, setSubmitLoader] = useState(false);
const { data } = useSession(); const { data } = useSession();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>( const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>(
@ -88,12 +91,26 @@ export default function EditLink({
}; };
const submit = async () => { const submit = async () => {
setSubmitLoader(true);
let response; let response;
const load = toast.loading(
method === "UPDATE" ? "Applying..." : "Creating..."
);
if (method === "UPDATE") response = await updateLink(link); if (method === "UPDATE") response = await updateLink(link);
else if (method === "CREATE") response = await addLink(link); else response = await addLink(link);
response && toggleLinkModal(); toast.dismiss(load);
if (response.ok) {
toast.success(`Link ${method === "UPDATE" ? "Saved!" : "Created!"}`);
toggleLinkModal();
} else toast.error(response.data as string);
setSubmitLoader(false);
return response;
}; };
return ( return (
@ -188,7 +205,8 @@ export default function EditLink({
onClick={submit} onClick={submit}
label={method === "CREATE" ? "Add" : "Save"} label={method === "CREATE" ? "Add" : "Save"}
icon={method === "CREATE" ? faPlus : faPenToSquare} icon={method === "CREATE" ? faPlus : faPenToSquare}
className="mx-auto mt-2" loading={submitLoader}
className={`mx-auto mt-2`}
/> />
</div> </div>
); );

View File

@ -116,12 +116,12 @@ export default function LinkDetails({ link }: Props) {
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">
{!imageError && ( {!imageError && (
<div id="link-banner" className="link-banner h-44 -mx-5 -mt-5 relative"> <div id="link-banner" className="link-banner h-32 -mx-5 -mt-5 relative">
<div id="link-banner-inner" className="link-banner-inner"></div> <div id="link-banner-inner" className="link-banner-inner"></div>
</div> </div>
)} )}
<div <div
className={`relative flex gap-5 items-start ${!imageError && "-mt-32"}`} className={`relative flex gap-5 items-start ${!imageError && "-mt-24"}`}
> >
{!imageError && ( {!imageError && (
<Image <Image

View File

@ -4,6 +4,7 @@ import useAccountStore from "@/store/account";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons"; import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import { toast } from "react-hot-toast";
type Props = { type Props = {
togglePasswordFormModal: Function; togglePasswordFormModal: Function;
@ -20,6 +21,8 @@ export default function ChangePassword({
const [newPassword, setNewPassword1] = useState(""); const [newPassword, setNewPassword1] = useState("");
const [newPassword2, setNewPassword2] = useState(""); const [newPassword2, setNewPassword2] = useState("");
const [submitLoader, setSubmitLoader] = useState(false);
const { account, updateAccount } = useAccountStore(); const { account, updateAccount } = useAccountStore();
const { update } = useSession(); const { update } = useSession();
@ -34,57 +37,73 @@ export default function ChangePassword({
const submit = async () => { const submit = async () => {
if (oldPassword == "" || newPassword == "" || newPassword2 == "") { if (oldPassword == "" || newPassword == "" || newPassword2 == "") {
console.log("Please fill all the fields."); toast.error("Please fill all the fields.");
} else if (newPassword === newPassword2) { } else if (newPassword === newPassword2) {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({ const response = await updateAccount({
...user, ...user,
}); });
toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
togglePasswordFormModal();
} else toast.error(response.data as string);
setSubmitLoader(false);
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 });
if (response) { if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); setUser({ ...user, oldPassword: undefined, newPassword: undefined });
togglePasswordFormModal(); togglePasswordFormModal();
} }
} else { } else {
console.log("Passwords do not match."); toast.error("Passwords do not match.");
} }
}; };
return ( return (
<div className="mx-auto flex flex-col gap-3 justify-between sm:w-[35rem] w-80"> <div className="mx-auto sm:w-[35rem] w-80">
<p className="text-sm text-sky-500">Old Password</p> <div className="max-w-[25rem] w-full mx-auto flex flex-col gap-3 justify-between">
<p className="text-sm text-sky-500">Old Password</p>
<input <input
value={oldPassword} value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)} onChange={(e) => setOldPassword(e.target.value)}
type="password" 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" 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> <p className="text-sm text-sky-500">New Password</p>
<input <input
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword1(e.target.value)} onChange={(e) => setNewPassword1(e.target.value)}
type="password" 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" 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> <p className="text-sm text-sky-500">Re-enter New Password</p>
<input <input
value={newPassword2} value={newPassword2}
onChange={(e) => setNewPassword2(e.target.value)} onChange={(e) => setNewPassword2(e.target.value)}
type="password" 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" className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/> />
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
label="Apply Settings" loading={submitLoader}
icon={faPenToSquare} label="Apply Settings"
className="mx-auto mt-2" icon={faPenToSquare}
/> className="mx-auto mt-2"
/>
</div>
</div> </div>
); );
} }

View File

@ -5,6 +5,7 @@ import { AccountSettings } from "@/types/global";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
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 { toast } from "react-hot-toast";
type Props = { type Props = {
toggleSettingsModal: Function; toggleSettingsModal: Function;
@ -20,6 +21,8 @@ export default function PrivacySettings({
const { update } = useSession(); const { update } = useSession();
const { account, updateAccount } = useAccountStore(); const { account, updateAccount } = useAccountStore();
const [submitLoader, setSubmitLoader] = useState(false);
const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState( const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState(
user.whitelistedUsers.join(", ") user.whitelistedUsers.join(", ")
); );
@ -44,16 +47,30 @@ export default function PrivacySettings({
}; };
const submit = async () => { const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({ const response = await updateAccount({
...user, ...user,
}); });
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
toggleSettingsModal();
} else toast.error(response.data as string);
setSubmitLoader(false);
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 });
if (response) toggleSettingsModal(); if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
toggleSettingsModal();
}
}; };
return ( return (
@ -93,6 +110,7 @@ export default function PrivacySettings({
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
loading={submitLoader}
label="Apply Settings" label="Apply Settings"
icon={faPenToSquare} icon={faPenToSquare}
className="mx-auto mt-2" className="mx-auto mt-2"

View File

@ -8,6 +8,7 @@ import { resizeImage } from "@/lib/client/resizeImage";
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";
import { toast } from "react-hot-toast";
type Props = { type Props = {
toggleSettingsModal: Function; toggleSettingsModal: Function;
@ -24,6 +25,8 @@ export default function ProfileSettings({
const { account, updateAccount } = useAccountStore(); const { account, updateAccount } = useAccountStore();
const [profileStatus, setProfileStatus] = useState(true); const [profileStatus, setProfileStatus] = useState(true);
const [submitLoader, setSubmitLoader] = useState(false);
const handleProfileStatus = (e: boolean) => { const handleProfileStatus = (e: boolean) => {
setProfileStatus(!e); setProfileStatus(!e);
}; };
@ -48,10 +51,10 @@ export default function ProfileSettings({
reader.readAsDataURL(resizedFile); reader.readAsDataURL(resizedFile);
} else { } else {
console.log("Please select a PNG or JPEG file thats less than 1MB."); toast.error("Please select a PNG or JPEG file thats less than 1MB.");
} }
} else { } else {
console.log("Invalid file format."); toast.error("Invalid file format.");
} }
}; };
@ -60,16 +63,30 @@ export default function ProfileSettings({
}, []); }, []);
const submit = async () => { const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({ const response = await updateAccount({
...user, ...user,
}); });
setUser({ ...user, oldPassword: undefined, newPassword: undefined }); toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
toggleSettingsModal();
} else toast.error(response.data as string);
setSubmitLoader(false);
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 });
if (response) toggleSettingsModal(); if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
toggleSettingsModal();
}
}; };
return ( return (
@ -151,6 +168,7 @@ export default function ProfileSettings({
</div> */} </div> */}
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
loading={submitLoader}
label="Apply Settings" label="Apply Settings"
icon={faPenToSquare} icon={faPenToSquare}
className="mx-auto mt-2" className="mx-auto mt-2"

View File

@ -41,7 +41,7 @@ export default function ModalManagement() {
<CollectionModal <CollectionModal
toggleCollectionModal={toggleModal} toggleCollectionModal={toggleModal}
method={modal.method} method={modal.method}
isOwner={modal.isOwner} isOwner={modal.isOwner as boolean}
defaultIndex={modal.defaultIndex} defaultIndex={modal.defaultIndex}
activeCollection={ activeCollection={
modal.active as CollectionIncludingMembersAndLinkCount modal.active as CollectionIncludingMembersAndLinkCount

View File

@ -2,6 +2,7 @@ import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
export default function Search() { export default function Search() {
const router = useRouter(); const router = useRouter();
@ -35,7 +36,7 @@ export default function Search() {
value={searchQuery} value={searchQuery}
onChange={(e) => { onChange={(e) => {
e.target.value.includes("%") && e.target.value.includes("%") &&
console.log("The search query should not contain '%'."); toast.error("The search query should not contain '%'.");
setSearchQuery(e.target.value.replace("%", "")); setSearchQuery(e.target.value.replace("%", ""));
}} }}
onKeyDown={(e) => onKeyDown={(e) =>

View File

@ -1,11 +1,11 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition } from "@fortawesome/free-regular-svg-icons"; import { IconDefinition } from "@fortawesome/free-regular-svg-icons";
import { MouseEventHandler } from "react";
type Props = { type Props = {
onClick: Function; onClick: Function;
icon: IconDefinition; icon?: IconDefinition;
label: string; label: string;
loading: boolean;
className?: string; className?: string;
}; };
@ -13,15 +13,22 @@ export default function SubmitButton({
onClick, onClick,
icon, icon,
label, label,
loading,
className, className,
}: Props) { }: Props) {
return ( return (
<div <div
className={`bg-sky-500 text-white flex items-center gap-2 py-2 px-5 rounded-md text-lg tracking-wide select-none font-semibold cursor-pointer duration-100 hover:bg-sky-400 w-fit ${className}`} className={`text-white flex items-center gap-2 py-2 px-5 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit ${
onClick={onClick as MouseEventHandler<HTMLDivElement>} loading
? "bg-sky-400 cursor-auto"
: "bg-sky-500 hover:bg-sky-400 cursor-pointer"
} ${className}`}
onClick={() => {
if (!loading) onClick();
}}
> >
<FontAwesomeIcon icon={icon} className="h-5" /> {icon && <FontAwesomeIcon icon={icon} className="h-5" />}
{label} <p className="text-center w-full">{label}</p>
</div> </div>
); );
} }

View File

@ -32,8 +32,6 @@ export default function useLinks(
const encodedData = encodeURIComponent(JSON.stringify(requestBody)); const encodedData = encodeURIComponent(JSON.stringify(requestBody));
console.log(encodedData);
const response = await fetch( const response = await fetch(
`/api/routes/links?body=${encodeURIComponent(encodedData)}` `/api/routes/links?body=${encodeURIComponent(encodedData)}`
); );

View File

@ -1,5 +1,6 @@
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
import getPublicUserDataByEmail from "./getPublicUserDataByEmail"; import getPublicUserDataByEmail from "./getPublicUserDataByEmail";
import { toast } from "react-hot-toast";
const addMemberToCollection = async ( const addMemberToCollection = async (
ownerEmail: string, ownerEmail: string,
@ -7,7 +8,6 @@ const addMemberToCollection = async (
collection: CollectionIncludingMembersAndLinkCount, collection: CollectionIncludingMembersAndLinkCount,
setMember: (newMember: Member) => null | undefined setMember: (newMember: Member) => null | undefined
) => { ) => {
console.log(collection.members);
const checkIfMemberAlreadyExists = collection.members.find((e) => { const checkIfMemberAlreadyExists = collection.members.find((e) => {
const email = e.user.email; const email = e.user.email;
return email === memberEmail; return email === memberEmail;
@ -24,8 +24,6 @@ const addMemberToCollection = async (
// Lookup, get data/err, list ... // Lookup, get data/err, list ...
const user = await getPublicUserDataByEmail(memberEmail.trim()); const user = await getPublicUserDataByEmail(memberEmail.trim());
console.log(collection);
if (user.email) { if (user.email) {
setMember({ setMember({
collectionId: collection.id, collectionId: collection.id,
@ -39,7 +37,9 @@ const addMemberToCollection = async (
}, },
}); });
} }
} } else if (checkIfMemberAlreadyExists) toast.error("User already exists.");
else if (memberEmail.trim() === ownerEmail)
toast.error("You are already the collection owner.");
}; };
export default addMemberToCollection; export default addMemberToCollection;

View File

@ -1,9 +1,11 @@
import { toast } from "react-hot-toast";
export default async function getPublicUserDataByEmail(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();
console.log(data); if (!response.ok) toast.error(data.response);
return data.response; return data.response;
} }

View File

@ -39,6 +39,7 @@
"react": "18.2.0", "react": "18.2.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hot-toast": "^2.4.1",
"react-image-file-resizer": "^0.4.8", "react-image-file-resizer": "^0.4.8",
"react-select": "^5.7.0", "react-select": "^5.7.0",
"typescript": "4.9.4", "typescript": "4.9.4",

View File

@ -4,6 +4,7 @@ import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import Head from "next/head"; import Head from "next/head";
import AuthRedirect from "@/layouts/AuthRedirect"; import AuthRedirect from "@/layouts/AuthRedirect";
import { Toaster } from "react-hot-toast";
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
return ( return (
@ -30,6 +31,11 @@ export default function App({ Component, pageProps }: AppProps) {
/> />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
</Head> </Head>
<Toaster
position="top-center"
reverseOrder={false}
toastOptions={{ className: "border border-sky-100" }}
/>
<AuthRedirect> <AuthRedirect>
<Component {...pageProps} /> <Component {...pageProps} />
</AuthRedirect> </AuthRedirect>

View File

@ -19,8 +19,6 @@ export const authOptions: AuthOptions = {
password: string; password: string;
}; };
console.log(email, password);
const findUser = await prisma.user.findFirst({ const findUser = await prisma.user.findFirst({
where: { where: {
email: email, email: email,

View File

@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
interface Data { interface Data {
message: string | object; response: string | object;
} }
interface User { interface User {
@ -19,7 +19,9 @@ export default async function Index(
const body: User = req.body; const body: User = req.body;
if (!body.email || !body.password || !body.name) if (!body.email || !body.password || !body.name)
return res.status(400).json({ message: "Please fill out all the fields." }); return res
.status(400)
.json({ response: "Please fill out all the fields." });
const checkIfUserExists = await prisma.user.findFirst({ const checkIfUserExists = await prisma.user.findFirst({
where: { where: {
@ -40,8 +42,8 @@ export default async function Index(
}, },
}); });
res.status(201).json({ message: "User successfully created." }); res.status(201).json({ response: "User successfully created." });
} else if (checkIfUserExists) { } else if (checkIfUserExists) {
res.status(400).json({ message: "User already exists." }); res.status(400).json({ response: "User already exists." });
} }
} }

View File

@ -1,6 +1,8 @@
import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "react-hot-toast";
interface FormData { interface FormData {
email: string; email: string;
@ -8,56 +10,82 @@ interface FormData {
} }
export default function Login() { export default function Login() {
const [submitLoader, setSubmitLoader] = useState(false);
const [form, setForm] = useState<FormData>({ const [form, setForm] = useState<FormData>({
email: "", email: "",
password: "", password: "",
}); });
async function loginUser() { async function loginUser() {
console.log(form); if (form.email !== "" && form.password !== "") {
if (form.email != "" && form.password != "") { setSubmitLoader(true);
const load = toast.loading("Authenticating...");
const res = await signIn("credentials", { const res = await signIn("credentials", {
email: form.email, email: form.email,
password: form.password, password: form.password,
redirect: false, redirect: false,
}); });
console.log(res); toast.dismiss(load);
setSubmitLoader(false);
if (!res?.ok) { if (!res?.ok) {
console.log("User not found or password does not match.", res); toast.error("Invalid login.");
} }
} else { } else {
console.log("Please fill out all the fields."); toast.error("Please fill out all the fields.");
} }
} }
return ( return (
<div className="p-5"> <>
<p className="text-3xl font-bold text-center mb-10">Linkwarden</p> <p className="text-xl font-bold text-center text-sky-500 my-10">
<input Linkwarden
type="text" </p>
placeholder="Email" <div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
value={form.email} <div className="my-5 text-center">
onChange={(e) => setForm({ ...form, email: e.target.value })} <p className="text-3xl font-bold text-sky-500">Welcome back</p>
className="border border-gray-700 rounded-md block m-2 mx-auto p-2" <p className="text-md font-semibold text-sky-400">
/> Sign in to your account
<input </p>
type="text" </div>
placeholder="Password"
value={form.password} <p className="text-sm text-sky-500 w-fit font-semibold">Email</p>
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="border border-gray-700 rounded-md block m-2 mx-auto p-2" <input
/> type="text"
<div placeholder="johnny@example.com"
className="mx-auto bg-black w-min p-3 m-5 text-white rounded-md cursor-pointer" value={form.email}
onClick={loginUser} onChange={(e) => setForm({ ...form, email: e.target.value })}
> className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
Login />
<p className="text-sm text-sky-500 w-fit font-semibold">Password</p>
<input
type="password"
placeholder="*****************"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
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={loginUser}
label="Login"
className="mt-2 w-full text-center"
loading={submitLoader}
/>
</div> </div>
<Link href={"/register"} className="block mx-auto w-min"> <div className="flex items-baseline gap-1 justify-center mt-10">
Register <p className="w-fit text-gray-500">New here?</p>
</Link> <Link href={"/register"} className="block text-sky-500 font-bold">
</div> Sign Up
</Link>
</div>
</>
); );
} }

View File

@ -1,86 +1,147 @@
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
import SubmitButton from "@/components/SubmitButton";
interface FormData { interface FormData {
name: string; name: string;
email: string; email: string;
password: string; password: string;
passwordConfirmation: string;
} }
export default function Register() { export default function Register() {
const router = useRouter(); const router = useRouter();
const [submitLoader, setSubmitLoader] = useState(false);
const [form, setForm] = useState<FormData>({ const [form, setForm] = useState<FormData>({
name: "", name: "",
email: "", email: "",
password: "", password: "",
passwordConfirmation: "",
}); });
async function registerUser() { async function registerUser() {
let success: boolean = false; if (
form.name !== "" &&
form.email !== "" &&
form.password !== "" &&
form.passwordConfirmation !== ""
) {
if (form.password === form.passwordConfirmation) {
const { passwordConfirmation, ...request } = form;
if (form.name != "" && form.email != "" && form.password != "") { setSubmitLoader(true);
await fetch("/api/auth/register", {
body: JSON.stringify(form),
headers: {
"Content-Type": "application/json",
},
method: "POST",
})
.then((res) => {
success = res.ok;
return res.json();
})
.then((data) => console.log(data));
if (success) { const load = toast.loading("Creating Account...");
setForm({
name: "", const response = await fetch("/api/auth/register", {
email: "", body: JSON.stringify(request),
password: "", headers: {
"Content-Type": "application/json",
},
method: "POST",
}); });
router.push("/login"); const data = await response.json();
toast.dismiss(load);
setSubmitLoader(false);
if (response.ok) {
setForm({
name: "",
email: "",
password: "",
passwordConfirmation: "",
});
toast.success("User Created!");
router.push("/login");
} else {
toast.error(data.response);
}
} else {
toast.error("Passwords do not match.");
} }
} else { } else {
console.log("Please fill out all the fields."); toast.error("Please fill out all the fields.");
} }
} }
return ( return (
<div className="p-5"> <>
<p className="text-3xl font-bold text-center mb-10">Linkwarden</p> <p className="text-xl font-bold text-center my-10 text-sky-500">
<input Linkwarden
type="text" </p>
placeholder="Display Name" <div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
value={form.name} <div className="my-5 text-center">
onChange={(e) => setForm({ ...form, name: e.target.value })} <p className="text-3xl font-bold text-sky-500">Get started</p>
className="border border-gray-700 rounded-md block m-2 mx-auto p-2" <p className="text-md font-semibold text-sky-400">
/> Create a new account
<input </p>
type="text" </div>
placeholder="Email"
value={form.email} <p className="text-sm text-sky-500 w-fit font-semibold">Display Name</p>
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="border border-gray-700 rounded-md block m-2 mx-auto p-2" <input
/> type="text"
<input placeholder="Johnny"
type="text" value={form.name}
placeholder="Password" onChange={(e) => setForm({ ...form, name: e.target.value })}
value={form.password} className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
onChange={(e) => setForm({ ...form, password: e.target.value })} />
className="border border-gray-700 rounded-md block m-2 mx-auto p-2"
/> <p className="text-sm text-sky-500 w-fit font-semibold">Email</p>
<div
className="mx-auto bg-black w-min p-3 m-5 text-white rounded-md cursor-pointer" <input
onClick={registerUser} type="text"
> placeholder="johnny@example.com"
Register value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
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 w-fit font-semibold">Password</p>
<input
type="password"
placeholder="*****************"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
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 w-fit font-semibold">
Re-enter Password
</p>
<input
type="password"
placeholder="*****************"
value={form.passwordConfirmation}
onChange={(e) =>
setForm({ ...form, passwordConfirmation: e.target.value })
}
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={registerUser}
label="Sign Up"
className="mt-2 w-full text-center"
loading={submitLoader}
/>
</div> </div>
<Link href={"/login"} className="block mx-auto w-min"> <div className="flex items-baseline gap-1 justify-center mt-10">
Login <p className="w-fit text-gray-500">Have an account?</p>
</Link> <Link href={"/login"} className="block w-min text-sky-500 font-bold">
</div> Login
</Link>
</div>
</>
); );
} }

View File

@ -4,7 +4,7 @@ generator client {
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model User { model User {

View File

@ -1,10 +1,15 @@
import { create } from "zustand"; import { create } from "zustand";
import { AccountSettings } from "@/types/global"; import { AccountSettings } from "@/types/global";
type ResponseObject = {
ok: boolean;
data: object | string;
};
type AccountStore = { type AccountStore = {
account: AccountSettings; account: AccountSettings;
setAccount: (email: string) => void; setAccount: (email: string) => void;
updateAccount: (user: AccountSettings) => Promise<boolean>; updateAccount: (user: AccountSettings) => Promise<ResponseObject>;
}; };
const useAccountStore = create<AccountStore>()((set) => ({ const useAccountStore = create<AccountStore>()((set) => ({
@ -29,11 +34,9 @@ const useAccountStore = create<AccountStore>()((set) => ({
const data = await response.json(); const data = await response.json();
console.log(data);
if (response.ok) set({ account: { ...data.response } }); if (response.ok) set({ account: { ...data.response } });
return response.ok; return { ok: response.ok, data: data.response };
}, },
})); }));

View File

@ -2,16 +2,21 @@ import { create } from "zustand";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import useTagStore from "./tags"; import useTagStore from "./tags";
type ResponseObject = {
ok: boolean;
data: object | string;
};
type CollectionStore = { type CollectionStore = {
collections: CollectionIncludingMembersAndLinkCount[]; collections: CollectionIncludingMembersAndLinkCount[];
setCollections: () => void; setCollections: () => void;
addCollection: ( addCollection: (
body: CollectionIncludingMembersAndLinkCount body: CollectionIncludingMembersAndLinkCount
) => Promise<boolean>; ) => Promise<ResponseObject>;
updateCollection: ( updateCollection: (
collection: CollectionIncludingMembersAndLinkCount collection: CollectionIncludingMembersAndLinkCount
) => Promise<boolean>; ) => Promise<ResponseObject>;
removeCollection: (collectionId: number) => Promise<boolean>; removeCollection: (collectionId: number) => Promise<ResponseObject>;
}; };
const useCollectionStore = create<CollectionStore>()((set) => ({ const useCollectionStore = create<CollectionStore>()((set) => ({
@ -39,7 +44,7 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
collections: [...state.collections, data.response], collections: [...state.collections, data.response],
})); }));
return response.ok; return { ok: response.ok, data: data.response };
}, },
updateCollection: async (collection) => { updateCollection: async (collection) => {
const response = await fetch("/api/routes/collections", { const response = await fetch("/api/routes/collections", {
@ -52,8 +57,6 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
const data = await response.json(); const data = await response.json();
console.log(data);
if (response.ok) if (response.ok)
set((state) => ({ set((state) => ({
collections: state.collections.map((e) => collections: state.collections.map((e) =>
@ -61,7 +64,7 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
), ),
})); }));
return response.ok; return { ok: response.ok, data: data.response };
}, },
removeCollection: async (id) => { removeCollection: async (id) => {
const response = await fetch("/api/routes/collections", { const response = await fetch("/api/routes/collections", {
@ -74,8 +77,6 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
const data = await response.json(); const data = await response.json();
console.log(data);
if (response.ok) { if (response.ok) {
set((state) => ({ set((state) => ({
collections: state.collections.filter((e) => e.id !== id), collections: state.collections.filter((e) => e.id !== id),
@ -83,7 +84,7 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
useTagStore.getState().setTags(); useTagStore.getState().setTags();
} }
return response.ok; return { ok: response.ok, data: data.response };
}, },
})); }));

View File

@ -3,19 +3,26 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import useTagStore from "./tags"; import useTagStore from "./tags";
import useCollectionStore from "./collections"; import useCollectionStore from "./collections";
type ResponseObject = {
ok: boolean;
data: object | string;
};
type LinkStore = { type LinkStore = {
links: LinkIncludingShortenedCollectionAndTags[]; links: LinkIncludingShortenedCollectionAndTags[];
setLinks: ( setLinks: (
data: LinkIncludingShortenedCollectionAndTags[], data: LinkIncludingShortenedCollectionAndTags[],
isInitialCall: boolean isInitialCall: boolean
) => void; ) => void;
addLink: (body: LinkIncludingShortenedCollectionAndTags) => Promise<boolean>; addLink: (
body: LinkIncludingShortenedCollectionAndTags
) => Promise<ResponseObject>;
updateLink: ( updateLink: (
link: LinkIncludingShortenedCollectionAndTags link: LinkIncludingShortenedCollectionAndTags
) => Promise<boolean>; ) => Promise<ResponseObject>;
removeLink: ( removeLink: (
link: LinkIncludingShortenedCollectionAndTags link: LinkIncludingShortenedCollectionAndTags
) => Promise<boolean>; ) => Promise<ResponseObject>;
resetLinks: () => void; resetLinks: () => void;
}; };
@ -41,8 +48,6 @@ const useLinkStore = create<LinkStore>()((set) => ({
const data = await response.json(); const data = await response.json();
console.log(data);
if (response.ok) { if (response.ok) {
set((state) => ({ set((state) => ({
links: [data.response, ...state.links], links: [data.response, ...state.links],
@ -51,7 +56,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
useCollectionStore.getState().setCollections(); useCollectionStore.getState().setCollections();
} }
return response.ok; return { ok: response.ok, data: data.response };
}, },
updateLink: async (link) => { updateLink: async (link) => {
const response = await fetch("/api/routes/links", { const response = await fetch("/api/routes/links", {
@ -64,8 +69,6 @@ const useLinkStore = create<LinkStore>()((set) => ({
const data = await response.json(); const data = await response.json();
console.log(data);
if (response.ok) { if (response.ok) {
set((state) => ({ set((state) => ({
links: state.links.map((e) => links: state.links.map((e) =>
@ -76,7 +79,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
useCollectionStore.getState().setCollections(); useCollectionStore.getState().setCollections();
} }
return response.ok; return { ok: response.ok, data: data.response };
}, },
removeLink: async (link) => { removeLink: async (link) => {
const response = await fetch("/api/routes/links", { const response = await fetch("/api/routes/links", {
@ -89,8 +92,6 @@ const useLinkStore = create<LinkStore>()((set) => ({
const data = await response.json(); const data = await response.json();
console.log(data);
if (response.ok) { if (response.ok) {
set((state) => ({ set((state) => ({
links: state.links.filter((e) => e.id !== link.id), links: state.links.filter((e) => e.id !== link.id),
@ -98,7 +99,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
useTagStore.getState().setTags(); useTagStore.getState().setTags();
} }
return response.ok; return { ok: response.ok, data: data.response };
}, },
resetLinks: () => set({ links: [] }), resetLinks: () => set({ links: [] }),
})); }));

View File

@ -86,6 +86,19 @@
} }
} }
.spin {
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* For react-colorful */ /* For react-colorful */
.color-picker .react-colorful { .color-picker .react-colorful {
width: 100%; width: 100%;

View File

@ -2168,6 +2168,11 @@ globrex@^0.1.2:
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
goober@^2.1.10:
version "2.1.13"
resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c"
integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==
gopd@^1.0.1: gopd@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@ -3486,6 +3491,13 @@ react-dom@18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-hot-toast@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994"
integrity sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==
dependencies:
goober "^2.1.10"
react-image-file-resizer@^0.4.8: react-image-file-resizer@^0.4.8:
version "0.4.8" version "0.4.8"
resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af" resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af"