mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 03:32:14 +10:00
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
245 lines
7.0 KiB
TypeScript
245 lines
7.0 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import type { Signature } from '@prisma/client';
|
|
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
|
|
import { P, match } from 'ts-pattern';
|
|
|
|
import { cn } from '../lib/utils';
|
|
import { Card, CardContent } from '../primitives/card';
|
|
|
|
export type SigningCardProps = {
|
|
className?: string;
|
|
name: string;
|
|
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'>;
|
|
signingCelebrationImage?: string;
|
|
};
|
|
|
|
/**
|
|
* 2D signing card.
|
|
*/
|
|
export const SigningCard = ({
|
|
className,
|
|
name,
|
|
signature,
|
|
signingCelebrationImage,
|
|
}: SigningCardProps) => {
|
|
return (
|
|
<div className={cn('relative w-full max-w-sm md:max-w-md', className)}>
|
|
<SigningCardContent name={name} signature={signature} />
|
|
|
|
{signingCelebrationImage && (
|
|
<SigningCardImage signingCelebrationImage={signingCelebrationImage} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 3D signing card that follows the mouse movement within a certain range.
|
|
*/
|
|
export const SigningCard3D = ({
|
|
className,
|
|
name,
|
|
signature,
|
|
signingCelebrationImage,
|
|
}: SigningCardProps) => {
|
|
// Should use % based dimensions by calculating the window height/width.
|
|
const boundary = 400;
|
|
|
|
const [trackMouse, setTrackMouse] = useState(false);
|
|
|
|
const timeoutRef = useRef<number | undefined>();
|
|
|
|
const cardX = useMotionValue(0);
|
|
const cardY = useMotionValue(0);
|
|
const rotateX = useTransform(cardY, [-600, 600], [8, -8]);
|
|
const rotateY = useTransform(cardX, [-600, 600], [-8, 8]);
|
|
|
|
const diagonalMovement = useTransform<number, number>(
|
|
[rotateX, rotateY],
|
|
([newRotateX, newRotateY]) => newRotateX + newRotateY,
|
|
);
|
|
|
|
const sheenPosition = useTransform(diagonalMovement, [-16, 16], [-100, 200]);
|
|
const sheenOpacity = useTransform(sheenPosition, [-100, 50, 200], [0, 0.1, 0]);
|
|
const sheenGradient = useMotionTemplate`linear-gradient(
|
|
30deg,
|
|
transparent,
|
|
rgba(var(--sheen-color) / ${sheenOpacity}) ${sheenPosition}%,
|
|
transparent)`;
|
|
|
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
|
|
const cardCenterPosition = useCallback(() => {
|
|
if (!cardRef.current) {
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
const { x, y, width, height } = cardRef.current.getBoundingClientRect();
|
|
|
|
return { x: x + width / 2, y: y + height / 2 };
|
|
}, [cardRef]);
|
|
|
|
const onMouseMove = useCallback(
|
|
(event: MouseEvent) => {
|
|
const { x, y } = cardCenterPosition();
|
|
|
|
const offsetX = event.clientX - x;
|
|
const offsetY = event.clientY - y;
|
|
|
|
// Calculate distance between the mouse pointer and center of the card.
|
|
const distance = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
|
|
|
|
// Mouse enters enter boundary.
|
|
if (distance <= boundary && !trackMouse) {
|
|
setTrackMouse(true);
|
|
} else if (!trackMouse) {
|
|
return;
|
|
}
|
|
|
|
cardX.set(offsetX);
|
|
cardY.set(offsetY);
|
|
|
|
clearTimeout(timeoutRef.current);
|
|
|
|
// Revert the card back to the center position after the mouse stops moving.
|
|
timeoutRef.current = window.setTimeout(() => {
|
|
void animate(cardX, 0, { duration: 2, ease: 'backInOut' });
|
|
void animate(cardY, 0, { duration: 2, ease: 'backInOut' });
|
|
|
|
void animate(sheenOpacity, 0, { duration: 2, ease: 'backInOut' });
|
|
|
|
setTrackMouse(false);
|
|
}, 1000);
|
|
},
|
|
[cardX, cardY, cardCenterPosition, trackMouse, sheenOpacity],
|
|
);
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('mousemove', onMouseMove);
|
|
|
|
return () => {
|
|
window.removeEventListener('mousemove', onMouseMove);
|
|
if (timeoutRef.current) {
|
|
window.clearTimeout(timeoutRef.current);
|
|
}
|
|
};
|
|
}, [onMouseMove]);
|
|
|
|
return (
|
|
<div
|
|
className={cn('relative w-full max-w-sm md:max-w-md', className)}
|
|
style={{ perspective: 800 }}
|
|
>
|
|
<motion.div
|
|
className="bg-background w-full rounded-lg [--sheen-color:180_180_180] dark:[--sheen-color:200_200_200]"
|
|
ref={cardRef}
|
|
style={{
|
|
perspective: '800',
|
|
backgroundImage: sheenGradient,
|
|
transformStyle: 'preserve-3d',
|
|
rotateX,
|
|
rotateY,
|
|
}}
|
|
>
|
|
<SigningCardContent className="bg-transparent" name={name} signature={signature} />
|
|
</motion.div>
|
|
|
|
{signingCelebrationImage && (
|
|
<SigningCardImage signingCelebrationImage={signingCelebrationImage} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type SigningCardContentProps = {
|
|
name: string;
|
|
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'>;
|
|
className?: string;
|
|
};
|
|
|
|
const SigningCardContent = ({ className, name, signature }: SigningCardContentProps) => {
|
|
return (
|
|
<Card
|
|
className={cn(
|
|
'group z-10 mx-auto flex aspect-[21/9] w-full items-center justify-center',
|
|
className,
|
|
)}
|
|
degrees={-145}
|
|
gradient
|
|
>
|
|
<CardContent
|
|
className="font-signature p-6 text-center"
|
|
style={{
|
|
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={{
|
|
fontSize: `max(min(4rem, ${(100 / name.length / 2).toFixed(4)}cqw), 1.875rem)`,
|
|
}}
|
|
>
|
|
{name}
|
|
</span>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
type SigningCardImageProps = {
|
|
signingCelebrationImage: string;
|
|
};
|
|
|
|
const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) => {
|
|
return (
|
|
<motion.div
|
|
className="pointer-events-none absolute -inset-32 -z-50 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
|
|
initial={{
|
|
opacity: 0,
|
|
scale: 0.6,
|
|
}}
|
|
animate={{
|
|
scale: 1,
|
|
opacity: 0.6,
|
|
}}
|
|
transition={{
|
|
delay: 0.5,
|
|
duration: 0.5,
|
|
}}
|
|
>
|
|
<img
|
|
src={signingCelebrationImage}
|
|
alt="background pattern"
|
|
className="w-full dark:brightness-150 dark:contrast-[70%] dark:invert dark:sepia"
|
|
style={{
|
|
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
|
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
);
|
|
};
|