mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
fix: update singleplayer add signature to card
This commit is contained in:
@ -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} />;
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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
3
package-lock.json
generated
@ -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": "*",
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user