From cc182649c2686a631eecbff60c9a6899bd9b863c Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 10 Jun 2023 22:33:12 +1000 Subject: [PATCH] wip: create document workflow --- .../src/app/(dashboard)/dashboard/page.tsx | 2 +- .../documents/[id]/loadable-pdf-card.tsx | 6 +- .../app/(dashboard)/documents/[id]/page.tsx | 44 +---- .../(dashboard)/metric-card/metric-card.tsx | 2 +- .../(dashboard)/pdf-viewer/pdf-viewer.tsx | 74 +++++++- .../src/components/forms/edit-document.tsx | 131 ++++++++++++++ .../forms/edit-document/add-fields.tsx | 163 ++++++++++++++++++ .../forms/edit-document/add-signers.tsx | 125 ++++++++++++++ .../components/forms/edit-document/types.ts | 13 ++ 9 files changed, 506 insertions(+), 54 deletions(-) create mode 100644 apps/web/src/components/forms/edit-document.tsx create mode 100644 apps/web/src/components/forms/edit-document/add-fields.tsx create mode 100644 apps/web/src/components/forms/edit-document/add-signers.tsx create mode 100644 apps/web/src/components/forms/edit-document/types.ts diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx index a97736bea..849917ff2 100644 --- a/apps/web/src/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/src/app/(dashboard)/dashboard/page.tsx @@ -30,7 +30,7 @@ export default async function DashboardPage() { findDocuments({ userId: session.id, perPage: 10, - }).then((r) => ({ ...r, data: [] })), + }), ]); return ( diff --git a/apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx b/apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx index ff9459a97..b2008c921 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/loadable-pdf-card.tsx @@ -1,7 +1,5 @@ 'use client'; -import { Suspense } from 'react'; - import dynamic from 'next/dynamic'; import { Loader } from 'lucide-react'; @@ -15,7 +13,7 @@ export type LoadablePDFCard = PDFViewerProps & { pdfClassName?: string; }; -const PDFCard = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), { +const PDFViewer = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), { ssr: false, loading: () => (
@@ -30,7 +28,7 @@ export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadableP return ( - + ); diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index 810a24caa..40ee92e97 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -5,9 +5,8 @@ import { ChevronLeft } from 'lucide-react'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; -import { Button } from '@documenso/ui/primitives/button'; -import { LoadablePDFCard } from './loadable-pdf-card'; +import { EditDocumentForm } from '~/components/forms/edit-document'; export type DocumentPageProps = { params: { @@ -49,46 +48,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { Document.pdf -
- - -
-
-

Add Signers

- -

Add the people who will sign the document.

- -
- -
- -
-

Add Signers (1/3)

- -
-
-
- -
- - - -
-
-
-
-
+
); } diff --git a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx index 17e12e487..885feda12 100644 --- a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx +++ b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx @@ -13,7 +13,7 @@ export type CardMetricProps = { export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricProps) => { return ( -
+
{Icon && } diff --git a/apps/web/src/components/(dashboard)/pdf-viewer/pdf-viewer.tsx b/apps/web/src/components/(dashboard)/pdf-viewer/pdf-viewer.tsx index c125332d9..cc98d1dfa 100644 --- a/apps/web/src/components/(dashboard)/pdf-viewer/pdf-viewer.tsx +++ b/apps/web/src/components/(dashboard)/pdf-viewer/pdf-viewer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Loader } from 'lucide-react'; import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; @@ -11,18 +11,32 @@ import { cn } from '@documenso/ui/lib/utils'; type LoadedPDFDocument = pdfjs.PDFDocumentProxy; +/** + * This imports the worker from the `pdfjs-dist` package. + */ pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.js', import.meta.url, ).toString(); +export type OnPDFViewerPageClick = (_event: { + pageNumber: number; + numPages: number; + originalEvent: React.MouseEvent; + pageHeight: number; + pageWidth: number; + pageX: number; + pageY: number; +}) => void | Promise; + export type PDFViewerProps = { className?: string; document: string; + onPageClick?: OnPDFViewerPageClick; [key: string]: unknown; }; -export const PDFViewer = ({ className, document, ...props }: PDFViewerProps) => { +export const PDFViewer = ({ className, document, onPageClick, ...props }: PDFViewerProps) => { const $el = useRef(null); const [width, setWidth] = useState(0); @@ -32,6 +46,50 @@ export const PDFViewer = ({ className, document, ...props }: PDFViewerProps) => setNumPages(doc.numPages); }; + const onDocumentPageClick = ( + event: React.MouseEvent, + pageNumber: number, + ) => { + const $el = event.target instanceof HTMLElement ? event.target : null; + + if (!$el) { + return; + } + + const $page = $el.closest('.react-pdf__Page'); + + if (!$page) { + return; + } + + const { height, width, top, left } = $page.getBoundingClientRect(); + + const pageX = event.clientX - left; + const pageY = event.clientY - top; + + console.log({ + pageNumber, + numPages, + originalEvent: event, + pageHeight: height, + pageWidth: width, + pageX, + pageY, + }); + + if (onPageClick) { + onPageClick({ + pageNumber, + numPages, + originalEvent: event, + pageHeight: height, + pageWidth: width, + pageX, + pageY, + }); + } + }; + useEffect(() => { if ($el.current) { const $current = $el.current; @@ -55,9 +113,9 @@ export const PDFViewer = ({ className, document, ...props }: PDFViewerProps) => }, []); return ( -
+
onDocumentLoaded(d)} externalLinkTarget="_blank" @@ -74,9 +132,13 @@ export const PDFViewer = ({ className, document, ...props }: PDFViewerProps) => .map((_, i) => (
- + onDocumentPageClick(e, i + 1)} + />
))}
diff --git a/apps/web/src/components/forms/edit-document.tsx b/apps/web/src/components/forms/edit-document.tsx new file mode 100644 index 000000000..d89892de4 --- /dev/null +++ b/apps/web/src/components/forms/edit-document.tsx @@ -0,0 +1,131 @@ +'use client'; + +import { useState } from 'react'; + +import dynamic from 'next/dynamic'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Loader } from 'lucide-react'; +import { useForm } from 'react-hook-form'; + +import { Document, User } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; + +import { AddFieldsFormPartial } from './edit-document/add-fields'; +import { AddSignersFormPartial } from './edit-document/add-signers'; +import { TEditDocumentFormSchema, ZEditDocumentFormSchema } from './edit-document/types'; + +const PDFViewer = dynamic(async () => import('~/components/(dashboard)/pdf-viewer/pdf-viewer'), { + ssr: false, + loading: () => ( +
+ + +

Loading document...

+
+ ), +}); + +const MAX_STEP = 2; + +export type EditDocumentFormProps = { + className?: string; + user: User; + document: Document; +}; + +export const EditDocumentForm = ({ className, document, user: _user }: EditDocumentFormProps) => { + const documentUrl = `data:application/pdf;base64,${document.document}`; + + const [step, setStep] = useState(0); + + const { + control, + // handleSubmit, + watch, + formState: { errors, isSubmitting, isValid }, + } = useForm({ + mode: 'onBlur', + defaultValues: { + signers: [ + { + name: '', + email: '', + }, + ], + }, + resolver: zodResolver(ZEditDocumentFormSchema), + }); + + const canGoBack = step > 0; + const canGoNext = isValid && step < MAX_STEP; + + const onGoBackClick = () => setStep((prev) => Math.max(0, prev - 1)); + const onGoNextClick = () => setStep((prev) => Math.min(MAX_STEP, prev + 1)); + + return ( +
+ + + + + + +
+
+ {step === 0 && ( + + )} + + {step === 1 && ( + + )} + +
+

+ Add Signers ({step + 1}/{MAX_STEP + 1}) +

+ +
+
+
+ +
+ + + +
+
+
+
+
+ ); +}; diff --git a/apps/web/src/components/forms/edit-document/add-fields.tsx b/apps/web/src/components/forms/edit-document/add-fields.tsx new file mode 100644 index 000000000..4a5e495f8 --- /dev/null +++ b/apps/web/src/components/forms/edit-document/add-fields.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { useState } from 'react'; + +import { Caveat } from 'next/font/google'; + +import { Check, ChevronsUpDown } from 'lucide-react'; +import { Control, FieldErrors, UseFormWatch } from 'react-hook-form'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@documenso/ui/primitives/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; + +import { TEditDocumentFormSchema } from './types'; + +const fontCaveat = Caveat({ + weight: ['500'], + subsets: ['latin'], + display: 'swap', + variable: '--font-caveat', +}); + +export type AddFieldsFormProps = { + className?: string; + control: Control; + watch: UseFormWatch; + errors: FieldErrors; + isSubmitting: boolean; +}; + +export const AddFieldsFormPartial = ({ + className, + control: _control, + watch, + errors: _errors, + isSubmitting: _isSubmitting, +}: AddFieldsFormProps) => { + const signers = watch('signers'); + + const [selectedSigner, setSelectedSigner] = useState(() => signers[0]); + + return ( +
+

Edit Document

+ +

Add all relevant fields for each recipient.

+ +
+ + + + + + + + + + + + + {signers.map((signer, index) => ( + setSelectedSigner(signer)}> + + {signer.name && ( + + {signer.name} ({signer.email}) + + )} + + {!signer.name && {signer.email}} + + ))} + + + + + +
+
+ + + + + + + +
+
+
+ ); +}; diff --git a/apps/web/src/components/forms/edit-document/add-signers.tsx b/apps/web/src/components/forms/edit-document/add-signers.tsx new file mode 100644 index 000000000..2f0f1c6ec --- /dev/null +++ b/apps/web/src/components/forms/edit-document/add-signers.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { AnimatePresence, motion } from 'framer-motion'; +import { Plus, Trash } from 'lucide-react'; +import { Control, Controller, FieldErrors, useFieldArray } from 'react-hook-form'; + +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 { FormErrorMessage } from '~/components/form/form-error-message'; + +import { TEditDocumentFormSchema } from './types'; + +export type AddSignersFormProps = { + className?: string; + control: Control; + errors: FieldErrors; + isSubmitting: boolean; +}; + +export const AddSignersFormPartial = ({ + className, + control, + errors, + isSubmitting, +}: AddSignersFormProps) => { + const { + append, + fields: signers, + remove, + } = useFieldArray({ + control, + name: 'signers', + }); + + return ( +
+

Add Signers

+ +

Add the people who will sign the document.

+ +
+ +
+
+ + {signers.map((field, index) => ( + +
+ + + ( + + )} + /> +
+ +
+ + + ( + + )} + /> +
+ +
+ +
+ +
+ + +
+
+ ))} +
+
+ +
+ +
+
+
+ ); +}; diff --git a/apps/web/src/components/forms/edit-document/types.ts b/apps/web/src/components/forms/edit-document/types.ts new file mode 100644 index 000000000..7ca551f48 --- /dev/null +++ b/apps/web/src/components/forms/edit-document/types.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const ZEditDocumentFormSchema = z.object({ + signers: z.array( + z.object({ + id: z.number().optional(), + email: z.string().min(1).email(), + name: z.string(), + }), + ), +}); + +export type TEditDocumentFormSchema = z.infer;