mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add uninserted field validation
This commit is contained in:
@ -1,12 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
||||||
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import { Document, Field, Recipient } from '@documenso/prisma/client';
|
import { Document, Field, Recipient } from '@documenso/prisma/client';
|
||||||
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@ -27,15 +30,22 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||||
|
|
||||||
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
} = useForm();
|
} = useForm();
|
||||||
|
|
||||||
const isComplete = fields.every((f) => f.inserted);
|
const uninsertedFields = useMemo(() => {
|
||||||
|
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
const onFormSubmit = async () => {
|
||||||
if (!isComplete) {
|
setValidateUninsertedFields(true);
|
||||||
|
const isFieldsValid = validateFieldsInserted(fields);
|
||||||
|
|
||||||
|
if (!isFieldsValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +64,16 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
)}
|
)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
>
|
>
|
||||||
<div className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}>
|
{validateUninsertedFields && uninsertedFields[0] && (
|
||||||
|
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||||
|
Click to insert field
|
||||||
|
</FieldToolTip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<fieldset
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
|
||||||
|
>
|
||||||
<div className={cn('flex flex-1 flex-col')}>
|
<div className={cn('flex flex-1 flex-col')}>
|
||||||
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
||||||
|
|
||||||
@ -106,19 +125,13 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button className="w-full" type="submit" size="lg" loading={isSubmitting}>
|
||||||
className="w-full"
|
|
||||||
type="submit"
|
|
||||||
size="lg"
|
|
||||||
disabled={!isComplete || isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
|
|
||||||
Complete
|
Complete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
|
|
||||||
export type SignatureFieldProps = {
|
export type SignatureFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
@ -22,8 +20,6 @@ export const SigningFieldContainer = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
children,
|
children,
|
||||||
}: SignatureFieldProps) => {
|
}: SignatureFieldProps) => {
|
||||||
const coords = useFieldPageCoords(field);
|
|
||||||
|
|
||||||
const onSignFieldClick = async () => {
|
const onSignFieldClick = async () => {
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
return;
|
return;
|
||||||
@ -41,24 +37,7 @@ export const SigningFieldContainer = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<FieldRootContainer field={field}>
|
||||||
className="absolute"
|
|
||||||
style={{
|
|
||||||
top: `${coords.y}px`,
|
|
||||||
left: `${coords.x}px`,
|
|
||||||
height: `${coords.height}px`,
|
|
||||||
width: `${coords.width}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
className="bg-background relative h-full w-full"
|
|
||||||
data-inserted={field.inserted ? 'true' : 'false'}
|
|
||||||
>
|
|
||||||
<CardContent
|
|
||||||
className={cn(
|
|
||||||
'text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!field.inserted && !loading && (
|
{!field.inserted && !loading && (
|
||||||
<button type="submit" className="absolute inset-0 z-10" onClick={onSignFieldClick} />
|
<button type="submit" className="absolute inset-0 z-10" onClick={onSignFieldClick} />
|
||||||
)}
|
)}
|
||||||
@ -73,8 +52,6 @@ export const SigningFieldContainer = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</CardContent>
|
</FieldRootContainer>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
41
packages/lib/utils/fields.ts
Normal file
41
packages/lib/utils/fields.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Field } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort the fields by the Y position on the document.
|
||||||
|
*/
|
||||||
|
export const sortFieldsByPosition = (fields: Field[]): Field[] => {
|
||||||
|
const clonedFields: Field[] = JSON.parse(JSON.stringify(fields));
|
||||||
|
|
||||||
|
// Sort by page first, then position on page second.
|
||||||
|
return clonedFields.sort((a, b) => a.page - b.page || Number(a.positionY) - Number(b.positionY));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate whether all the provided fields are inserted.
|
||||||
|
*
|
||||||
|
* If there are any non-inserted fields it will be highlighted and scrolled into view.
|
||||||
|
*
|
||||||
|
* @returns `true` if all fields are inserted, `false` otherwise.
|
||||||
|
*/
|
||||||
|
export const validateFieldsInserted = (fields: Field[]): boolean => {
|
||||||
|
const fieldCardElements = document.getElementsByClassName('field-card-container');
|
||||||
|
|
||||||
|
// Attach validate attribute on all fields.
|
||||||
|
Array.from(fieldCardElements).forEach((element) => {
|
||||||
|
element.setAttribute('data-validate', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
const uninsertedFields = sortFieldsByPosition(fields.filter((field) => !field.inserted));
|
||||||
|
|
||||||
|
const firstUninsertedField = uninsertedFields[0];
|
||||||
|
|
||||||
|
const firstUninsertedFieldElement =
|
||||||
|
firstUninsertedField && document.getElementById(`field-${firstUninsertedField.id}`);
|
||||||
|
|
||||||
|
if (firstUninsertedFieldElement) {
|
||||||
|
firstUninsertedFieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uninsertedFields.length === 0;
|
||||||
|
};
|
||||||
@ -39,7 +39,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className={cn('absolute', className)}
|
className={cn('absolute')}
|
||||||
style={{
|
style={{
|
||||||
top: `${coords.y}px`,
|
top: `${coords.y}px`,
|
||||||
left: `${coords.x}px`,
|
left: `${coords.x}px`,
|
||||||
@ -49,7 +49,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
|
|||||||
>
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0} open={!field.inserted}>
|
<Tooltip delayDuration={0} open={!field.inserted}>
|
||||||
<TooltipTrigger className="absolute top-0 w-full"></TooltipTrigger>
|
<TooltipTrigger className="absolute inset-0 w-full"></TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className={tooltipVariants({ color, className })} sideOffset={2}>
|
<TooltipContent className={tooltipVariants({ color, className })} sideOffset={2}>
|
||||||
{children}
|
{children}
|
||||||
90
packages/ui/components/field/field.tsx
Normal file
90
packages/ui/components/field/field.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||||
|
import { Field } from '@documenso/prisma/client';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
|
export type FieldRootContainerProps = {
|
||||||
|
field: Field;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FieldContainerPortalProps = {
|
||||||
|
field: Field;
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FieldContainerPortal({
|
||||||
|
field,
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
}: FieldContainerPortalProps) {
|
||||||
|
const coords = useFieldPageCoords(field);
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className={cn('absolute', className)}
|
||||||
|
style={{
|
||||||
|
top: `${coords.y}px`,
|
||||||
|
left: `${coords.x}px`,
|
||||||
|
height: `${coords.height}px`,
|
||||||
|
width: `${coords.width}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldRootContainer({ field, children }: FieldContainerPortalProps) {
|
||||||
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver((_mutations) => {
|
||||||
|
if (ref.current) {
|
||||||
|
setIsValidating(ref.current.getAttribute('data-validate') === 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(ref.current, {
|
||||||
|
attributes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldContainerPortal field={field}>
|
||||||
|
<Card
|
||||||
|
id={`field-${field.id}`}
|
||||||
|
className={cn(
|
||||||
|
'field-card-container bg-background relative z-20 h-full w-full transition-all',
|
||||||
|
{
|
||||||
|
'border-orange-300 ring-1 ring-orange-300': !field.inserted && isValidating,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
data-inserted={field.inserted ? 'true' : 'false'}
|
||||||
|
>
|
||||||
|
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2">
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</FieldContainerPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,8 +8,10 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import { Field, FieldType } from '@documenso/prisma/client';
|
import { Field, FieldType } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
||||||
@ -24,7 +26,6 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
|
|
||||||
import { FieldToolTip } from '../field/field-tooltip';
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||||
import { ZAddSignatureFormSchema } from './add-signature.types';
|
import { ZAddSignatureFormSchema } from './add-signature.types';
|
||||||
import {
|
import {
|
||||||
@ -89,40 +90,14 @@ export const AddSignatureFormPartial = ({
|
|||||||
const uninsertedFields = useMemo(() => {
|
const uninsertedFields = useMemo(() => {
|
||||||
const fields = localFields.filter((field) => !field.inserted);
|
const fields = localFields.filter((field) => !field.inserted);
|
||||||
|
|
||||||
return fields.sort((a, b) => {
|
return sortFieldsByPosition(fields);
|
||||||
if (a.page < b.page) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.page > b.page) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aTop = a.positionY;
|
|
||||||
const bTop = b.positionY;
|
|
||||||
|
|
||||||
if (aTop < bTop) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aTop > bTop) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}, [localFields]);
|
}, [localFields]);
|
||||||
|
|
||||||
const onValidateFields = async (values: TAddSignatureFormSchema) => {
|
const onValidateFields = async (values: TAddSignatureFormSchema) => {
|
||||||
setValidateUninsertedFields(true);
|
setValidateUninsertedFields(true);
|
||||||
|
const isFieldsValid = validateFieldsInserted(localFields);
|
||||||
|
|
||||||
const firstUninsertedField = uninsertedFields[0];
|
if (!isFieldsValid) {
|
||||||
|
|
||||||
const firstUninsertedFieldElement =
|
|
||||||
firstUninsertedField && document.getElementById(`field-${firstUninsertedField.id}`);
|
|
||||||
|
|
||||||
if (firstUninsertedFieldElement) {
|
|
||||||
firstUninsertedFieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,6 +187,24 @@ export const AddSignatureFormPartial = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a form value changes, reset all the corresponding fields to be uninserted.
|
||||||
|
*/
|
||||||
|
const onFormValueChange = (fieldType: FieldType) => {
|
||||||
|
setLocalFields((fields) =>
|
||||||
|
fields.map((field) => {
|
||||||
|
if (field.type !== fieldType) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
inserted: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
|
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
|
||||||
@ -224,7 +217,16 @@ export const AddSignatureFormPartial = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel required>Email</FormLabel>
|
<FormLabel required>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input className="bg-background" type="email" autoComplete="email" {...field} />
|
<Input
|
||||||
|
className="bg-background"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
{...field}
|
||||||
|
onChange={(value) => {
|
||||||
|
onFormValueChange(FieldType.EMAIL);
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -239,7 +241,14 @@ export const AddSignatureFormPartial = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel required={requireName}>Name</FormLabel>
|
<FormLabel required={requireName}>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input className="bg-background" {...field} />
|
<Input
|
||||||
|
className="bg-background"
|
||||||
|
{...field}
|
||||||
|
onChange={(value) => {
|
||||||
|
onFormValueChange(FieldType.NAME);
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -267,7 +276,11 @@ export const AddSignatureFormPartial = ({
|
|||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full"
|
className="h-44 w-full"
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
{...field}
|
onBlur={field.onBlur}
|
||||||
|
onChange={(value) => {
|
||||||
|
onFormValueChange(FieldType.SIGNATURE);
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -309,7 +322,6 @@ export const AddSignatureFormPartial = ({
|
|||||||
return (
|
return (
|
||||||
<SinglePlayerModeCustomTextField
|
<SinglePlayerModeCustomTextField
|
||||||
onClick={insertField(field)}
|
onClick={insertField(field)}
|
||||||
validateUninsertedField={validateUninsertedFields}
|
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
/>
|
/>
|
||||||
@ -318,7 +330,6 @@ export const AddSignatureFormPartial = ({
|
|||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
<SinglePlayerModeSignatureField
|
<SinglePlayerModeSignatureField
|
||||||
onClick={insertField(field)}
|
onClick={insertField(field)}
|
||||||
validateUninsertedField={validateUninsertedFields}
|
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useElementScaleSize } from '@documenso/lib/client-only/hooks/use-element-scale-size';
|
import { useElementScaleSize } from '@documenso/lib/client-only/hooks/use-element-scale-size';
|
||||||
@ -16,66 +15,24 @@ import {
|
|||||||
} from '@documenso/lib/constants/pdf';
|
} from '@documenso/lib/constants/pdf';
|
||||||
import { Field, FieldType } from '@documenso/prisma/client';
|
import { Field, FieldType } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
|
|
||||||
export type FieldContainerPortalProps = {
|
|
||||||
field: FieldWithSignature;
|
|
||||||
className?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SinglePlayerModeFieldContainerProps = {
|
export type SinglePlayerModeFieldContainerProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
validateUninsertedField?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SinglePlayerModeFieldProps<T> = {
|
export type SinglePlayerModeFieldProps<T> = {
|
||||||
field: T;
|
field: T;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
validateUninsertedField?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FieldContainerPortal({
|
|
||||||
field,
|
|
||||||
children,
|
|
||||||
className = '',
|
|
||||||
}: FieldContainerPortalProps) {
|
|
||||||
const coords = useFieldPageCoords(field);
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<div
|
|
||||||
className={cn('absolute', className)}
|
|
||||||
style={{
|
|
||||||
top: `${coords.y}px`,
|
|
||||||
left: `${coords.x}px`,
|
|
||||||
height: `${coords.height}px`,
|
|
||||||
width: `${coords.width}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>,
|
|
||||||
document.body,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SinglePlayerModeFieldCardContainer({
|
export function SinglePlayerModeFieldCardContainer({
|
||||||
field,
|
field,
|
||||||
children,
|
children,
|
||||||
validateUninsertedField = false,
|
|
||||||
}: SinglePlayerModeFieldContainerProps) {
|
}: SinglePlayerModeFieldContainerProps) {
|
||||||
return (
|
return (
|
||||||
<FieldContainerPortal field={field}>
|
<FieldRootContainer field={field}>
|
||||||
<motion.div className="h-full w-full" animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
|
||||||
<Card
|
|
||||||
id={`field-${field.id}`}
|
|
||||||
className={cn('bg-background relative z-20 h-full w-full transition-all', {
|
|
||||||
'border-orange-300 ring-1 ring-orange-300': !field.inserted && validateUninsertedField,
|
|
||||||
})}
|
|
||||||
data-inserted={field.inserted ? 'true' : 'false'}
|
|
||||||
>
|
|
||||||
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2">
|
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
<motion.div
|
<motion.div
|
||||||
key={field.inserted ? 'inserted' : 'not-inserted'}
|
key={field.inserted ? 'inserted' : 'not-inserted'}
|
||||||
@ -92,16 +49,12 @@ export function SinglePlayerModeFieldCardContainer({
|
|||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</CardContent>
|
</FieldRootContainer>
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</FieldContainerPortal>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SinglePlayerModeSignatureField({
|
export function SinglePlayerModeSignatureField({
|
||||||
field,
|
field,
|
||||||
validateUninsertedField,
|
|
||||||
onClick,
|
onClick,
|
||||||
}: SinglePlayerModeFieldProps<FieldWithSignature>) {
|
}: SinglePlayerModeFieldProps<FieldWithSignature>) {
|
||||||
const fontVariable = '--font-signature';
|
const fontVariable = '--font-signature';
|
||||||
@ -136,10 +89,7 @@ export function SinglePlayerModeSignatureField({
|
|||||||
const insertedTypeSignature = field.inserted && field.Signature?.typedSignature;
|
const insertedTypeSignature = field.inserted && field.Signature?.typedSignature;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SinglePlayerModeFieldCardContainer
|
<SinglePlayerModeFieldCardContainer field={field}>
|
||||||
validateUninsertedField={validateUninsertedField}
|
|
||||||
field={field}
|
|
||||||
>
|
|
||||||
{insertedBase64Signature ? (
|
{insertedBase64Signature ? (
|
||||||
<img
|
<img
|
||||||
src={insertedBase64Signature}
|
src={insertedBase64Signature}
|
||||||
@ -171,7 +121,6 @@ export function SinglePlayerModeSignatureField({
|
|||||||
|
|
||||||
export function SinglePlayerModeCustomTextField({
|
export function SinglePlayerModeCustomTextField({
|
||||||
field,
|
field,
|
||||||
validateUninsertedField,
|
|
||||||
onClick,
|
onClick,
|
||||||
}: SinglePlayerModeFieldProps<Field>) {
|
}: SinglePlayerModeFieldProps<Field>) {
|
||||||
const fontVariable = '--font-sans';
|
const fontVariable = '--font-sans';
|
||||||
@ -203,10 +152,7 @@ export function SinglePlayerModeCustomTextField({
|
|||||||
const fontSize = maxFontSize * scalingFactor;
|
const fontSize = maxFontSize * scalingFactor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SinglePlayerModeFieldCardContainer
|
<SinglePlayerModeFieldCardContainer field={field}>
|
||||||
validateUninsertedField={validateUninsertedField}
|
|
||||||
field={field}
|
|
||||||
>
|
|
||||||
{field.inserted ? (
|
{field.inserted ? (
|
||||||
<p
|
<p
|
||||||
ref={$paragraphEl}
|
ref={$paragraphEl}
|
||||||
|
|||||||
Reference in New Issue
Block a user