Merge branch 'feat/refresh' of https://github.com/documenso/documenso into feat/refresh

This commit is contained in:
Timur Ercan
2023-09-27 12:19:14 +02:00
12 changed files with 123 additions and 71 deletions

View File

@ -20,11 +20,11 @@ Today, I'm pleased to share with you a preview of the next Documenso.
We redesigned the whole signing flow to make it more appealing and more convenient. We redesigned the whole signing flow to make it more appealing and more convenient.
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it. We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
**We call it happy minimalism.** **We call it happy minimalism.**
We paid particular attention to the moment of signing, which should be celebrated. We paid particular attention to the moment of signing, which should be celebrated.
The image below is the final bloom of the completion celebration we added: The image below is the final bloom of the completion celebration we added:

View File

@ -73,7 +73,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
<ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} /> <ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} />
)} )}
<h2 className="text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl"> <h2 className="relative z-10 text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
You have signed You have signed
<span className="mt-2 block">{document.title}</span> <span className="mt-2 block">{document.title}</span>
</h2> </h2>
@ -84,17 +84,17 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
signingCelebrationImage={signingCelebration} signingCelebrationImage={signingCelebration}
/> />
<div className="mt-8 w-full"> <div className="relative mt-8 w-full">
<div className={cn('flex flex-col items-center', className)}> <div className={cn('flex flex-col items-center', className)}>
<div className="grid w-full max-w-sm grid-cols-2 gap-4"> <div className="grid w-full max-w-sm grid-cols-2 gap-4">
{/* TODO: Hook this up */} {/* TODO: Hook this up */}
<Button variant="outline" className="flex-1" disabled> <Button variant="outline" className="flex-1 bg-transparent backdrop-blur-sm" disabled>
<Share className="mr-2 h-5 w-5" /> <Share className="mr-2 h-5 w-5" />
Share Share
</Button> </Button>
<DocumentDownloadButton <DocumentDownloadButton
className="flex-1" className="flex-1 bg-transparent backdrop-blur-sm"
fileName={document.title} fileName={document.title}
documentData={document.documentData} documentData={document.documentData}
disabled={document.status !== DocumentStatus.COMPLETED} disabled={document.status !== DocumentStatus.COMPLETED}

View File

