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 { toast } from "react-hot-toast";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/client/isValidUrl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
@ -161,7 +162,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
{count + 1}
|
{count + 1}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Link as LinkType, Tag } from "@prisma/client";
|
import { Link as LinkType, Tag } from "@prisma/client";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/client/isValidUrl";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
|
||||||
interface LinksIncludingTags extends LinkType {
|
interface LinksIncludingTags extends LinkType {
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
|
@ -60,12 +61,12 @@ export default function LinkCard({ link, count }: Props) {
|
||||||
<div className="flex items-baseline gap-1">
|
<div className="flex items-baseline gap-1">
|
||||||
<p className="text-xs text-gray-500">{count + 1}</p>
|
<p className="text-xs text-gray-500">{count + 1}</p>
|
||||||
<p className="text-lg text-black">
|
<p className="text-lg text-black">
|
||||||
{link.name || link.description}
|
{unescapeString(link.name || link.description)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-500 text-sm font-medium">
|
<p className="text-gray-500 text-sm font-medium">
|
||||||
{link.description}
|
{unescapeString(link.description)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3 items-center flex-wrap my-3">
|
<div className="flex gap-3 items-center flex-wrap my-3">
|
||||||
<div className="flex gap-1 items-center flex-wrap mt-1">
|
<div className="flex gap-1 items-center flex-wrap mt-1">
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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">
|
<div className="flex flex-col gap-1">
|
||||||
<Link href="/settings/profile">
|
<Link href="/settings/profile">
|
||||||
|
|
|
@ -27,8 +27,8 @@ export default function SubmitButton({
|
||||||
if (!loading) onClick();
|
if (!loading) onClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon && <FontAwesomeIcon icon={icon} className="h-5" />}
|
{icon && <FontAwesomeIcon icon={icon} className="h-5 select-none" />}
|
||||||
<p className="text-center w-full">{label}</p>
|
<p className="text-center w-full select-none">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ import useModalStore from "@/store/modals";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -38,12 +39,12 @@ export default function SettingsLayout({ children }: Props) {
|
||||||
<>
|
<>
|
||||||
<ModalManagement />
|
<ModalManagement />
|
||||||
|
|
||||||
<div className="flex">
|
<div className="flex max-w-screen-lg mx-auto">
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<SettingsSidebar className="fixed top-0" />
|
<SettingsSidebar />
|
||||||
</div>
|
</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 className="flex gap-3">
|
||||||
<div
|
<div
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
|
@ -52,11 +53,18 @@ export default function SettingsLayout({ children }: Props) {
|
||||||
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||||
</div>
|
</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">
|
<p className="capitalize text-3xl">
|
||||||
{router.asPath.split("/").pop()} Settings
|
{router.asPath.split("/").pop()} Settings
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr className="my-3" />
|
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{sidebar ? (
|
{sidebar ? (
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default function App({
|
||||||
reverseOrder={false}
|
reverseOrder={false}
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
className:
|
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} />
|
<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 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() {
|
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 (
|
return (
|
||||||
<SettingsLayout>
|
<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>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Ŝarĝante…
Reference in New Issue