Update: Implement delete account feature + package updates

This commit is contained in:
Aaron William Po
2023-06-01 19:56:14 -04:00
parent 6fcd7b53d0
commit b644e7eeee
9 changed files with 518 additions and 492 deletions

868
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,41 +13,41 @@
}, },
"dependencies": { "dependencies": {
"@hapi/iron": "^7.0.1", "@hapi/iron": "^7.0.1",
"@headlessui/react": "^1.7.14", "@headlessui/react": "^1.7.15",
"@headlessui/tailwindcss": "^0.1.3", "@headlessui/tailwindcss": "^0.1.3",
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",
"@mapbox/mapbox-sdk": "^0.15.1", "@mapbox/mapbox-sdk": "^0.15.1",
"@next/bundle-analyzer": "^13.4.3", "@next/bundle-analyzer": "^13.4.4",
"@prisma/client": "^4.13.0", "@prisma/client": "^4.15.0",
"@react-email/components": "^0.0.6", "@react-email/components": "^0.0.6",
"@react-email/render": "^0.0.7", "@react-email/render": "^0.0.7",
"@react-email/tailwind": "^0.0.8", "@react-email/tailwind": "^0.0.8",
"@vercel/analytics": "^1.0.0", "@vercel/analytics": "^1.0.1",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"cloudinary": "^1.36.4", "cloudinary": "^1.37.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"dotenv": "^16.0.3", "dotenv": "^16.1.3",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mapbox-gl": "^2.14.1", "mapbox-gl": "^2.15.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"multer-storage-cloudinary": "^4.0.0", "multer-storage-cloudinary": "^4.0.0",
"next": "^13.3.4", "next": "^13.4.4",
"next-connect": "^1.0.0-next.3", "next-connect": "^1.0.0-next.3",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pino": "^8.12.0", "pino": "^8.14.1",
"pino-pretty": "^10.0.0", "pino-pretty": "^10.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-daisyui": "^3.1.2", "react-daisyui": "^3.1.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-email": "^1.9.3", "react-email": "^1.9.3",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.44.3",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-icons": "^4.8.0", "react-icons": "^4.9.0",
"react-intersection-observer": "^9.4.3", "react-intersection-observer": "^9.4.4",
"react-map-gl": "^7.0.23", "react-map-gl": "^7.0.25",
"react-responsive-carousel": "^3.2.23", "react-responsive-carousel": "^3.2.23",
"sparkpost": "^2.1.4", "sparkpost": "^2.1.4",
"swr": "^2.1.5", "swr": "^2.1.5",
@@ -55,37 +55,37 @@
"zod": "^3.21.4" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^7.6.0", "@faker-js/faker": "^8.0.2",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2",
"@types/lodash": "^4.14.194", "@types/lodash": "^4.14.195",
"@types/mapbox__mapbox-sdk": "^0.13.4", "@types/mapbox__mapbox-sdk": "^0.13.4",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^18.16.3", "@types/node": "^20.2.5",
"@types/passport-local": "^1.0.35", "@types/passport-local": "^1.0.35",
"@types/react": "^18.2.0", "@types/react": "^18.2.8",
"@types/react-dom": "^18.2.1", "@types/react-dom": "^18.2.4",
"@types/sparkpost": "^2.1.5", "@types/sparkpost": "^2.1.5",
"@vercel/fetch": "^6.2.0", "@vercel/fetch": "^6.2.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"daisyui": "^2.51.6", "daisyui": "^3.0.0",
"dotenv-cli": "^7.2.1", "dotenv-cli": "^7.2.1",
"eslint": "^8.39.0", "eslint": "^8.41.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0", "eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-next": "^13.3.4", "eslint-config-next": "^13.4.4",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",
"postcss": "^8.4.23", "postcss": "^8.4.24",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-jsdoc": "^0.4.2", "prettier-plugin-jsdoc": "^0.4.2",
"prettier-plugin-tailwindcss": "^0.2.8", "prettier-plugin-tailwindcss": "^0.3.0",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"prisma": "^4.13.0", "prisma": "^4.15.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"tailwindcss-animate": "^1.0.5", "tailwindcss-animate": "^1.0.5",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.4" "typescript": "^5.1.3"
}, },
"prisma": { "prisma": {
"schema": "./src/prisma/schema.prisma", "schema": "./src/prisma/schema.prisma",

View File

@@ -0,0 +1,70 @@
import { Switch } from '@headlessui/react';
import { useRouter } from 'next/router';
import { FunctionComponent, useRef, useState } from 'react';
const DeleteAccount: FunctionComponent = () => {
const [deleteToggled, setDeleteToggled] = useState(false);
const deleteRef = useRef<null | HTMLDialogElement>(null);
const router = useRouter();
return (
<div className="card w-full space-y-4">
<div className="card-body">
<div className="flex w-full items-center justify-between space-x-5">
<div className="">
<h1 className="text-lg font-bold">Delete Your Account</h1>
<p>Want to leave? Delete your account here.</p>
</div>
<div>
<Switch
className="toggle"
id="edit-toggle"
checked={deleteToggled}
onClick={() => {
setDeleteToggled((val) => !val);
}}
/>
</div>
</div>
{deleteToggled && (
<>
<div className="mt-3">
<button
className="btn-primary btn w-full"
onClick={() => deleteRef.current!.showModal()}
>
Delete my account
</button>
<dialog id="delete-modal" className="modal" ref={deleteRef}>
<div className="modal-box text-center">
<h3 className="text-lg font-bold">{`You're about to delete your account.`}</h3>
<p className="">This action is permanent and cannot be reversed.</p>
<div className="modal-action flex-col space-x-0 space-y-3">
<button
className="btn-error btn-sm btn w-full"
onClick={async () => {
deleteRef.current!.close();
await router.replace('/api/users/logout');
}}
>
Okay, delete my account
</button>
<button
className="btn-success btn-sm btn w-full"
onClick={() => deleteRef.current!.close()}
>
Go back
</button>
</div>
</div>
</dialog>
</div>
</>
)}
</div>
</div>
);
};
export default DeleteAccount;

View File

@@ -20,7 +20,7 @@ const Security: FunctionComponent = () => {
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => { const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
await sendUpdatePasswordRequest(data); await sendUpdatePasswordRequest(data);
setEditToggled(value => !value) setEditToggled((value) => !value);
reset(); reset();
}; };

View File

@@ -27,9 +27,9 @@ const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
const alertType = toastToClassName(t.type); const alertType = toastToClassName(t.type);
return ( return (
<div <div
className={`alert ${alertType} w-11/12 flex-row items-center shadow-lg animate-in fade-in duration-200 lg:w-2/12`} className={`alert ${alertType} w-11/12 flex-row items-center shadow-lg animate-in fade-in duration-200 lg:w-4/12`}
> >
<p className='text-sm'>{resolveValue(t.message, t)}</p> <p>{resolveValue(t.message, t)}</p>
{t.type !== 'loading' && ( {t.type !== 'loading' && (
<div> <div>
<button <button

View File

@@ -12,7 +12,7 @@ const DesktopLinks: FC = () => {
return ( return (
<div className="block flex-none"> <div className="block flex-none">
<ul className="menu menu-horizontal p-0"> <ul className="menu menu-horizontal menu-sm px-1">
{pages.map((page) => { {pages.map((page) => {
return ( return (
<li key={page.slug}> <li key={page.slug}>
@@ -43,7 +43,7 @@ const MobileLinks: FC = () => {
</label> </label>
<ul <ul
tabIndex={0} tabIndex={0}
className="dropdown-content menu rounded-box menu-compact mt-3 w-48 bg-base-100 p-2 shadow" className="menu-compact dropdown-content menu rounded-box mt-3 w-48 bg-base-100 p-2 shadow"
> >
{pages.map((page) => ( {pages.map((page) => (
<li key={page.slug}> <li key={page.slug}>

View File

@@ -14,7 +14,8 @@ const Button: FunctionComponent<FormButtonProps> = ({
// eslint-disable-next-line react/button-has-type // eslint-disable-next-line react/button-has-type
<button <button
type={type} type={type}
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`} className={`btn-primary btn w-full rounded-xl`}
disabled={isSubmitting}
> >
{children} {children}
</button> </button>

View File

@@ -7,6 +7,7 @@ import AccountInfo from '@/components/Account/AccountInfo';
import { useContext } from 'react'; import { useContext } from 'react';
import UserContext from '@/contexts/UserContext'; import UserContext from '@/contexts/UserContext';
import Security from '@/components/Account/Security'; import Security from '@/components/Account/Security';
import DeleteAccount from '@/components/Account/DeleteAccount';
const AccountPage: NextPage = () => { const AccountPage: NextPage = () => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
@@ -48,6 +49,7 @@ const AccountPage: NextPage = () => {
<Tab.Panel className="h-full space-y-5"> <Tab.Panel className="h-full space-y-5">
<AccountInfo /> <AccountInfo />
<Security /> <Security />
<DeleteAccount />
</Tab.Panel> </Tab.Panel>
<Tab.Panel>Your posts!</Tab.Panel> <Tab.Panel>Your posts!</Tab.Panel>
</Tab.Panels> </Tab.Panels>

View File

@@ -5,6 +5,7 @@ import Welcome from '@/emails/Welcome';
import { render } from '@react-email/render'; import { render } from '@react-email/render';
import { z } from 'zod'; import { z } from 'zod';
import { BASE_URL } from '@/config/env'; import { BASE_URL } from '@/config/env';
import { ReactElement } from 'react';
import GetUserSchema from './schema/GetUserSchema'; import GetUserSchema from './schema/GetUserSchema';
type UserSchema = z.infer<typeof GetUserSchema>; type UserSchema = z.infer<typeof GetUserSchema>;
@@ -17,8 +18,10 @@ const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => {
const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`; const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`;
const address = email; const address = email;
const html = render(Welcome({ name, url, subject })!); const component = Welcome({ name, url, subject })! as ReactElement<unknown, string>;
const text = render(Welcome({ name, url, subject })!, { plainText: true });
const html = render(component);
const text = render(component, { plainText: true });
await sendEmail({ address, subject, text, html }); await sendEmail({ address, subject, text, html });
}; };