From e341b1e1f026743ec2b81c48401b1dbee8e3d595 Mon Sep 17 00:00:00 2001 From: harkiratsm Date: Mon, 5 Feb 2024 19:53:59 +0530 Subject: [PATCH 1/6] feat: add support for uploading signature Signed-off-by: harkiratsm --- apps/web/src/components/forms/profile.tsx | 23 +- apps/web/src/components/forms/signup.tsx | 20 +- packages/lib/server-only/user/create-user.ts | 6 +- .../lib/server-only/user/update-profile.ts | 5 +- .../migration.sql | 5 + packages/prisma/schema.prisma | 6 + packages/trpc/server/auth-router/router.ts | 4 +- packages/trpc/server/auth-router/schema.ts | 2 + packages/trpc/server/profile-router/router.ts | 3 +- packages/trpc/server/profile-router/schema.ts | 2 + packages/ui/primitives/signature-dropzone.tsx | 81 +++++ .../ui/primitives/signature-pad/drawpad.tsx | 257 ++++++++++++++++ .../signature-pad/signature-pad.tsx | 289 ++++-------------- 13 files changed, 458 insertions(+), 245 deletions(-) create mode 100644 packages/prisma/migrations/20240205103549_add_signature_type/migration.sql create mode 100644 packages/ui/primitives/signature-dropzone.tsx create mode 100644 packages/ui/primitives/signature-pad/drawpad.tsx diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 7036f4e432..56c773e01e 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -21,8 +21,9 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; -import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { useToast } from '@documenso/ui/primitives/use-toast'; +import { useState } from 'react'; +import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-pad'; export const ZProfileFormSchema = z.object({ name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), @@ -40,6 +41,8 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { const router = useRouter(); const { toast } = useToast(); + const [isUploaded, setIsUploaded] = useState(false); + const form = useForm({ values: { @@ -58,6 +61,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { await updateProfile({ name, signature, + signatureType: isUploaded ? 'UPLOAD' : 'DRAW', }); toast({ @@ -85,6 +89,12 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { } }; + const handleSignatureChange = (signature: string, isUploaded: boolean) => { + setIsUploaded(isUploaded); + form.setValue('signature', signature); + } + + return (
{ ( + render={() => ( Signature @@ -124,9 +134,12 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { containerClassName={cn( 'rounded-lg border bg-background', isSubmitting ? 'pointer-events-none opacity-50' : null, - )} - defaultValue={user.signature ?? undefined} - onChange={(v) => onChange(v ?? '')} + )} + signature={{ + value: user.signature, + type: user.signatureType, + }} + onChange={(v: any, isUploaded: any) => handleSignatureChange(v, isUploaded)} /> diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index 3f2723ec8a..8b974756f2 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -21,8 +21,10 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { PasswordInput } from '@documenso/ui/primitives/password-input'; -import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { useToast } from '@documenso/ui/primitives/use-toast'; +import { SignatureType } from '@documenso/prisma/client'; +import { useState } from 'react'; +import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-pad'; const SIGN_UP_REDIRECT_PATH = '/documents'; @@ -34,6 +36,7 @@ export const ZSignUpFormSchema = z.object({ .min(6, { message: 'Password should contain at least 6 characters' }) .max(72, { message: 'Password should not contain more than 72 characters' }), signature: z.string().min(1, { message: 'We need your signature to sign documents' }), + signatureType: z.nativeEnum(SignatureType) }); export type TSignUpFormSchema = z.infer; @@ -46,6 +49,7 @@ export type SignUpFormProps = { export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) => { const { toast } = useToast(); const analytics = useAnalytics(); + const [isUploaded, setIsUploaded] = useState(false); const form = useForm({ values: { @@ -53,6 +57,7 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) = email: '', password: '', signature: '', + signatureType: 'DRAW' }, resolver: zodResolver(ZSignUpFormSchema), }); @@ -63,7 +68,7 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) = const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => { try { - await signup({ name, email, password, signature }); + await signup({ name, email, password, signature, signatureType: isUploaded ? 'UPLOAD' : 'DRAW' }); await signIn('credentials', { email, @@ -106,6 +111,11 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) = } }; + const handleSignatureChange = (signature: string, isUploaded: boolean) => { + setIsUploaded(isUploaded); + form.setValue('signature', signature); + } + return ( onChange(v ?? '')} + signature={{ + value: form.watch('signature'), + type: isUploaded ? 'UPLOAD' : 'DRAW', + }} + onChange={(v: any, isUploaded: any) => handleSignatureChange(v, isUploaded)} /> diff --git a/packages/lib/server-only/user/create-user.ts b/packages/lib/server-only/user/create-user.ts index f7db60c85c..c2954bb4b2 100644 --- a/packages/lib/server-only/user/create-user.ts +++ b/packages/lib/server-only/user/create-user.ts @@ -2,7 +2,7 @@ import { hash } from 'bcrypt'; import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { prisma } from '@documenso/prisma'; -import { IdentityProvider } from '@documenso/prisma/client'; +import { IdentityProvider, SignatureType } from '@documenso/prisma/client'; import { SALT_ROUNDS } from '../../constants/auth'; import { getFlag } from '../../universal/get-feature-flag'; @@ -12,9 +12,10 @@ export interface CreateUserOptions { email: string; password: string; signature?: string | null; + signatureType: SignatureType; } -export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => { +export const createUser = async ({ name, email, password, signature, signatureType }: CreateUserOptions) => { const isBillingEnabled = await getFlag('app_billing'); const hashedPassword = await hash(password, SALT_ROUNDS); @@ -35,6 +36,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse email: email.toLowerCase(), password: hashedPassword, signature, + signatureType, identityProvider: IdentityProvider.DOCUMENSO, }, }); diff --git a/packages/lib/server-only/user/update-profile.ts b/packages/lib/server-only/user/update-profile.ts index a28fd21c54..31d5e2f75f 100644 --- a/packages/lib/server-only/user/update-profile.ts +++ b/packages/lib/server-only/user/update-profile.ts @@ -1,12 +1,14 @@ import { prisma } from '@documenso/prisma'; +import { SignatureType } from '@documenso/prisma/client'; export type UpdateProfileOptions = { userId: number; name: string; signature: string; + signatureType: SignatureType; }; -export const updateProfile = async ({ userId, name, signature }: UpdateProfileOptions) => { +export const updateProfile = async ({ userId, name, signature, signatureType }: UpdateProfileOptions) => { // Existence check await prisma.user.findFirstOrThrow({ where: { @@ -21,6 +23,7 @@ export const updateProfile = async ({ userId, name, signature }: UpdateProfileOp data: { name, signature, + signatureType }, }); diff --git a/packages/prisma/migrations/20240205103549_add_signature_type/migration.sql b/packages/prisma/migrations/20240205103549_add_signature_type/migration.sql new file mode 100644 index 0000000000..2d98937227 --- /dev/null +++ b/packages/prisma/migrations/20240205103549_add_signature_type/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "SignatureType" AS ENUM ('DRAW', 'UPLOAD'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "signatureType" "SignatureType" NOT NULL DEFAULT 'DRAW'; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index e1549e0722..0ea116e4ab 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -18,6 +18,11 @@ enum Role { USER } +enum SignatureType { + DRAW + UPLOAD +} + model User { id Int @id @default(autoincrement()) name String? @@ -27,6 +32,7 @@ model User { password String? source String? signature String? + signatureType SignatureType @default(DRAW) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt lastSignedIn DateTime @default(now()) diff --git a/packages/trpc/server/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts index 24dd272eeb..44a439961a 100644 --- a/packages/trpc/server/auth-router/router.ts +++ b/packages/trpc/server/auth-router/router.ts @@ -18,9 +18,9 @@ export const authRouter = router({ }); } - const { name, email, password, signature } = input; + const { name, email, password, signature, signatureType } = input; - const user = await createUser({ name, email, password, signature }); + const user = await createUser({ name, email, password, signature, signatureType }); await sendConfirmationToken({ email: user.email }); diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts index cc969c679a..5fa2912dad 100644 --- a/packages/trpc/server/auth-router/schema.ts +++ b/packages/trpc/server/auth-router/schema.ts @@ -1,3 +1,4 @@ +import { SignatureType } from '@documenso/prisma/client'; import { z } from 'zod'; export const ZSignUpMutationSchema = z.object({ @@ -5,6 +6,7 @@ export const ZSignUpMutationSchema = z.object({ email: z.string().email(), password: z.string().min(6), signature: z.string().min(1, { message: 'A signature is required.' }), + signatureType: z.nativeEnum(SignatureType), }); export type TSignUpMutationSchema = z.infer; diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index 4dcf4ca93f..e6a5cf1b8a 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -35,12 +35,13 @@ export const profileRouter = router({ .input(ZUpdateProfileMutationSchema) .mutation(async ({ input, ctx }) => { try { - const { name, signature } = input; + const { name, signature, signatureType } = input; return await updateProfile({ userId: ctx.user.id, name, signature, + signatureType, }); } catch (err) { console.error(err); diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index ef9ca2a14f..7bbac8ac5a 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -1,3 +1,4 @@ +import { SignatureType } from '@documenso/prisma/client'; import { z } from 'zod'; export const ZRetrieveUserByIdQuerySchema = z.object({ @@ -7,6 +8,7 @@ export const ZRetrieveUserByIdQuerySchema = z.object({ export const ZUpdateProfileMutationSchema = z.object({ name: z.string().min(1), signature: z.string(), + signatureType: z.nativeEnum(SignatureType), }); export const ZUpdatePasswordMutationSchema = z.object({ diff --git a/packages/ui/primitives/signature-dropzone.tsx b/packages/ui/primitives/signature-dropzone.tsx new file mode 100644 index 0000000000..9f5ec58126 --- /dev/null +++ b/packages/ui/primitives/signature-dropzone.tsx @@ -0,0 +1,81 @@ +'use client' + +import { useDropzone } from 'react-dropzone'; +import type { Variants } from 'framer-motion'; +import { motion } from 'framer-motion'; + +import { megabytesToBytes } from "@documenso/lib/universal/unit-convertions"; +import { Card, CardContent } from './card'; +import { cn } from '../lib/utils'; + +const DocumentDropzoneContainerVariants: Variants = { + initial: { + scale: 1, + }, + animate: { + scale: 1, + }, + hover: { + transition: { + staggerChildren: 0.05, + }, + }, +}; + + +export type SignatureDropzoneProps = { + className?: string; + onDrop?: (_file: File) => void | Promise; + disabled?: boolean; + disabledMessage?: string; +} + +export const SignatureDropzone = ({ + className, + onDrop, + disabledMessage = 'You cannot upload a signature', + ...props +}: SignatureDropzoneProps) => { + + const {getRootProps, getInputProps } = useDropzone({ + accept: { + 'image/png': ['.png'], + }, + maxFiles: 1, + onDrop: ([accceptedFile]) => { + if (accceptedFile && onDrop) { + void onDrop(accceptedFile); + } + }, + maxSize: megabytesToBytes(40), + }) + + return ( + + + + +

+ Add a signature +

+ +

+ Drag & drop your signature here. +

+
+
+
+ ) +} diff --git a/packages/ui/primitives/signature-pad/drawpad.tsx b/packages/ui/primitives/signature-pad/drawpad.tsx new file mode 100644 index 0000000000..30943fe266 --- /dev/null +++ b/packages/ui/primitives/signature-pad/drawpad.tsx @@ -0,0 +1,257 @@ +'use client'; + +import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { Undo2 } from 'lucide-react'; +import type { StrokeOptions } from 'perfect-freehand'; +import { getStroke } from 'perfect-freehand'; + +import { cn } from '../../lib/utils'; +import { getSvgPathFromStroke } from './helper'; +import { Point } from './point'; +import { SignatureType } from '.prisma/client'; + +const DPI = 2; + +export type DrawPadProps = Omit, 'onChange'> & { + onChange?: (_signatureDataUrl: string | null, isUploaded: boolean) => void; + signature: { value: string | null;type :string | null;} + containerClassName?: string; +}; + +export const DrawPad = ({ + className, + containerClassName, + signature, + onChange, + ...props +}: DrawPadProps) => { + const $el = useRef(null); + + const [isPressed, setIsPressed] = useState(false); + const [lines, setLines] = useState([]); + const [currentLine, setCurrentLine] = useState([]); + + const perfectFreehandOptions = useMemo(() => { + const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10; + + return { + size, + thinning: 0.25, + streamline: 0.5, + smoothing: 0.5, + end: { + taper: size * 2, + }, + } satisfies StrokeOptions; + }, []); + + const onMouseDown = (event: MouseEvent | PointerEvent | TouchEvent) => { + if (event.cancelable) { + event.preventDefault(); + } + + setIsPressed(true); + + const point = Point.fromEvent(event, DPI, $el.current); + + setCurrentLine([point]); + }; + + const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => { + if (event.cancelable) { + event.preventDefault(); + } + + if (!isPressed) { + return; + } + + const point = Point.fromEvent(event, DPI, $el.current); + + if (point.distanceTo(currentLine[currentLine.length - 1]) > 5) { + setCurrentLine([...currentLine, point]); + + // Update the canvas here to draw the lines + if ($el.current) { + const ctx = $el.current.getContext('2d'); + + if (ctx) { + ctx.restore(); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + + lines.forEach((line) => { + const pathData = new Path2D( + getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)), + ); + + ctx.fill(pathData); + }); + + const pathData = new Path2D( + getSvgPathFromStroke(getStroke([...currentLine, point], perfectFreehandOptions)), + ); + ctx.fill(pathData); + } + } + } + }; + + const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addLine = true) => { + if (event.cancelable) { + event.preventDefault(); + } + + setIsPressed(false); + + const point = Point.fromEvent(event, DPI, $el.current); + + const newLines = [...lines]; + + if (addLine && currentLine.length > 0) { + newLines.push([...currentLine, point]); + setCurrentLine([]); + } + + setLines(newLines); + + if ($el.current && newLines.length > 0) { + const ctx = $el.current.getContext('2d'); + + if (ctx) { + ctx.restore(); + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + + newLines.forEach((line) => { + const pathData = new Path2D( + getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)), + ); + ctx.fill(pathData); + }); + + onChange?.($el.current.toDataURL(), false); + + ctx.save(); + } + } + }; + + const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => { + if (event.cancelable) { + event.preventDefault(); + } + + if ('buttons' in event && event.buttons === 1) { + onMouseDown(event); + } + }; + + const onMouseLeave = (event: MouseEvent | PointerEvent | TouchEvent) => { + if (event.cancelable) { + event.preventDefault(); + } + + onMouseUp(event, false); + }; + + const onClearClick = () => { + if ($el.current) { + const ctx = $el.current.getContext('2d'); + + ctx?.clearRect(0, 0, $el.current.width, $el.current.height); + } + + onChange?.(null, false); + + setLines([]); + setCurrentLine([]); + }; + + const onUndoClick = () => { + if (lines.length === 0) { + return; + } + + const newLines = [...lines]; + newLines.pop(); // Remove the last line + setLines(newLines); + + // Clear the canvas + if ($el.current) { + const ctx = $el.current.getContext('2d'); + ctx?.clearRect(0, 0, $el.current.width, $el.current.height); + + newLines.forEach((line) => { + const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions))); + ctx?.fill(pathData); + }); + } + }; + + useEffect(() => { + if ($el.current) { + $el.current.width = $el.current.clientWidth * DPI; + $el.current.height = $el.current.clientHeight * DPI; + } + }, []); + + useEffect(() => { + if ($el.current && typeof signature.value === 'string' && signature.type === SignatureType.DRAW) { + const ctx = $el.current.getContext('2d'); + + const { width, height } = $el.current; + + const img = new Image(); + + img.onload = () => { + ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height)); + }; + + img.src = signature.value; + } + }, [signature]); + + return ( +
+ onMouseMove(event)} + onPointerDown={(event) => onMouseDown(event)} + onPointerUp={(event) => onMouseUp(event)} + onPointerLeave={(event) => onMouseLeave(event)} + onPointerEnter={(event) => onMouseEnter(event)} + {...props} + /> + +
+ +
+ + {lines.length > 0 && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx index 80bac0e182..a24376cca7 100644 --- a/packages/ui/primitives/signature-pad/signature-pad.tsx +++ b/packages/ui/primitives/signature-pad/signature-pad.tsx @@ -1,255 +1,82 @@ 'use client'; -import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react'; -import { useEffect, useMemo, useRef, useState } from 'react'; - -import { Undo2 } from 'lucide-react'; -import type { StrokeOptions } from 'perfect-freehand'; -import { getStroke } from 'perfect-freehand'; - -import { cn } from '../../lib/utils'; -import { getSvgPathFromStroke } from './helper'; -import { Point } from './point'; - -const DPI = 2; +import { useState, type HTMLAttributes } from "react"; +import { cn } from "../../lib/utils"; +import { base64 } from "@documenso/lib/universal/base64"; +import { Tabs, TabsList, TabsTrigger } from "../tabs"; +import { TabsContent } from "@radix-ui/react-tabs"; +import { SignatureDropzone } from "../signature-dropzone"; +import { SignatureIcon } from '@documenso/ui/icons/signature'; +import { UploadIcon } from "lucide-react"; +import { DrawPad } from "./drawpad"; +import { SignatureType } from "@prisma/client"; +import { Button } from "../button"; export type SignaturePadProps = Omit, 'onChange'> & { - onChange?: (_signatureDataUrl: string | null) => void; containerClassName?: string; + signature: { value: string | null;type :SignatureType;} + onChange?: (_signatureDataUrl: string | null, isUploaded: boolean) => void; }; export const SignaturePad = ({ className, containerClassName, - defaultValue, onChange, + signature, ...props }: SignaturePadProps) => { - const $el = useRef(null); - - const [isPressed, setIsPressed] = useState(false); - const [lines, setLines] = useState([]); - const [currentLine, setCurrentLine] = useState([]); - - const perfectFreehandOptions = useMemo(() => { - const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10; - - return { - size, - thinning: 0.25, - streamline: 0.5, - smoothing: 0.5, - end: { - taper: size * 2, - }, - } satisfies StrokeOptions; - }, []); - - const onMouseDown = (event: MouseEvent | PointerEvent | TouchEvent) => { - if (event.cancelable) { - event.preventDefault(); - } - - setIsPressed(true); - - const point = Point.fromEvent(event, DPI, $el.current); - - setCurrentLine([point]); - }; - - const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => { - if (event.cancelable) { - event.preventDefault(); - } - - if (!isPressed) { - return; - } - - const point = Point.fromEvent(event, DPI, $el.current); - - if (point.distanceTo(currentLine[currentLine.length - 1]) > 5) { - setCurrentLine([...currentLine, point]); - - // Update the canvas here to draw the lines - if ($el.current) { - const ctx = $el.current.getContext('2d'); - - if (ctx) { - ctx.restore(); - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - - lines.forEach((line) => { - const pathData = new Path2D( - getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)), - ); - - ctx.fill(pathData); - }); - - const pathData = new Path2D( - getSvgPathFromStroke(getStroke([...currentLine, point], perfectFreehandOptions)), - ); - ctx.fill(pathData); - } - } - } - }; - - const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addLine = true) => { - if (event.cancelable) { - event.preventDefault(); - } - - setIsPressed(false); - - const point = Point.fromEvent(event, DPI, $el.current); - - const newLines = [...lines]; - - if (addLine && currentLine.length > 0) { - newLines.push([...currentLine, point]); - setCurrentLine([]); - } - - setLines(newLines); - - if ($el.current && newLines.length > 0) { - const ctx = $el.current.getContext('2d'); - - if (ctx) { - ctx.restore(); - - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; + const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string} | null >(); - newLines.forEach((line) => { - const pathData = new Path2D( - getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)), - ); - ctx.fill(pathData); - }); + const onSignatureDrop = async (file: File) => { + try { + const arrayBuffer = await file.arrayBuffer(); + const base64String = base64.encode(new Uint8Array(arrayBuffer)); - onChange?.($el.current.toDataURL()); - - ctx.save(); - } - } - }; - - const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => { - if (event.cancelable) { - event.preventDefault(); - } - - if ('buttons' in event && event.buttons === 1) { - onMouseDown(event); - } - }; - - const onMouseLeave = (event: MouseEvent | PointerEvent | TouchEvent) => { - if (event.cancelable) { - event.preventDefault(); - } - - onMouseUp(event, false); - }; - - const onClearClick = () => { - if ($el.current) { - const ctx = $el.current.getContext('2d'); - - ctx?.clearRect(0, 0, $el.current.width, $el.current.height); - } - - onChange?.(null); - - setLines([]); - setCurrentLine([]); - }; - - const onUndoClick = () => { - if (lines.length === 0) { - return; - } - - const newLines = [...lines]; - newLines.pop(); // Remove the last line - setLines(newLines); - - // Clear the canvas - if ($el.current) { - const ctx = $el.current.getContext('2d'); - ctx?.clearRect(0, 0, $el.current.width, $el.current.height); - - newLines.forEach((line) => { - const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions))); - ctx?.fill(pathData); + setUploadedFile({ + file, + fileBase64: `data:image/png;base64,${base64String}`, }); + onChange?.(`data:image/png;base64,${base64String}`, true) } - }; - - useEffect(() => { - if ($el.current) { - $el.current.width = $el.current.clientWidth * DPI; - $el.current.height = $el.current.clientHeight * DPI; + catch (error) { + console.error(error); } - }, []); - - useEffect(() => { - if ($el.current && typeof defaultValue === 'string') { - const ctx = $el.current.getContext('2d'); - - const { width, height } = $el.current; - - const img = new Image(); - - img.onload = () => { - ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height)); - }; - - img.src = defaultValue; - } - }, [defaultValue]); + }; return (
- onMouseMove(event)} - onPointerDown={(event) => onMouseDown(event)} - onPointerUp={(event) => onMouseUp(event)} - onPointerLeave={(event) => onMouseLeave(event)} - onPointerEnter={(event) => onMouseEnter(event)} - {...props} - /> - -
- -
- - {lines.length > 0 && ( -
- -
- )} + + + + + Draw + + + + Upload + + + + + + +
+ {uploadedFile || (signature.type === SignatureType.UPLOAD && signature.value ) ? ( +
+ + +
+ ) + : ( + + )} + +
+
+
); }; + From 7abd156b2812d8ffea4789783187007c4cee0c4c Mon Sep 17 00:00:00 2001 From: harkiratsm Date: Fri, 9 Feb 2024 22:44:00 +0530 Subject: [PATCH 2/6] add removing of the uploaded sign Signed-off-by: harkiratsm --- apps/web/src/components/forms/profile.tsx | 2 +- packages/ui/primitives/signature-dropzone.tsx | 2 +- .../signature-pad/signature-pad.tsx | 32 ++++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 56c773e01e..792b25358f 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -27,7 +27,7 @@ import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-p export const ZProfileFormSchema = z.object({ name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), - signature: z.string().min(1, 'Signature Pad cannot be empty'), + signature: z.string().min(0, 'Signature Pad cannot be empty'), }); export type TProfileFormSchema = z.infer; diff --git a/packages/ui/primitives/signature-dropzone.tsx b/packages/ui/primitives/signature-dropzone.tsx index 9f5ec58126..abde1fc98f 100644 --- a/packages/ui/primitives/signature-dropzone.tsx +++ b/packages/ui/primitives/signature-dropzone.tsx @@ -48,7 +48,7 @@ export const SignatureDropzone = ({ } }, maxSize: megabytesToBytes(40), - }) + }); return ( , 'onChange'> & { containerClassName?: string; @@ -45,7 +45,7 @@ export const SignaturePad = ({ return (
- + @@ -60,20 +60,30 @@ export const SignaturePad = ({ -
{uploadedFile || (signature.type === SignatureType.UPLOAD && signature.value ) ? ( -
- - -
+ + + + + + ) : ( )} - -
From 6241500b34c704ada25fcef97ab2f4c4bc87c2ad Mon Sep 17 00:00:00 2001 From: harkiratsm Date: Sat, 10 Feb 2024 18:56:00 +0530 Subject: [PATCH 3/6] fix the signature pad component Signed-off-by: harkiratsm --- .../src/components/(marketing)/widget.tsx | 7 ++++++- .../src/app/(signing)/sign/[token]/form.tsx | 12 +++++++---- .../src/app/(signing)/sign/[token]/page.tsx | 1 + .../app/(signing)/sign/[token]/provider.tsx | 8 +++++++ .../sign/[token]/signature-field.tsx | 21 ++++++++++++------- apps/web/src/components/forms/profile.tsx | 9 +++++++- .../document-flow/add-signature.tsx | 8 +++++-- .../ui/primitives/signature-pad/drawpad.tsx | 2 +- .../signature-pad/signature-pad.tsx | 20 +++++++++++------- 9 files changed, 64 insertions(+), 24 deletions(-) diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx index 80c13b2757..ad8823085c 100644 --- a/apps/marketing/src/components/(marketing)/widget.tsx +++ b/apps/marketing/src/components/(marketing)/widget.tsx @@ -400,7 +400,12 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => { diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index f5c94e6ec6..d0758368c5 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -33,7 +33,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = const analytics = useAnalytics(); const { data: session } = useSession(); - const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); + const { fullName, signature, setFullName, setSignature, signatureType, setSignatureType } = useRequiredSigningContext(); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); @@ -132,9 +132,13 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = { - setSignature(value); + signature={{ + value: signature, + type: signatureType ?? 'DRAW', + }} + onChange={(v: any, isUploaded: any) => { + setSignature(v); + setSignatureType(isUploaded ? 'UPLOAD' : 'DRAW'); }} /> diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 004c59329e..9f48dd805c 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -102,6 +102,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp email={recipient.email} fullName={user?.email === recipient.email ? user.name : recipient.name} signature={user?.email === recipient.email ? user.signature : undefined} + signatureType={user?.email === recipient.email ? user.signatureType : undefined} >

diff --git a/apps/web/src/app/(signing)/sign/[token]/provider.tsx b/apps/web/src/app/(signing)/sign/[token]/provider.tsx index 454007cb05..0bbb080d53 100644 --- a/apps/web/src/app/(signing)/sign/[token]/provider.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/provider.tsx @@ -1,5 +1,6 @@ 'use client'; +import { SignatureType } from '@documenso/prisma/client'; import { createContext, useContext, useState } from 'react'; export type SigningContextValue = { @@ -9,6 +10,8 @@ export type SigningContextValue = { setEmail: (_value: string) => void; signature: string | null; setSignature: (_value: string | null) => void; + signatureType: SignatureType | null; + setSignatureType: (_value: SignatureType | null) => void; }; const SigningContext = createContext(null); @@ -31,6 +34,7 @@ export interface SigningProviderProps { fullName?: string | null; email?: string | null; signature?: string | null; + signatureType?: SignatureType | null; children: React.ReactNode; } @@ -38,11 +42,13 @@ export const SigningProvider = ({ fullName: initialFullName, email: initialEmail, signature: initialSignature, + signatureType: initialSignatureType, children, }: SigningProviderProps) => { const [fullName, setFullName] = useState(initialFullName || ''); const [email, setEmail] = useState(initialEmail || ''); const [signature, setSignature] = useState(initialSignature || null); + const [signatureType, setSignatureType] = useState(initialSignatureType || null); return ( {children} diff --git a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx index 220d3084ae..f2c7c61bea 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import type { Recipient } from '@documenso/prisma/client'; +import { Recipient, SignatureType } from '@documenso/prisma/client'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; @@ -29,7 +29,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { const router = useRouter(); const { toast } = useToast(); - const { signature: providedSignature, setSignature: setProvidedSignature } = + const { signature: providedSignature, setSignature: setProvidedSignature, setSignatureType: setProvidedSignatureType } = useRequiredSigningContext(); const [isPending, startTransition] = useTransition(); @@ -47,7 +47,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const [showSignatureModal, setShowSignatureModal] = useState(false); - const [localSignature, setLocalSignature] = useState(null); + const [localSignature, setLocalSignature] = useState<{value: string, type: SignatureType | null} | null>(); const [isLocalSignatureSet, setIsLocalSignatureSet] = useState(false); const state = useMemo(() => { @@ -70,13 +70,13 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { const onSign = async (source: 'local' | 'provider' = 'provider') => { try { - if (!providedSignature && !localSignature) { + if (!providedSignature && !localSignature?.value) { setIsLocalSignatureSet(false); setShowSignatureModal(true); return; } - const value = source === 'local' && localSignature ? localSignature : providedSignature ?? ''; + const value = source === 'local' && localSignature?.value ? localSignature.value : providedSignature ?? ''; if (!value) { return; @@ -90,7 +90,8 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { }); if (source === 'local' && !providedSignature) { - setProvidedSignature(localSignature); + setProvidedSignature(localSignature?.value ?? ''); + setProvidedSignatureType(localSignature?.type ?? 'DRAW') } setLocalSignature(null); @@ -167,8 +168,14 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { setLocalSignature(value)} + onChange={(value: any, isUploaded:any) => { + setLocalSignature({ value, type: isUploaded ? SignatureType.UPLOAD: SignatureType.DRAW }); + }} />

diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 792b25358f..b1e32309ec 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -27,7 +27,7 @@ import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-p export const ZProfileFormSchema = z.object({ name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), - signature: z.string().min(0, 'Signature Pad cannot be empty'), + signature: z.string(), }); export type TProfileFormSchema = z.infer; @@ -58,6 +58,13 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => { try { + if (signature === "") { + form.setError('signature',{ + type: 'manual', + message: "Signature Pad cannot be empty" + }) + return; + } await updateProfile({ name, signature, diff --git a/packages/ui/primitives/document-flow/add-signature.tsx b/packages/ui/primitives/document-flow/add-signature.tsx index 5accdca16c..242873de0c 100644 --- a/packages/ui/primitives/document-flow/add-signature.tsx +++ b/packages/ui/primitives/document-flow/add-signature.tsx @@ -287,9 +287,13 @@ export const AddSignatureFormPartial = ({ { + onChange={(value: any, _: any) => { onFormValueChange(FieldType.SIGNATURE); field.onChange(value); }} diff --git a/packages/ui/primitives/signature-pad/drawpad.tsx b/packages/ui/primitives/signature-pad/drawpad.tsx index 30943fe266..c2bc89b44b 100644 --- a/packages/ui/primitives/signature-pad/drawpad.tsx +++ b/packages/ui/primitives/signature-pad/drawpad.tsx @@ -200,7 +200,7 @@ export const DrawPad = ({ }, []); useEffect(() => { - if ($el.current && typeof signature.value === 'string' && signature.type === SignatureType.DRAW) { + if ($el.current && typeof signature?.value === 'string' && signature?.type === SignatureType.DRAW) { const ctx = $el.current.getContext('2d'); const { width, height } = $el.current; diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx index 7d8bf0506d..5932ecccab 100644 --- a/packages/ui/primitives/signature-pad/signature-pad.tsx +++ b/packages/ui/primitives/signature-pad/signature-pad.tsx @@ -15,6 +15,7 @@ import { Card, CardContent } from "../card"; export type SignaturePadProps = Omit, 'onChange'> & { containerClassName?: string; signature: { value: string | null;type :SignatureType;} + disabled?: boolean; onChange?: (_signatureDataUrl: string | null, isUploaded: boolean) => void; }; @@ -22,10 +23,13 @@ export const SignaturePad = ({ className, containerClassName, onChange, + disabled = false, signature, ...props }: SignaturePadProps) => { - const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string} | null >(); + const [uploadedFile, setUploadedFile] = useState<{fileBase64: string | null} | null >({ + fileBase64: signature.type === SignatureType.UPLOAD ? signature.value : null + }); const onSignatureDrop = async (file: File) => { try { @@ -33,7 +37,6 @@ export const SignaturePad = ({ const base64String = base64.encode(new Uint8Array(arrayBuffer)); setUploadedFile({ - file, fileBase64: `data:image/png;base64,${base64String}`, }); onChange?.(`data:image/png;base64,${base64String}`, true) @@ -42,16 +45,16 @@ export const SignaturePad = ({ console.error(error); } }; - + return (
- + - + Draw - + Upload @@ -60,7 +63,7 @@ export const SignaturePad = ({ - {uploadedFile || (signature.type === SignatureType.UPLOAD && signature.value ) ? ( + {uploadedFile?.fileBase64 && signature.type === SignatureType.UPLOAD ? ( - +
diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index b1e32309ec..c89309b510 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -1,5 +1,7 @@ 'use client'; +import { useState } from 'react'; + import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -21,9 +23,8 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; -import { useToast } from '@documenso/ui/primitives/use-toast'; -import { useState } from 'react'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-pad'; +import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZProfileFormSchema = z.object({ name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), @@ -42,7 +43,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { const { toast } = useToast(); const [isUploaded, setIsUploaded] = useState(false); - const form = useForm({ values: { @@ -58,11 +58,11 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => { try { - if (signature === "") { - form.setError('signature',{ + if (signature === '') { + form.setError('signature', { type: 'manual', - message: "Signature Pad cannot be empty" - }) + message: 'Signature Pad cannot be empty', + }); return; } await updateProfile({ @@ -97,10 +97,9 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { }; const handleSignatureChange = (signature: string, isUploaded: boolean) => { - setIsUploaded(isUploaded); - form.setValue('signature', signature); - } - + setIsUploaded(isUploaded); + form.setValue('signature', signature); + }; return ( @@ -141,12 +140,12 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { containerClassName={cn( 'rounded-lg border bg-background', isSubmitting ? 'pointer-events-none opacity-50' : null, - )} + )} signature={{ value: user.signature, type: user.signatureType, - }} - onChange={(v: any, isUploaded: any) => handleSignatureChange(v, isUploaded)} + }} + onChange={(v: any, isUploaded: any) => handleSignatureChange(v, isUploaded)} /> diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index 8b974756f2..b275463870 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -1,5 +1,7 @@ 'use client'; +import { useState } from 'react'; + import { zodResolver } from '@hookform/resolvers/zod'; import { signIn } from 'next-auth/react'; import { useForm } from 'react-hook-form'; @@ -7,6 +9,7 @@ import { FcGoogle } from 'react-icons/fc'; import { z } from 'zod'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; +import { SignatureType } from '@documenso/prisma/client'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -21,10 +24,8 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { PasswordInput } from '@documenso/ui/primitives/password-input'; -import { useToast } from '@documenso/ui/primitives/use-toast'; -import { SignatureType } from '@documenso/prisma/client'; -import { useState } from 'react'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-pad'; +import { useToast } from '@documenso/ui/primitives/use-toast'; const SIGN_UP_REDIRECT_PATH = '/documents'; @@ -36,7 +37,7 @@ export const ZSignUpFormSchema = z.object({ .min(6, { message: 'Password should contain at least 6 characters' }) .max(72, { message: 'Password should not contain more than 72 characters' }), signature: z.string().min(1, { message: 'We need your signature to sign documents' }), - signatureType: z.nativeEnum(SignatureType) + signatureType: z.nativeEnum(SignatureType), }); export type TSignUpFormSchema = z.infer; @@ -57,7 +58,7 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) = email: '', password: '', signature: '', - signatureType: 'DRAW' + signatureType: 'DRAW', }, resolver: zodResolver(ZSignUpFormSchema), }); @@ -68,7 +69,13 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) = const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => { try { - await signup({ name, email, password, signature, signatureType: isUploaded ? 'UPLOAD' : 'DRAW' }); + await signup({ + name, + email, + password, + signature, + signatureType: isUploaded ? 'UPLOAD' : 'DRAW', + }); await signIn('credentials', { email, @@ -114,7 +121,7 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) = const handleSignatureChange = (signature: string, isUploaded: boolean) => { setIsUploaded(isUploaded); form.setValue('signature', signature); - } + }; return ( diff --git a/packages/lib/server-only/user/create-user.ts b/packages/lib/server-only/user/create-user.ts index c2954bb4b2..baa1b99d69 100644 --- a/packages/lib/server-only/user/create-user.ts +++ b/packages/lib/server-only/user/create-user.ts @@ -2,7 +2,7 @@ import { hash } from 'bcrypt'; import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { prisma } from '@documenso/prisma'; -import { IdentityProvider, SignatureType } from '@documenso/prisma/client'; +import { IdentityProvider, SignatureType } from '@documenso/prisma/client'; import { SALT_ROUNDS } from '../../constants/auth'; import { getFlag } from '../../universal/get-feature-flag'; @@ -15,7 +15,13 @@ export interface CreateUserOptions { signatureType: SignatureType; } -export const createUser = async ({ name, email, password, signature, signatureType }: CreateUserOptions) => { +export const createUser = async ({ + name, + email, + password, + signature, + signatureType, +}: CreateUserOptions) => { const isBillingEnabled = await getFlag('app_billing'); const hashedPassword = await hash(password, SALT_ROUNDS); diff --git a/packages/lib/server-only/user/update-profile.ts b/packages/lib/server-only/user/update-profile.ts index 31d5e2f75f..96bd847821 100644 --- a/packages/lib/server-only/user/update-profile.ts +++ b/packages/lib/server-only/user/update-profile.ts @@ -8,7 +8,12 @@ export type UpdateProfileOptions = { signatureType: SignatureType; }; -export const updateProfile = async ({ userId, name, signature, signatureType }: UpdateProfileOptions) => { +export const updateProfile = async ({ + userId, + name, + signature, + signatureType, +}: UpdateProfileOptions) => { // Existence check await prisma.user.findFirstOrThrow({ where: { @@ -23,7 +28,7 @@ export const updateProfile = async ({ userId, name, signature, signatureType }: data: { name, signature, - signatureType + signatureType, }, }); diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts index 5fa2912dad..e60582d360 100644 --- a/packages/trpc/server/auth-router/schema.ts +++ b/packages/trpc/server/auth-router/schema.ts @@ -1,6 +1,7 @@ -import { SignatureType } from '@documenso/prisma/client'; import { z } from 'zod'; +import { SignatureType } from '@documenso/prisma/client'; + export const ZSignUpMutationSchema = z.object({ name: z.string().min(1), email: z.string().email(), diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index 7bbac8ac5a..0a70f58bef 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -1,6 +1,7 @@ -import { SignatureType } from '@documenso/prisma/client'; import { z } from 'zod'; +import { SignatureType } from '@documenso/prisma/client'; + export const ZRetrieveUserByIdQuerySchema = z.object({ id: z.number().min(1), }); diff --git a/packages/ui/primitives/constants.ts b/packages/ui/primitives/constants.ts index 9771eb35a8..00207eb67f 100644 --- a/packages/ui/primitives/constants.ts +++ b/packages/ui/primitives/constants.ts @@ -1,5 +1,5 @@ export const THEMES_TYPE = { DARK: 'dark', LIGHT: 'light', - SYSTEM: 'system' -}; \ No newline at end of file + SYSTEM: 'system', +}; diff --git a/packages/ui/primitives/signature-dropzone.tsx b/packages/ui/primitives/signature-dropzone.tsx index abde1fc98f..c4762da965 100644 --- a/packages/ui/primitives/signature-dropzone.tsx +++ b/packages/ui/primitives/signature-dropzone.tsx @@ -1,12 +1,13 @@ -'use client' +'use client'; -import { useDropzone } from 'react-dropzone'; import type { Variants } from 'framer-motion'; import { motion } from 'framer-motion'; +import { useDropzone } from 'react-dropzone'; + +import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; -import { megabytesToBytes } from "@documenso/lib/universal/unit-convertions"; -import { Card, CardContent } from './card'; import { cn } from '../lib/utils'; +import { Card, CardContent } from './card'; const DocumentDropzoneContainerVariants: Variants = { initial: { @@ -22,13 +23,12 @@ const DocumentDropzoneContainerVariants: Variants = { }, }; - -export type SignatureDropzoneProps = { +export type SignatureDropzoneProps = { className?: string; onDrop?: (_file: File) => void | Promise; disabled?: boolean; disabledMessage?: string; -} +}; export const SignatureDropzone = ({ className, @@ -36,8 +36,7 @@ export const SignatureDropzone = ({ disabledMessage = 'You cannot upload a signature', ...props }: SignatureDropzoneProps) => { - - const {getRootProps, getInputProps } = useDropzone({ + const { getRootProps, getInputProps } = useDropzone({ accept: { 'image/png': ['.png'], }, @@ -50,7 +49,7 @@ export const SignatureDropzone = ({ maxSize: megabytesToBytes(40), }); - return ( + return ( -

- Drag & drop your signature here. -

+

Drag & drop your signature here.

- ) -} + ); +}; diff --git a/packages/ui/primitives/signature-pad/drawpad.tsx b/packages/ui/primitives/signature-pad/drawpad.tsx index c2bc89b44b..2d8364ad36 100644 --- a/packages/ui/primitives/signature-pad/drawpad.tsx +++ b/packages/ui/primitives/signature-pad/drawpad.tsx @@ -16,7 +16,7 @@ const DPI = 2; export type DrawPadProps = Omit, 'onChange'> & { onChange?: (_signatureDataUrl: string | null, isUploaded: boolean) => void; - signature: { value: string | null;type :string | null;} + signature: { value: string | null; type: string | null }; containerClassName?: string; }; @@ -200,7 +200,11 @@ export const DrawPad = ({ }, []); useEffect(() => { - if ($el.current && typeof signature?.value === 'string' && signature?.type === SignatureType.DRAW) { + if ( + $el.current && + typeof signature?.value === 'string' && + signature?.type === SignatureType.DRAW + ) { const ctx = $el.current.getContext('2d'); const { width, height } = $el.current; diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx index 5932ecccab..b2d7029cdc 100644 --- a/packages/ui/primitives/signature-pad/signature-pad.tsx +++ b/packages/ui/primitives/signature-pad/signature-pad.tsx @@ -1,20 +1,23 @@ 'use client'; -import { useState, type HTMLAttributes } from "react"; -import { cn } from "../../lib/utils"; -import { base64 } from "@documenso/lib/universal/base64"; -import { Tabs, TabsList, TabsTrigger } from "../tabs"; -import { TabsContent } from "@radix-ui/react-tabs"; -import { SignatureDropzone } from "../signature-dropzone"; +import { type HTMLAttributes, useState } from 'react'; + +import { SignatureType } from '@prisma/client'; +import { TabsContent } from '@radix-ui/react-tabs'; +import { UploadIcon } from 'lucide-react'; + +import { base64 } from '@documenso/lib/universal/base64'; import { SignatureIcon } from '@documenso/ui/icons/signature'; -import { UploadIcon } from "lucide-react"; -import { DrawPad } from "./drawpad"; -import { SignatureType } from "@prisma/client"; -import { Card, CardContent } from "../card"; + +import { cn } from '../../lib/utils'; +import { Card, CardContent } from '../card'; +import { SignatureDropzone } from '../signature-dropzone'; +import { Tabs, TabsList, TabsTrigger } from '../tabs'; +import { DrawPad } from './drawpad'; export type SignaturePadProps = Omit, 'onChange'> & { containerClassName?: string; - signature: { value: string | null;type :SignatureType;} + signature: { value: string | null; type: SignatureType }; disabled?: boolean; onChange?: (_signatureDataUrl: string | null, isUploaded: boolean) => void; }; @@ -23,12 +26,12 @@ export const SignaturePad = ({ className, containerClassName, onChange, - disabled = false, + disabled = false, signature, ...props }: SignaturePadProps) => { - const [uploadedFile, setUploadedFile] = useState<{fileBase64: string | null} | null >({ - fileBase64: signature.type === SignatureType.UPLOAD ? signature.value : null + const [uploadedFile, setUploadedFile] = useState<{ fileBase64: string | null } | null>({ + fileBase64: signature.type === SignatureType.UPLOAD ? signature.value : null, }); const onSignatureDrop = async (file: File) => { @@ -39,58 +42,58 @@ export const SignaturePad = ({ setUploadedFile({ fileBase64: `data:image/png;base64,${base64String}`, }); - onChange?.(`data:image/png;base64,${base64String}`, true) - } - catch (error) { + onChange?.(`data:image/png;base64,${base64String}`, true); + } catch (error) { console.error(error); } }; - + return (
- - - - - Draw - - - - Upload - - - - - - - {uploadedFile?.fileBase64 && signature.type === SignatureType.UPLOAD ? ( - - - - - - - ) - : ( - + + + + + Draw + + + + Upload + + + + + + + {uploadedFile?.fileBase64 ? ( + - + > + + + + + + ) : ( + + )} + +
); }; - From 194371bebd00f1a6eb7ff74b4dbc74a22bf21e79 Mon Sep 17 00:00:00 2001 From: harkiratsm Date: Sat, 10 Feb 2024 21:19:42 +0530 Subject: [PATCH 5/6] fix: tidy code Signed-off-by: harkiratsm --- .../src/components/(marketing)/widget.tsx | 4 +-- .../src/app/(signing)/sign/[token]/form.tsx | 36 +++++++++---------- .../sign/[token]/signature-field.tsx | 7 ++-- apps/web/src/components/forms/profile.tsx | 8 +++-- apps/web/src/components/forms/signup.tsx | 10 +++--- packages/lib/server-only/user/create-user.ts | 3 +- .../lib/server-only/user/update-profile.ts | 4 +-- packages/trpc/server/auth-router/schema.ts | 3 +- .../document-flow/add-signature.tsx | 4 +-- .../signature-pad/signature-pad.tsx | 4 ++- 10 files changed, 46 insertions(+), 37 deletions(-) diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx index c8065305ad..f5de083708 100644 --- a/apps/marketing/src/components/(marketing)/widget.tsx +++ b/apps/marketing/src/components/(marketing)/widget.tsx @@ -406,8 +406,8 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => { type: 'DRAW', }} // Disabling the uploading of a signature for marketing website - disabled={true} - onChange={setDraftSignatureDataUrl} + uploadDisable={true} + onChange={(value: string | null, _: boolean) => setDraftSignatureDataUrl(value)} /> diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index da5e9ca9fd..d06faf8526 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -167,24 +167,24 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
- - - { - setSignature(v); - setSignatureType(isUploaded ? 'UPLOAD' : 'DRAW'); - }} - /> - - -
- + + + { + setSignature(v); + setSignatureType(isUploaded ? 'UPLOAD' : 'DRAW'); + }} + /> + + + +