From 0ba316111bd1352848ae61c182b2e6ab85497d69 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Thu, 16 Feb 2023 18:31:06 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20pdfsigner,=20readonly=20field,?= =?UTF-8?q?=20signpage,=20signgn=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/{field.tsx => editable-field.tsx} | 3 +- apps/web/components/editor/pdf-editor.tsx | 1 + apps/web/components/editor/pdf-signer.tsx | 48 +++++++ apps/web/components/editor/pdf-viewer.jsx | 30 ++-- apps/web/components/editor/readonly-field.tsx | 56 ++++++++ .../components/editor/signature-dialog.tsx | 132 ++++++++++++++++++ apps/web/pages/api/documents/[id].ts | 3 + apps/web/pages/documents/[id]/sign.tsx | 49 +++---- 8 files changed, 287 insertions(+), 35 deletions(-) rename apps/web/components/editor/{field.tsx => editable-field.tsx} (97%) create mode 100644 apps/web/components/editor/pdf-signer.tsx create mode 100644 apps/web/components/editor/readonly-field.tsx create mode 100644 apps/web/components/editor/signature-dialog.tsx diff --git a/apps/web/components/editor/field.tsx b/apps/web/components/editor/editable-field.tsx similarity index 97% rename from apps/web/components/editor/field.tsx rename to apps/web/components/editor/editable-field.tsx index da921e03b..60795d87e 100644 --- a/apps/web/components/editor/field.tsx +++ b/apps/web/components/editor/editable-field.tsx @@ -22,7 +22,7 @@ type FieldPropsType = { onDelete: any; }; -export default function Field(props: FieldPropsType) { +export default function EditableField(props: FieldPropsType) { const [field, setField]: any = useState(props.field); const [position, setPosition]: any = useState({ x: props.field.positionX, @@ -40,6 +40,7 @@ export default function Field(props: FieldPropsType) { props.onPositionChanged({ x, y }, props.field.id); }; + return ( import("./pdf-viewer"), { + ssr: false, +}); + +export default function PDFSigner(props: any) { + const router = useRouter(); + const [open, setOpen] = useState(false); + + function onClick(item: any) { + if (item.type === "SIGNATURE") { + setOpen(true); + } + } + + return ( + <> + +
+
+
+ +
+
+

+ Timur Ercan (timur.ercan31@gmail.com) would like you to sign this + document. +

+
+
+
+ {/* todo use public url with token auth to get document */} + + + ); +} diff --git a/apps/web/components/editor/pdf-viewer.jsx b/apps/web/components/editor/pdf-viewer.jsx index abb03bf0d..c68bd161f 100644 --- a/apps/web/components/editor/pdf-viewer.jsx +++ b/apps/web/components/editor/pdf-viewer.jsx @@ -1,6 +1,7 @@ import { Fragment, useState } from "react"; import { Document, Page } from "react-pdf/dist/esm/entry.webpack5"; -import Field from "./field"; +import EditableField from "./editable-field"; +import ReadOnlyField from "./readonly-field"; import short from "short-uuid"; export default function PDFViewer(props) { @@ -60,15 +61,24 @@ export default function PDFViewer(props) { > {props?.fields .filter((item) => item.page === index) - .map((item) => ( - - ))} + .map((item) => + props.readonly ? ( + + ) : ( + + ) + )} ))} diff --git a/apps/web/components/editor/readonly-field.tsx b/apps/web/components/editor/readonly-field.tsx new file mode 100644 index 000000000..8d75a0371 --- /dev/null +++ b/apps/web/components/editor/readonly-field.tsx @@ -0,0 +1,56 @@ +import { ResizableBox, ResizeCallbackData } from "react-resizable"; +import React, { SyntheticEvent, useEffect, useState } from "react"; +import Draggable from "react-draggable"; +import { CircleStackIcon, TrashIcon } from "@heroicons/react/24/solid"; +import Logo from "../logo"; +import { IconButton } from "@documenso/ui"; +import toast from "react-hot-toast"; +import { XCircleIcon } from "@heroicons/react/20/solid"; +const stc = require("string-to-color"); + +type FieldPropsType = { + field: { + color: string; + type: string; + position: any; + positionX: number; + positionY: number; + id: string; + Recipient: { name: ""; email: "" }; + }; + onClick: any; +}; + +export default function ReadOnlyField(props: FieldPropsType) { + const [field, setField]: any = useState(props.field); + const [position, setPosition]: any = useState({ + x: props.field.positionX, + y: props.field.positionY, + }); + const nodeRef = React.createRef(); + + return ( + +
{ + props.onClick(props.field); + }} + ref={nodeRef} + className="cursor-pointer opacity-80 p-2 m-auto w-auto flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none hover:brightness-50" + style={{ + background: stc(props.field.Recipient.email), + }} + > +
+ {field.type === "SIGNATURE" ? "SIGN HERE" : ""} +
+
+
+ ); +} diff --git a/apps/web/components/editor/signature-dialog.tsx b/apps/web/components/editor/signature-dialog.tsx new file mode 100644 index 000000000..c22b0a7a8 --- /dev/null +++ b/apps/web/components/editor/signature-dialog.tsx @@ -0,0 +1,132 @@ +import { classNames } from "@documenso/lib"; +import { Button, IconButton } from "@documenso/ui"; +import { Dialog, Transition } from "@headlessui/react"; +import { + BuildingOfficeIcon, + CreditCardIcon, + LanguageIcon, + PencilIcon, + UserIcon, + UsersIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import React from "react"; +import { Fragment, useState } from "react"; + +const tabs = [ + { name: "Type", href: "#", icon: LanguageIcon, current: true }, + { name: "Draw", href: "#", icon: PencilIcon, current: false }, +]; + +export default function SignatureDialog(props: any) { + const [currentTab, setCurrentTab] = useState(tabs[0]); + + return ( + <> + + + + + + + ); + + function isCurrentTab(tabName: string): boolean { + return currentTab.name === tabName; + } + + function setCurrent(t: any) { + tabs.forEach((tab) => { + tab.current = tab.name === t.name; + }); + setCurrentTab(t); + } +} diff --git a/apps/web/pages/api/documents/[id].ts b/apps/web/pages/api/documents/[id].ts index d24f6bfef..ebea48ef7 100644 --- a/apps/web/pages/api/documents/[id].ts +++ b/apps/web/pages/api/documents/[id].ts @@ -12,6 +12,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { const user = await getUserFromToken(req, res); const { id: documentId } = req.query; + // TODO Check if this is a public link with token and validate the token + const { token: recipientToken } = req.query; + if (!user) return; if (!documentId) { diff --git a/apps/web/pages/documents/[id]/sign.tsx b/apps/web/pages/documents/[id]/sign.tsx index 95c082777..40a73c9ed 100644 --- a/apps/web/pages/documents/[id]/sign.tsx +++ b/apps/web/pages/documents/[id]/sign.tsx @@ -4,28 +4,18 @@ import { useEffect } from "react"; import { NextPageWithLayout } from "../../_app"; import { ReadStatus } from "@prisma/client"; import SignaturePad from "signature_pad"; - +import { InformationCircleIcon, PencilIcon } from "@heroicons/react/24/outline"; +import Logo from "../../../components/logo"; +import PDFSigner from "../../../components/editor/pdf-signer"; +import fields from "../../api/documents/[id]/fields"; +//http://localhost:3000/documents/40/sign?token=wu82JFMxLvdYVJ9sKy9jvd const SignPage: NextPageWithLayout = (props: any) => { - useEffect(() => { - const canvas: any = document.querySelector("canvas"); - const signaturePad = new SignaturePad(canvas); - const resizeCanvas = () => { - const ratio = Math.max(window.devicePixelRatio || 1, 1); - canvas.width = canvas.offsetWidth * ratio; - canvas.height = canvas.offsetHeight * ratio; - canvas.getContext("2d").scale(ratio, ratio); - // signaturePad.clear(); // otherwise isEmpty() might return incorrect value - }; - window.addEventListener("resize", resizeCanvas); - }); return ( <> Sign | Documenso - Hello, please sign at the dotted line. - -
+ {/* todo read/ sign version of editor => flag or own component */} ); @@ -43,13 +33,23 @@ export async function getServerSideProps(context: any) { }, }); - const document = await prisma.recipient - .findFirstOrThrow({ - where: { - token: recipientToken, - }, - }) - .Document(); + const recipient = await prisma.recipient.findFirstOrThrow({ + where: { + token: recipientToken, + }, + include: { + Document: true, + }, + }); + + const fields = await prisma.field.findMany({ + where: { + documentId: recipient.Document.id, + }, + include: { + Recipient: true, + }, + }); // todo get r @@ -57,7 +57,8 @@ export async function getServerSideProps(context: any) { return { props: { - document: document, + document: recipient.Document, + fields: fields, }, }; }