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 { notFound } from 'next/navigation';
import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token'; 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 { DocumentStatus } from '@documenso/prisma/client';
import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success'; import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success';
@ -26,5 +27,7 @@ export default async function SinglePlayerModeSuccessPage({
return notFound(); 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) => { const onFileDrop = async (file: File) => {
try { try {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const base64String = base64.encode(new Uint8Array(arrayBuffer)); const fileBase64 = base64.encode(new Uint8Array(arrayBuffer));
setUploadedFile({ setUploadedFile({
file, file,
fileBase64: `data:application/pdf;base64,${base64String}`, fileBase64,
}); });
analytics.capture('Marketing: SPM - Document uploaded'); 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> <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"> <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 <Link
href={'/pricing'} href={'/pricing'}
target="_blank" 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 { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useWindowSize } from '@documenso/lib/client-only/hooks/use-window-size'; import { useWindowSize } from '@documenso/lib/client-only/hooks/use-window-size';
export default function ConfettiScreen({ export const ConfettiScreen = ({
numberOfPieces: numberOfPiecesProp = 200, numberOfPieces: numberOfPiecesProp = 200,
...props ...props
}: React.ComponentPropsWithoutRef<typeof Confetti> & { duration?: number }) { }: React.ComponentPropsWithoutRef<typeof Confetti> & { duration?: number }) => {
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const { width, height } = useWindowSize(); const { width, height } = useWindowSize();
@ -43,4 +43,4 @@ export default function ConfettiScreen({
/>, />,
document.body, document.body,
); );
} };

View File

@ -5,8 +5,7 @@ import { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { base64 } from '@documenso/lib/universal/base64'; import { DocumentStatus, Signature } from '@documenso/prisma/client';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient'; import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
import DocumentDialog from '@documenso/ui/components/document/document-dialog'; import DocumentDialog from '@documenso/ui/components/document/document-dialog';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button'; 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 { SigningCard3D } from '@documenso/ui/components/signing-card';
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 { useToast } from '@documenso/ui/primitives/use-toast';
import signingCelebration from '~/assets/signing-celebration.png'; import signingCelebration from '~/assets/signing-celebration.png';
import ConfettiScreen from '~/components/(marketing)/confetti-screen'; import { ConfettiScreen } from '~/components/(marketing)/confetti-screen';
import { DocumentStatus } from '.prisma/client';
interface SinglePlayerModeSuccessProps { interface SinglePlayerModeSuccessProps {
className?: string; className?: string;
document: DocumentWithRecipient; document: DocumentWithRecipient;
signatures: Signature[];
} }
export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerModeSuccessProps) => { export const SinglePlayerModeSuccess = ({
className,
document,
signatures,
}: SinglePlayerModeSuccessProps) => {
const { getFlag } = useFeatureFlags(); const { getFlag } = useFeatureFlags();
const isConfettiEnabled = getFlag('marketing_spm_confetti'); const isConfettiEnabled = getFlag('marketing_spm_confetti');
const [showDocumentDialog, setShowDocumentDialog] = useState(false); const [showDocumentDialog, setShowDocumentDialog] = useState(false);
const [isFetchingDocumentFile, setIsFetchingDocumentFile] = useState(false);
const [documentFile, setDocumentFile] = useState<string | null>(null);
const { toast } = useToast(); const { documentData } = document;
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);
};
useEffect(() => { useEffect(() => {
window.scrollTo({ top: 0 }); window.scrollTo({ top: 0 });
@ -80,6 +54,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
<SigningCard3D <SigningCard3D
className="mt-8" className="mt-8"
name={document.Recipient.name || document.Recipient.email} name={document.Recipient.name || document.Recipient.email}
signature={signatures.at(0)}
signingCelebrationImage={signingCelebration} signingCelebrationImage={signingCelebration}
/> />
@ -99,11 +74,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
disabled={document.status !== DocumentStatus.COMPLETED} disabled={document.status !== DocumentStatus.COMPLETED}
/> />
<Button <Button onClick={() => setShowDocumentDialog(true)} className="z-10 col-span-2">
onClick={async () => onShowDocumentClick()}
loading={isFetchingDocumentFile}
className="z-10 col-span-2"
>
Show document Show document
</Button> </Button>
</div> </div>
@ -123,7 +94,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
</p> </p>
<DocumentDialog <DocumentDialog
document={documentFile ?? ''} documentData={documentData}
open={showDocumentDialog} open={showDocumentDialog}
onOpenChange={setShowDocumentDialog} 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 { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-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 { 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 { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button'; import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
@ -46,6 +47,8 @@ export default async function CompletedSigningPage({
return notFound(); return notFound();
} }
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
const recipientName = const recipientName =
recipient.name || recipient.name ||
fields.find((field) => field.type === FieldType.NAME)?.customText || fields.find((field) => field.type === FieldType.NAME)?.customText ||
@ -54,7 +57,11 @@ export default async function CompletedSigningPage({
return ( 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"> <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 */} {/* 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"> <div className="relative mt-6 flex w-full flex-col items-center">
{match(document.status) {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"> <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> </h2>
{match(document.status) {match(document.status)

3
package-lock.json generated
View File

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

View File

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

View File

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