From 9c45ce61b8d6733fbc160bc59666eb8fcc03f73e Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sat, 26 Aug 2023 12:31:40 +0530 Subject: [PATCH 01/67] feat: added show password feature Signed-off-by: Adithya Krishna --- apps/web/src/components/forms/password.tsx | 70 ++++++++++++++++++---- apps/web/src/components/forms/signin.tsx | 37 +++++++++--- apps/web/src/components/forms/signup.tsx | 37 +++++++++--- 3 files changed, 117 insertions(+), 27 deletions(-) diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index eba0c9a43..4689e0dbe 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -1,7 +1,9 @@ 'use client'; +import { useState } from 'react'; + import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader } from 'lucide-react'; +import { Eye, EyeOff, Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -35,6 +37,8 @@ export type PasswordFormProps = { export const PasswordForm = ({ className }: PasswordFormProps) => { const { toast } = useToast(); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); const { register, @@ -79,6 +83,14 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { } }; + const onShowPassword = () => { + setShowPassword(!showPassword); + }; + + const onShowConfirmPassword = () => { + setShowConfirmPassword(!showConfirmPassword); + }; + return (
{ Password - +
+ + + +
@@ -104,12 +132,28 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { Repeat Password - +
+ + + +
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index ae9540869..2f3c3a4d6 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -1,7 +1,9 @@ 'use client'; +import { useState } from 'react'; + import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader } from 'lucide-react'; +import { Eye, EyeOff, Loader } from 'lucide-react'; import { signIn } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { FcGoogle } from 'react-icons/fc'; @@ -26,6 +28,7 @@ export type SignInFormProps = { export const SignInForm = ({ className }: SignInFormProps) => { const { toast } = useToast(); + const [showPassword, setShowPassword] = useState(false); const { register, @@ -73,6 +76,10 @@ export const SignInForm = ({ className }: SignInFormProps) => { } }; + const onShowPassword = () => { + setShowPassword(!showPassword); + }; + return ( { Password - +
+ + + +
{errors.password && ( {errors.password.message} diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index ce449f850..a66e43caf 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -1,7 +1,9 @@ 'use client'; +import { useState } from 'react'; + import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader } from 'lucide-react'; +import { Eye, EyeOff, Loader } from 'lucide-react'; import { signIn } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -29,6 +31,7 @@ export type SignUpFormProps = { export const SignUpForm = ({ className }: SignUpFormProps) => { const { toast } = useToast(); + const [showPassword, setShowPassword] = useState(false); const { register, @@ -72,6 +75,10 @@ export const SignUpForm = ({ className }: SignUpFormProps) => { } }; + const onShowPassword = () => { + setShowPassword(!showPassword); + }; + return ( { Password - +
+ + + +
From 7c6b5ac59d227a2020cbb7760f61751098b27c86 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sat, 26 Aug 2023 20:42:15 +0530 Subject: [PATCH 02/67] fix: updated padding and set patterns Signed-off-by: Adithya Krishna --- apps/web/src/components/forms/password.tsx | 20 ++++++-------------- apps/web/src/components/forms/signin.tsx | 10 +++------- apps/web/src/components/forms/signup.tsx | 10 +++------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index 4689e0dbe..d258b2693 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -83,14 +83,6 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { } }; - const onShowPassword = () => { - setShowPassword(!showPassword); - }; - - const onShowConfirmPassword = () => { - setShowConfirmPassword(!showConfirmPassword); - }; - return ( {
@@ -140,9 +140,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { onClick={() => setShowConfirmPassword((showConfirmPassword) => !showConfirmPassword)} > {showConfirmPassword ? ( - + ) : ( - + )} diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index d62e376ff..f8add10c4 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -115,9 +115,9 @@ export const SignInForm = ({ className }: SignInFormProps) => { onClick={() => setShowPassword((showPassword) => !showPassword)} > {showPassword ? ( - + ) : ( - + )} diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index 795dad25d..fae5d6277 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -121,9 +121,9 @@ export const SignUpForm = ({ className }: SignUpFormProps) => { onClick={() => setShowPassword((showPassword) => !showPassword)} > {showPassword ? ( - + ) : ( - + )} From fd6350b3973a72365c7175d8c4cd5f2694ecbb77 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sun, 3 Sep 2023 23:30:48 +0530 Subject: [PATCH 04/67] feat: added 404 page for marketing app Signed-off-by: Adithya Krishna --- apps/marketing/src/app/not-found.tsx | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 apps/marketing/src/app/not-found.tsx diff --git a/apps/marketing/src/app/not-found.tsx b/apps/marketing/src/app/not-found.tsx new file mode 100644 index 000000000..2935c5698 --- /dev/null +++ b/apps/marketing/src/app/not-found.tsx @@ -0,0 +1,65 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +import { motion } from 'framer-motion'; +import { ChevronLeft } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export default function NotFound() { + const router = useRouter(); + return ( + +
+ + background pattern + +
+
+
+

+ 404 Page not found +

+

+ Sorry, the page you are looking for doesn't exist or has been moved. +

+ +
+ + +
+
+
+
+ ); +} From 118483b6cc40a2c5ce4989de9384e0300b7bdccf Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sun, 3 Sep 2023 23:52:51 +0530 Subject: [PATCH 05/67] chore: updated 404 page for marketing app Signed-off-by: Adithya Krishna --- apps/marketing/src/app/not-found.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/marketing/src/app/not-found.tsx b/apps/marketing/src/app/not-found.tsx index 2935c5698..7dfaf9dba 100644 --- a/apps/marketing/src/app/not-found.tsx +++ b/apps/marketing/src/app/not-found.tsx @@ -25,6 +25,7 @@ export default function NotFound() { src={backgroundPattern} alt="background pattern" className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover md:scale-100 lg:scale-[100%]" + priority={true} /> From 12c45fb882c6700eea2fbf3199ac0ae70fe805dd Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sun, 3 Sep 2023 23:53:07 +0530 Subject: [PATCH 06/67] feat: added 404 page for web app Signed-off-by: Adithya Krishna --- apps/web/src/app/not-found.tsx | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 apps/web/src/app/not-found.tsx diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx new file mode 100644 index 000000000..c723d5255 --- /dev/null +++ b/apps/web/src/app/not-found.tsx @@ -0,0 +1,66 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +import { motion } from 'framer-motion'; +import { ChevronLeft } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export default function NotFound() { + const router = useRouter(); + return ( + +
+ + background pattern + +
+
+
+

404 Error

+

+ Oops! You found a secret page +

+

+ The page you are looking for may not exist :/ +

+
+ + +
+
+
+
+ ); +} From 2524458b0c10914c487bedd40674afd9b60032fa Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Sun, 3 Sep 2023 23:59:02 +0530 Subject: [PATCH 07/67] chore: updated wording Signed-off-by: Adithya Krishna --- apps/web/src/app/not-found.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index c723d5255..07124c6ca 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -31,7 +31,7 @@ export default function NotFound() {
-

404 Error

+

404 Page not found

Oops! You found a secret page

From d524ea77abe2bc0bb40dac159a1e3d2f063a5793 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 5 Sep 2023 13:15:45 +1000 Subject: [PATCH 08/67] fix: update styling --- apps/marketing/src/app/not-found.tsx | 51 ++++++------ apps/web/src/app/not-found.tsx | 78 +++++-------------- apps/web/src/components/motion.tsx | 7 -- .../web/src/components/partials/not-found.tsx | 66 ++++++++++++++++ 4 files changed, 110 insertions(+), 92 deletions(-) delete mode 100644 apps/web/src/components/motion.tsx create mode 100644 apps/web/src/components/partials/not-found.tsx diff --git a/apps/marketing/src/app/not-found.tsx b/apps/marketing/src/app/not-found.tsx index 7dfaf9dba..9f87cdc88 100644 --- a/apps/marketing/src/app/not-found.tsx +++ b/apps/marketing/src/app/not-found.tsx @@ -1,6 +1,7 @@ 'use client'; import Image from 'next/image'; +import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { motion } from 'framer-motion'; @@ -13,54 +14,52 @@ import backgroundPattern from '~/assets/background-pattern.png'; export default function NotFound() { const router = useRouter(); + return ( - +
background pattern
-
-
-

- 404 Page not found -

-

- Sorry, the page you are looking for doesn't exist or has been moved. + +

+
+

404 Page not found

+ +

Oops! Something went wrong.

+ +

+ The page you are looking for was moved, removed, renamed or might never have existed.

-
+
-
- +
); } diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index 07124c6ca..c8dc15086 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -1,66 +1,26 @@ -'use client'; +import Link from 'next/link'; -import Image from 'next/image'; -import { useRouter } from 'next/navigation'; - -import { motion } from 'framer-motion'; -import { ChevronLeft } from 'lucide-react'; - -import { cn } from '@documenso/ui/lib/utils'; +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { Button } from '@documenso/ui/primitives/button'; -import backgroundPattern from '~/assets/background-pattern.png'; +import NotFoundPartial from '~/components/partials/not-found'; + +export default async function NotFound() { + const session = await getServerComponentSession(); -export default function NotFound() { - const router = useRouter(); return ( - -
- - background pattern - -
-
-
-

404 Page not found

-

- Oops! You found a secret page -

-

- The page you are looking for may not exist :/ -

-
- - -
-
-
-
+ + {session && ( + + )} + + {!session && ( + + )} + ); } diff --git a/apps/web/src/components/motion.tsx b/apps/web/src/components/motion.tsx deleted file mode 100644 index 2e9d19eae..000000000 --- a/apps/web/src/components/motion.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -import { motion } from 'framer-motion'; - -export * from 'framer-motion'; - -export const MotionDiv = motion.div; diff --git a/apps/web/src/components/partials/not-found.tsx b/apps/web/src/components/partials/not-found.tsx new file mode 100644 index 000000000..0b5c2ad18 --- /dev/null +++ b/apps/web/src/components/partials/not-found.tsx @@ -0,0 +1,66 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +import { motion } from 'framer-motion'; +import { ChevronLeft } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export type NotFoundPartialProps = { + children?: React.ReactNode; +}; + +export default function NotFoundPartial({ children }: NotFoundPartialProps) { + const router = useRouter(); + + return ( +
+
+ + background pattern + +
+ +
+
+

404 Page not found

+ +

Oops! Something went wrong.

+ +

+ The page you are looking for was moved, removed, renamed or might never have existed. +

+ +
+ + + {children} +
+
+
+
+ ); +} From 551918ab9b6fa6037a3585c5d85d5e8b98fcbfd7 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 5 Sep 2023 13:53:18 +0000 Subject: [PATCH 09/67] feat: redirect signed document to completed page --- apps/web/src/app/(signing)/sign/[token]/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 35621068a..a55687196 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -1,4 +1,4 @@ -import { notFound } from 'next/navigation'; +import { notFound, redirect } from 'next/navigation'; import { match } from 'ts-pattern'; @@ -44,6 +44,10 @@ export default async function SigningPage({ params: { token } }: SigningPageProp return notFound(); } + if (document?.status === 'COMPLETED') { + redirect(`/sign/${token}/complete`); + } + const user = await getServerComponentSession(); const documentUrl = `data:application/pdf;base64,${document.document}`; From a74374e39f07c0001d63de356a3f8395b25b400a Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 6 Sep 2023 11:11:13 +0000 Subject: [PATCH 10/67] feat: add initial empty state for no results --- apps/web/src/app/(dashboard)/documents/page.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index 4ea55936b..3c4c13e30 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -1,5 +1,7 @@ import Link from 'next/link'; +import { CheckCircle2 } from 'lucide-react'; + import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; import { getStats } from '@documenso/lib/server-only/document/get-stats'; @@ -95,8 +97,21 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
- + {results.count > 0 && } + {results.count === 0 && }
); } + +const EmptyDocumentState = () => { + return ( +
+ +
+

All done

+

All documents signed for now.

+
+
+ ); +}; From 8220b2f086d1a422adfd643c0acf836c64321161 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 6 Sep 2023 11:47:58 +0000 Subject: [PATCH 11/67] feat: add empty state for different status --- .../app/(dashboard)/documents/empty-state.tsx | 46 +++++++++++++++++++ .../src/app/(dashboard)/documents/page.tsx | 17 +------ 2 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/documents/empty-state.tsx diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx new file mode 100644 index 000000000..fef3af5cc --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/empty-state.tsx @@ -0,0 +1,46 @@ +import { ArrowRight, CheckCircle2 } from 'lucide-react'; + +export default function EmptyDocumentState({ status }: { status: string }) { + let headerText = 'All done'; + let bodyText = 'All documents signed for now.'; + let extraText = ''; + let showArrow = false; + + switch (status) { + case 'COMPLETED': + headerText = 'Nothing here'; + bodyText = 'There are no signed documents yet.'; + extraText = 'Start by adding a document'; + showArrow = true; + break; + case 'DRAFT': + headerText = 'Nothing here'; + bodyText = 'There are no drafts yet.'; + extraText = 'Start by adding a document'; + showArrow = true; + break; + case 'ALL': + headerText = 'Nothing here'; + bodyText = 'There are no documents yet.'; + extraText = 'Start by adding a document'; + showArrow = true; + break; + default: + break; + } + + return ( +
+ +
+

{headerText}

+

{bodyText}

+ {extraText && ( +

+ {extraText} {showArrow && } +

+ )} +
+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index 3c4c13e30..0a90d1535 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -1,7 +1,5 @@ import Link from 'next/link'; -import { CheckCircle2 } from 'lucide-react'; - import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; import { getStats } from '@documenso/lib/server-only/document/get-stats'; @@ -15,6 +13,7 @@ import { DocumentStatus } from '~/components/formatter/document-status'; import { UploadDocument } from '../dashboard/upload-document'; import { DocumentsDataTable } from './data-table'; +import EmptyDocumentState from './empty-state'; export type DocumentsPageProps = { searchParams?: { @@ -98,20 +97,8 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
{results.count > 0 && } - {results.count === 0 && } + {results.count === 0 && }
); } - -const EmptyDocumentState = () => { - return ( -
- -
-

All done

-

All documents signed for now.

-
-
- ); -}; From 1ba7767f8e7675aa81d4799e1e59dd2d1a73750f Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 6 Sep 2023 11:52:15 +0000 Subject: [PATCH 12/67] chore: correct types on component --- apps/web/src/app/(dashboard)/documents/empty-state.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx index fef3af5cc..680f67bf5 100644 --- a/apps/web/src/app/(dashboard)/documents/empty-state.tsx +++ b/apps/web/src/app/(dashboard)/documents/empty-state.tsx @@ -1,6 +1,10 @@ import { ArrowRight, CheckCircle2 } from 'lucide-react'; -export default function EmptyDocumentState({ status }: { status: string }) { +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; + +export type EmptyDocumentProps = { status: ExtendedDocumentStatus }; + +export default function EmptyDocumentState({ status }: EmptyDocumentProps) { let headerText = 'All done'; let bodyText = 'All documents signed for now.'; let extraText = ''; From 1f027d75d3b85e4ff403ecc6f1581258634e26b4 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 6 Sep 2023 11:55:02 +0000 Subject: [PATCH 13/67] chore: fix eslint errors --- .../lib/server-only/pdf/insert-field-in-pdf.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts index 61726f53c..e7b1e7c5a 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts @@ -50,10 +50,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu let imageWidth = image.width; let imageHeight = image.height; - const initialDimensions = { - width: imageWidth, - height: imageHeight, - }; + // const initialDimensions = { + // width: imageWidth, + // height: imageHeight, + // }; const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1); @@ -76,10 +76,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu let textWidth = font.widthOfTextAtSize(field.customText, fontSize); const textHeight = font.heightAtSize(fontSize); - const initialDimensions = { - width: textWidth, - height: textHeight, - }; + // const initialDimensions = { + // width: textWidth, + // height: textHeight, + // }; const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1); From f6e49e3f2143c428710c96c0c8df2af85b94ad15 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 6 Sep 2023 12:00:23 +0000 Subject: [PATCH 14/67] chore: remove code from different branch --- apps/web/src/app/(signing)/sign/[token]/page.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index a55687196..f25183968 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -44,10 +44,6 @@ export default async function SigningPage({ params: { token } }: SigningPageProp return notFound(); } - if (document?.status === 'COMPLETED') { - redirect(`/sign/${token}/complete`); - } - const user = await getServerComponentSession(); const documentUrl = `data:application/pdf;base64,${document.document}`; From 0c145fab0bff601d5ce1cdcca042cd5248b99ed9 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 6 Sep 2023 12:01:30 +0000 Subject: [PATCH 15/67] chore: fix eslint errors --- apps/web/src/app/(signing)/sign/[token]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index f25183968..35621068a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -1,4 +1,4 @@ -import { notFound, redirect } from 'next/navigation'; +import { notFound } from 'next/navigation'; import { match } from 'ts-pattern'; From dbbe17a0a8a662ff9ea43d0db9dc244134ea190c Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 7 Sep 2023 19:58:48 +0000 Subject: [PATCH 16/67] feat: send email when recipient is done signing --- .../document/complete-document-with-token.ts | 3 ++ .../document/send-recipient-signed-email.ts | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 packages/lib/server-only/document/send-recipient-signed-email.ts diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index a013d5e69..4d4bacabf 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { sealDocument } from './seal-document'; +import { sendPendingEmail } from './send-recipient-signed-email'; export type CompleteDocumentWithTokenOptions = { token: string; @@ -69,6 +70,8 @@ export const completeDocumentWithToken = async ({ }, }); + await sendPendingEmail({ document, recipient }); + const documents = await prisma.document.updateMany({ where: { id: document.id, diff --git a/packages/lib/server-only/document/send-recipient-signed-email.ts b/packages/lib/server-only/document/send-recipient-signed-email.ts new file mode 100644 index 000000000..5deed2343 --- /dev/null +++ b/packages/lib/server-only/document/send-recipient-signed-email.ts @@ -0,0 +1,36 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending'; +import { Document, Recipient } from '@documenso/prisma/client'; + +export interface SendPendingEmailOptions { + document: Document; + recipient: Recipient; +} + +export const sendPendingEmail = async ({ document, recipient }: SendPendingEmailOptions) => { + const { email, name } = recipient; + + const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; + + const template = createElement(DocumentPendingEmailTemplate, { + documentName: document.title, + assetBaseUrl, + }); + + await mailer.sendMail({ + to: { + address: email, + name, + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'You are done signing.', + html: render(template), + text: render(template, { plainText: true }), + }); +}; From da2033692c157f12f57832bb26287f8e49038569 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 7 Sep 2023 20:14:04 +0000 Subject: [PATCH 17/67] feat: send email when all recipients have signed --- .../document/complete-document-with-token.ts | 1 + .../lib/server-only/document/seal-document.ts | 3 + .../document/send-completed-email.ts | 57 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 packages/lib/server-only/document/send-completed-email.ts diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 4d4bacabf..77ab33d04 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -70,6 +70,7 @@ export const completeDocumentWithToken = async ({ }, }); + // TODO: Send email to documents with two or more recipients await sendPendingEmail({ document, recipient }); const documents = await prisma.document.updateMany({ diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 1a74cfaac..f0806919f 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -6,6 +6,7 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { insertFieldInPDF } from '../pdf/insert-field-in-pdf'; +import { sendCompletedEmail } from './send-completed-email'; export type SealDocumentOptions = { documentId: number; @@ -67,4 +68,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { document: Buffer.from(pdfBytes).toString('base64'), }, }); + + await sendCompletedEmail({ documentId }); }; diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts new file mode 100644 index 000000000..b8d50dba4 --- /dev/null +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -0,0 +1,57 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed'; +import { prisma } from '@documenso/prisma'; + +export interface SendDocumentOptions { + documentId: number; +} + +export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => { + const document = await prisma.document.findUnique({ + where: { + id: documentId, + }, + include: { + Recipient: true, + }, + }); + + if (!document) { + throw new Error('Document not found'); + } + + if (document.Recipient.length === 0) { + throw new Error('Document has no recipients'); + } + + await Promise.all([ + document.Recipient.map(async (recipient) => { + const { email, name } = recipient; + + const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; + + const template = createElement(DocumentCompletedEmailTemplate, { + documentName: document.title, + assetBaseUrl, + downloadLink: 'https://documenso.com', + }); + + await mailer.sendMail({ + to: { + address: email, + name, + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'Everyone has signed!', + html: render(template), + text: render(template, { plainText: true }), + }); + }), + ]); +}; From 863e53a2d5cc8077302ac7a37789f69e7163de92 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 7 Sep 2023 20:38:18 +0000 Subject: [PATCH 18/67] refactor: pass document id as arguments --- .../document/complete-document-with-token.ts | 2 +- .../document/send-recipient-signed-email.ts | 36 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 77ab33d04..ea440beb9 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -71,7 +71,7 @@ export const completeDocumentWithToken = async ({ }); // TODO: Send email to documents with two or more recipients - await sendPendingEmail({ document, recipient }); + await sendPendingEmail({ documentId, recipientId: recipient.id }); const documents = await prisma.document.updateMany({ where: { diff --git a/packages/lib/server-only/document/send-recipient-signed-email.ts b/packages/lib/server-only/document/send-recipient-signed-email.ts index 5deed2343..ece75caec 100644 --- a/packages/lib/server-only/document/send-recipient-signed-email.ts +++ b/packages/lib/server-only/document/send-recipient-signed-email.ts @@ -3,14 +3,42 @@ import { createElement } from 'react'; import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending'; -import { Document, Recipient } from '@documenso/prisma/client'; +import { prisma } from '@documenso/prisma'; export interface SendPendingEmailOptions { - document: Document; - recipient: Recipient; + documentId: number; + recipientId: number; } -export const sendPendingEmail = async ({ document, recipient }: SendPendingEmailOptions) => { +export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => { + const document = await prisma.document.findFirst({ + where: { + id: documentId, + Recipient: { + some: { + id: recipientId, + }, + }, + }, + include: { + Recipient: { + where: { + id: recipientId, + }, + }, + }, + }); + + if (!document) { + throw new Error('Document not found'); + } + + if (document.Recipient.length === 0) { + throw new Error('Document has no recipients'); + } + + const [recipient] = document.Recipient; + const { email, name } = recipient; const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; From 525ff215634c02fb9f456568795c2ed917266515 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 7 Sep 2023 20:52:18 +0000 Subject: [PATCH 19/67] feat: avoid sending pending email to document with 1 recipients --- .../document/complete-document-with-token.ts | 11 +++++++++-- .../lib/server-only/document/send-completed-email.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index ea440beb9..464b7fe4f 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -70,8 +70,15 @@ export const completeDocumentWithToken = async ({ }, }); - // TODO: Send email to documents with two or more recipients - await sendPendingEmail({ documentId, recipientId: recipient.id }); + const numberOfRecipients = await prisma.recipient.count({ + where: { + documentId: document.id, + }, + }); + + if (numberOfRecipients > 1) { + await sendPendingEmail({ documentId, recipientId: recipient.id }); + } const documents = await prisma.document.updateMany({ where: { diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts index b8d50dba4..0a1817964 100644 --- a/packages/lib/server-only/document/send-completed-email.ts +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -48,7 +48,7 @@ export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', }, - subject: 'Everyone has signed!', + subject: 'Signing Complete!', html: render(template), text: render(template, { plainText: true }), }); From 73b0dc315e77a750df232cdf4364495eeb45f071 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 9 Sep 2023 03:31:17 +0000 Subject: [PATCH 20/67] fix: use ts-pattern --- .../app/(dashboard)/documents/empty-state.tsx | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx index 680f67bf5..a7d25a208 100644 --- a/apps/web/src/app/(dashboard)/documents/empty-state.tsx +++ b/apps/web/src/app/(dashboard)/documents/empty-state.tsx @@ -1,41 +1,40 @@ import { ArrowRight, CheckCircle2 } from 'lucide-react'; +import { match } from 'ts-pattern'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; export type EmptyDocumentProps = { status: ExtendedDocumentStatus }; export default function EmptyDocumentState({ status }: EmptyDocumentProps) { - let headerText = 'All done'; - let bodyText = 'All documents signed for now.'; - let extraText = ''; - let showArrow = false; - - switch (status) { - case 'COMPLETED': - headerText = 'Nothing here'; - bodyText = 'There are no signed documents yet.'; - extraText = 'Start by adding a document'; - showArrow = true; - break; - case 'DRAFT': - headerText = 'Nothing here'; - bodyText = 'There are no drafts yet.'; - extraText = 'Start by adding a document'; - showArrow = true; - break; - case 'ALL': - headerText = 'Nothing here'; - bodyText = 'There are no documents yet.'; - extraText = 'Start by adding a document'; - showArrow = true; - break; - default: - break; - } + const { headerText, bodyText, extraText, showArrow } = match(status) + .with(ExtendedDocumentStatus.COMPLETED, () => ({ + headerText: 'Nothing here', + bodyText: 'There are no signed documents yet.', + extraText: 'Start by adding a document', + showArrow: true, + })) + .with(ExtendedDocumentStatus.DRAFT, () => ({ + headerText: 'Nothing here', + bodyText: 'There are no drafts yet.', + extraText: 'Start by adding a document', + showArrow: true, + })) + .with(ExtendedDocumentStatus.ALL, () => ({ + headerText: 'Nothing here', + bodyText: 'There are no documents yet.', + extraText: 'Start by adding a document', + showArrow: true, + })) + .otherwise(() => ({ + headerText: 'All done', + bodyText: 'All documents signed for now.', + extraText: '', + showArrow: false, + })); return (
- +

{headerText}

{bodyText}

From 2bad1b9f558430fd337d443e6229c14a0b906104 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 9 Sep 2023 03:45:15 +0000 Subject: [PATCH 21/67] fix: tidy messaging --- .../app/(dashboard)/documents/empty-state.tsx | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx index a7d25a208..181f31fa5 100644 --- a/apps/web/src/app/(dashboard)/documents/empty-state.tsx +++ b/apps/web/src/app/(dashboard)/documents/empty-state.tsx @@ -1,4 +1,4 @@ -import { ArrowRight, CheckCircle2 } from 'lucide-react'; +import { ArrowRight, Bird, CheckCircle2 } from 'lucide-react'; import { match } from 'ts-pattern'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; @@ -6,43 +6,44 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen export type EmptyDocumentProps = { status: ExtendedDocumentStatus }; export default function EmptyDocumentState({ status }: EmptyDocumentProps) { - const { headerText, bodyText, extraText, showArrow } = match(status) + const { + title, + message, + icon: Icon, + } = match(status) .with(ExtendedDocumentStatus.COMPLETED, () => ({ - headerText: 'Nothing here', - bodyText: 'There are no signed documents yet.', - extraText: 'Start by adding a document', - showArrow: true, + title: 'Nothing to do', + message: + 'There are no completed documents yet. Documents that you have created or received that become completed will appear here later.', + icon: CheckCircle2, })) .with(ExtendedDocumentStatus.DRAFT, () => ({ - headerText: 'Nothing here', - bodyText: 'There are no drafts yet.', - extraText: 'Start by adding a document', - showArrow: true, + title: 'No active drafts', + message: + 'There are no active drafts at then current moment. You can upload a document to start drafting.', + icon: CheckCircle2, })) .with(ExtendedDocumentStatus.ALL, () => ({ - headerText: 'Nothing here', - bodyText: 'There are no documents yet.', - extraText: 'Start by adding a document', - showArrow: true, + title: "We're all empty", + message: + 'You have not yet created or received any documents. To create a document please upload one.', + icon: Bird, })) .otherwise(() => ({ - headerText: 'All done', - bodyText: 'All documents signed for now.', - extraText: '', - showArrow: false, + title: 'Nothing to do', + message: + 'All documents are currently actioned. Any new documents are sent or recieved they will start to appear here.', + icon: CheckCircle2, })); return ( -
- +
+ +
-

{headerText}

-

{bodyText}

- {extraText && ( -

- {extraText} {showArrow && } -

- )} +

{title}

+ +

{message}

); From 6f4c2805833f9ad90051b88796cf0591f8f0f7cc Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Sun, 17 Sep 2023 14:31:44 +0000 Subject: [PATCH 22/67] chore: match file name and method name --- .../lib/server-only/document/complete-document-with-token.ts | 2 +- .../{send-recipient-signed-email.ts => send-pending-email.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/lib/server-only/document/{send-recipient-signed-email.ts => send-pending-email.ts} (100%) diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 464b7fe4f..e442f3061 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -4,7 +4,7 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { sealDocument } from './seal-document'; -import { sendPendingEmail } from './send-recipient-signed-email'; +import { sendPendingEmail } from './send-pending-email'; export type CompleteDocumentWithTokenOptions = { token: string; diff --git a/packages/lib/server-only/document/send-recipient-signed-email.ts b/packages/lib/server-only/document/send-pending-email.ts similarity index 100% rename from packages/lib/server-only/document/send-recipient-signed-email.ts rename to packages/lib/server-only/document/send-pending-email.ts From 776324c8750a409f366363fb27c1c60f65da30c3 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Sun, 17 Sep 2023 14:38:39 +0000 Subject: [PATCH 23/67] fix: fitler only unsigned documents --- .../lib/server-only/document/complete-document-with-token.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index e442f3061..8b1b4d576 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -73,6 +73,9 @@ export const completeDocumentWithToken = async ({ const numberOfRecipients = await prisma.recipient.count({ where: { documentId: document.id, + signingStatus: { + not: SigningStatus.SIGNED, + }, }, }); From 47d55a5eaba6675eb1472bf6950721a028150bc9 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Mon, 18 Sep 2023 06:47:03 +0000 Subject: [PATCH 24/67] feat: add password reset token to schema --- .../migration.sql | 16 +++++++++ packages/prisma/schema.prisma | 36 ++++++++++++------- 2 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 packages/prisma/migrations/20230917190854_password_reset_token/migration.sql diff --git a/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql new file mode 100644 index 000000000..d22107691 --- /dev/null +++ b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "PasswordResetToken" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiry" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token"); + +-- AddForeignKey +ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 1ff3d7a75..96b7db0a3 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -19,19 +19,29 @@ enum Role { } model User { - id Int @id @default(autoincrement()) - name String? - email String @unique - emailVerified DateTime? - password String? - source String? - signature String? - roles Role[] @default([USER]) - identityProvider IdentityProvider @default(DOCUMENSO) - accounts Account[] - sessions Session[] - Document Document[] - Subscription Subscription[] + id Int @id @default(autoincrement()) + name String? + email String @unique + emailVerified DateTime? + password String? + source String? + signature String? + roles Role[] @default([USER]) + identityProvider IdentityProvider @default(DOCUMENSO) + accounts Account[] + sessions Session[] + Document Document[] + Subscription Subscription[] + PasswordResetToken PasswordResetToken[] +} + +model PasswordResetToken { + id Int @id @default(autoincrement()) + token String @unique + createdAt DateTime @default(now()) + expiry DateTime + userId Int + User User @relation(fields: [userId], references: [id]) } enum SubscriptionStatus { From f88e529111716be88bdce7d1211dc94bf58266ef Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Mon, 18 Sep 2023 10:18:33 +0000 Subject: [PATCH 25/67] feat: add forgot passoword page --- .../(unauthenticated)/check-email/page.tsx | 34 ++++++ .../forgot-password/page.tsx | 38 +++++++ .../src/components/forms/forgot-password.tsx | 104 ++++++++++++++++++ apps/web/src/components/forms/signin.tsx | 8 +- 4 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(unauthenticated)/check-email/page.tsx create mode 100644 apps/web/src/app/(unauthenticated)/forgot-password/page.tsx create mode 100644 apps/web/src/components/forms/forgot-password.tsx diff --git a/apps/web/src/app/(unauthenticated)/check-email/page.tsx b/apps/web/src/app/(unauthenticated)/check-email/page.tsx new file mode 100644 index 000000000..26039f4ae --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/check-email/page.tsx @@ -0,0 +1,34 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export default function ForgotPasswordPage() { + return ( +
+
+
+ background pattern +
+ +
+

Reset Pasword

+ +

+ Please check your email for reset instructions +

+ +

+ + Sign in + +

+
+
+
+ ); +} diff --git a/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx new file mode 100644 index 000000000..73cfd01ca --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx @@ -0,0 +1,38 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import backgroundPattern from '~/assets/background-pattern.png'; +import { ForgotPasswordForm } from '~/components/forms/forgot-password'; + +export default function ForgotPasswordPage() { + return ( +
+
+
+ background pattern +
+ +
+

Forgot Password?

+ +

+ No worries, we'll send you reset instructions. +

+ + + +

+ Don't have an account?{' '} + + Sign up + +

+
+
+
+ ); +} diff --git a/apps/web/src/components/forms/forgot-password.tsx b/apps/web/src/components/forms/forgot-password.tsx new file mode 100644 index 000000000..94b5ea0cd --- /dev/null +++ b/apps/web/src/components/forms/forgot-password.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { useEffect } from 'react'; + +import { useRouter, useSearchParams } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Loader } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { Input } from '@documenso/ui/primitives/input'; +import { Label } from '@documenso/ui/primitives/label'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +const ERROR_MESSAGES = { + [ErrorCode.CREDENTIALS_NOT_FOUND]: 'No account found with that email address.', + [ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'No account found with that email address.', + [ErrorCode.USER_MISSING_PASSWORD]: + 'This account appears to be using a social login method, please sign in using that method', +}; + +export const ZForgotPasswordFormSchema = z.object({ + email: z.string().email().min(1), +}); + +export type TForgotPasswordFormSchema = z.infer; + +export type ForgotPasswordFormProps = { + className?: string; +}; + +export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => { + const searchParams = useSearchParams(); + const router = useRouter(); + + const { toast } = useToast(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + values: { + email: '', + }, + resolver: zodResolver(ZForgotPasswordFormSchema), + }); + + const errorCode = searchParams?.get('error'); + + useEffect(() => { + let timeout: NodeJS.Timeout | null = null; + + if (isErrorCode(errorCode)) { + timeout = setTimeout(() => { + toast({ + variant: 'destructive', + description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred', + }); + }, 0); + } + + return () => { + if (timeout) { + clearTimeout(timeout); + } + }; + }, [errorCode, toast]); + + const onFormSubmit = ({ email }: TForgotPasswordFormSchema) => { + // check if the email is available + // if not, throw an error + // if the email is available, create a password reset token and send an email + + console.log(email); + router.push('/check-email'); + }; + + return ( + +
+ + + + + {errors.email && {errors.email.message}} +
+ + + + ); +}; diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index d9d727afc..a06c04c72 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; +import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -123,8 +124,11 @@ export const SignInForm = ({ className }: SignInFormProps) => {
-