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 { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
import usePermissions from "@/hooks/usePermissions";
import { toast } from "react-hot-toast";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@ -48,6 +49,35 @@ export default function LinkCard({ link, count, className }: Props) {
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 formattedDate = new Date(link.createdAt as string).toLocaleString(
"en-US",
@ -97,7 +127,7 @@ export default function LinkCard({ link, count, className }: Props) {
width={64}
height={64}
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"
onError={(e) => {
const target = e.target as HTMLElement;
@ -141,16 +171,7 @@ export default function LinkCard({ link, count, className }: Props) {
link?.pinnedBy && link.pinnedBy[0]
? "Unpin"
: "Pin to Dashboard",
onClick: () => {
updateLink({
...link,
pinnedBy:
link?.pinnedBy && link.pinnedBy[0]
? undefined
: [{ id: account.id }],
});
setExpandDropdown(false);
},
onClick: pinLink,
}
: undefined,
permissions === true || permissions?.canUpdate
@ -173,10 +194,7 @@ export default function LinkCard({ link, count, className }: Props) {
permissions === true || permissions?.canDelete
? {
name: "Delete",
onClick: () => {
removeLink(link);
setExpandDropdown(false);
},
onClick: deleteLink,
}
: undefined,
]}

View File

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction } from "react";
import { Dispatch, SetStateAction, useState } from "react";
import {
faFolder,
faPenToSquare,
@ -10,6 +10,7 @@ import RequiredBadge from "../../RequiredBadge";
import SubmitButton from "@/components/SubmitButton";
import { HexColorPicker } from "react-colorful";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { toast } from "react-hot-toast";
type Props = {
toggleCollectionModal: Function;
@ -26,18 +27,33 @@ export default function CollectionInfo({
collection,
method,
}: Props) {
const [submitLoader, setSubmitLoader] = useState(false);
const { updateCollection, addCollection } = useCollectionStore();
const submit = async () => {
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);
else if (method === "UPDATE") response = await updateCollection(collection);
else console.log("Unknown method.");
else response = await updateCollection(collection);
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 (
@ -102,6 +118,7 @@ export default function CollectionInfo({
<SubmitButton
onClick={submit}
loading={submitLoader}
label={method === "CREATE" ? "Add" : "Save"}
icon={method === "CREATE" ? faPlus : faPenToSquare}
className="mx-auto mt-2"

View File

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

View File

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

View File

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

View File

@ -116,12 +116,12 @@ export default function LinkDetails({ link }: Props) {
return (
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
{!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>
)}
<div
className={`relative flex gap-5 items-start ${!imageError && "-mt-32"}`}
className={`relative flex gap-5 items-start ${!imageError && "-mt-24"}`}
>
{!imageError && (
<Image

View File

@ -4,6 +4,7 @@ import useAccountStore from "@/store/account";
import { useSession } from "next-auth/react";
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import SubmitButton from "@/components/SubmitButton";
import { toast } from "react-hot-toast";
type Props = {
togglePasswordFormModal: Function;
@ -20,6 +21,8 @@ export default function ChangePassword({
const [newPassword, setNewPassword1] = useState("");
const [newPassword2, setNewPassword2] = useState("");
const [submitLoader, setSubmitLoader] = useState(false);
const { account, updateAccount } = useAccountStore();
const { update } = useSession();
@ -34,26 +37,40 @@ export default function ChangePassword({
const submit = async () => {
if (oldPassword == "" || newPassword == "" || newPassword2 == "") {
console.log("Please fill all the fields.");
toast.error("Please fill all the fields.");
} else if (newPassword === newPassword2) {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
...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)
update({ email: user.email, name: user.name });
if (response) {
if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
togglePasswordFormModal();
}
} else {
console.log("Passwords do not match.");
toast.error("Passwords do not match.");
}
};
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">
<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
@ -81,10 +98,12 @@ export default function ChangePassword({
<SubmitButton
onClick={submit}
loading={submitLoader}
label="Apply Settings"
icon={faPenToSquare}
className="mx-auto mt-2"
/>
</div>
</div>
);
}

View File

@ -5,6 +5,7 @@ import { AccountSettings } from "@/types/global";
import { useSession } from "next-auth/react";
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import SubmitButton from "../../SubmitButton";
import { toast } from "react-hot-toast";
type Props = {
toggleSettingsModal: Function;
@ -20,6 +21,8 @@ export default function PrivacySettings({
const { update } = useSession();
const { account, updateAccount } = useAccountStore();
const [submitLoader, setSubmitLoader] = useState(false);
const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState(
user.whitelistedUsers.join(", ")
);
@ -44,16 +47,30 @@ export default function PrivacySettings({
};
const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
...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)
update({ email: user.email, name: user.name });
if (response) toggleSettingsModal();
if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
toggleSettingsModal();
}
};
return (
@ -93,6 +110,7 @@ export default function PrivacySettings({
<SubmitButton
onClick={submit}
loading={submitLoader}
label="Apply Settings"
icon={faPenToSquare}
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 SubmitButton from "../../SubmitButton";
import ProfilePhoto from "../../ProfilePhoto";
import { toast } from "react-hot-toast";
type Props = {
toggleSettingsModal: Function;
@ -24,6 +25,8 @@ export default function ProfileSettings({
const { account, updateAccount } = useAccountStore();
const [profileStatus, setProfileStatus] = useState(true);
const [submitLoader, setSubmitLoader] = useState(false);
const handleProfileStatus = (e: boolean) => {
setProfileStatus(!e);
};
@ -48,10 +51,10 @@ export default function ProfileSettings({
reader.readAsDataURL(resizedFile);
} 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 {
console.log("Invalid file format.");
toast.error("Invalid file format.");
}
};
@ -60,16 +63,30 @@ export default function ProfileSettings({
}, []);
const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
...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)
update({ email: user.email, name: user.name });
if (response) toggleSettingsModal();
if (response.ok) {
setUser({ ...user, oldPassword: undefined, newPassword: undefined });
toggleSettingsModal();
}
};
return (
@ -151,6 +168,7 @@ export default function ProfileSettings({
</div> */}
<SubmitButton
onClick={submit}
loading={submitLoader}
label="Apply Settings"
icon={faPenToSquare}
className="mx-auto mt-2"

View File

@ -41,7 +41,7 @@ export default function ModalManagement() {
<CollectionModal
toggleCollectionModal={toggleModal}
method={modal.method}
isOwner={modal.isOwner}
isOwner={modal.isOwner as boolean}
defaultIndex={modal.defaultIndex}
activeCollection={
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 { useState } from "react";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
export default function Search() {
const router = useRouter();
@ -35,7 +36,7 @@ export default function Search() {
value={searchQuery}
onChange={(e) => {
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("%", ""));
}}
onKeyDown={(e) =>

View File

@ -1,11 +1,11 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition } from "@fortawesome/free-regular-svg-icons";
import { MouseEventHandler } from "react";
type Props = {
onClick: Function;
icon: IconDefinition;
icon?: IconDefinition;
label: string;
loading: boolean;
className?: string;
};
@ -13,15 +13,22 @@ export default function SubmitButton({
onClick,
icon,
label,
loading,
className,
}: Props) {
return (
<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}`}
onClick={onClick as MouseEventHandler<HTMLDivElement>}
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 ${
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" />
{label}
{icon && <FontAwesomeIcon icon={icon} className="h-5" />}
<p className="text-center w-full">{label}</p>
</div>
);
}

View File

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

View File

@ -1,5 +1,6 @@
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
import getPublicUserDataByEmail from "./getPublicUserDataByEmail";
import { toast } from "react-hot-toast";
const addMemberToCollection = async (
ownerEmail: string,
@ -7,7 +8,6 @@ const addMemberToCollection = async (
collection: CollectionIncludingMembersAndLinkCount,
setMember: (newMember: Member) => null | undefined
) => {
console.log(collection.members);
const checkIfMemberAlreadyExists = collection.members.find((e) => {
const email = e.user.email;
return email === memberEmail;
@ -24,8 +24,6 @@ const addMemberToCollection = async (
// Lookup, get data/err, list ...
const user = await getPublicUserDataByEmail(memberEmail.trim());
console.log(collection);
if (user.email) {
setMember({
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;

View File

@ -1,9 +1,11 @@
import { toast } from "react-hot-toast";
export default async function getPublicUserDataByEmail(email: string) {
const response = await fetch(`/api/routes/users?email=${email}`);
const data = await response.json();
console.log(data);
if (!response.ok) toast.error(data.response);
return data.response;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,15 @@
import { create } from "zustand";
import { AccountSettings } from "@/types/global";
type ResponseObject = {
ok: boolean;
data: object | string;
};
type AccountStore = {
account: AccountSettings;
setAccount: (email: string) => void;
updateAccount: (user: AccountSettings) => Promise<boolean>;
updateAccount: (user: AccountSettings) => Promise<ResponseObject>;
};
const useAccountStore = create<AccountStore>()((set) => ({
@ -29,11 +34,9 @@ const useAccountStore = create<AccountStore>()((set) => ({
const data = await response.json();
console.log(data);
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 useTagStore from "./tags";
type ResponseObject = {
ok: boolean;
data: object | string;
};
type CollectionStore = {
collections: CollectionIncludingMembersAndLinkCount[];
setCollections: () => void;
addCollection: (
body: CollectionIncludingMembersAndLinkCount
) => Promise<boolean>;
) => Promise<ResponseObject>;
updateCollection: (
collection: CollectionIncludingMembersAndLinkCount
) => Promise<boolean>;
removeCollection: (collectionId: number) => Promise<boolean>;
) => Promise<ResponseObject>;
removeCollection: (collectionId: number) => Promise<ResponseObject>;
};
const useCollectionStore = create<CollectionStore>()((set) => ({
@ -39,7 +44,7 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
collections: [...state.collections, data.response],
}));
return response.ok;
return { ok: response.ok, data: data.response };
},
updateCollection: async (collection) => {
const response = await fetch("/api/routes/collections", {
@ -52,8 +57,6 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
const data = await response.json();
console.log(data);
if (response.ok)
set((state) => ({
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) => {
const response = await fetch("/api/routes/collections", {
@ -74,8 +77,6 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
const data = await response.json();
console.log(data);
if (response.ok) {
set((state) => ({
collections: state.collections.filter((e) => e.id !== id),
@ -83,7 +84,7 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
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 useCollectionStore from "./collections";
type ResponseObject = {
ok: boolean;
data: object | string;
};
type LinkStore = {
links: LinkIncludingShortenedCollectionAndTags[];
setLinks: (
data: LinkIncludingShortenedCollectionAndTags[],
isInitialCall: boolean
) => void;
addLink: (body: LinkIncludingShortenedCollectionAndTags) => Promise<boolean>;
addLink: (
body: LinkIncludingShortenedCollectionAndTags
) => Promise<ResponseObject>;
updateLink: (
link: LinkIncludingShortenedCollectionAndTags
) => Promise<boolean>;
) => Promise<ResponseObject>;
removeLink: (
link: LinkIncludingShortenedCollectionAndTags
) => Promise<boolean>;
) => Promise<ResponseObject>;
resetLinks: () => void;
};
@ -41,8 +48,6 @@ const useLinkStore = create<LinkStore>()((set) => ({
const data = await response.json();
console.log(data);
if (response.ok) {
set((state) => ({
links: [data.response, ...state.links],
@ -51,7 +56,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
useCollectionStore.getState().setCollections();
}
return response.ok;
return { ok: response.ok, data: data.response };
},
updateLink: async (link) => {
const response = await fetch("/api/routes/links", {
@ -64,8 +69,6 @@ const useLinkStore = create<LinkStore>()((set) => ({
const data = await response.json();
console.log(data);
if (response.ok) {
set((state) => ({
links: state.links.map((e) =>
@ -76,7 +79,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
useCollectionStore.getState().setCollections();
}
return response.ok;
return { ok: response.ok, data: data.response };
},
removeLink: async (link) => {
const response = await fetch("/api/routes/links", {
@ -89,8 +92,6 @@ const useLinkStore = create<LinkStore>()((set) => ({
const data = await response.json();
console.log(data);
if (response.ok) {
set((state) => ({
links: state.links.filter((e) => e.id !== link.id),
@ -98,7 +99,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
useTagStore.getState().setTags();
}
return response.ok;
return { ok: response.ok, data: data.response };
},
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 */
.color-picker .react-colorful {
width: 100%;

View File

@ -2168,6 +2168,11 @@ globrex@^0.1.2:
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
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:
version "1.0.1"
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"
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:
version "0.4.8"
resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af"