Merge branch 'main' into test/sign-redirect-url

This commit is contained in:
Ephraim Atta-Duncan
2024-03-08 15:03:21 +00:00
17 changed files with 366 additions and 220 deletions

View File

@ -27,7 +27,7 @@
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso"> <a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso">
<img alt="open in devcontainer" src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Enabled&color=blue&logo=visualstudiocode" /> <img alt="open in devcontainer" src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Enabled&color=blue&logo=visualstudiocode" />
</a> </a>
<a href="code_of_conduct.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"></a> <a href="CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"></a>
</p> </p>
<div> <div>

View File

@ -4,7 +4,7 @@ description: 'This article talks about the need for the public API and the proce
authorName: 'Lucas Smith' authorName: 'Lucas Smith'
authorImage: '/blog/blog-author-lucas.png' authorImage: '/blog/blog-author-lucas.png'
authorRole: 'Co-Founder' authorRole: 'Co-Founder'
date: 2024-07-23 date: 2024-03-07
tags: tags:
- Development - Development
--- ---

View File

@ -76,9 +76,7 @@ export const DocumentsDataTable = ({
{ {
header: 'Recipient', header: 'Recipient',
accessorKey: 'recipient', accessorKey: 'recipient',
cell: ({ row }) => { cell: ({ row }) => <StackAvatarsWithTooltip recipients={row.original.Recipient} />,
return <StackAvatarsWithTooltip recipients={row.original.Recipient} />;
},
}, },
{ {
header: 'Status', header: 'Status',

View File

@ -2,7 +2,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
@ -139,27 +138,6 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
<Loader className="text-muted-foreground h-12 w-12 animate-spin" /> <Loader className="text-muted-foreground h-12 w-12 animate-spin" />
</div> </div>
)} )}
{team?.id === undefined && remaining.documents === 0 && (
<div className="bg-background/60 absolute inset-0 flex items-center justify-center rounded-lg backdrop-blur-sm">
<div className="text-center">
<h2 className="text-muted-foreground/80 text-xl font-semibold">
You have reached your document limit.
</h2>
<p className="text-muted-foreground/60 mt-2 text-sm">
You can upload up to {quota.documents} documents per month on your current plan.
</p>
<Link
className="text-primary hover:text-primary/80 mt-6 block font-medium"
href="/settings/billing"
>
Upgrade your account to upload more documents.
</Link>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -112,7 +112,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
variant="destructive" variant="destructive"
disabled={hasTwoFactorAuthentication} disabled={hasTwoFactorAuthentication}
> >
{isDeletingAccount ? 'Deleting account...' : 'Delete Account'} {isDeletingAccount ? 'Deleting account...' : 'Confirm Deletion'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@ -1,13 +1,12 @@
'use client';
import { useRef, useState } from 'react';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client'; import type { Recipient } from '@documenso/prisma/client';
import { import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@documenso/ui/primitives/tooltip';
import { AvatarWithRecipient } from './avatar-with-recipient'; import { AvatarWithRecipient } from './avatar-with-recipient';
import { StackAvatar } from './stack-avatar'; import { StackAvatar } from './stack-avatar';
@ -24,6 +23,11 @@ export const StackAvatarsWithTooltip = ({
position, position,
children, children,
}: StackAvatarsWithTooltipProps) => { }: StackAvatarsWithTooltipProps) => {
const [open, setOpen] = useState(false);
const isControlled = useRef(false);
const isMouseOverTimeout = useRef<NodeJS.Timeout | null>(null);
const waitingRecipients = recipients.filter( const waitingRecipients = recipients.filter(
(recipient) => getRecipientType(recipient) === 'waiting', (recipient) => getRecipientType(recipient) === 'waiting',
); );
@ -40,66 +44,105 @@ export const StackAvatarsWithTooltip = ({
(recipient) => getRecipientType(recipient) === 'unsigned', (recipient) => getRecipientType(recipient) === 'unsigned',
); );
const onMouseEnter = () => {
if (isMouseOverTimeout.current) {
clearTimeout(isMouseOverTimeout.current);
}
if (isControlled.current) {
return;
}
isMouseOverTimeout.current = setTimeout(() => {
setOpen((o) => (!o ? true : o));
}, 200);
};
const onMouseLeave = () => {
if (isMouseOverTimeout.current) {
clearTimeout(isMouseOverTimeout.current);
}
if (isControlled.current) {
return;
}
setTimeout(() => {
setOpen((o) => (o ? false : o));
}, 200);
};
const onOpenChange = (newOpen: boolean) => {
isControlled.current = newOpen;
setOpen(newOpen);
};
return ( return (
<TooltipProvider> <Popover open={open} onOpenChange={onOpenChange}>
<Tooltip delayDuration={200}> <PopoverTrigger
<TooltipTrigger className="flex cursor-pointer"> className="flex cursor-pointer"
{children || <StackAvatars recipients={recipients} />} onMouseEnter={onMouseEnter}
</TooltipTrigger> onMouseLeave={onMouseLeave}
>
{children || <StackAvatars recipients={recipients} />}
</PopoverTrigger>
<TooltipContent side={position}> <PopoverContent
<div className="flex flex-col gap-y-5 p-1"> side={position}
{completedRecipients.length > 0 && ( onMouseEnter={onMouseEnter}
<div> onMouseLeave={onMouseLeave}
<h1 className="text-base font-medium">Completed</h1> className="flex flex-col gap-y-5 py-2"
{completedRecipients.map((recipient: Recipient) => ( >
<div key={recipient.id} className="my-1 flex items-center gap-2"> {completedRecipients.length > 0 && (
<StackAvatar <div>
first={true} <h1 className="text-base font-medium">Completed</h1>
key={recipient.id} {completedRecipients.map((recipient: Recipient) => (
type={getRecipientType(recipient)} <div key={recipient.id} className="my-1 flex items-center gap-2">
fallbackText={recipientAbbreviation(recipient)} <StackAvatar
/> first={true}
<div className=""> key={recipient.id}
<p className="text-muted-foreground text-sm">{recipient.email}</p> type={getRecipientType(recipient)}
<p className="text-muted-foreground/70 text-xs"> fallbackText={recipientAbbreviation(recipient)}
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName} />
</p> <div className="">
</div> <p className="text-muted-foreground text-sm">{recipient.email}</p>
</div> <p className="text-muted-foreground/70 text-xs">
))} {RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
</p>
</div>
</div> </div>
)} ))}
{waitingRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Waiting</h1>
{waitingRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
))}
</div>
)}
{openedRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Opened</h1>
{openedRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
))}
</div>
)}
{uncompletedRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Uncompleted</h1>
{uncompletedRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
))}
</div>
)}
</div> </div>
</TooltipContent> )}
</Tooltip>
</TooltipProvider> {waitingRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Waiting</h1>
{waitingRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
))}
</div>
)}
{openedRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Opened</h1>
{openedRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
))}
</div>
)}
{uncompletedRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Uncompleted</h1>
{uncompletedRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
))}
</div>
)}
</PopoverContent>
</Popover>
); );
}; };

