fix: update singleplayer add signature to card

This commit is contained in:
Mythie
2023-10-22 12:40:09 +11:00
parent 0b50176178
commit fd8f6da2c6
10 changed files with 112 additions and 74 deletions

View File

@ -1,6 +1,7 @@
import { notFound } from 'next/navigation';
import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { DocumentStatus } from '@documenso/prisma/client';
import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success';
@ -26,5 +27,7 @@ export default async function SinglePlayerModeSuccessPage({
return notFound();
}
return <SinglePlayerModeSuccess document={document} />;
const signatures = await getRecipientSignatures({ recipientId: document.Recipient.id });
return <SinglePlayerModeSuccess document={document} signatures={signatures} />;
}

View File

@ -159,11 +159,11 @@ export const SinglePlayerClient = () => {
const onFileDrop = async (file: File) => {
try {
const arrayBuffer = await file.arrayBuffer();
const base64String = base64.encode(new Uint8Array(arrayBuffer));
const fileBase64 = base64.encode(new Uint8Array(arrayBuffer));
setUploadedFile({
file,
fileBase64: `data:application/pdf;base64,${base64String}`,
fileBase64,
});
analytics.capture('Marketing: SPM - Document uploaded');
@ -182,7 +182,15 @@ export const SinglePlayerClient = () => {
<h1 className="text-3xl font-bold lg:text-5xl">Single Player Mode</h1>
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
View our{' '}
Create a{' '}
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
target="_blank"
className="hover:text-foreground/80 font-semibold transition-colors"
>
free account
</Link>{' '}
or view our{' '}
<Link
href={'/pricing'}
target="_blank"

View File

@ -8,10 +8,10 @@ import { createPortal } from 'react-dom';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useWindowSize } from '@documenso/lib/client-only/hooks/use-window-size';
export default function ConfettiScreen({
export const ConfettiScreen = ({
numberOfPieces: numberOfPiecesProp = 200,
...props
}: React.ComponentPropsWithoutRef<typeof Confetti> & { duration?: number }) {
}: React.ComponentPropsWithoutRef<typeof Confetti> & { duration?: number }) => {
const isMounted = useIsMounted();
const { width, height } = useWindowSize();
@ -43,4 +43,4 @@ export default function ConfettiScreen({
/>,
document.body,
);
}
};

View File

@ -5,8 +5,7 @@ import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { base64 } from '@documenso/lib/universal/base64';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentStatus, Signature } from '@documenso/prisma/client';
import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
@ -14,53 +13,28 @@ import { DocumentShareButton } from '@documenso/ui/components/document/document-
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import signingCelebration from '~/assets/signing-celebration.png';
import ConfettiScreen from '~/components/(marketing)/confetti-screen';
import { DocumentStatus } from '.prisma/client';
import { ConfettiScreen } from '~/components/(marketing)/confetti-screen';
interface SinglePlayerModeSuccessProps {
className?: string;
document: DocumentWithRecipient;
signatures: Signature[];
}
export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerModeSuccessProps) => {
export const SinglePlayerModeSuccess = ({
className,
document,
signatures,
}: SinglePlayerModeSuccessProps) => {
const { getFlag } = useFeatureFlags();
const isConfettiEnabled = getFlag('marketing_spm_confetti');
const [showDocumentDialog, setShowDocumentDialog] = useState(false);
const [isFetchingDocumentFile, setIsFetchingDocumentFile] = useState(false);
const [documentFile, setDocumentFile] = useState<string | null>(null);
const { toast } = useToast();
const onShowDocumentClick = async () => {
if (isFetchingDocumentFile) {
return;
}
setIsFetchingDocumentFile(true);
try {
const data = await getFile(document.documentData);
setDocumentFile(base64.encode(data));
setShowDocumentDialog(true);
} catch {
toast({
title: 'Something went wrong.',
description: 'We were unable to retrieve the document at this time. Please try again.',
variant: 'destructive',
duration: 7500,
});
}
setIsFetchingDocumentFile(false);
};
const { documentData } = document;
useEffect(() => {
window.scrollTo({ top: 0 });
@ -80,6 +54,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
<SigningCard3D
className="mt-8"
name={document.Recipient.name || document.Recipient.email}
signature={signatures.at(0)}
signingCelebrationImage={signingCelebration}
/>
@ -99,11 +74,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
disabled={document.status !== DocumentStatus.COMPLETED}
/>
<Button
onClick={async () => onShowDocumentClick()}
loading={isFetchingDocumentFile}
className="z-10 col-span-2"
>
<Button onClick={() => setShowDocumentDialog(true)} className="z-10 col-span-2">
Show document
</Button>
</div>
@ -123,7 +94,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
</p>
<DocumentDialog
document={documentFile ?? ''}
documentData={documentData}
open={showDocumentDialog}
onOpenChange={setShowDocumentDialog}
/>

View File

@ -7,6 +7,7 @@ import { match } from 'ts-pattern';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
@ -46,6 +47,8 @@ export default async function CompletedSigningPage({
return notFound();
}
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
const recipientName =
recipient.name ||
fields.find((field) => field.type === FieldType.NAME)?.customText ||
@ -54,7 +57,11 @@ export default async function CompletedSigningPage({
return (
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44">
{/* Card with recipient */}
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
<SigningCard3D
name={recipientName}
signature={signatures.at(0)}
signingCelebrationImage={signingCelebration}
/>
<div className="relative mt-6 flex w-full flex-col items-center">
{match(document.status)
@ -72,7 +79,8 @@ export default async function CompletedSigningPage({
))}
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
You have signed "{document.title}"
You have signed
<span className="mt-1.5 block">"{document.title}"</span>
</h2>
{match(document.status)

3
package-lock.json generated
View File

@ -19913,7 +19913,8 @@
"react-pdf": "7.3.3",
"react-rnd": "^10.4.1",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5"
"tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5"
},
"devDependencies": {
"@documenso/tailwind-config": "*",

View File

@ -0,0 +1,15 @@
import { prisma } from '@documenso/prisma';
export type GetRecipientSignaturesOptions = {
recipientId: number;
};
export const getRecipientSignatures = async ({ recipientId }: GetRecipientSignaturesOptions) => {
return await prisma.signature.findMany({
where: {
Field: {
recipientId,
},
},
});
};

View File

@ -5,20 +5,20 @@ import { useState } from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { DocumentDataType } from '@documenso/prisma/client';
import { DocumentData } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';
import { Dialog, DialogOverlay, DialogPortal } from '../../primitives/dialog';
import { LazyPDFViewerNoLoader } from '../../primitives/lazy-pdf-viewer';
export type DocumentDialogProps = {
document: string;
documentData: DocumentData;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
/**
* A dialog which renders the provided document.
*/
export default function DocumentDialog({ document, ...props }: DocumentDialogProps) {
export default function DocumentDialog({ documentData, ...props }: DocumentDialogProps) {
const [documentLoaded, setDocumentLoaded] = useState(false);
const onDocumentLoad = () => {
@ -41,12 +41,7 @@ export default function DocumentDialog({ document, ...props }: DocumentDialogPro
>
<LazyPDFViewerNoLoader
className="mx-auto w-full max-w-3xl xl:max-w-5xl"
documentData={{
id: '',
data: document,
initialData: document,
type: DocumentDataType.BYTES_64,
}}
documentData={documentData}
onClick={(e) => e.stopPropagation()}
onDocumentLoad={onDocumentLoad}
/>

View File

@ -5,23 +5,31 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import Image, { StaticImageData } from 'next/image';
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
import { P, match } from 'ts-pattern';
import { Signature } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
export type SigningCardProps = {
className?: string;
name: string;
signature?: Signature;
signingCelebrationImage?: StaticImageData;
};
/**
* 2D signing card.
*/
export const SigningCard = ({ className, name, signingCelebrationImage }: SigningCardProps) => {
export const SigningCard = ({
className,
name,
signature,
signingCelebrationImage,
}: SigningCardProps) => {
return (
<div className={cn('relative w-full max-w-xs md:max-w-sm', className)}>
<SigningCardContent name={name} />
<SigningCardContent name={name} signature={signature} />
{signingCelebrationImage && (
<SigningCardImage signingCelebrationImage={signingCelebrationImage} />
@ -33,7 +41,12 @@ export const SigningCard = ({ className, name, signingCelebrationImage }: Signin
/**
* 3D signing card that follows the mouse movement within a certain range.
*/
export const SigningCard3D = ({ className, name, signingCelebrationImage }: SigningCardProps) => {
export const SigningCard3D = ({
className,
name,
signature,
signingCelebrationImage,
}: SigningCardProps) => {
// Should use % based dimensions by calculating the window height/width.
const boundary = 400;
@ -130,7 +143,7 @@ export const SigningCard3D = ({ className, name, signingCelebrationImage }: Sign
rotateY,
}}
>
<SigningCardContent className="bg-transparent" name={name} />
<SigningCardContent className="bg-transparent" name={name} signature={signature} />
</motion.div>
{signingCelebrationImage && (
@ -142,10 +155,11 @@ export const SigningCard3D = ({ className, name, signingCelebrationImage }: Sign
type SigningCardContentProps = {
name: string;
signature?: Signature;
className?: string;
};
const SigningCardContent = ({ className, name }: SigningCardContentProps) => {
const SigningCardContent = ({ className, name, signature }: SigningCardContentProps) => {
return (
<Card
className={cn(
@ -161,6 +175,27 @@ const SigningCardContent = ({ className, name }: SigningCardContentProps) => {
container: 'main',
}}
>
{match(signature)
.with({ signatureImageAsBase64: P.string }, (signature) => (
<img
src={signature.signatureImageAsBase64}
alt="signature"
className="h-full max-w-[100%] dark:invert"
/>
))
.with({ typedSignature: P.string }, (signature) => (
<span
className="text-muted-foreground/60 group-hover:text-primary/80 break-all font-semibold duration-300"
style={{
fontSize: `max(min(4rem, ${(100 / signature.typedSignature.length / 2).toFixed(
4,
)}cqw), 1.875rem)`,
}}
>
{signature.typedSignature}
</span>
))
.otherwise(() => (
<span
className="text-muted-foreground/60 group-hover:text-primary/80 break-all font-semibold duration-300"
style={{
@ -169,6 +204,7 @@ const SigningCardContent = ({ className, name }: SigningCardContentProps) => {
>
{name}
</span>
))}
</CardContent>
</Card>
);

View File

@ -67,6 +67,7 @@
"react-pdf": "7.3.3",
"react-rnd": "^10.4.1",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5"
"tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5"
}
}