@ -1,4 +1,4 @@
import { ImageResponse } from 'next/server'; import { ImageResponse, NextResponse } from 'next/server';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
@ -21,7 +21,7 @@ type SharePageOpenGraphImageProps = {
params: { slug: string }; params: { slug: string };
}; };
export default async function Image({ params: { slug } }: SharePageOpenGraphImageProps) { export async function GET(_request: Request, { params: { slug } }: SharePageOpenGraphImageProps) {
const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([ const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([
getAssetBuffer('/fonts/inter-semibold.ttf'), getAssetBuffer('/fonts/inter-semibold.ttf'),
getAssetBuffer('/fonts/inter-regular.ttf'), getAssetBuffer('/fonts/inter-regular.ttf'),
@ -32,7 +32,7 @@ export default async function Image({ params: { slug } }: SharePageOpenGraphImag
const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null); const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null);
if (!recipientOrSender) { if (!recipientOrSender) {
return null; return NextResponse.json({ error: 'Not found' }, { status: 404 });
} }
const isRecipient = 'Signature' in recipientOrSender; const isRecipient = 'Signature' in recipientOrSender;

View File

@ -1,11 +1,39 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { Redirect } from './redirect'; import { APP_BASE_URL } from '@documenso/lib/constants/app';
export const metadata: Metadata = { type SharePageProps = {
title: 'Documenso - Share', params: { slug: string };
}; };
export default function SharePage() { export function generateMetadata({ params: { slug } }: SharePageProps) {
return <Redirect />; return {
title: 'Documenso - Share',
description: 'I just signed a document with Documenso!',
openGraph: {
title: 'Documenso - Join the open source signing revolution',
description: 'I just signed with Documenso!',
type: 'website',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
description: 'I just signed with Documenso!',
},
} satisfies Metadata;
}
export default function SharePage() {
const userAgent = headers().get('User-Agent') ?? '';
// https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent
if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) {
return null;
}
redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001');
} }

View File

@ -1,11 +0,0 @@
'use client';
import { useEffect } from 'react';
export const Redirect = () => {
useEffect(() => {
window.location.href = process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001';
}, []);
return null;
};

View File

@ -9,7 +9,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
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 { 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 { SigningCard } from '@documenso/ui/components/signing-card'; import { SigningCard3D } from '@documenso/ui/components/signing-card';
import signingCelebration from '~/assets/signing-celebration.png'; import signingCelebration from '~/assets/signing-celebration.png';
@ -53,11 +53,11 @@ export default async function CompletedSigningPage({
recipient.email; recipient.email;
return ( return (
<div className="flex flex-col items-center pt-24"> <div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44">
{/* Card with recipient */} {/* Card with recipient */}
<SigningCard name={recipientName} signingCelebrationImage={signingCelebration} /> <SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
<div className="mt-6"> <div className="relative mt-6 flex w-full flex-col items-center">
{match(document.status) {match(document.status)
.with(DocumentStatus.COMPLETED, () => ( .with(DocumentStatus.COMPLETED, () => (
<div className="text-documenso-700 flex items-center text-center"> <div className="text-documenso-700 flex items-center text-center">
@ -71,41 +71,44 @@ export default async function CompletedSigningPage({
<span className="text-sm">Waiting for others to sign</span> <span className="text-sm">Waiting for others to sign</span>
</div> </div>
))} ))}
<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}"
</h2>
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
Everyone has signed! You will receive an Email copy of the signed document.
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
You will receive an Email copy of the signed document once everyone has signed.
</p>
))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<ShareButton documentId={document.id} token={recipient.token} />
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link
href="https://documenso.com"
className="text-documenso-700 hover:text-documenso-600"
>
Check out Documenso.
</Link>
</p>
</div> </div>
<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}"
</h2>
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
Everyone has signed! You will receive an Email copy of the signed document.
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
You will receive an Email copy of the signed document once everyone has signed.
</p>
))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<ShareButton documentId={document.id} token={recipient.token} />
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600">
Check out Documenso.
</Link>
</p>
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import { notFound } from 'next/navigation'; import { notFound, redirect } from 'next/navigation';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
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 { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { FieldType } from '@documenso/prisma/client'; import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@ -53,6 +53,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const user = await getServerComponentSession(); const user = await getServerComponentSession();
if (
document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED
) {
redirect(`/sign/${token}/complete`);
}
return ( return (
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}> <SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">

View File

@ -40,7 +40,7 @@ export const StackAvatarsWithTooltip = ({
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip delayDuration={200}>
<TooltipTrigger className="flex cursor-pointer"> <TooltipTrigger className="flex cursor-pointer">
{children || <StackAvatars recipients={recipients} />} {children || <StackAvatars recipients={recipients} />}
</TooltipTrigger> </TooltipTrigger>

View File

@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" SERIAL NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
-- AddForeignKey
ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `expiry` to the `PasswordResetToken` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PasswordResetToken" ADD COLUMN "expiry" TIMESTAMP(3) NOT NULL;

View File

@ -1,3 +1,5 @@
DROP TABLE IF EXISTS "PasswordResetToken" CASCADE;
-- CreateTable -- CreateTable
CREATE TABLE "PasswordResetToken" ( CREATE TABLE "PasswordResetToken" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,

View File

@ -148,7 +148,7 @@ const SigningCardContent = ({ className, name }: SigningCardContentProps) => {
return ( return (
<Card <Card
className={cn( className={cn(
'group mx-auto flex aspect-[21/9] w-full items-center justify-center', 'group z-10 mx-auto flex aspect-[21/9] w-full items-center justify-center',
className, className,
)} )}
degrees={-145} degrees={-145}
@ -180,14 +180,14 @@ type SigningCardImageProps = {
const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) => { const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) => {
return ( return (
<motion.div <motion.div
className="pointer-events-none absolute -inset-32 -z-10 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80" className="pointer-events-none absolute -inset-32 -z-50 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
initial={{ initial={{
opacity: 0, opacity: 0,
scale: 0.8, scale: 0.6,
}} }}
animate={{ animate={{
scale: 1, scale: 1,
opacity: 0.5, opacity: 0.6,
}} }}
transition={{ transition={{
delay: 0.5, delay: 0.5,
@ -197,7 +197,7 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
<Image <Image
src={signingCelebrationImage} src={signingCelebrationImage}
alt="background pattern" alt="background pattern"
className="w-full dark:invert dark:sepia" className="w-full dark:brightness-150 dark:contrast-[70%] dark:invert dark:sepia"
style={{ style={{
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)', WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',