Compare commits

...

27 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan
536adf6e0a fix: reduce text size? 2024-02-15 10:05:36 +00:00
Ephraim Atta-Duncan
be3ab09738 fix: set fixed with for signature pad and input 2024-02-05 11:18:03 +00:00
Ephraim Atta-Duncan
1c4a5449bb feat: use dancing script font locally 2024-01-28 18:19:42 +00:00
Ephraim Atta-Duncan
144bd4782b chore: remove console.logs 2024-01-28 18:05:17 +00:00
Ephraim Duncan
857e35c10a Update apps/web/src/app/(signing)/sign/[token]/form.tsx
Co-authored-by: Adithya Krishna  <aadithya794@gmail.com>
2024-01-28 18:03:10 +00:00
Ephraim Atta-Duncan
e9d6c24137 chore: remove console.log 2024-01-16 23:16:37 +00:00
Ephraim Atta-Duncan
dd5f39205a fix: position of undo signature button 2024-01-16 23:14:34 +00:00
Ephraim Atta-Duncan
f6ce7be61f fix: disable input for only drawn signature 2024-01-16 23:10:08 +00:00
Ephraim Atta-Duncan
9979d32a56 Merge branch 'main' into feat/accept-text-signature 2024-01-16 15:26:39 +00:00
Ephraim Atta-Duncan
daa541d570 fix: disable input when there is a signature available 2024-01-16 02:52:58 +00:00
Ephraim Atta-Duncan
1d91a9e813 fix: canvas for drawing signature and clear signature position 2024-01-16 02:40:43 +00:00
Lucas Smith
560352492d fix: keyboard shortcut ctrl+k fixed (#830) 2024-01-16 13:07:42 +11:00
Ephraim Atta-Duncan
075e15d428 feat: replace caveat font with dancing script 2024-01-16 01:54:28 +00:00
Ephraim Atta-Duncan
27e5ef0a51 Revert "INCOMPLETE: refactor signature pad and input into a single component"
This reverts commit e17e4566cd.
2024-01-16 01:17:11 +00:00
Lucas Smith
84b0c2756b fix: fixed the deleting signature block issue on touchscreens (#809)
Fixed the deleting signature block issue on touchscreens, for some
reason the `onClick` event isn't working on the touchscreens that's why
I've added `onTouchEnd` event to delete the signature block when the
user clicks on it.
2024-01-15 19:33:40 +11:00
Adithya Krishna
58b3a127ea chore: fix color for light mode icon (#806) 2024-01-15 10:48:55 +11:00
hiteshwadhwani
7e71e06e04 fix: keyboard shortcut ctrl+k default behaviour fixed 2024-01-13 14:19:37 +05:30
Adithya Krishna
0b8e84b6b7 refactor: singature pad & provider stuff
Signed-off-by: Adithya Krishna <adi@documenso.com>
2024-01-12 17:13:00 +05:30
Ephraim Atta-Duncan
e17e4566cd INCOMPLETE: refactor signature pad and input into a single component 2024-01-12 11:04:22 +00:00
Ashraf Chowdury
d73ef57794 Merge branch 'main' into fix/bug-798-signatures-block 2024-01-11 16:50:43 +08:00
Lucas Smith
b09071ebc7 feat: jump to next field (#805)
When the fields are not filled, the button will say "Next field". Clicking on the button takes you to
the unfilled field.
2024-01-10 15:27:18 +11:00
Timur Ercan
66bb56047a chore: update roadmap links 2024-01-09 14:32:49 +01:00
Catalin Pit
3054d84ba7 chore: implemented feedback 2024-01-08 09:58:34 +02:00
Ashraf Chowdury
a71078cbd5 fix: fixed the deleting signature block issue on touchscreens 2024-01-08 13:17:30 +08:00
Catalin Pit
4fd6a0d5b6 chore: update onOpenChange 2024-01-05 13:06:16 +02:00
Catalin Pit
fface15a22 feat: jump to next field 2024-01-05 12:56:07 +02:00
Ephraim Atta-Duncan
6ad3edb6c8 feat: sign document with text 2023-12-11 12:03:22 +00:00
18 changed files with 195 additions and 42 deletions

View File

@@ -13,9 +13,9 @@
·
<a href="https://github.com/documenso/documenso/issues">Issues</a>
·
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
<a href="https://documen.so/live">Upcoming Releases</a>
·
<a href="https://documen.so/launches">Upcoming Launches</a>
<a href="https://documen.so/roadmap">Roadmap</a>
</p>
</p>

View File

@@ -1,6 +1,6 @@
---
title: Announcing Pre-Seed and Open Metrics
description: We are exicited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
description: We are excited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'

View File

@@ -30,7 +30,7 @@ We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania)
## Documenso Merch Shop
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contributing to Documenso.
<figure>
<MdxNextImage

View File

@@ -1,7 +1,7 @@
'use client';
import type { HTMLAttributes, KeyboardEvent } from 'react';
import { useMemo, useState } from 'react';
import type { HTMLAttributes, KeyboardEvent } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
@@ -355,6 +355,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<div
className="absolute inset-x-0 bottom-0 flex cursor-auto items-center justify-between px-4 pb-2"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<Input
id="signatureText"
@@ -392,10 +393,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
</DialogHeader>
<DialogDescription>
By signing you signal your support of Documenso's mission in a <br></br>
<strong>non-legally binding, but heartfelt way</strong>. <br></br>
<br></br>You also unlock the option to purchase the early supporter plan including
everything we build this year for fixed price.
By signing you signal your support of Documenso's mission in a <br />
<strong>non-legally binding, but heartfelt way</strong>. <br />
<br />
You also unlock the option to purchase the early supporter plan including everything we
build this year for fixed price.
</DialogDescription>
<SignaturePad

View File

@@ -17,6 +17,10 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
);
const FONT_DANCING_SCRIPT_BYTES = fs.readFileSync(
path.join(__dirname, '../../packages/assets/fonts/dancing-script.ttf'),
);
/** @type {import('next').NextConfig} */
const config = {
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
@@ -40,6 +44,7 @@ const config = {
APP_VERSION: version,
NEXT_PUBLIC_PROJECT: 'web',
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
FONT_DANCING_SCRIPT_URI: `data:font/ttf;base64,${FONT_DANCING_SCRIPT_BYTES.toString('base64')}`,
},
modularizeImports: {
'lucide-react': {

View File

@@ -4,10 +4,18 @@ import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useElementScaleSize } from '@documenso/lib/client-only/hooks/use-element-scale-size';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import {
DEFAULT_HANDWRITING_FONT_SIZE,
MIN_HANDWRITING_FONT_SIZE,
} from '@documenso/lib/constants/pdf';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { Document, Field, Recipient } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
@@ -28,7 +36,27 @@ export type SigningFormProps = {
fields: Field[];
};
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
const ZSigningpadSchema = z.union([
z.object({
signatureDataUrl: z.string().min(1),
signatureText: z.null().or(z.string().max(0)),
}),
z.object({
signatureDataUrl: z.null().or(z.string().max(0)),
signatureText: z.string().trim().min(1),
}),
]);
export type TSigningpadSchema = z.infer<typeof ZSigningpadSchema>;
export const SigningForm = ({ document: _document, recipient, fields }: SigningFormProps) => {
const fontVariable = '--font-signature';
const minFontSize = MIN_HANDWRITING_FONT_SIZE;
const maxFontSize = DEFAULT_HANDWRITING_FONT_SIZE;
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
fontVariable,
);
const router = useRouter();
const analytics = useAnalytics();
const { data: session } = useSession();
@@ -41,14 +69,34 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
trpc.recipient.completeDocumentWithToken.useMutation();
const {
register,
handleSubmit,
setValue,
watch,
formState: { isSubmitting },
} = useForm();
} = useForm<TSigningpadSchema>({
mode: 'onChange',
defaultValues: {
signatureDataUrl: signature || null,
signatureText: '',
},
resolver: zodResolver(ZSigningpadSchema),
});
const { height, width } = useFieldPageCoords(fields.find((field) => field.type === 'SIGNATURE')!);
const signatureDataUrl = watch('signatureDataUrl');
const signatureText = watch('signatureText');
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
}, [fields]);
const fieldsValidated = () => {
setValidateUninsertedFields(true);
validateFieldsInserted(fields);
};
const onFormSubmit = async () => {
setValidateUninsertedFields(true);
@@ -60,18 +108,30 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
await completeDocumentWithToken({
token: recipient.token,
documentId: document.id,
documentId: _document.id,
});
analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id,
documentId: document.id,
documentId: _document.id,
timestamp: new Date().toISOString(),
});
router.push(`/sign/${recipient.token}/complete`);
};
const scalingFactor = useElementScaleSize(
{
height,
width,
},
signatureText || '',
maxFontSize,
fontVariableValue,
);
const fontSize = maxFontSize * scalingFactor;
return (
<form
className={cn(
@@ -123,15 +183,79 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
<div>
<Label htmlFor="Signature">Signature</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
/>
<Card id="signature" className="mt-4" degrees={-120} gradient>
<CardContent role="button" className="relative cursor-pointer pt-6">
<div className="flex h-44 max-w-[18rem] items-center justify-center pb-6">
{!signatureText && (
<SignaturePad
className="h-44"
defaultValue={signature ?? undefined}
clearSignatureClassName="absolute -bottom-6 -right-2 z-10 cursor-pointer"
undoSignatureClassName="absolute -top-32 -left-4 z-10 cursor-pointer"
onChange={(value) => {
setSignature(value);
}}
/>
)}
{signatureText && (
<p
style={{
fontSize: `clamp(${minFontSize}px, ${fontSize}px, ${maxFontSize}px)`,
fontFamily: `var(${fontVariable})`,
}}
className={cn(
'text-foreground font-signature max-w-[18rem] text-4xl font-semibold',
)}
>
{signatureText}
</p>
)}
</div>
<div
className="absolute inset-x-0 bottom-0 flex cursor-auto items-end justify-between px-4 pb-1 pt-2"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<Input
id="signatureText"
className="text-foreground placeholder:text-muted-foreground max-w-[15rem] border-0 border-none bg-transparent p-0 text-sm focus-visible:ring-transparent"
placeholder="Draw or type your name here"
disabled={isSubmitting || signature?.startsWith('data:')}
{...register('signatureText', {
onChange: (e) => {
if (e.target.value !== '') {
setValue('signatureDataUrl', null);
}
setValue('signatureText', e.target.value);
},
onBlur: (e) => {
if (e.target.value === '') {
return setValue('signatureText', '');
}
setSignature(e.target.value.trimStart());
},
})}
/>
{signatureText && (
<div className="absolute bottom-3 right-4 z-10 cursor-pointer">
<button
type="button"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2"
onClick={() => {
setValue('signatureText', '');
setValue('signatureDataUrl', null);
}}
>
Clear Signature
</button>
</div>
)}
</div>
</CardContent>
</Card>
</div>
@@ -152,8 +276,9 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
<SignDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
document={document}
document={_document}
fields={fields}
fieldsValidated={fieldsValidated}
/>
</div>
</div>

View File

@@ -15,6 +15,7 @@ export type SignDialogProps = {
isSubmitting: boolean;
document: Document;
fields: Field[];
fieldsValidated: () => void | Promise<void>;
onSignatureComplete: () => void | Promise<void>;
};
@@ -22,6 +23,7 @@ export const SignDialog = ({
isSubmitting,
document,
fields,
fieldsValidated,
onSignatureComplete,
}: SignDialogProps) => {
const [showDialog, setShowDialog] = useState(false);
@@ -29,16 +31,16 @@ export const SignDialog = ({
const isComplete = fields.every((field) => field.inserted);
return (
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<Dialog open={showDialog && isComplete} onOpenChange={setShowDialog}>
<DialogTrigger asChild>
<Button
className="w-full"
type="button"
size="lg"
disabled={!isComplete}
onClick={fieldsValidated}
loading={isSubmitting}
>
Complete
{isComplete ? 'Complete' : 'Next field'}
</Button>
</DialogTrigger>
<DialogContent>

View File

@@ -86,7 +86,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
token: recipient.token,
fieldId: field.id,
value,
isBase64: true,
isBase64: typeof value === 'string' && value.startsWith('data:image/png;base64,'),
});
if (source === 'local' && !providedSignature) {

View File

@@ -95,8 +95,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const currentPage = pages[pages.length - 1];
const toggleOpen = (e: KeyboardEvent) => {
e.preventDefault();
const toggleOpen = () => {
setIsOpen((isOpen) => !isOpen);
onOpenChange?.(!isOpen);
@@ -136,7 +135,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]);
const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]);
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen);
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen, { preventDefault: true });
useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings);
useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments);
useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates);