View File

@ -227,9 +227,9 @@ export const SignUpFormV2 = ({
<div className="border-border dark:bg-background relative z-10 flex min-h-[min(800px,80vh)] w-full max-w-lg flex-col rounded-xl border bg-neutral-100 p-6"> <div className="border-border dark:bg-background relative z-10 flex min-h-[min(800px,80vh)] w-full max-w-lg flex-col rounded-xl border bg-neutral-100 p-6">
{step === 'BASIC_DETAILS' && ( {step === 'BASIC_DETAILS' && (
<div className="h-20"> <div className="h-20">
<h1 className="text-2xl font-semibold">Create a new account</h1> <h1 className="text-xl font-semibold md:text-2xl">Create a new account</h1>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-xs md:text-sm">
Create your account and start using state-of-the-art document signing. Open and Create your account and start using state-of-the-art document signing. Open and
beautiful signing is within your grasp. beautiful signing is within your grasp.
</p> </p>
@ -238,9 +238,9 @@ export const SignUpFormV2 = ({
{step === 'CLAIM_USERNAME' && ( {step === 'CLAIM_USERNAME' && (
<div className="h-20"> <div className="h-20">
<h1 className="text-2xl font-semibold">Claim your username now</h1> <h1 className="text-xl font-semibold md:text-2xl">Claim your username now</h1>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-xs md:text-sm">
You will get notified & be able to set up your documenso public profile when we launch You will get notified & be able to set up your documenso public profile when we launch
the feature. the feature.
</p> </p>
@ -378,7 +378,7 @@ export const SignUpFormV2 = ({
<FormMessage /> <FormMessage />
<div className="bg-muted/50 border-border text-muted-foreground mt-2 inline-block truncate rounded-md border px-2 py-1 text-sm lowercase"> <div className="bg-muted/50 border-border text-muted-foreground mt-2 inline-block max-w-[16rem] truncate rounded-md border px-2 py-1 text-sm lowercase">
{baseUrl.host}/u/{field.value || '<username>'} {baseUrl.host}/u/{field.value || '<username>'}
</div> </div>
</FormItem> </FormItem>

View File

@ -24,9 +24,8 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
className, className,
)} )}
> >
<div className="border-border bg-background text-muted-foreground inline-flex items-center rounded-md border px-2.5 py-1.5 text-sm"> <div className="border-border bg-background text-muted-foreground inline-block max-w-full truncate rounded-md border px-2.5 py-1.5 text-sm">
<span>{baseUrl.host}/u/</span> {baseUrl.host}/u/{user.url}
<span className="inline-block max-w-[8rem] truncate lowercase">{user.url}</span>
</div> </div>
<div className="mt-4"> <div className="mt-4">

View File

@ -25,7 +25,7 @@ export const UserProfileTimur = ({ className, rows = 2 }: UserProfileTimurProps)
className, className,
)} )}
> >
<div className="border-border bg-background text-muted-foreground inline-block rounded-md border px-2.5 py-1.5 text-sm"> <div className="border-border bg-background text-muted-foreground inline-block max-w-full truncate rounded-md border px-2.5 py-1.5 text-sm">
{baseUrl.host}/u/timur {baseUrl.host}/u/timur
</div> </div>

View File

@ -60,15 +60,13 @@ docker pull ghcr.io/documenso/documenso
``` ```
docker run -d \ docker run -d \
-p 3000:3000 \ -p 3000:3000 \
-e POSTGRES_USER="<your-postgres-user>"
-e POSTGRES_PASSWORD="<your-postgres-password>"
-e POSTGRES_DB="<your-postgres-db>"
-e NEXTAUTH_URL="<your-nextauth-url>" -e NEXTAUTH_URL="<your-nextauth-url>"
-e NEXTAUTH_SECRET="<your-nextauth-secret>" -e NEXTAUTH_SECRET="<your-nextauth-secret>"
-e NEXT_PRIVATE_ENCRYPTION_KEY="<your-next-private-encryption-key>" -e NEXT_PRIVATE_ENCRYPTION_KEY="<your-next-private-encryption-key>"
-e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-next-private-encryption-secondary-key>" -e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-next-private-encryption-secondary-key>"
-e NEXT_PUBLIC_WEBAPP_URL="<your-next-public-webapp-url>" -e NEXT_PUBLIC_WEBAPP_URL="<your-next-public-webapp-url>"
-e NEXT_PRIVATE_DATABASE_URL="<your-next-private-database-url>" -e NEXT_PRIVATE_DATABASE_URL="<your-next-private-database-url>"
-e NEXT_PRIVATE_DIRECT_DATABASE_URL="<your-next-private-database-url>"
-e NEXT_PRIVATE_SMTP_TRANSPORT="<your-next-private-smtp-transport>" -e NEXT_PRIVATE_SMTP_TRANSPORT="<your-next-private-smtp-transport>"
-e NEXT_PRIVATE_SMTP_FROM_NAME="<your-next-private-smtp-from-name>" -e NEXT_PRIVATE_SMTP_FROM_NAME="<your-next-private-smtp-from-name>"
-e NEXT_PRIVATE_SMTP_FROM_ADDRESS="<your-next-private-smtp-from-address>" -e NEXT_PRIVATE_SMTP_FROM_ADDRESS="<your-next-private-smtp-from-address>"

View File

@ -12,7 +12,7 @@
"commitlint": "commitlint --edit", "commitlint": "commitlint --edit",
"clean": "turbo run clean && rimraf node_modules", "clean": "turbo run clean && rimraf node_modules",
"d": "npm run dx && npm run dev", "d": "npm run dx && npm run dev",
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev", "dx": "npm i && npm run dx:up && npm run prisma:migrate-dev && npm run prisma:seed",
"dx:up": "docker compose -f docker/development/compose.yml up -d", "dx:up": "docker compose -f docker/development/compose.yml up -d",
"dx:down": "docker compose -f docker/development/compose.yml down", "dx:down": "docker compose -f docker/development/compose.yml down",
"ci": "turbo run test:e2e", "ci": "turbo run test:e2e",
@ -20,7 +20,7 @@
"prisma:migrate-dev": "npm run with:env -- npm run prisma:migrate-dev -w @documenso/prisma", "prisma:migrate-dev": "npm run with:env -- npm run prisma:migrate-dev -w @documenso/prisma",
"prisma:migrate-deploy": "npm run with:env -- npm run prisma:migrate-deploy -w @documenso/prisma", "prisma:migrate-deploy": "npm run with:env -- npm run prisma:migrate-deploy -w @documenso/prisma",
"prisma:seed": "npm run with:env -- npm run prisma:seed -w @documenso/prisma", "prisma:seed": "npm run with:env -- npm run prisma:seed -w @documenso/prisma",
"prisma:studio": "npm run with:env -- npx prisma studio --schema packages/prisma/schema.prisma", "prisma:studio": "npm run with:env -- npm run prisma:studio -w @documenso/prisma",
"with:env": "dotenv -e .env -e .env.local --", "with:env": "dotenv -e .env -e .env.local --",
"reset:hard": "npm run clean && npm i && npm run prisma:generate", "reset:hard": "npm run clean && npm i && npm run prisma:generate",
"precommit": "npm install && git add package.json package-lock.json" "precommit": "npm install && git add package.json package-lock.json"

View File

@ -0,0 +1,25 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { seedUser } from '@documenso/prisma/seed/users';
import { manualLogin } from './fixtures/authentication';
test('delete user', async ({ page }) => {
const user = await seedUser();
await manualLogin({
page,
email: user.email,
redirectPath: '/settings',
});
await page.getByRole('button', { name: 'Delete Account' }).click();
await page.getByRole('button', { name: 'Confirm Deletion' }).click();
await page.waitForURL(`${WEBAPP_BASE_URL}/signin`);
// Verify that the user no longer exists in the database
await expect(getUserByEmail({ email: user.email })).rejects.toThrow();
});

View File

@ -10,9 +10,10 @@
"clean": "rimraf node_modules", "clean": "rimraf node_modules",
"post-install": "prisma generate", "post-install": "prisma generate",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:migrate-dev": "prisma migrate dev", "prisma:migrate-dev": "prisma migrate dev --skip-seed",
"prisma:migrate-deploy": "prisma migrate deploy", "prisma:migrate-deploy": "prisma migrate deploy",
"prisma:seed": "prisma db seed" "prisma:seed": "prisma db seed",
"prisma:studio": "prisma studio"
}, },
"prisma": { "prisma": {
"seed": "ts-node --transpileOnly --project ./tsconfig.seed.json ./seed-database.ts" "seed": "ts-node --transpileOnly --project ./tsconfig.seed.json ./seed-database.ts"

View File

@ -28,6 +28,9 @@ module.exports = {
DEFAULT: 'hsl(var(--secondary))', DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))', foreground: 'hsl(var(--secondary-foreground))',
}, },
warning: {
DEFAULT: 'hsl(var(--warning))',
},
destructive: { destructive: {
DEFAULT: 'hsl(var(--destructive))', DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))', foreground: 'hsl(var(--destructive-foreground))',

View File

@ -0,0 +1,120 @@
import type { Variants } from 'framer-motion';
export const DocumentDropzoneContainerVariants: Variants = {
initial: {
scale: 1,
},
animate: {
scale: 1,
},
hover: {
transition: {
staggerChildren: 0.05,
},
},
};
export const DocumentDropzoneCardLeftVariants: Variants = {
initial: {
x: 40,
y: -10,
rotate: -14,
},
animate: {
x: 40,
y: -10,
rotate: -14,
},
hover: {
x: -25,
y: -25,
rotate: -22,
},
};
export const DocumentDropzoneCardRightVariants: Variants = {
initial: {
x: -40,
y: -10,
rotate: 14,
},
animate: {
x: -40,
y: -10,
rotate: 14,
},
hover: {
x: 25,
y: -25,
rotate: 22,
},
};
export const DocumentDropzoneCardCenterVariants: Variants = {
initial: {
x: 0,
y: 0,
},
animate: {
x: 0,
y: 0,
},
hover: {
x: 0,
y: -25,
},
};
export const DocumentDropzoneDisabledCardLeftVariants: Variants = {
initial: {
x: 50,
y: 0,
rotate: -14,
},
animate: {
x: 50,
y: 0,
rotate: -14,
},
hover: {
x: 30,
y: 0,
rotate: -17,
transition: { type: 'spring', duration: 0.3, stiffness: 500 },
},
};
export const DocumentDropzoneDisabledCardRightVariants: Variants = {
initial: {
x: -50,
y: 0,
rotate: 14,
},
animate: {
x: -50,
y: 0,
rotate: 14,
},
hover: {
x: -30,
y: 0,
rotate: 17,
transition: { type: 'spring', duration: 0.3, stiffness: 500 },
},
};
export const DocumentDropzoneDisabledCardCenterVariants: Variants = {
initial: {
x: -10,
y: 0,
},
animate: {
x: -10,
y: 0,
},
hover: {
x: [-15, -10, -5, -10],
y: 0,
transition: { type: 'spring', duration: 0.3, stiffness: 1000 },
},
};

View File

@ -1,90 +1,27 @@
'use client'; 'use client';
import type { Variants } from 'framer-motion'; import Link from 'next/link';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Plus } from 'lucide-react'; import { AlertTriangle, Plus } from 'lucide-react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import {
DocumentDropzoneCardCenterVariants,
DocumentDropzoneCardLeftVariants,
DocumentDropzoneCardRightVariants,
DocumentDropzoneContainerVariants,
DocumentDropzoneDisabledCardCenterVariants,
DocumentDropzoneDisabledCardLeftVariants,
DocumentDropzoneDisabledCardRightVariants,
} from '../lib/document-dropzone-constants';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { Button } from './button';
import { Card, CardContent } from './card'; import { Card, CardContent } from './card';
const DocumentDropzoneContainerVariants: Variants = {
initial: {
scale: 1,
},
animate: {
scale: 1,
},
hover: {
transition: {
staggerChildren: 0.05,
},
},
};
const DocumentDropzoneCardLeftVariants: Variants = {
initial: {
x: 40,
y: -10,
rotate: -14,
},
animate: {
x: 40,
y: -10,
rotate: -14,
},
hover: {
x: -25,
y: -25,
rotate: -22,
},
};
const DocumentDropzoneCardRightVariants: Variants = {
initial: {
x: -40,
y: -10,
rotate: 14,
},
animate: {
x: -40,
y: -10,
rotate: 14,
},
hover: {
x: 25,
y: -25,
rotate: 22,
},
};
const DocumentDropzoneCardCenterVariants: Variants = {
initial: {
x: 0,
y: 0,
},
animate: {
x: 0,
y: 0,
},
hover: {
x: 0,
y: -25,
},
};
const DocumentDescription = {
document: {
headline: 'Add a document',
},
template: {
headline: 'Upload Template Document',
},
};
export type DocumentDropzoneProps = { export type DocumentDropzoneProps = {
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
@ -123,68 +60,108 @@ export const DocumentDropzone = ({
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT), maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
}); });
const heading = {
document: disabled ? 'You have reached your document limit.' : 'Add a document',
template: 'Upload Template Document',
};
return ( return (
<motion.div <motion.div
className={cn('flex aria-disabled:cursor-not-allowed', className)}
variants={DocumentDropzoneContainerVariants} variants={DocumentDropzoneContainerVariants}
initial="initial" initial="initial"
animate="animate" animate="animate"
whileHover="hover" whileHover="hover"
aria-disabled={disabled}
> >
<Card <Card
role="button" role="button"
className={cn( className={cn(
'focus-visible:ring-ring ring-offset-background flex flex-1 cursor-pointer flex-col items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 aria-disabled:pointer-events-none aria-disabled:opacity-60', 'focus-visible:ring-ring ring-offset-background group flex flex-1 cursor-pointer flex-col items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
className, className,
)} )}
gradient={true} gradient={!disabled}
degrees={120} degrees={120}
aria-disabled={disabled} aria-disabled={disabled}
{...getRootProps()} {...getRootProps()}
{...props} {...props}
> >
<CardContent className="text-muted-foreground/40 flex flex-col items-center justify-center p-6"> <CardContent className="text-muted-foreground/40 flex flex-col items-center justify-center p-6">
{/* <FilePlus strokeWidth="1px" className="h-16 w-16"/> */} {disabled ? (
<div className="flex"> // Disabled State
<motion.div <div className="flex">
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm" <motion.div
variants={!disabled ? DocumentDropzoneCardLeftVariants : undefined} className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
> variants={DocumentDropzoneDisabledCardLeftVariants}
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" /> >
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" /> <div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" /> <div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
</motion.div> <div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
</motion.div>
<motion.div <motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm" className="group-hover:bg-destructive/5 border-muted-foreground/20 group-hover:border-destructive/50 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={!disabled ? DocumentDropzoneCardCenterVariants : undefined} variants={DocumentDropzoneDisabledCardCenterVariants}
> >
<Plus <AlertTriangle
strokeWidth="2px" strokeWidth="2px"
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12" className="text-muted-foreground/20 group-hover:text-destructive h-12 w-12"
/> />
</motion.div> </motion.div>
<motion.div <motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm" className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={!disabled ? DocumentDropzoneCardRightVariants : undefined} variants={DocumentDropzoneDisabledCardRightVariants}
> >
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" /> <div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" /> <div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" /> <div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
</motion.div> </motion.div>
</div> </div>
) : (
// Non Disabled State
<div className="flex">
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardLeftVariants}
>
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardCenterVariants}
>
<Plus
strokeWidth="2px"
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12"
/>
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardRightVariants}
>
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</motion.div>
</div>
)}
<input {...getInputProps()} /> <input {...getInputProps()} />
<p className="group-hover:text-foreground text-muted-foreground mt-8 font-medium"> <p className="text-foreground mt-8 font-medium">{heading[type]}</p>
{DocumentDescription[type].headline}
</p>
<p className="text-muted-foreground/80 mt-1 text-sm"> <p className="text-muted-foreground/80 mt-1 text-center text-sm">
{disabled ? disabledMessage : 'Drag & drop your PDF here.'} {disabled ? disabledMessage : 'Drag & drop your PDF here.'}
</p> </p>
{disabled && IS_BILLING_ENABLED() && (
<Button className="hover:bg-warning/80 bg-warning mt-4 w-32" asChild>
<Link href="/settings/billing">Upgrade</Link>
</Button>
)}
</CardContent> </CardContent>
</Card> </Card>
</motion.div> </motion.div>

View File

@ -42,6 +42,8 @@
--ring: 95.08 71.08% 67.45%; --ring: 95.08 71.08% 67.45%;
--radius: 0.5rem; --radius: 0.5rem;
--warning: 54 96% 45%;
} }
.dark { .dark {
@ -79,6 +81,8 @@
--ring: 95.08 71.08% 67.45%; --ring: 95.08 71.08% 67.45%;
--radius: 0.5rem; --radius: 0.5rem;
--warning: 54 96% 45%;
} }
} }