bug fixed + redesigned profile settings page
This commit is contained in:
parent
6259405045
commit
f5eaee8dc0
|
@ -20,6 +20,7 @@ import usePermissions from "@/hooks/usePermissions";
|
|||
import { toast } from "react-hot-toast";
|
||||
import isValidUrl from "@/lib/client/isValidUrl";
|
||||
import Link from "next/link";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
|
@ -161,7 +162,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
{count + 1}
|
||||
</p>
|
||||
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
|
||||
{link.name || link.description}
|
||||
{unescapeString(link.name || link.description)}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
|
|
|
@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||
import Image from "next/image";
|
||||
import { Link as LinkType, Tag } from "@prisma/client";
|
||||
import isValidUrl from "@/lib/client/isValidUrl";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
|
||||
interface LinksIncludingTags extends LinkType {
|
||||
tags: Tag[];
|
||||
|
@ -60,12 +61,12 @@ export default function LinkCard({ link, count }: Props) {
|
|||
<div className="flex items-baseline gap-1">
|
||||
<p className="text-xs text-gray-500">{count + 1}</p>
|
||||
<p className="text-lg text-black">
|
||||
{link.name || link.description}
|
||||
{unescapeString(link.name || link.description)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500 text-sm font-medium">
|
||||
{link.description}
|
||||
{unescapeString(link.description)}
|
||||
</p>
|
||||
<div className="flex gap-3 items-center flex-wrap my-3">
|
||||
<div className="flex gap-1 items-center flex-wrap mt-1">
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`bg-gray-100 dark:bg-neutral-800 h-full w-64 xl:w-80 overflow-y-auto border-solid border dark:border-neutral-800 border-r-sky-100 dark:border-r-neutral-700 p-5 z-20 flex flex-col gap-5 justify-between ${className}`}
|
||||
className={`dark:bg-neutral-900 bg-white h-full w-64 xl:w-80 overflow-y-auto border-solid border dark:border-neutral-900 border-r-sky-100 dark:border-r-neutral-700 p-5 z-20 flex flex-col gap-5 justify-between ${className}`}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link href="/settings/profile">
|
||||
|
|
|
@ -27,8 +27,8 @@ export default function SubmitButton({
|
|||
if (!loading) onClick();
|
||||
}}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="h-5" />}
|
||||
<p className="text-center w-full">{label}</p>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="h-5 select-none" />}
|
||||
<p className="text-center w-full select-none">{label}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import useModalStore from "@/store/modals";
|
|||
import { useRouter } from "next/router";
|
||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBars } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
@ -38,12 +39,12 @@ export default function SettingsLayout({ children }: Props) {
|
|||
<>
|
||||
<ModalManagement />
|
||||
|
||||
<div className="flex">
|
||||
<div className="flex max-w-screen-lg mx-auto">
|
||||
<div className="hidden lg:block">
|
||||
<SettingsSidebar className="fixed top-0" />
|
||||
<SettingsSidebar />
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col h-screen lg:ml-64 xl:ml-80 p-5">
|
||||
<div className="w-full flex flex-col h-screen p-5">
|
||||
<div className="flex gap-3">
|
||||
<div
|
||||
onClick={toggleSidebar}
|
||||
|
@ -52,11 +53,18 @@ export default function SettingsLayout({ children }: Props) {
|
|||
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex gap-1 items-center select-none cursor-pointer p-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5" />
|
||||
</Link>
|
||||
|
||||
<p className="capitalize text-3xl">
|
||||
{router.asPath.split("/").pop()} Settings
|
||||
</p>
|
||||
</div>
|
||||
<hr className="my-3" />
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
{children}
|
||||
|
||||
{sidebar ? (
|
||||
|
|
|
@ -52,7 +52,7 @@ export default function App({
|
|||
reverseOrder={false}
|
||||
toastOptions={{
|
||||
className:
|
||||
"border border-sky-100 dark:dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
|
||||
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
|
||||
}}
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
|
|
|
@ -1,10 +1,183 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faClose, faPenToSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import useAccountStore from "@/store/account";
|
||||
import { AccountSettings } from "@/types/global";
|
||||
import { toast } from "react-hot-toast";
|
||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||
import React from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import { resizeImage } from "@/lib/client/resizeImage";
|
||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||
import SubmitButton from "@/components/SubmitButton";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
|
||||
export default function profile() {
|
||||
const { update, data } = useSession();
|
||||
|
||||
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
||||
|
||||
const [profileStatus, setProfileStatus] = useState(true);
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const handleProfileStatus = (e: boolean) => {
|
||||
setProfileStatus(!e);
|
||||
};
|
||||
|
||||
const { account, updateAccount } = useAccountStore();
|
||||
|
||||
const [user, setUser] = useState<AccountSettings>({
|
||||
...account,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setUser({ ...account });
|
||||
}, [account]);
|
||||
|
||||
const handleImageUpload = async (e: any) => {
|
||||
const file: File = e.target.files[0];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
const allowedExtensions = ["png", "jpeg", "jpg"];
|
||||
if (allowedExtensions.includes(fileExtension as string)) {
|
||||
const resizedFile = await resizeImage(file);
|
||||
if (
|
||||
resizedFile.size < 1048576 // 1048576 Bytes == 1MB
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setUser({ ...user, profilePic: reader.result as string });
|
||||
};
|
||||
reader.readAsDataURL(resizedFile);
|
||||
} else {
|
||||
toast.error("Please select a PNG or JPEG file thats less than 1MB.");
|
||||
}
|
||||
} else {
|
||||
toast.error("Invalid file format.");
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Applying...");
|
||||
|
||||
const response = await updateAccount({
|
||||
...user,
|
||||
});
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Settings Applied!");
|
||||
|
||||
if (user.email !== account.email) {
|
||||
update({
|
||||
id: data?.user.id,
|
||||
});
|
||||
|
||||
signOut();
|
||||
} else if (
|
||||
user.username !== account.username ||
|
||||
user.name !== account.name
|
||||
)
|
||||
update({
|
||||
id: data?.user.id,
|
||||
});
|
||||
|
||||
setUser({ ...user, newPassword: undefined });
|
||||
} else toast.error(response.data as string);
|
||||
setSubmitLoader(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<div>profile</div>
|
||||
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80 mx-auto">
|
||||
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white mb-2">
|
||||
Display Name
|
||||
</p>
|
||||
<TextInput
|
||||
value={user.name || ""}
|
||||
onChange={(e) => setUser({ ...user, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white mb-2">
|
||||
Username
|
||||
</p>
|
||||
<TextInput
|
||||
value={user.username || ""}
|
||||
onChange={(e) => setUser({ ...user, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{emailEnabled ? (
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white mb-2">Email</p>
|
||||
<TextInput
|
||||
value={user.email || ""}
|
||||
onChange={(e) => setUser({ ...user, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
{user.email !== account.email ? (
|
||||
<p className="text-gray-500">
|
||||
You will need to log back in after you apply this Email.
|
||||
</p>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
<div className="sm:row-span-2 sm:justify-self-center mx-auto mb-3">
|
||||
<p className="text-sm text-black dark:text-white mb-2 text-center">
|
||||
Profile Photo
|
||||
</p>
|
||||
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
|
||||
<ProfilePhoto
|
||||
priority={true}
|
||||
src={user.profilePic}
|
||||
className="h-auto border-none w-28"
|
||||
status={handleProfileStatus}
|
||||
/>
|
||||
{profileStatus && (
|
||||
<div
|
||||
onClick={() =>
|
||||
setUser({
|
||||
...user,
|
||||
profilePic: "",
|
||||
})
|
||||
}
|
||||
className="absolute top-1 left-1 w-5 h-5 flex items-center justify-center border p-1 border-slate-200 dark:border-neutral-700 rounded-full bg-white dark:bg-neutral-800 text-center select-none cursor-pointer duration-100 hover:text-red-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} className="w-3 h-3" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -bottom-3 left-0 right-0 mx-auto w-fit text-center">
|
||||
<label className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
|
||||
Browse...
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="upload-photo"
|
||||
accept=".png, .jpeg, .jpg"
|
||||
className="hidden"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SubmitButton
|
||||
onClick={submit}
|
||||
loading={submitLoader}
|
||||
label="Apply Settings"
|
||||
icon={faPenToSquare}
|
||||
className="mt-2 mx-auto lg:mx-0"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
|
Ŝarĝante…
Reference in New Issue