Binary file not shown.

View File

@@ -1,7 +1,7 @@
import { APP_BASE_URL } from './app';
export const DEFAULT_STANDARD_FONT_SIZE = 15;
export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
export const DEFAULT_HANDWRITING_FONT_SIZE = 30;
export const MIN_STANDARD_FONT_SIZE = 8;
export const MIN_HANDWRITING_FONT_SIZE = 20;

View File

@@ -12,7 +12,7 @@ import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-fiel
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
const fontCaveat = await fetch(process.env.FONT_CAVEAT_URI).then(async (res) =>
const fontDancingScript = await fetch(process.env.FONT_DANCING_SCRIPT_URI).then(async (res) =>
res.arrayBuffer(),
);
@@ -40,14 +40,28 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const fieldX = pageWidth * (Number(field.positionX) / 100);
const fieldY = pageHeight * (Number(field.positionY) / 100);
const font = await pdf.embedFont(isSignatureField ? fontCaveat : StandardFonts.Helvetica);
const font = await pdf.embedFont(isSignatureField ? fontDancingScript : StandardFonts.Helvetica, {
subset: true,
features: {
liga: false,
},
});
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await pdf.embedFont(fontCaveat);
await pdf.embedFont(fontDancingScript, {
subset: true,
features: {
liga: false,
},
});
}
const CUSTOM_TEXT = field.customText || field.Signature?.typedSignature || '';
const isInsertingImage =
isSignatureField && typeof field.Signature?.signatureImageAsBase64 === 'string';
isSignatureField &&
typeof field.Signature?.signatureImageAsBase64 === 'string' &&
field.Signature?.signatureImageAsBase64.startsWith('data:image/png;base64,');
if (isSignatureField && isInsertingImage) {
const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? '');
@@ -73,13 +87,13 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
height: imageHeight,
});
} else {
let textWidth = font.widthOfTextAtSize(field.customText, fontSize);
let textWidth = font.widthOfTextAtSize(CUSTOM_TEXT, fontSize);
const textHeight = font.heightAtSize(fontSize);
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1);
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
textWidth = font.widthOfTextAtSize(field.customText, fontSize);
textWidth = font.widthOfTextAtSize(CUSTOM_TEXT, fontSize);
const textX = fieldX + (fieldWidth - textWidth) / 2;
let textY = fieldY + (fieldHeight - textHeight) / 2;
@@ -87,7 +101,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
// Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight;
page.drawText(field.customText, {
page.drawText(CUSTOM_TEXT, {
x: textX,
y: textY,
size: fontSize,

View File

@@ -238,7 +238,6 @@ export const documentRouter = router({
userId: ctx.user.id,
});
} catch (err) {
console.log(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to duplicate this document. Please try again later.',

View File

@@ -64,6 +64,7 @@ declare namespace NodeJS {
DEPLOYMENT_TARGET?: 'webapp' | 'marketing';
FONT_CAVEAT_URI: string;
FONT_DANCING_SCRIPT_URI: string;
POSTGRES_URL?: string;
DATABASE_URL?: string;

View File

@@ -121,6 +121,7 @@ export const FieldItem = ({
<button
className="text-muted-foreground/50 hover:text-muted-foreground/80 bg-background absolute -right-2 -top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full border"
onClick={() => onRemove?.()}
onTouchEnd={() => onRemove?.()}
>
<Trash className="h-4 w-4" />
</button>

View File

@@ -16,12 +16,16 @@ const DPI = 2;
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
onChange?: (_signatureDataUrl: string | null) => void;
containerClassName?: string;
clearSignatureClassName?: string;
undoSignatureClassName?: string;
};
export const SignaturePad = ({
className,
containerClassName,
defaultValue,
clearSignatureClassName,
undoSignatureClassName,
onChange,
...props
}: SignaturePadProps) => {
@@ -227,7 +231,7 @@ export const SignaturePad = ({
{...props}
/>
<div className="absolute bottom-4 right-4 flex gap-2">
<div className={cn('absolute bottom-4 right-4', clearSignatureClassName)}>
<button
type="button"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2"
@@ -238,7 +242,7 @@ export const SignaturePad = ({
</div>
{lines.length > 0 && (
<div className="absolute bottom-4 left-4 flex gap-2">
<div className={cn('absolute bottom-4 left-4 flex gap-2', undoSignatureClassName)}>
<button
type="button"
title="undo"

View File

@@ -18,7 +18,7 @@ export const ThemeSwitcher = () => {
>
{isMounted && theme === THEMES_TYPE.LIGHT && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-exclusion"
className="bg-background absolute inset-0 rounded-full mix-blend-color-burn"
layoutId="selected-theme"
/>
)}

View File

@@ -86,6 +86,7 @@
"VERCEL_URL",
"DEPLOYMENT_TARGET",
"FONT_CAVEAT_URI",
"FONT_DANCING_SCRIPT_URI",
"POSTGRES_URL",
"DATABASE_URL",
"POSTGRES_PRISMA_URL",