Merge branch 'feat/refresh' into feat/open-early-adopters

This commit is contained in:
Timur Ercan
2023-09-25 09:28:04 +02:00
committed by GitHub
196 changed files with 5226 additions and 1046 deletions

View File

@ -6,7 +6,7 @@ NEXTAUTH_SECRET="secret"
NEXT_PRIVATE_GOOGLE_CLIENT_ID="" NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET="" NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
# [[APP]] # [[URLS]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000" NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001" NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"

Binary file not shown.

View File

@ -2,7 +2,7 @@
const path = require('path'); const path = require('path');
const { withContentlayer } = require('next-contentlayer'); const { withContentlayer } = require('next-contentlayer');
const { parsed: env } = require('dotenv').config({ require('dotenv').config({
path: path.join(__dirname, '../../.env.local'), path: path.join(__dirname, '../../.env.local'),
}); });
@ -10,9 +10,13 @@ const { parsed: env } = require('dotenv').config({
const config = { const config = {
experimental: { experimental: {
serverActions: true, serverActions: true,
serverActionsBodySizeLimit: '10mb',
}, },
reactStrictMode: true, reactStrictMode: true,
transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'], transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'],
env: {
NEXT_PUBLIC_PROJECT: 'marketing',
},
modularizeImports: { modularizeImports: {
'lucide-react': { 'lucide-react': {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',

View File

@ -8,6 +8,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs" "copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
}, },
"dependencies": { "dependencies": {
@ -18,14 +19,16 @@
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",
"contentlayer": "^0.3.4", "contentlayer": "^0.3.4",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"lucide-react": "^0.214.0", "lucide-react": "^0.279.0",
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "13.4.12", "next": "13.4.19",
"next-auth": "4.22.3", "next-auth": "4.22.3",
"next-contentlayer": "^0.3.4", "next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
"posthog-js": "^1.77.3",
"react": "18.2.0", "react": "18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",

View File

@ -39,7 +39,7 @@ export default function ContentPage({ params }: { params: { content: string } })
const MDXContent = useMDXComponent(post.body.code); const MDXContent = useMDXComponent(post.body.code);
return ( return (
<article className="prose prose-slate mx-auto"> <article className="prose dark:prose-invert mx-auto">
<MDXContent components={mdxComponents} /> <MDXContent components={mdxComponents} />
</article> </article>
); );

View File

@ -39,21 +39,21 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
const MDXContent = useMDXComponent(post.body.code); const MDXContent = useMDXComponent(post.body.code);
return ( return (
<article className="prose prose-slate mx-auto py-8"> <article className="prose dark:prose-invert mx-auto py-8">
<div className="mb-6 text-center"> <div className="mb-6 text-center">
<time dateTime={post.date} className="mb-1 text-xs text-gray-600"> <time dateTime={post.date} className="text-muted-foreground mb-1 text-xs">
{new Date(post.date).toLocaleDateString()} {new Date(post.date).toLocaleDateString()}
</time> </time>
<h1 className="text-3xl font-bold">{post.title}</h1> <h1 className="text-3xl font-bold">{post.title}</h1>
<div className="not-prose relative -mt-2 flex items-center gap-x-4 border-b border-t py-4"> <div className="not-prose relative -mt-2 flex items-center gap-x-4 border-b border-t py-4">
<div className="h-10 w-10 rounded-full bg-gray-50"> <div className="bg-foreground h-10 w-10 rounded-full">
{post.authorImage && ( {post.authorImage && (
<img <img
src={post.authorImage} src={post.authorImage}
alt={`Image of ${post.authorName}`} alt={`Image of ${post.authorName}`}
className="h-10 w-10 rounded-full bg-gray-50" className="bg-foreground/10 h-10 w-10 rounded-full"
/> />
)} )}
</div> </div>

View File

@ -13,13 +13,13 @@ export default function BlogPage() {
<div className="text-center"> <div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">From the blog</h1> <h1 className="text-3xl font-bold lg:text-5xl">From the blog</h1>
<p className="mx-auto mt-4 max-w-xl text-center text-lg leading-normal text-[#31373D]"> <p className="text-muted-foreground mx-auto mt-4 max-w-xl text-center text-lg leading-normal">
Get the latest news from Documenso, including product updates, team announcements and Get the latest news from Documenso, including product updates, team announcements and
more! more!
</p> </p>
</div> </div>
<div className="mt-10 divide-y divide-slate-100 border-t border-slate-200 "> <div className="divide-muted-foreground/20 border-muted-foreground/20 mt-10 divide-y border-t">
{blogPosts.map((post, i) => ( {blogPosts.map((post, i) => (
<article <article
key={`blog-${i}`} key={`blog-${i}`}
@ -57,12 +57,12 @@ export default function BlogPage() {
</div> </div>
<div className="relative mt-4 flex items-center gap-x-4"> <div className="relative mt-4 flex items-center gap-x-4">
<div className="h-10 w-10 rounded-full bg-slate-50"> <div className="bg-foreground/5 h-10 w-10 rounded-full">
{post.authorImage && ( {post.authorImage && (
<img <img
src={post.authorImage} src={post.authorImage}
alt={`Image of ${post.authorName}`} alt={`Image of ${post.authorName}`}
className="h-10 w-10 rounded-full bg-slate-50" className="bg-foreground/5 h-10 w-10 rounded-full"
/> />
)} )}
</div> </div>

View File

@ -58,40 +58,40 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
return ( return (
<div className="mt-12"> <div className="mt-12">
<h1 className="text-3xl font-bold text-slate-900 md:text-4xl"> <h1 className="text-foreground text-3xl font-bold md:text-4xl">
Welcome to the <span className="text-primary">open signing</span> revolution{' '} Welcome to the <span className="text-primary">open signing</span> revolution{' '}
<u>{user.name}</u> <u>{user.name}</u>
</h1> </h1>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg"> <p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
It's not every day you get to be part of a revolution. It's not every day you get to be part of a revolution.
</p> </p>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg"> <p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
But today is that day, by signing up to Documenso, you're joining a movement of people who But today is that day, by signing up to Documenso, you're joining a movement of people who
want to make the world a better place. want to make the world a better place.
</p> </p>
<p className="mt-4 max-w-prose text-base text-slate-500 md:text-lg"> <p className="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
We're going to change the way people sign documents. We're going to make it easier, faster, We're going to change the way people sign documents. We're going to make it easier, faster,
and more secure. And we're going to do it together. and more secure. And we're going to do it together.
</p> </p>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-2xl font-bold text-slate-900">Let's do it together</h2> <h2 className="text-foreground text-2xl font-bold">Let's do it together</h2>
<div className="-mx-4 mt-8 flex md:-mx-8"> <div className="-mx-4 mt-8 flex md:-mx-8">
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none"> <div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
<p <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl', 'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className, fontCaveat.className,
)} )}
> >
Timur Timur
</p> </p>
<p className="text-sm text-slate-500 md:text-lg"> <p className="text-muted-foreground text-sm md:text-lg">
Timur Ercan Timur Ercan
<span className="block lg:hidden" /> <span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span> <span className="hidden lg:inline"> - </span>
@ -102,14 +102,14 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none"> <div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
<p <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl', 'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className, fontCaveat.className,
)} )}
> >
Lucas Lucas
</p> </p>
<p className="text-sm text-slate-500 md:text-lg"> <p className="text-muted-foreground text-sm md:text-lg">
Lucas Smith Lucas Smith
<span className="block lg:hidden" /> <span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span> <span className="hidden lg:inline"> - </span>
@ -119,12 +119,16 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
<div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none"> <div className="flex flex-1 flex-col justify-end gap-y-4 border-r px-4 last:border-r-0 md:px-8 lg:flex-none">
{signatureDataUrl && ( {signatureDataUrl && (
<img src={signatureDataUrl} alt="your-signature" className="max-w-[172px]" /> <img
src={signatureDataUrl}
alt="your-signature"
className="max-w-[172px] dark:invert"
/>
)} )}
{!signatureDataUrl && ( {!signatureDataUrl && (
<p <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 md:text-5xl', 'text-foreground text-4xl font-semibold md:text-5xl',
fontCaveat.className, fontCaveat.className,
)} )}
> >
@ -132,7 +136,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</p> </p>
)} )}
<p className="text-sm text-slate-500 md:text-lg"> <p className="text-muted-foreground text-sm md:text-lg">
{user.name} {user.name}
<span className="block lg:hidden" /> <span className="block lg:hidden" />
<span className="hidden lg:inline"> - </span> <span className="hidden lg:inline"> - </span>
@ -143,20 +147,20 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</div> </div>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-2xl font-bold text-slate-900">Your sign in details</h2> <h2 className="text-foreground text-2xl font-bold">Your sign in details</h2>
<div className="mt-4"> <div className="mt-4">
<p className="text-lg text-slate-500"> <p className="text-muted-foreground text-lg">
<span className="font-bold">Email:</span> {user.email} <span className="font-bold">Email:</span> {user.email}
</p> </p>
<p className="mt-2 text-lg text-slate-500"> <p className="text-muted-foreground mt-2 text-lg">
<span className="font-bold">Password:</span>{' '} <span className="font-bold">Password:</span>{' '}
<PasswordReveal password={password ?? 'password'} /> <PasswordReveal password={password ?? 'password'} />
</p> </p>
</div> </div>
<p className="mt-4 text-sm italic text-slate-500"> <p className="text-muted-foreground mt-4 text-sm italic">
This is a temporary password. Please change it as soon as possible. This is a temporary password. Please change it as soon as possible.
</p> </p>

View File

@ -1,4 +1,8 @@
import React from 'react'; 'use client';
import React, { useEffect, useState } from 'react';
import { cn } from '@documenso/ui/lib/utils';
import { Footer } from '~/components/(marketing)/footer'; import { Footer } from '~/components/(marketing)/footer';
import { Header } from '~/components/(marketing)/header'; import { Header } from '~/components/(marketing)/header';
@ -8,15 +12,31 @@ export type MarketingLayoutProps = {
}; };
export default function MarketingLayout({ children }: MarketingLayoutProps) { export default function MarketingLayout({ children }: MarketingLayoutProps) {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return ( return (
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28"> <div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md"> <div
className={cn('fixed left-0 top-0 z-50 w-full bg-transparent', {
'bg-background/50 backdrop-blur-md': scrollY > 5,
})}
>
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" /> <Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
</div> </div>
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div> <div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div>
<Footer className="mt-24 bg-transparent backdrop-blur-[2px]" /> <Footer className="bg-background border-muted mt-24 border-t" />
</div> </div>
); );
} }

View File

@ -83,7 +83,7 @@ export default async function OpenPage() {
.then((res) => ZEarlyAdoptersResponse.parse(res)); .then((res) => ZEarlyAdoptersResponse.parse(res));
return ( return (
<div className="mx-auto mt-12 max-w-screen-lg"> <div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1> <h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>

View File

@ -0,0 +1,84 @@
'use client';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import { TOSSFriendsSchema } from './schema';
const ContainerVariants: Variants = {
initial: {
opacity: 0,
},
animate: {
opacity: 1,
transition: {
staggerChildren: 0.075,
},
},
};
const CardVariants: Variants = {
initial: {
opacity: 0,
y: 50,
},
animate: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
const randomDegrees = () => {
const degrees = [45, 120, -140, -45];
return degrees[Math.floor(Math.random() * degrees.length)];
};
export type OSSFriendsContainerProps = {
className?: string;
ossFriends: TOSSFriendsSchema;
};
export const OSSFriendsContainer = ({ className, ossFriends }: OSSFriendsContainerProps) => {
return (
<motion.div
className={cn('grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3', className)}
variants={ContainerVariants}
initial="initial"
animate="animate"
>
{ossFriends.map((friend, index) => (
<motion.div key={index} className="h-full w-full" variants={CardVariants}>
<Card
className="h-full"
degrees={randomDegrees()}
gradient={index % 2 === 0}
spotlight={index % 2 !== 0}
>
<CardContent className="flex h-full flex-col p-6">
<CardTitle>
<Link href={friend.href}>{friend.name}</Link>
</CardTitle>
<p className="text-foreground mt-4 flex-1 text-sm">{friend.description}</p>
<div className="mt-8">
<Link target="_blank" href={friend.href}>
<Button>Learn more</Button>
</Link>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
);
};

View File

@ -1,152 +1,23 @@
'use client';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion'; import { z } from 'zod';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png'; import backgroundPattern from '~/assets/background-pattern.png';
const OSSFriends = [ import { OSSFriendsContainer } from './container';
{ import { TOSSFriendsSchema, ZOSSFriendsSchema } from './schema';
name: 'BoxyHQ',
description:
'BoxyHQs suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.',
href: 'https://boxyhq.com',
},
{
name: 'Cal.com',
description:
'Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.',
href: 'https://cal.com',
},
{
name: 'Crowd.dev',
description:
'Centralize community, product, and customer data to understand which companies are engaging with your open source project.',
href: 'https://www.crowd.dev',
},
{
name: 'Documenso',
description:
'The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.',
href: 'https://documenso.com',
},
{
name: 'Erxes',
description:
'The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.',
href: 'https://erxes.io',
},
{
name: 'Formbricks',
description:
'Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.',
href: 'https://formbricks.com',
},
{
name: 'Forward Email',
description:
'Free email forwarding for custom domains. For 6 years and counting, we are the go-to email service for thousands of creators, developers, and businesses.',
href: 'https://forwardemail.net',
},
{
name: 'GitWonk',
description:
'GitWonk is an open-source technical documentation tool, designed and built focusing on the developer experience.',
href: 'https://gitwonk.com',
},
{
name: 'Hanko',
description:
'Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.',
href: 'https://www.hanko.io',
},
{
name: 'HTMX',
description:
'HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.',
href: 'https://htmx.org',
},
{
name: 'Infisical',
description:
'Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.',
href: 'https://infisical.com',
},
{
name: 'Novu',
description:
'The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.',
href: 'https://novu.co',
},
{
name: 'OpenBB',
description:
'Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.',
href: 'https://openbb.co',
},
{
name: 'Sniffnet',
description:
'Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.',
href: 'https://www.sniffnet.net',
},
{
name: 'Typebot',
description:
'Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.',
href: 'https://typebot.io',
},
{
name: 'Webiny',
description:
'Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.',
href: 'https://www.webiny.com',
},
{
name: 'Webstudio',
description: 'Webstudio is an open source alternative to Webflow',
href: 'https://webstudio.is',
},
];
const ContainerVariants: Variants = { export default async function OSSFriendsPage() {
initial: { const ossFriends: TOSSFriendsSchema = await fetch('https://formbricks.com/api/oss-friends', {
opacity: 0, next: {
}, revalidate: 3600,
animate: {
opacity: 1,
transition: {
staggerChildren: 0.075,
}, },
}, })
}; .then(async (res) => res.json())
.then(async (data) => z.object({ data: ZOSSFriendsSchema }).parseAsync(data))
.then(({ data }) => data)
.catch(() => []);
const CardVariants: Variants = {
initial: {
opacity: 0,
y: 50,
},
animate: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
const randomDegrees = () => {
const degrees = [45, 120, -140, -45];
return degrees[Math.floor(Math.random() * degrees.length)];
};
export default function OSSFriendsPage() {
return ( return (
<div className="relative mt-12"> <div className="relative mt-12">
<div className="text-center"> <div className="text-center">
@ -154,49 +25,19 @@ export default function OSSFriendsPage() {
Our <span title="Open Source Software">OSS</span> Friends Our <span title="Open Source Software">OSS</span> Friends
</h1> </h1>
<p className="mx-auto mt-4 max-w-[55ch] text-lg leading-normal text-[#31373D]"> <p className="text-foreground mx-auto mt-4 max-w-[55ch] text-lg leading-normal">
We love open source and so should you, below you can find a list of our friends who are We love open source and so should you, below you can find a list of our friends who are
just as passionate about open source as we are. just as passionate about open source as we are.
</p> </p>
</div> </div>
<motion.div <OSSFriendsContainer className="mt-12" ossFriends={ossFriends} />
className="mt-12 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"
variants={ContainerVariants}
initial="initial"
animate="animate"
>
{OSSFriends.map((friend, index) => (
<motion.div key={index} className="h-full w-full" variants={CardVariants}>
<Card
className="h-full"
degrees={randomDegrees()}
gradient={index % 2 === 0}
spotlight={index % 2 !== 0}
>
<CardContent className="flex h-full flex-col p-6">
<CardTitle>
<Link href={friend.href}>{friend.name}</Link>
</CardTitle>
<p className="mt-4 flex-1 text-sm text-slate-700">{friend.description}</p>
<div className="mt-8">
<Link target="_blank" href={friend.href}>
<Button>Learn more</Button>
</Link>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
<div className="absolute inset-0 -z-10 flex items-start justify-center"> <div className="absolute inset-0 -z-10 flex items-start justify-center">
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover md:-mr-[50vw] md:scale-150 lg:scale-[175%]" className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:-mr-[50vw] md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZOSSFriendsSchema = z.array(
z.object({
name: z.string(),
href: z.string().url(),
description: z.string(),
}),
);
export type TOSSFriendsSchema = z.infer<typeof ZOSSFriendsSchema>;

View File

@ -20,14 +20,14 @@ export type PricingPageProps = {
export default function PricingPage() { export default function PricingPage() {
return ( return (
<div className="mt-12"> <div className="mt-6 sm:mt-12">
<div className="text-center"> <div className="text-center">
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1> <h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
<p className="mt-4 text-lg leading-normal text-[#31373D]"> <p className="text-foreground mt-4 text-lg leading-normal">
Designed for every stage of your journey. Designed for every stage of your journey.
</p> </p>
<p className="text-lg leading-normal text-[#31373D]">Get started today.</p> <p className="text-foreground text-lg leading-normal">Get started today.</p>
</div> </div>
<div className="mt-12"> <div className="mt-12">
@ -45,7 +45,7 @@ export default function PricingPage() {
What is the difference between the plans? What is the difference between the plans?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
You can self-host Documenso for free or use our ready-to-use hosted version. The You can self-host Documenso for free or use our ready-to-use hosted version. The
hosted version comes with additional support, painless scalability and more. Early hosted version comes with additional support, painless scalability and more. Early
adopters of the community plan will get access to all features we build this year, for adopters of the community plan will get access to all features we build this year, for
@ -59,7 +59,7 @@ export default function PricingPage() {
How do you handle my data? How do you handle my data?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Securely. Our data centers are located in Frankfurt (Germany), giving us the best Securely. Our data centers are located in Frankfurt (Germany), giving us the best
local privacy laws. We are very aware of the sensitive nature of our data and follow local privacy laws. We are very aware of the sensitive nature of our data and follow
best practices to ensure the security and integrity of the data entrusted to us. best practices to ensure the security and integrity of the data entrusted to us.
@ -71,7 +71,7 @@ export default function PricingPage() {
Why should I use your hosting service? Why should I use your hosting service?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Using our hosted version is the easiest way to get started, you can simply subscribe Using our hosted version is the easiest way to get started, you can simply subscribe
and start signing your documents. We take care of the infrastructure, so you can focus and start signing your documents. We take care of the infrastructure, so you can focus
on your business. Additionally, when using our hosted version you benefit from our on your business. Additionally, when using our hosted version you benefit from our
@ -84,7 +84,7 @@ export default function PricingPage() {
How can I contribute? How can I contribute?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
That's awesome. You can take a look at the current{' '} That's awesome. You can take a look at the current{' '}
<Link <Link
className="text-documenso-700 font-bold" className="text-documenso-700 font-bold"
@ -111,7 +111,7 @@ export default function PricingPage() {
Can I use Documenso commercially? Can I use Documenso commercially?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you
can use it for free and even modify it to fit your needs, as long as you publish your can use it for free and even modify it to fit your needs, as long as you publish your
changes under the same license. changes under the same license.
@ -123,7 +123,7 @@ export default function PricingPage() {
Why should I prefer Documenso over DocuSign or some other signing tool? Why should I prefer Documenso over DocuSign or some other signing tool?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
Documenso is a community effort to create an open and vibrant ecosystem around a tool, Documenso is a community effort to create an open and vibrant ecosystem around a tool,
everybody is free to use and adapt. By being truly open we want to create trusted everybody is free to use and adapt. By being truly open we want to create trusted
infrastructure for the future of the internet. infrastructure for the future of the internet.
@ -135,7 +135,7 @@ export default function PricingPage() {
Where can I get support? Where can I get support?
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
We are happy to assist you at{' '} We are happy to assist you at{' '}
<Link <Link
className="text-documenso-700 font-bold" className="text-documenso-700 font-bold"

View File

@ -0,0 +1,30 @@
import { notFound } from 'next/navigation';
import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { DocumentStatus } from '@documenso/prisma/client';
import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success';
export type SinglePlayerModeSuccessPageProps = {
params: {
token?: string;
};
};
export default async function SinglePlayerModeSuccessPage({
params: { token },
}: SinglePlayerModeSuccessPageProps) {
if (!token) {
return notFound();
}
const document = await getDocumentAndRecipientByToken({
token,
}).catch(() => null);
if (!document || document.status !== DocumentStatus.COMPLETED) {
return notFound();
}
return <SinglePlayerModeSuccess document={document} />;
}

View File

@ -0,0 +1,244 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { base64 } from '@documenso/lib/universal/base64';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import { Field, Prisma, Recipient } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
import {
DocumentFlowFormContainer,
DocumentFlowFormContainerHeader,
} from '@documenso/ui/primitives/document-flow/document-flow-root';
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { createSinglePlayerDocument } from '~/components/(marketing)/single-player-mode/create-single-player-document.action';
type SinglePlayerModeStep = 'fields' | 'sign';
export default function SinglePlayerModePage() {
const analytics = useAnalytics();
const router = useRouter();
const { toast } = useToast();
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
const [step, setStep] = useState<SinglePlayerModeStep>('fields');
const [fields, setFields] = useState<Field[]>([]);
const documentFlow: Record<SinglePlayerModeStep, DocumentFlowStep> = {
fields: {
title: 'Add document',
description: 'Upload a document and add fields.',
stepIndex: 1,
onBackStep: uploadedFile
? () => {
setUploadedFile(null);
setFields([]);
}
: undefined,
onNextStep: () => setStep('sign'),
},
sign: {
title: 'Sign',
description: 'Enter your details.',
stepIndex: 2,
onBackStep: () => setStep('fields'),
},
};
const currentDocumentFlow = documentFlow[step];
useEffect(() => {
analytics.startSessionRecording('marketing_session_recording_spm');
return () => {
analytics.stopSessionRecording();
};
}, [analytics]);
/**
* Insert the selected fields into the local state.
*/
const onFieldsSubmit = (data: TAddFieldsFormSchema) => {
if (!uploadedFile) {
return;
}
setFields(
data.fields.map((field, i) => ({
id: i,
documentId: -1,
recipientId: -1,
type: field.type,
page: field.pageNumber,
positionX: new Prisma.Decimal(field.pageX),
positionY: new Prisma.Decimal(field.pageY),
width: new Prisma.Decimal(field.pageWidth),
height: new Prisma.Decimal(field.pageHeight),
customText: '',
inserted: false,
})),
);
analytics.capture('Marketing: SPM - Fields added');
documentFlow.fields.onNextStep?.();
};
/**
* Upload, create, sign and send the document.
*/
const onSignSubmit = async (data: TAddSignatureFormSchema) => {
if (!uploadedFile) {
return;
}
try {
const putFileData = await putFile(uploadedFile.file);
const documentToken = await createSinglePlayerDocument({
documentData: {
type: putFileData.type,
data: putFileData.data,
},
documentName: uploadedFile.file.name,
signer: data,
fields: fields.map((field) => ({
page: field.page,
type: field.type,
positionX: field.positionX.toNumber(),
positionY: field.positionY.toNumber(),
width: field.width.toNumber(),
height: field.height.toNumber(),
})),
});
analytics.capture('Marketing: SPM - Document signed', {
signer: data.email,
});
router.push(`/single-player-mode/${documentToken}/success`);
} catch {
toast({
title: 'Something went wrong',
description: 'Please try again later.',
variant: 'destructive',
});
}
};
const placeholderRecipient: Recipient = {
id: -1,
documentId: -1,
email: '',
name: '',
token: '',
expired: null,
signedAt: null,
readStatus: 'OPENED',
signingStatus: 'NOT_SIGNED',
sendStatus: 'NOT_SENT',
};
const onFileDrop = async (file: File) => {
try {
const arrayBuffer = await file.arrayBuffer();
const base64String = base64.encode(new Uint8Array(arrayBuffer));
setUploadedFile({
file,
fileBase64: `data:application/pdf;base64,${base64String}`,
});
analytics.capture('Marketing: SPM - Document uploaded');
} catch {
toast({
title: 'Something went wrong',
description: 'Please try again later.',
variant: 'destructive',
});
}
};
return (
<div className="mt-6 sm:mt-12">
<div className="text-center">
<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{' '}
<Link
href={'/pricing'}
target="_blank"
className="hover:text-foreground/80 font-semibold transition-colors"
>
community plan
</Link>{' '}
for exclusive features, including the ability to collaborate with multiple signers.
</p>
</div>
<div className="mt-12 grid w-full grid-cols-12 gap-8">
<div className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7">
{uploadedFile ? (
<Card gradient>
<CardContent className="p-2">
<LazyPDFViewer document={uploadedFile.fileBase64} />
</CardContent>
</Card>
) : (
<DocumentDropzone className="h-[80vh] max-h-[60rem]" onDrop={onFileDrop} />
)}
</div>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
<DocumentFlowFormContainerHeader
title={currentDocumentFlow.title}
description={currentDocumentFlow.description}
/>
{/* Add fields to PDF page. */}
{step === 'fields' && (
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
<AddFieldsFormPartial
documentFlow={documentFlow.fields}
hideRecipients={true}
recipients={uploadedFile ? [placeholderRecipient] : []}
numberOfSteps={Object.keys(documentFlow).length}
fields={fields}
onSubmit={onFieldsSubmit}
/>
</fieldset>
)}
{/* Enter user details and signature. */}
{step === 'sign' && (
<AddSignatureFormPartial
documentFlow={documentFlow.sign}
numberOfSteps={Object.keys(documentFlow).length}
fields={fields}
onSubmit={onSignSubmit}
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
/>
)}
</DocumentFlowFormContainer>
</div>
</div>
</div>
);
}

View File

@ -1,12 +1,20 @@
import { Inter } from 'next/font/google'; import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster'; import { Toaster } from '@documenso/ui/primitives/toaster';
import { ThemeProvider } from '~/providers/next-theme';
import { PlausibleProvider } from '~/providers/plausible'; import { PlausibleProvider } from '~/providers/plausible';
import { PostHogPageview } from '~/providers/posthog';
import './globals.css'; import './globals.css';
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = { export const metadata = {
title: 'Documenso - The Open Source DocuSign Alternative', title: 'Documenso - The Open Source DocuSign Alternative',
@ -32,9 +40,15 @@ export const metadata = {
}, },
}; };
export default function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags();
return ( return (
<html lang="en" className={fontInter.variable} suppressHydrationWarning> <html
lang="en"
className={cn(fontInter.variable, fontCaveat.variable)}
suppressHydrationWarning
>
<head> <head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
@ -42,8 +56,17 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
</head> </head>
<Suspense>
<PostHogPageview />
</Suspense>
<body> <body>
<PlausibleProvider>{children}</PlausibleProvider> <FeatureFlagProvider initialFlags={flags}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<PlausibleProvider>{children}</PlausibleProvider>
</ThemeProvider>
</FeatureFlagProvider>
<Toaster /> <Toaster />
</body> </body>
</html> </html>

View File

@ -0,0 +1,65 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ChevronLeft } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
export default function NotFound() {
const router = useRouter();
return (
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
>
<Image
src={backgroundPattern}
alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
priority
/>
</motion.div>
</div>
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
<div>
<p className="text-muted-foreground font-semibold">404 Page not found</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<p className="text-muted-foreground mt-4 text-sm">
The page you are looking for was moved, removed, renamed or might never have existed.
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
variant="ghost"
className="w-32"
onClick={() => {
void router.back();
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
</Button>
<Button className="w-32" asChild>
<Link href="/">Home</Link>
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@ -4,11 +4,11 @@ import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
return { return {
rules: { rules: [
userAgent: '*', {
allow: '/*', userAgent: '*',
disallow: ['/_next/*'], },
}, ],
sitemap: `${getBaseUrl()}/sitemap.xml`, sitemap: `${getBaseUrl()}/sitemap.xml`,
}; };
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

View File

@ -41,7 +41,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
onClick={onSignUpClick} onClick={onSignUpClick}
> >
Get the Community Plan Get the Community Plan
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever! $30/mo. forever!
</span> </span>
</Button> </Button>
@ -55,7 +55,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
<Github className="mr-2 h-5 w-5" /> <Github className="mr-2 h-5 w-5" />
Star on Github Star on Github
{starCount && starCount > 0 && ( {starCount && starCount > 0 && (
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
{starCount.toLocaleString('en-US')} {starCount.toLocaleString('en-US')}
</span> </span>
)} )}

View File

@ -10,6 +10,7 @@ import { usePlausible } from 'next-plausible';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
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 { import {
@ -43,9 +44,11 @@ export type ClaimPlanDialogProps = {
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => { export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
const params = useSearchParams(); const params = useSearchParams();
const { toast } = useToast(); const analytics = useAnalytics();
const event = usePlausible(); const event = usePlausible();
const { toast } = useToast();
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true'); const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
const { const {
@ -73,10 +76,12 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
]); ]);
event('claim-plan-pricing'); event('claim-plan-pricing');
analytics.capture('Marketing: Claim plan', { planId, email });
window.location.href = redirectUrl; window.location.href = redirectUrl;
} catch (error) { } catch (error) {
event('claim-plan-failed'); event('claim-plan-failed');
analytics.capture('Marketing: Claim plan failure', { planId, email });
toast({ toast({
title: 'Something went wrong', title: 'Something went wrong',
@ -118,7 +123,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
)} )}
<div> <div>
<Label className="text-slate-500">Name</Label> <Label className="text-muted-foreground">Name</Label>
<Input type="text" className="mt-2" {...register('name')} autoFocus /> <Input type="text" className="mt-2" {...register('name')} autoFocus />
@ -126,7 +131,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
</div> </div>
<div> <div>
<Label className="text-slate-500">Email</Label> <Label className="text-muted-foreground">Email</Label>
<Input type="email" className="mt-2" {...register('email')} /> <Input type="email" className="mt-2" {...register('email')} />

View File

@ -0,0 +1,46 @@
'use client';
import React, { useEffect, useState } from 'react';
import Confetti from 'react-confetti';
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({
numberOfPieces: numberOfPiecesProp = 200,
...props
}: React.ComponentPropsWithoutRef<typeof Confetti> & { duration?: number }) {
const isMounted = useIsMounted();
const { width, height } = useWindowSize();
const [numberOfPieces, setNumberOfPieces] = useState(numberOfPiecesProp);
useEffect(() => {
if (!props.duration) {
return;
}
const timer = setTimeout(() => {
setNumberOfPieces(0);
}, props.duration);
return () => clearTimeout(timer);
}, [props.duration]);
if (!isMounted) {
return null;
}
return createPortal(
<Confetti
{...props}
className="w-full"
numberOfPieces={numberOfPieces}
width={width}
height={height}
/>,
document.body,
);
}

View File

@ -22,7 +22,7 @@ export const FasterSmarterBeautifulBento = ({
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24"> <h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
@ -33,41 +33,53 @@ export const FasterSmarterBeautifulBento = ({
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8"> <div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient> <Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]"> <CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6"> <p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
<strong className="block">Fast.</strong> <strong className="block">Fast.</strong>
When it comes to sending or receiving a contract, you can count on lightning-fast When it comes to sending or receiving a contract, you can count on lightning-fast
speeds. speeds.
</p> </p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6"> <div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardFastFigure} alt="its fast" className="max-w-[80%] lg:max-w-none" /> <Image
src={cardFastFigure}
alt="its fast"
className="max-w-[80%] dark:contrast-[70%] dark:hue-rotate-180 dark:invert lg:max-w-none"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Beautiful.</strong> <strong className="block">Beautiful.</strong>
Because signing should be celebrated. Thats why we care about the smallest detail in Because signing should be celebrated. Thats why we care about the smallest detail in
our product. our product.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardBeautifulFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardBeautifulFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Smart.</strong> <strong className="block">Smart.</strong>
Our custom templates come with smart rules that can help you save time and energy. Our custom templates come with smart rules that can help you save time and energy.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardSmartFigure} alt="its fast" className="w-full max-w-[16rem]" /> <Image
src={cardSmartFigure}
alt="its fast"
className="w-full max-w-[16rem] dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,9 +1,12 @@
'use client';
import { HTMLAttributes } from 'react'; import { HTMLAttributes } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { Github, MessagesSquare, Twitter } from 'lucide-react'; import { Github, MessagesSquare, Moon, Sun, Twitter } from 'lucide-react';
import { useTheme } from 'next-themes';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -17,6 +20,7 @@ const SOCIAL_LINKS = [
const FOOTER_LINKS = [ const FOOTER_LINKS = [
{ href: '/pricing', text: 'Pricing' }, { href: '/pricing', text: 'Pricing' },
{ href: '/single-player-mode', text: 'Single Player Mode' },
{ href: '/blog', text: 'Blog' }, { href: '/blog', text: 'Blog' },
{ href: '/open', text: 'Open' }, { href: '/open', text: 'Open' },
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' }, { href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
@ -26,17 +30,30 @@ const FOOTER_LINKS = [
]; ];
export const Footer = ({ className, ...props }: FooterProps) => { export const Footer = ({ className, ...props }: FooterProps) => {
const { setTheme } = useTheme();
return ( return (
<div className={cn('border-t py-12', className)} {...props}> <div className={cn('border-t py-12', className)} {...props}>
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8"> <div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
<div> <div>
<Link href="/"> <Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image> <Image
src="/logo.png"
alt="Documenso Logo"
className="dark:invert"
width={170}
height={0}
/>
</Link> </Link>
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4 text-[#8D8D8D]"> <div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4">
{SOCIAL_LINKS.map((link, index) => ( {SOCIAL_LINKS.map((link, index) => (
<Link key={index} href={link.href} target="_blank" className="hover:text-[#6D6D6D]"> <Link
key={index}
href={link.href}
target="_blank"
className="text-muted-foreground hover:text-muted-foreground/80"
>
{link.icon} {link.icon}
</Link> </Link>
))} ))}
@ -49,17 +66,29 @@ export const Footer = ({ className, ...props }: FooterProps) => {
key={index} key={index}
href={link.href} href={link.href}
target={link.target} target={link.target}
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 text-sm"
> >
{link.text} {link.text}
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24"> <div className="mx-auto mt-4 flex w-full max-w-screen-xl flex-wrap justify-between gap-4 px-8 md:mt-12 lg:mt-24">
<p className="text-sm text-[#8D8D8D]"> <p className="text-muted-foreground text-sm">
© {new Date().getFullYear()} Documenso, Inc. All rights reserved. © {new Date().getFullYear()} Documenso, Inc. All rights reserved.
</p> </p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5">
<button type="button" className="text-muted-foreground" onClick={() => setTheme('light')}>
<Sun className="h-5 w-5" />
<span className="sr-only">Light</span>
</button>
<button type="button" className="text-muted-foreground" onClick={() => setTheme('dark')}>
<Moon className="h-5 w-5" />
<span className="sr-only">Dark</span>
</button>
</div>
</div> </div>
</div> </div>
); );

View File

@ -5,6 +5,7 @@ import { HTMLAttributes, useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { HamburgerMenu } from './mobile-hamburger'; import { HamburgerMenu } from './mobile-hamburger';
@ -15,29 +16,59 @@ export type HeaderProps = HTMLAttributes<HTMLElement>;
export const Header = ({ className, ...props }: HeaderProps) => { export const Header = ({ className, ...props }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false); const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const { getFlag } = useFeatureFlags();
const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
return ( return (
<header className={cn('flex items-center justify-between', className)} {...props}> <header className={cn('flex items-center justify-between', className)} {...props}>
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}> <div className="flex items-center space-x-4">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} /> <Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
</Link> <Image
src="/logo.png"
alt="Documenso Logo"
className="dark:invert"
width={170}
height={25}
/>
</Link>
{isSinglePlayerModeMarketingEnabled && (
<Link
href="/single-player-mode"
className="bg-primary dark:text-background rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
>
Try now!
</Link>
)}
</div>
<div className="hidden items-center gap-x-6 md:flex"> <div className="hidden items-center gap-x-6 md:flex">
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"> <Link
href="/pricing"
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
>
Pricing Pricing
</Link> </Link>
<Link href="/blog" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"> <Link
href="/blog"
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
>
Blog Blog
</Link> </Link>
<Link href="/open" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"> <Link
href="/open"
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
>
Open Open
</Link> </Link>
<Link <Link
href="https://app.documenso.com/login" href="https://app.documenso.com/login"
target="_blank" target="_blank"
className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
> >
Sign in Sign in
</Link> </Link>

View File

@ -6,7 +6,9 @@ import Link from 'next/link';
import { Variants, motion } from 'framer-motion'; import { Variants, motion } from 'framer-motion';
import { Github } from 'lucide-react'; import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible'; import { usePlausible } from 'next-plausible';
import { match } from 'ts-pattern';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
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';
@ -51,6 +53,10 @@ const HeroTitleVariants: Variants = {
export const Hero = ({ className, ...props }: HeroProps) => { export const Hero = ({ className, ...props }: HeroProps) => {
const event = usePlausible(); const event = usePlausible();
const { getFlag } = useFeatureFlags();
const heroMarketingCTA = getFlag('marketing_landing_hero_cta');
const onSignUpClick = () => { const onSignUpClick = () => {
const el = document.getElementById('email'); const el = document.getElementById('email');
@ -80,7 +86,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</motion.div> </motion.div>
</div> </div>
@ -109,7 +115,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
onClick={onSignUpClick} onClick={onSignUpClick}
> >
Get the Community Plan Get the Community Plan
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever! $30/mo. forever!
</span> </span>
</Button> </Button>
@ -122,23 +128,45 @@ export const Hero = ({ className, ...props }: HeroProps) => {
</Link> </Link>
</motion.div> </motion.div>
<motion.div {match(heroMarketingCTA)
variants={HeroTitleVariants} .with('spm', () => (
initial="initial" <motion.div
animate="animate" variants={HeroTitleVariants}
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4" initial="initial"
> animate="animate"
<Link className="border-primary bg-background hover:bg-muted mx-auto mt-8 w-60 rounded-xl border transition duration-300"
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" >
target="_blank" <Link href="/single-player-mode" className="block px-4 py-2 text-center">
> <h2 className="text-muted-foreground text-xs font-semibold">
<img Introducing Single Player Mode
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" </h2>
alt="Documenso - The open source DocuSign alternative | Product Hunt"
style={{ width: '250px', height: '54px' }} <h1 className="text-foreground mt-1.5 font-medium leading-5">
/> Self sign for free!
</Link> </h1>
</motion.div> </Link>
</motion.div>
))
.with('productHunt', () => (
<motion.div
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
>
<Link
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
target="_blank"
>
<img
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
alt="Documenso - The open source DocuSign alternative | Product Hunt"
style={{ width: '250px', height: '54px' }}
/>
</Link>
</motion.div>
))
.otherwise(() => null)}
<motion.div <motion.div
className="mt-12" className="mt-12"

View File

@ -14,6 +14,10 @@ export type MobileNavigationProps = {
}; };
export const MENU_NAVIGATION_LINKS = [ export const MENU_NAVIGATION_LINKS = [
{
href: '/single-player-mode',
text: 'Single Player Mode',
},
{ {
href: '/blog', href: '/blog',
text: 'Blog', text: 'Blog',
@ -55,7 +59,13 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}> <Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
<SheetContent className="w-full max-w-[400px]"> <SheetContent className="w-full max-w-[400px]">
<Link href="/" className="z-10" onClick={handleMenuItemClick}> <Link href="/" className="z-10" onClick={handleMenuItemClick}>
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} /> <Image
src="/logo.png"
alt="Documenso Logo"
className="dark:invert"
width={170}
height={25}
/>
</Link> </Link>
<motion.div <motion.div
@ -85,7 +95,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
}} }}
> >
<Link <Link
className="text-2xl font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
href={href} href={href}
onClick={() => handleMenuItemClick()} onClick={() => handleMenuItemClick()}
> >
@ -99,7 +109,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link <Link
href="https://twitter.com/documenso" href="https://twitter.com/documenso"
target="_blank" target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80"
> >
<Twitter className="h-6 w-6" /> <Twitter className="h-6 w-6" />
</Link> </Link>
@ -107,7 +117,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link <Link
href="https://github.com/documenso/documenso" href="https://github.com/documenso/documenso"
target="_blank" target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80"
> >
<Github className="h-6 w-6" /> <Github className="h-6 w-6" />
</Link> </Link>
@ -115,7 +125,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
<Link <Link
href="https://documen.so/discord" href="https://documen.so/discord"
target="_blank" target="_blank"
className="text-[#8D8D8D] hover:text-[#6D6D6D]" className="text-foreground hover:text-foreground/80"
> >
<MessagesSquare className="h-6 w-6" /> <MessagesSquare className="h-6 w-6" />
</Link> </Link>

View File

@ -19,7 +19,7 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24"> <h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
@ -30,41 +30,53 @@ export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplat
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8"> <div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient> <Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]"> <CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6"> <p className="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
<strong className="block">Open Source or Hosted.</strong> <strong className="block">Open Source or Hosted.</strong>
Its up to you. Either clone our repository or rely on our easy to use hosting Its up to you. Either clone our repository or rely on our easy to use hosting
solution. solution.
</p> </p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6"> <div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardOpenFigure} alt="its fast" className="max-w-[80%] lg:max-w-full" /> <Image
src={cardOpenFigure}
alt="its fast"
className="max-w-[80%] dark:contrast-[70%] dark:hue-rotate-180 dark:invert lg:max-w-full"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Build on top.</strong> <strong className="block">Build on top.</strong>
Make it your own through advanced customization and adjustability. Make it your own through advanced customization and adjustability.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardBuildFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardBuildFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Template Store (Soon).</strong> <strong className="block">Template Store (Soon).</strong>
Choose a template from the community app store. Or submit your own template for others Choose a template from the community app store. Or submit your own template for others
to use. to use.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardTemplateFigure} alt="its fast" className="w-full max-w-sm" /> <Image
src={cardTemplateFigure}
alt="its fast"
className="w-full max-w-sm dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -41,10 +41,13 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<AnimatePresence> <AnimatePresence>
<motion.button <motion.button
key="MONTHLY" key="MONTHLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', { className={cn(
'text-slate-900': period === 'MONTHLY', 'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
'hover:text-slate-900/80': period !== 'MONTHLY', {
})} 'text-foreground': period === 'MONTHLY',
'hover:text-foreground/80': period !== 'MONTHLY',
},
)}
onClick={() => setPeriod('MONTHLY')} onClick={() => setPeriod('MONTHLY')}
> >
Monthly Monthly
@ -58,14 +61,17 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<motion.button <motion.button
key="YEARLY" key="YEARLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', { className={cn(
'text-slate-900': period === 'YEARLY', 'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
'hover:text-slate-900/80': period !== 'YEARLY', {
})} 'text-foreground': period === 'YEARLY',
'hover:text-foreground/80': period !== 'YEARLY',
},
)}
onClick={() => setPeriod('YEARLY')} onClick={() => setPeriod('YEARLY')}
> >
Yearly Yearly
<div className="block rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-700"> <div className="bg-muted text-foreground block rounded-full px-2 py-0.5 text-xs">
Save $60 Save $60
</div> </div>
{period === 'YEARLY' && ( {period === 'YEARLY' && (
@ -81,12 +87,12 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 md:grid-cols-2 lg:grid-cols-3"> <div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 md:grid-cols-2 lg:grid-cols-3">
<div <div
data-plan="self-hosted" data-plan="self-hosted"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5" className="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
> >
<p className="text-4xl font-medium text-slate-900">Self Hosted</p> <p className="text-foreground text-4xl font-medium">Self Hosted</p>
<p className="text-primary mt-2.5 text-xl font-medium">Free</p> <p className="text-primary mt-2.5 text-xl font-medium">Free</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900"> <p className="text-foreground mt-4 max-w-[30ch] text-center">
For small teams and individuals who need a simple solution For small teams and individuals who need a simple solution
</p> </p>
@ -100,20 +106,20 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</Link> </Link>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Host your own instance</p> <p className="text-foreground py-4 font-medium">Host your own instance</p>
<p className="py-4 text-slate-900">Full Control</p> <p className="text-foreground py-4">Full Control</p>
<p className="py-4 text-slate-900">Customizability</p> <p className="text-foreground py-4">Customizability</p>
<p className="py-4 text-slate-900">Docker Ready</p> <p className="text-foreground py-4">Docker Ready</p>
<p className="py-4 text-slate-900">Community Support</p> <p className="text-foreground py-4">Community Support</p>
<p className="py-4 text-slate-900">Free, Forever</p> <p className="text-foreground py-4">Free, Forever</p>
</div> </div>
</div> </div>
<div <div
data-plan="community" data-plan="community"
className="border-primary flex flex-col items-center justify-center rounded-lg border-2 bg-white px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380] shadow-slate-900/5" className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
> >
<p className="text-4xl font-medium text-slate-900">Community</p> <p className="text-foreground text-4xl font-medium">Community</p>
<div className="text-primary mt-2.5 text-xl font-medium"> <div className="text-primary mt-2.5 text-xl font-medium">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>} {period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
@ -121,7 +127,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</AnimatePresence> </AnimatePresence>
</div> </div>
<p className="mt-4 max-w-[30ch] text-center text-slate-900"> <p className="text-foreground mt-4 max-w-[30ch] text-center">
For fast-growing companies that aim to scale across multiple teams. For fast-growing companies that aim to scale across multiple teams.
</p> </p>
@ -130,25 +136,25 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</ClaimPlanDialog> </ClaimPlanDialog>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Documenso Early Adopter Deal:</p> <p className="text-foreground py-4 font-medium">Documenso Early Adopter Deal:</p>
<p className="py-4 text-slate-900">Join the movement</p> <p className="text-foreground py-4">Join the movement</p>
<p className="py-4 text-slate-900">Simple signing solution</p> <p className="text-foreground py-4">Simple signing solution</p>
<p className="py-4 text-slate-900">Email and Slack assistance</p> <p className="text-foreground py-4">Email and Slack assistance</p>
<p className="py-4 text-slate-900"> <p className="text-foreground py-4">
<strong>Includes all upcoming features</strong> <strong>Includes all upcoming features</strong>
</p> </p>
<p className="py-4 text-slate-900">Fixed, straightforward pricing</p> <p className="text-foreground py-4">Fixed, straightforward pricing</p>
</div> </div>
</div> </div>
<div <div
data-plan="enterprise" data-plan="enterprise"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5" className="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
> >
<p className="text-4xl font-medium text-slate-900">Enterprise</p> <p className="text-foreground text-4xl font-medium">Enterprise</p>
<p className="text-primary mt-2.5 text-xl font-medium">Pricing on request</p> <p className="text-primary mt-2.5 text-xl font-medium">Pricing on request</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900"> <p className="text-foreground mt-4 max-w-[30ch] text-center">
For large organizations that need extra flexibility and control. For large organizations that need extra flexibility and control.
</p> </p>
@ -162,12 +168,12 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</Link> </Link>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Everything in Community, plus:</p> <p className="text-foreground py-4 font-medium">Everything in Community, plus:</p>
<p className="py-4 text-slate-900">Custom Subdomain</p> <p className="text-foreground py-4">Custom Subdomain</p>
<p className="py-4 text-slate-900">Compliance Check</p> <p className="text-foreground py-4">Compliance Check</p>
<p className="py-4 text-slate-900">Guaranteed Uptime</p> <p className="text-foreground py-4">Guaranteed Uptime</p>
<p className="py-4 text-slate-900">Reporting & Analysis</p> <p className="text-foreground py-4">Reporting & Analysis</p>
<p className="py-4 text-slate-900">24/7 Support</p> <p className="text-foreground py-4">24/7 Support</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -23,7 +23,7 @@ export const ShareConnectPaidWidgetBento = ({
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]" className="h-full scale-125 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
/> />
</div> </div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24"> <h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
@ -34,54 +34,70 @@ export const ShareConnectPaidWidgetBento = ({
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8"> <div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2 lg:col-span-1" degrees={120} gradient> <Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Easy Sharing (Soon).</strong> <strong className="block">Easy Sharing (Soon).</strong>
Receive your personal link to share with everyone you care about. Receive your personal link to share with everyone you care about.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardSharingFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardSharingFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Connections (Soon).</strong> <strong className="block">Connections (Soon).</strong>
Create connections and automations with Zapier and more to integrate with your Create connections and automations with Zapier and more to integrate with your
favorite tools. favorite tools.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardConnectionsFigure} alt="its fast" className="w-full max-w-sm" /> <Image
src={cardConnectionsFigure}
alt="its fast"
className="w-full max-w-sm dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">Get paid (Soon).</strong> <strong className="block">Get paid (Soon).</strong>
Integrated payments with stripe so you dont have to worry about getting paid. Integrated payments with stripe so you dont have to worry about getting paid.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardPaidFigure} alt="its fast" className="w-full max-w-[14rem]" /> <Image
src={cardPaidFigure}
alt="its fast"
className="w-full max-w-[14rem] dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-2 lg:col-span-1" spotlight> <Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6"> <CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]"> <p className="text-foreground/80 leading-relaxed">
<strong className="block">React Widget (Soon).</strong> <strong className="block">React Widget (Soon).</strong>
Easily embed Documenso into your product. Simply copy and paste our react widget into Easily embed Documenso into your product. Simply copy and paste our react widget into
your application. your application.
</p> </p>
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<Image src={cardWidgetFigure} alt="its fast" className="w-full max-w-xs" /> <Image
src={cardWidgetFigure}
alt="its fast"
className="w-full max-w-xs dark:contrast-[70%] dark:hue-rotate-180 dark:invert"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -0,0 +1,234 @@
'use server';
import { createElement } from 'react';
import { DateTime } from 'luxon';
import { PDFDocument } from 'pdf-lib';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
import { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { prisma } from '@documenso/prisma';
import {
DocumentDataType,
DocumentStatus,
FieldType,
Prisma,
ReadStatus,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
const ZCreateSinglePlayerDocumentSchema = z.object({
documentData: z.object({
data: z.string(),
type: z.nativeEnum(DocumentDataType),
}),
documentName: z.string(),
signer: z.object({
email: z.string().email().min(1),
name: z.string(),
signature: z.string(),
}),
fields: z.array(
z.object({
page: z.number(),
type: z.nativeEnum(FieldType),
positionX: z.number(),
positionY: z.number(),
width: z.number(),
height: z.number(),
}),
),
});
export type TCreateSinglePlayerDocumentSchema = z.infer<typeof ZCreateSinglePlayerDocumentSchema>;
/**
* Create and self signs a document.
*
* Returns the document token.
*/
export const createSinglePlayerDocument = async (
value: TCreateSinglePlayerDocumentSchema,
): Promise<string> => {
const { signer, fields, documentData, documentName } =
ZCreateSinglePlayerDocumentSchema.parse(value);
const document = await getFile({
data: documentData.data,
type: documentData.type,
});
const doc = await PDFDocument.load(document);
const createdAt = new Date();
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
const signatureImageAsBase64 = isBase64 ? signer.signature : null;
const typedSignature = !isBase64 ? signer.signature : null;
// Update the document with the fields inserted.
for (const field of fields) {
const isSignatureField = field.type === FieldType.SIGNATURE;
await insertFieldInPDF(doc, {
...mapField(field, signer),
Signature: isSignatureField
? {
created: createdAt,
signatureImageAsBase64,
typedSignature,
// Dummy data.
id: -1,
recipientId: -1,
fieldId: -1,
}
: null,
// Dummy data.
id: -1,
documentId: -1,
recipientId: -1,
});
}
const pdfBytes = await doc.save();
const documentToken = await prisma.$transaction(
async (tx) => {
const documentToken = alphaid();
// Fetch service user who will be the owner of the document.
const serviceUser = await tx.user.findFirstOrThrow({
where: {
email: SERVICE_USER_EMAIL,
},
});
const documentDataBytes = Buffer.from(pdfBytes).toString('base64');
const { id: documentDataId } = await tx.documentData.create({
data: {
type: DocumentDataType.BYTES_64,
data: documentDataBytes,
initialData: documentDataBytes,
},
});
// Create document.
const document = await tx.document.create({
data: {
title: documentName,
status: DocumentStatus.COMPLETED,
documentDataId,
userId: serviceUser.id,
createdAt,
},
});
// Create recipient.
const recipient = await tx.recipient.create({
data: {
documentId: document.id,
name: signer.name,
email: signer.email,
token: documentToken,
signedAt: createdAt,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
});
// Create fields and signatures.
await Promise.all(
fields.map(async (field) => {
const insertedField = await tx.field.create({
data: {
documentId: document.id,
recipientId: recipient.id,
...mapField(field, signer),
},
});
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await tx.signature.create({
data: {
fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
recipientId: recipient.id,
},
});
}
}),
);
return documentToken;
},
{
maxWait: 5000,
timeout: 30000,
},
);
// Todo: Handle `downloadLink`
const template = createElement(DocumentSelfSignedEmailTemplate, {
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
documentName: documentName,
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
});
// Send email to signer.
await mailer.sendMail({
to: {
address: signer.email,
name: signer.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document signed',
html: render(template),
text: render(template, { plainText: true }),
});
return documentToken;
};
/**
* Map the fields provided by the user to fields compatible with Prisma.
*
* Signature fields are handled separately.
*
* @param field The field passed in by the user.
* @param signer The details of the person who is signing this document.
* @returns A field compatible with Prisma.
*/
const mapField = (
field: TCreateSinglePlayerDocumentSchema['fields'][number],
signer: TCreateSinglePlayerDocumentSchema['signer'],
) => {
const customText = match(field.type)
.with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a'))
.with(FieldType.EMAIL, () => signer.email)
.with(FieldType.NAME, () => signer.name)
.otherwise(() => '');
return {
type: field.type,
page: field.page,
positionX: new Prisma.Decimal(field.positionX),
positionY: new Prisma.Decimal(field.positionY),
width: new Prisma.Decimal(field.width),
height: new Prisma.Decimal(field.height),
customText,
inserted: true,
};
};

View File

@ -0,0 +1,133 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { Share } from 'lucide-react';
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 { 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';
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';
interface SinglePlayerModeSuccessProps {
className?: string;
document: DocumentWithRecipient;
}
export const SinglePlayerModeSuccess = ({ className, document }: 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);
};
useEffect(() => {
window.scrollTo({ top: 0 });
}, []);
return (
<div className="flex min-h-[calc(100vh-10rem)] flex-col items-center justify-center sm:min-h-[calc(100vh-13rem)]">
{isConfettiEnabled && (
<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">
You have signed
<span className="mt-2 block">{document.title}</span>
</h2>
<SigningCard3D
className="mt-8"
name={document.Recipient.name || document.Recipient.email}
signingCelebrationImage={signingCelebration}
/>
<div className="mt-8 w-full">
<div className={cn('flex flex-col items-center', className)}>
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
{/* TODO: Hook this up */}
<Button variant="outline" className="flex-1" disabled>
<Share className="mr-2 h-5 w-5" />
Share
</Button>
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={document.documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
<Button
onClick={async () => onShowDocumentClick()}
loading={isFetchingDocumentFile}
className="col-span-2"
>
Show document
</Button>
</div>
</div>
</div>
<p className="text-muted-foreground/60 mt-16 text-center text-sm">
Create a{' '}
<Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>
free account
</Link>{' '}
to access your signed documents at any time
</p>
<DocumentDialog
document={documentFile ?? ''}
open={showDocumentDialog}
onOpenChange={setShowDocumentDialog}
/>
</div>
);
};

View File

@ -181,16 +181,16 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
{...props} {...props}
> >
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8"> <div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">
<div className="col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed text-[#727272] lg:col-span-7"> <div className="text-muted-foreground col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed lg:col-span-7">
{children} {children}
</div> </div>
<form <form
className="col-span-12 flex flex-col rounded-2xl bg-[#F7F7F7] p-6 lg:col-span-5" className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3> <h3 className="text-2xl font-semibold">Sign up for the community plan</h3>
<p className="mt-2 text-xs text-[#AFAFAF]"> <p className="text-muted-foreground mt-2 text-xs">
with Timur Ercan & Lucas Smith from Documenso with Timur Ercan & Lucas Smith from Documenso
</p> </p>
@ -198,7 +198,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<AnimatePresence> <AnimatePresence>
<motion.div key="email"> <motion.div key="email">
<label htmlFor="email" className="text-lg font-semibold text-slate-900 lg:text-xl"> <label htmlFor="email" className="text-foreground text-lg font-semibold lg:text-xl">
Whats your email? Whats your email?
</label> </label>
@ -211,7 +211,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
id="email" id="email"
type="email" type="email"
placeholder="" placeholder=""
className="w-full bg-white pr-16" className="bg-background w-full pr-16"
disabled={isSubmitting} disabled={isSubmitting}
onKeyDown={(e) => onKeyDown={(e) =>
field.value !== '' && field.value !== '' &&
@ -255,7 +255,10 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
transform: 'translateX(25%)', transform: 'translateX(25%)',
}} }}
> >
<label htmlFor="name" className="text-lg font-semibold text-slate-900 lg:text-xl"> <label
htmlFor="name"
className="text-foreground text-lg font-semibold lg:text-xl"
>
and your name? and your name?
</label> </label>
@ -268,7 +271,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
id="name" id="name"
type="text" type="text"
placeholder="" placeholder=""
className="w-full bg-white pr-16" className="bg-background w-full pr-16"
disabled={isSubmitting} disabled={isSubmitting}
onKeyDown={(e) => onKeyDown={(e) =>
field.value !== '' && field.value !== '' &&
@ -300,11 +303,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<div className="mt-12 flex-1" /> <div className="mt-12 flex-1" />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs text-[#AFAFAF]">{stepsRemaining} step(s) until signed</p> <p className="text-muted-foreground text-xs">{stepsRemaining} step(s) until signed</p>
<p className="block text-xs text-[#AFAFAF] md:hidden">Minimise contract</p> <p className="text-muted-foreground block text-xs md:hidden">Minimise contract</p>
</div> </div>
<div className="relative mt-2.5 h-[2px] w-full bg-[#E9E9E9]"> <div className="bg-background relative mt-2.5 h-[2px] w-full">
<div <div
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', { className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
'w-1/3': stepsRemaining === 3, 'w-1/3': stepsRemaining === 3,
@ -322,13 +325,17 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
> >
<div className="flex h-28 items-center justify-center pb-6"> <div className="flex h-28 items-center justify-center pb-6">
{!signatureText && signatureDataUrl && ( {!signatureText && signatureDataUrl && (
<img src={signatureDataUrl} alt="user signature" className="h-full" /> <img
src={signatureDataUrl}
alt="user signature"
className="h-full dark:invert"
/>
)} )}
{signatureText && ( {signatureText && (
<p <p
className={cn( className={cn(
'text-4xl font-semibold text-slate-900 [font-family:var(--font-caveat)]', 'text-foreground text-4xl font-semibold [font-family:var(--font-caveat)]',
)} )}
> >
{signatureText} {signatureText}
@ -342,7 +349,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
> >
<Input <Input
id="signatureText" id="signatureText"
className="border-none p-0 text-sm text-slate-700 placeholder:text-[#D6D6D6] focus-visible:ring-0" className="text-foreground placeholder:text-muted-foreground border-none p-0 text-sm focus-visible:ring-0"
placeholder="Draw or type name here" placeholder="Draw or type name here"
disabled={isSubmitting} disabled={isSubmitting}
{...register('signatureText', { {...register('signatureText', {
@ -356,7 +363,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<Button <Button
type="submit" type="submit"
className="h-8 disabled:bg-[#ECEEED] disabled:text-[#C6C6C6] disabled:hover:bg-[#ECEEED]" className="disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted h-8"
disabled={!isValid || isSubmitting} disabled={!isValid || isSubmitting}
> >
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />} {isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}

View File

@ -0,0 +1,7 @@
import handlerFeatureFlagAll from '@documenso/lib/server-only/feature-flags/all';
export const config = {
runtime: 'edge',
};
export default handlerFeatureFlagAll;

View File

@ -0,0 +1,7 @@
import handlerFeatureFlagGet from '@documenso/lib/server-only/feature-flags/get';
export const config = {
runtime: 'edge',
};
export default handlerFeatureFlagGet;

View File

@ -0,0 +1,10 @@
'use client';
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@ -0,0 +1,39 @@
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import posthog from 'posthog-js';
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
export function PostHogPageview() {
const postHogConfig = extractPostHogConfig();
const pathname = usePathname();
const searchParams = useSearchParams();
if (typeof window !== 'undefined' && postHogConfig) {
posthog.init(postHogConfig.key, {
api_host: postHogConfig.host,
disable_session_recording: true,
});
}
useEffect(() => {
if (!postHogConfig || !pathname) {
return;
}
let url = window.origin + pathname;
if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture('$pageview', {
$current_url: url,
});
}, [pathname, searchParams, postHogConfig]);
return null;
}

BIN
apps/web/example/cert.p12 Normal file

Binary file not shown.

View File

@ -2,7 +2,7 @@
const path = require('path'); const path = require('path');
const { version } = require('./package.json'); const { version } = require('./package.json');
const { parsed: env } = require('dotenv').config({ require('dotenv').config({
path: path.join(__dirname, '../../.env.local'), path: path.join(__dirname, '../../.env.local'),
}); });
@ -22,6 +22,7 @@ const config = {
], ],
env: { env: {
APP_VERSION: version, APP_VERSION: version,
NEXT_PUBLIC_PROJECT: 'web',
}, },
modularizeImports: { modularizeImports: {
'lucide-react': { 'lucide-react': {

View File

@ -8,6 +8,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs" "copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
}, },
"dependencies": { "dependencies": {
@ -21,10 +22,10 @@
"@tanstack/react-query": "^4.29.5", "@tanstack/react-query": "^4.29.5",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"lucide-react": "^0.214.0", "lucide-react": "^0.279.0",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "13.4.12", "next": "13.4.19",
"next-auth": "4.22.3", "next-auth": "4.22.3",
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 629 B

View File

@ -55,21 +55,18 @@ export const EditDocumentForm = ({
title: 'Add Signers', title: 'Add Signers',
description: 'Add the people who will sign the document.', description: 'Add the people who will sign the document.',
stepIndex: 1, stepIndex: 1,
onSubmit: () => onAddSignersFormSubmit,
}, },
fields: { fields: {
title: 'Add Fields', title: 'Add Fields',
description: 'Add all relevant fields for each recipient.', description: 'Add all relevant fields for each recipient.',
stepIndex: 2, stepIndex: 2,
onBackStep: () => setStep('signers'), onBackStep: () => setStep('signers'),
onSubmit: () => onAddFieldsFormSubmit,
}, },
subject: { subject: {
title: 'Add Subject', title: 'Add Subject',
description: 'Add the subject and message you wish to send to signers.', description: 'Add the subject and message you wish to send to signers.',
stepIndex: 3, stepIndex: 3,
onBackStep: () => setStep('fields'), onBackStep: () => setStep('fields'),
onSubmit: () => onAddSubjectFormSubmit,
}, },
}; };
@ -169,6 +166,7 @@ export const EditDocumentForm = ({
{step === 'signers' && ( {step === 'signers' && (
<AddSignersFormPartial <AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers} documentFlow={documentFlow.signers}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}
@ -179,6 +177,7 @@ export const EditDocumentForm = ({
{step === 'fields' && ( {step === 'fields' && (
<AddFieldsFormPartial <AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields} documentFlow={documentFlow.fields}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}

View File

@ -7,7 +7,11 @@ import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client'; import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type DataTableActionButtonProps = { export type DataTableActionButtonProps = {
row: Document & { row: Document & {
@ -18,11 +22,16 @@ export type DataTableActionButtonProps = {
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const { data: session } = useSession(); const { data: session } = useSession();
const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
if (!session) { if (!session) {
return null; return null;
} }
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
trpc.shareLink.createOrGetShareLink.useMutation();
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id; const isOwner = row.User.id === session.user.id;
@ -32,6 +41,20 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const isComplete = row.status === DocumentStatus.COMPLETED; const isComplete = row.status === DocumentStatus.COMPLETED;
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const onShareClick = async () => {
const { slug } = await createOrGetShareLink({
token: recipient?.token,
documentId: row.id,
});
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
toast({
title: 'Copied to clipboard',
description: 'The sharing link has been copied to your clipboard.',
});
};
return match({ return match({
isOwner, isOwner,
isRecipient, isRecipient,
@ -57,8 +80,8 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
</Button> </Button>
)) ))
.otherwise(() => ( .otherwise(() => (
<Button className="w-24" disabled> <Button className="w-24" loading={isCreatingShareLink} onClick={onShareClick}>
<Share className="-ml-1 mr-2 h-4 w-4" /> {!isCreatingShareLink && <Share className="-ml-1 mr-2 h-4 w-4" />}
Share Share
</Button> </Button>
)); ));

View File

@ -7,6 +7,7 @@ import {
Download, Download,
Edit, Edit,
History, History,
Loader,
MoreHorizontal, MoreHorizontal,
Pencil, Pencil,
Share, Share,
@ -18,7 +19,8 @@ import { useSession } from 'next-auth/react';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc } from '@documenso/trpc/client'; import { trpc as trpcClient } from '@documenso/trpc/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -26,6 +28,9 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu'; } from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type DataTableActionDropdownProps = { export type DataTableActionDropdownProps = {
row: Document & { row: Document & {
@ -36,11 +41,16 @@ export type DataTableActionDropdownProps = {
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
const { data: session } = useSession(); const { data: session } = useSession();
const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
if (!session) { if (!session) {
return null; return null;
} }
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
trpcReact.shareLink.createOrGetShareLink.useMutation();
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id; const isOwner = row.User.id === session.user.id;
@ -50,15 +60,29 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
const isComplete = row.status === DocumentStatus.COMPLETED; const isComplete = row.status === DocumentStatus.COMPLETED;
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const onShareClick = async () => {
const { slug } = await createOrGetShareLink({
token: recipient?.token,
documentId: row.id,
});
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
toast({
title: 'Copied to clipboard',
description: 'The sharing link has been copied to your clipboard.',
});
};
const onDownloadClick = async () => { const onDownloadClick = async () => {
let document: DocumentWithData | null = null; let document: DocumentWithData | null = null;
if (!recipient) { if (!recipient) {
document = await trpc.document.getDocumentById.query({ document = await trpcClient.document.getDocumentById.query({
id: row.id, id: row.id,
}); });
} else { } else {
document = await trpc.document.getDocumentByToken.query({ document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token, token: recipient.token,
}); });
} }
@ -88,7 +112,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<MoreHorizontal className="h-5 w-5 text-gray-500" /> <MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount> <DropdownMenuContent className="w-52" align="start" forceMount>
@ -135,8 +159,12 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
Resend Resend
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem disabled> <DropdownMenuItem onClick={onShareClick}>
<Share className="mr-2 h-4 w-4" /> {isCreatingShareLink ? (
<Loader className="mr-2 h-4 w-4" />
) : (
<Share className="mr-2 h-4 w-4" />
)}
Share Share
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -92,8 +92,8 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
</DataTable> </DataTable>
{isPending && ( {isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50"> <div className="bg-background/50 absolute inset-0 flex items-center justify-center">
<Loader className="h-8 w-8 animate-spin text-gray-500" /> <Loader className="text-muted-foreground h-8 w-8 animate-spin" />
</div> </div>
)} )}
</div> </div>

View File

@ -0,0 +1,50 @@
import { Bird, CheckCircle2 } from 'lucide-react';
import { match } from 'ts-pattern';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export type EmptyDocumentProps = { status: ExtendedDocumentStatus };
export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => {
const {
title,
message,
icon: Icon,
} = match(status)
.with(ExtendedDocumentStatus.COMPLETED, () => ({
title: 'Nothing to do',
message:
'There are no completed documents yet. Documents that you have created or received that become completed will appear here later.',
icon: CheckCircle2,
}))
.with(ExtendedDocumentStatus.DRAFT, () => ({
title: 'No active drafts',
message:
'There are no active drafts at then current moment. You can upload a document to start drafting.',
icon: CheckCircle2,
}))
.with(ExtendedDocumentStatus.ALL, () => ({
title: "We're all empty",
message:
'You have not yet created or received any documents. To create a document please upload one.',
icon: Bird,
}))
.otherwise(() => ({
title: 'Nothing to do',
message:
'All documents are currently actioned. Any new documents are sent or recieved they will start to appear here.',
icon: CheckCircle2,
}));
return (
<div className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4">
<Icon className="h-12 w-12" strokeWidth={1.5} />
<div className="text-center">
<h3 className="text-lg font-semibold">{title}</h3>
<p className="mt-2 max-w-[60ch]">{message}</p>
</div>
</div>
);
};

View File

@ -12,6 +12,7 @@ import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/ty
import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentsDataTable } from './data-table'; import { DocumentsDataTable } from './data-table';
import { EmptyDocumentState } from './empty-state';
import { UploadDocument } from './upload-document'; import { UploadDocument } from './upload-document';
export type DocumentsPageProps = { export type DocumentsPageProps = {
@ -62,41 +63,44 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
<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">
<UploadDocument /> <UploadDocument />
<h1 className="mt-12 text-4xl font-semibold">Documents</h1> <div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<h1 className="text-4xl font-semibold">Documents</h1>
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6"> <div className="flex flex-wrap gap-x-4 gap-y-6 overflow-hidden">
<Tabs defaultValue={status} className="overflow-x-auto"> <Tabs defaultValue={status} className="overflow-x-auto">
<TabsList> <TabsList>
{[ {[
ExtendedDocumentStatus.INBOX, ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING, ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED, ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT, ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL, ExtendedDocumentStatus.ALL,
].map((value) => ( ].map((value) => (
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild> <TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
<Link href={getTabHref(value)} scroll={false}> <Link href={getTabHref(value)} scroll={false}>
<DocumentStatus status={value} /> <DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && ( {value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 hidden opacity-50 md:inline-block"> <span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats[value], 99)} {Math.min(stats[value], 99)}
{stats[value] > 99 && '+'} {stats[value] > 99 && '+'}
</span> </span>
)} )}
</Link> </Link>
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>
</Tabs> </Tabs>
<div className="flex flex-1 flex-wrap items-center justify-between gap-x-2 gap-y-4"> <div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector /> <PeriodSelector />
</div>
</div> </div>
</div> </div>
<div className="mt-8"> <div className="mt-8">
<DocumentsDataTable results={results} /> {results.count > 0 && <DocumentsDataTable results={results} />}
{results.count === 0 && <EmptyDocumentState status={status} />}
</div> </div>
</div> </div>
); );

View File

@ -67,7 +67,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
<DocumentDropzone className="min-h-[40vh]" onDrop={onFileDrop} /> <DocumentDropzone className="min-h-[40vh]" onDrop={onFileDrop} />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50"> <div className="bg-background/50 absolute inset-0 flex items-center justify-center">
<Loader className="text-muted-foreground h-12 w-12 animate-spin" /> <Loader className="text-muted-foreground h-12 w-12 animate-spin" />
</div> </div>
)} )}

View File

@ -4,12 +4,12 @@ import { redirect } from 'next/navigation';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer'; import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { SubscriptionStatus } from '@documenso/prisma/client'; import { SubscriptionStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { LocaleDate } from '~/components/formatter/locale-date'; import { LocaleDate } from '~/components/formatter/locale-date';
import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag';
export default async function BillingSettingsPage() { export default async function BillingSettingsPage() {
const user = await getRequiredServerComponentSession(); const user = await getRequiredServerComponentSession();

View File

@ -0,0 +1,153 @@
import { ImageResponse } from 'next/server';
import { P, match } from 'ts-pattern';
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/share/get-recipient-or-sender-by-share-link-slug';
import { Logo } from '~/components/branding/logo';
import { getAssetBuffer } from '~/helpers/get-asset-buffer';
const CARD_OFFSET_TOP = 152;
const CARD_OFFSET_LEFT = 350;
const CARD_WIDTH = 500;
const CARD_HEIGHT = 250;
const size = {
width: 1200,
height: 630,
};
type SharePageOpenGraphImageProps = {
params: { slug: string };
};
export default async function Image({ params: { slug } }: SharePageOpenGraphImageProps) {
const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([
getAssetBuffer('/fonts/inter-semibold.ttf'),
getAssetBuffer('/fonts/inter-regular.ttf'),
getAssetBuffer('/fonts/caveat-regular.ttf'),
getAssetBuffer('/static/og-share-frame.png'),
]);
const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null);
if (!recipientOrSender) {
return null;
}
const isRecipient = 'Signature' in recipientOrSender;
const signatureImage = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => {
return recipient.Signature?.[0]?.signatureImageAsBase64 || null;
})
.otherwise((sender) => {
return sender.signature || null;
});
const signatureName = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => {
return recipient.name || recipient.email;
})
.otherwise((sender) => {
return sender.name || sender.email;
});
return new ImageResponse(
(
<div tw="relative flex h-full w-full">
{/* @ts-expect-error Lack of typing from ImageResponse */}
<img src={shareFrameImage} alt="og-share-frame" tw="absolute inset-0 w-full h-full" />
<div tw="absolute top-20 flex w-full items-center justify-center">
{/* @ts-expect-error Lack of typing from ImageResponse */}
<Logo tw="h-8 w-60" />
</div>
{signatureImage ? (
<div
tw="absolute py-6 px-12 flex items-center justify-center text-center"
style={{
top: `${CARD_OFFSET_TOP}px`,
left: `${CARD_OFFSET_LEFT}px`,
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
}}
>
<img src={signatureImage} alt="signature" tw="opacity-60 h-full max-w-[100%]" />
</div>
) : (
<p
tw="absolute py-6 px-12 -mt-2 flex items-center justify-center text-center text-slate-500"
style={{
fontFamily: 'Caveat',
fontSize: `${Math.max(
Math.min((CARD_WIDTH * 1.5) / signatureName.length, 80),
36,
)}px`,
top: `${CARD_OFFSET_TOP}px`,
left: `${CARD_OFFSET_LEFT}px`,
width: `${CARD_WIDTH}px`,
height: `${CARD_HEIGHT}px`,
}}
>
{signatureName}
</p>
)}
{/* <div
tw="absolute flex items-center justify-center text-slate-500"
style={{
top: `${CARD_OFFSET_TOP + CARD_HEIGHT - 45}px`,
left: `${CARD_OFFSET_LEFT}`,
width: `${CARD_WIDTH}px`,
fontSize: '30px',
}}
>
{signatureName}
</div> */}
<div
tw="absolute flex flex-col items-center justify-center pt-12 w-full"
style={{
top: `${CARD_OFFSET_TOP + CARD_HEIGHT}px`,
}}
>
<h2
tw="text-3xl text-slate-500"
style={{
fontFamily: 'Inter',
fontWeight: 600,
}}
>
{isRecipient
? 'I just signed with Documenso and you can too!'
: 'I just sent a document with Documenso and you can too!'}
</h2>
</div>
</div>
),
{
...size,
fonts: [
{
name: 'Caveat',
data: caveatRegular,
style: 'italic',
},
{
name: 'Inter',
data: interRegular,
style: 'normal',
weight: 400,
},
{
name: 'Inter',
data: interSemiBold,
style: 'normal',
weight: 600,
},
],
},
);
}

View File

@ -0,0 +1,11 @@
import { Metadata } from 'next';
import { Redirect } from './redirect';
export const metadata: Metadata = {
title: 'Documenso - Share',
};
export default function SharePage() {
return <Redirect />;
}

View File

@ -0,0 +1,11 @@
'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

@ -1,17 +1,19 @@
import Link from 'next/link'; import Link from 'next/link';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { CheckCircle2, Clock8, Share } from 'lucide-react'; import { CheckCircle2, Clock8 } from 'lucide-react';
import { match } from 'ts-pattern'; 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 { DocumentStatus, FieldType } from '@documenso/prisma/client'; import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button'; import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { SigningCard } from '@documenso/ui/components/signing-card';
import { DownloadButton } from './download-button'; import signingCelebration from '~/assets/signing-celebration.png';
import { SigningCard } from './signing-card';
import { ShareButton } from './share-button';
export type CompletedSigningPageProps = { export type CompletedSigningPageProps = {
params: { params: {
@ -53,7 +55,7 @@ export default async function CompletedSigningPage({
return ( return (
<div className="flex flex-col items-center pt-24"> <div className="flex flex-col items-center pt-24">
{/* Card with recipient */} {/* Card with recipient */}
<SigningCard name={recipientName} /> <SigningCard name={recipientName} signingCelebrationImage={signingCelebration} />
<div className="mt-6"> <div className="mt-6">
{match(document.status) {match(document.status)
@ -88,13 +90,9 @@ export default async function CompletedSigningPage({
))} ))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4"> <div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
{/* TODO: Hook this up */} <ShareButton documentId={document.id} token={recipient.token} />
<Button variant="outline" className="flex-1">
<Share className="mr-2 h-5 w-5" />
Share
</Button>
<DownloadButton <DocumentDownloadButton
className="flex-1" className="flex-1"
fileName={document.title} fileName={document.title}
documentData={documentData} documentData={documentData}
@ -103,7 +101,7 @@ export default async function CompletedSigningPage({
</div> </div>
<p className="text-muted-foreground/60 mt-36 text-sm"> <p className="text-muted-foreground/60 mt-36 text-sm">
Want so send slick signing links like this one?{' '} Want to send slick signing links like this one?{' '}
<Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600"> <Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600">
Check out Documenso. Check out Documenso.
</Link> </Link>

View File

@ -0,0 +1,51 @@
'use client';
import { HTMLAttributes } from 'react';
import { Share } from 'lucide-react';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type ShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
token: string;
documentId: number;
};
export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
const { toast } = useToast();
const [, copyToClipboard] = useCopyToClipboard();
const { mutateAsync: createOrGetShareLink, isLoading } =
trpc.shareLink.createOrGetShareLink.useMutation();
const onShareClick = async () => {
const { slug } = await createOrGetShareLink({
token: token,
documentId,
});
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
toast({
title: 'Copied to clipboard',
description: 'The sharing link has been copied to your clipboard.',
});
};
return (
<Button
variant="outline"
className="flex-1"
disabled={!token || !documentId}
loading={isLoading}
onClick={onShareClick}
>
{!isLoading && <Share className="mr-2 h-5 w-5" />}
Share
</Button>
);
};

View File

@ -1,67 +0,0 @@
'use client';
import Image from 'next/image';
import { motion } from 'framer-motion';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import signingCelebration from '~/assets/signing-celebration.png';
export type SigningCardProps = {
name: string;
};
export const SigningCard = ({ name }: SigningCardProps) => {
return (
<div className="relative w-full max-w-xs md:max-w-sm">
<Card
className="group mx-auto flex aspect-[21/9] w-full items-center justify-center"
degrees={-145}
gradient
>
<CardContent
className="font-signature p-6 text-center"
style={{
container: 'main',
}}
>
<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>
<motion.div
className="absolute -inset-32 -z-10 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
initial={{
opacity: 0,
scale: 0.8,
}}
animate={{
scale: 1,
opacity: 0.5,
}}
transition={{
delay: 0.5,
duration: 0.5,
}}
>
<Image
src={signingCelebration}
alt="background pattern"
className="w-full"
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>
</div>
);
};

View File

@ -77,7 +77,7 @@ export const DateField = ({ field, recipient }: DateFieldProps) => {
return ( return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}> <SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && ( {isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center"> <div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" /> <Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div> </div>
)} )}

View File

@ -81,7 +81,7 @@ export const EmailField = ({ field, recipient }: EmailFieldProps) => {
return ( return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}> <SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && ( {isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center"> <div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" /> <Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div> </div>
)} )}

View File

@ -1,12 +1,15 @@
'use client'; 'use client';
import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { Document, Field, Recipient } from '@documenso/prisma/client'; import { Document, Field, Recipient } from '@documenso/prisma/client';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
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 { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -27,15 +30,22 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const { const {
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting },
} = useForm(); } = useForm();
const isComplete = fields.every((f) => f.inserted); const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
}, [fields]);
const onFormSubmit = async () => { const onFormSubmit = async () => {
if (!isComplete) { setValidateUninsertedFields(true);
const isFieldsValid = validateFieldsInserted(fields);
if (!isFieldsValid) {
return; return;
} }
@ -54,7 +64,16 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
)} )}
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<div className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}> {validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
Click to insert field
</FieldToolTip>
)}
<fieldset
disabled={isSubmitting}
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
>
<div className={cn('flex flex-1 flex-col')}> <div className={cn('flex flex-1 flex-col')}>
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3> <h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
@ -101,23 +120,18 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10" className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary" variant="secondary"
size="lg" size="lg"
onClick={() => router.back()}
> >
Cancel Cancel
</Button> </Button>
<Button <Button className="w-full" type="submit" size="lg" loading={isSubmitting}>
className="w-full"
type="submit"
size="lg"
disabled={!isComplete || isSubmitting}
>
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
Complete Complete
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</div> </fieldset>
</form> </form>
); );
}; };

View File

@ -100,7 +100,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
return ( return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}> <SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && ( {isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center"> <div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" /> <Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div> </div>
)} )}

View File

@ -38,7 +38,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
}).catch(() => null), }).catch(() => null),
getFieldsForToken({ token }), getFieldsForToken({ token }),
getRecipientByToken({ token }).catch(() => null), getRecipientByToken({ token }).catch(() => null),
viewedDocument({ token }), viewedDocument({ token }).catch(() => null),
]); ]);
if (!document || !document.documentData || !recipient) { if (!document || !document.documentData || !recipient) {

View File

@ -115,7 +115,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
return ( return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}> <SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && ( {isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center"> <div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" /> <Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div> </div>
)} )}
@ -130,7 +130,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
<img <img
src={signature.signatureImageAsBase64} src={signature.signatureImageAsBase64}
alt={`Signature for ${recipient.name}`} alt={`Signature for ${recipient.name}`}
className="h-full w-full object-contain" className="h-full w-full object-contain dark:invert"
/> />
)} )}

View File

@ -3,10 +3,7 @@
import React from 'react'; import React from 'react';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { cn } from '@documenso/ui/lib/utils'; import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { useFieldPageCoords } from '~/hooks/use-field-page-coords';
export type SignatureFieldProps = { export type SignatureFieldProps = {
field: FieldWithSignature; field: FieldWithSignature;
@ -23,8 +20,6 @@ export const SigningFieldContainer = ({
onRemove, onRemove,
children, children,
}: SignatureFieldProps) => { }: SignatureFieldProps) => {
const coords = useFieldPageCoords(field);
const onSignFieldClick = async () => { const onSignFieldClick = async () => {
if (field.inserted) { if (field.inserted) {
return; return;
@ -42,40 +37,25 @@ export const SigningFieldContainer = ({
}; };
return ( return (
<div <FieldRootContainer field={field}>
className="absolute" {!field.inserted && !loading && (
style={{ <button
top: `${coords.y}px`, type="submit"
left: `${coords.x}px`, className="absolute inset-0 z-10 h-full w-full"
height: `${coords.height}px`, onClick={onSignFieldClick}
width: `${coords.width}px`, />
}} )}
>
<Card {field.inserted && !loading && (
className="bg-background relative h-full w-full" <button
data-inserted={field.inserted ? 'true' : 'false'} className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
> onClick={onRemoveSignedFieldClick}
<CardContent
className={cn(
'text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2',
)}
> >
{!field.inserted && !loading && ( Remove
<button type="submit" className="absolute inset-0 z-10" onClick={onSignFieldClick} /> </button>
)} )}
{field.inserted && !loading && ( {children}
<button </FieldRootContainer>
className="text-destructive bg-background/40 absolute inset-0 z-10 flex items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
onClick={onRemoveSignedFieldClick}
>
Remove
</button>
)}
{children}
</CardContent>
</Card>
</div>
); );
}; };

View File

@ -16,7 +16,7 @@ export default function UnauthenticatedLayout({ children }: UnauthenticatedLayou
<Image <Image
src={backgroundPattern} src={backgroundPattern}
alt="background pattern" alt="background pattern"
className="dark:brightness-95 dark:invert dark:sepia" className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
/> />
</div> </div>

View File

@ -2,15 +2,15 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google'; import { Caveat, Inter } from 'next/font/google';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale'; import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale'; import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster'; import { Toaster } from '@documenso/ui/primitives/toaster';
import { TooltipProvider } from '@documenso/ui/primitives/tooltip'; import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
import { getServerComponentAllFlags } from '~/helpers/get-server-component-feature-flag';
import { FeatureFlagProvider } from '~/providers/feature-flag';
import { ThemeProvider } from '~/providers/next-theme'; import { ThemeProvider } from '~/providers/next-theme';
import { PlausibleProvider } from '~/providers/plausible'; import { PlausibleProvider } from '~/providers/plausible';
import { PostHogPageview } from '~/providers/posthog'; import { PostHogPageview } from '~/providers/posthog';
@ -76,6 +76,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</TooltipProvider> </TooltipProvider>
</ThemeProvider> </ThemeProvider>
</PlausibleProvider> </PlausibleProvider>
<Toaster /> <Toaster />
</FeatureFlagProvider> </FeatureFlagProvider>
</LocaleProvider> </LocaleProvider>

View File

@ -0,0 +1,26 @@
import Link from 'next/link';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { Button } from '@documenso/ui/primitives/button';
import NotFoundPartial from '~/components/partials/not-found';
export default async function NotFound() {
const session = await getServerComponentSession();
return (
<NotFoundPartial>
{session && (
<Button className="w-32" asChild>
<Link href="/documents">Documents</Link>
</Button>
)}
{!session && (
<Button className="w-32" asChild>
<Link href="/signin">Sign In</Link>
</Button>
)}
</NotFoundPartial>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

View File

@ -58,7 +58,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)} type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)} fallbackText={recipientAbbreviation(recipient)}
/> />
<span className="text-sm text-gray-500">{recipient.email}</span> <span className="text-muted-foreground text-sm">{recipient.email}</span>
</div> </div>
))} ))}
</div> </div>
@ -75,7 +75,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)} type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)} fallbackText={recipientAbbreviation(recipient)}
/> />
<span className="text-sm text-gray-500">{recipient.email}</span> <span className="text-muted-foreground text-sm">{recipient.email}</span>
</div> </div>
))} ))}
</div> </div>
@ -92,7 +92,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)} type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)} fallbackText={recipientAbbreviation(recipient)}
/> />
<span className="text-sm text-gray-500">{recipient.email}</span> <span className="text-muted-foreground text-sm">{recipient.email}</span>
</div> </div>
))} ))}
</div> </div>
@ -109,7 +109,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)} type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)} fallbackText={recipientAbbreviation(recipient)}
/> />
<span className="text-sm text-gray-500">{recipient.email}</span> <span className="text-muted-foreground text-sm">{recipient.email}</span>
</div> </div>
))} ))}
</div> </div>

View File

@ -10,12 +10,14 @@ import {
User as LucideUser, User as LucideUser,
Monitor, Monitor,
Moon, Moon,
Palette,
Sun, Sun,
UserCog, UserCog,
} from 'lucide-react'; } from 'lucide-react';
import { signOut } from 'next-auth/react'; import { signOut } from 'next-auth/react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter'; import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
import { User } from '@documenso/prisma/client'; import { User } from '@documenso/prisma/client';
@ -26,19 +28,23 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu'; } from '@documenso/ui/primitives/dropdown-menu';
import { useFeatureFlags } from '~/providers/feature-flag';
export type ProfileDropdownProps = { export type ProfileDropdownProps = {
user: User; user: User;
}; };
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const { theme, setTheme } = useTheme();
const { getFlag } = useFeatureFlags(); const { getFlag } = useFeatureFlags();
const { theme, setTheme } = useTheme();
const isUserAdmin = isAdmin(user); const isUserAdmin = isAdmin(user);
const isBillingEnabled = getFlag('app_billing'); const isBillingEnabled = getFlag('app_billing');
@ -98,28 +104,30 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{theme === 'light' ? null : ( <DropdownMenuSub>
<DropdownMenuItem onClick={() => setTheme('light')}> <DropdownMenuSubTrigger>
<Sun className="mr-2 h-4 w-4" /> <Palette className="mr-2 h-4 w-4" />
Light Mode Themes
</DropdownMenuItem> </DropdownMenuSubTrigger>
)} <DropdownMenuPortal>
{theme === 'dark' ? null : ( <DropdownMenuSubContent>
<DropdownMenuItem onClick={() => setTheme('dark')}> <DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<Moon className="mr-2 h-4 w-4" /> <DropdownMenuRadioItem value="light">
Dark Mode <Sun className="mr-2 h-4 w-4" /> Light
</DropdownMenuItem> </DropdownMenuRadioItem>
)} <DropdownMenuRadioItem value="dark">
<Moon className="mr-2 h-4 w-4" />
{theme === 'system' ? null : ( Dark
<DropdownMenuItem onClick={() => setTheme('system')}> </DropdownMenuRadioItem>
<Monitor className="mr-2 h-4 w-4" /> <DropdownMenuRadioItem value="system">
System Theme <Monitor className="mr-2 h-4 w-4" />
</DropdownMenuItem> System
)} </DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href="https://github.com/documenso/documenso" className="cursor-pointer"> <Link href="https://github.com/documenso/documenso" className="cursor-pointer">
<Github className="mr-2 h-4 w-4" /> <Github className="mr-2 h-4 w-4" />

View File

@ -7,11 +7,10 @@ import { usePathname } from 'next/navigation';
import { CreditCard, Key, User } from 'lucide-react'; import { CreditCard, Key, User } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
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 { useFeatureFlags } from '~/providers/feature-flag';
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>; export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {

View File

@ -7,11 +7,10 @@ import { usePathname } from 'next/navigation';
import { CreditCard, Key, User } from 'lucide-react'; import { CreditCard, Key, User } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
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 { useFeatureFlags } from '~/providers/feature-flag';
export type MobileNavProps = HTMLAttributes<HTMLDivElement>; export type MobileNavProps = HTMLAttributes<HTMLDivElement>;
export const MobileNav = ({ className, ...props }: MobileNavProps) => { export const MobileNav = ({ className, ...props }: MobileNavProps) => {

View File

@ -17,17 +17,17 @@ const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
PENDING: { PENDING: {
label: 'Pending', label: 'Pending',
icon: Clock, icon: Clock,
color: 'text-blue-600', color: 'text-blue-600 dark:text-blue-300',
}, },
COMPLETED: { COMPLETED: {
label: 'Completed', label: 'Completed',
icon: CheckCircle2, icon: CheckCircle2,
color: 'text-green-500', color: 'text-green-500 dark:text-green-300',
}, },
DRAFT: { DRAFT: {
label: 'Draft', label: 'Draft',
icon: File, icon: File,
color: 'text-yellow-500', color: 'text-yellow-500 dark:text-yellow-200',
}, },
INBOX: { INBOX: {
label: 'Inbox', label: 'Inbox',

View File

@ -1,6 +1,7 @@
'use server'; 'use server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types'; import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
@ -8,12 +9,20 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
documentId: number; documentId: number;
}; };
export const completeDocument = async ({ documentId }: CompleteDocumentActionInput) => { export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
'use server'; 'use server';
const { id: userId } = await getRequiredServerComponentSession(); const { id: userId } = await getRequiredServerComponentSession();
await sendDocument({ if (email.message || email.subject) {
await upsertDocumentMeta({
documentId,
subject: email.subject,
message: email.message,
});
}
return await sendDocument({
userId, userId,
documentId, documentId,
}); });

View File

@ -1,7 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export * from 'framer-motion';
export const MotionDiv = motion.div;

View File

@ -0,0 +1,66 @@
'use client';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ChevronLeft } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
export type NotFoundPartialProps = {
children?: React.ReactNode;
};
export default function NotFoundPartial({ children }: NotFoundPartialProps) {
const router = useRouter();
return (
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
>
<Image
src={backgroundPattern}
alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
priority
/>
</motion.div>
</div>
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
<div>
<p className="text-muted-foreground font-semibold">404 Page not found</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<p className="text-muted-foreground mt-4 text-sm">
The page you are looking for was moved, removed, renamed or might never have existed.
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
variant="ghost"
className="w-32"
onClick={() => {
void router.back();
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
</Button>
{children}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
/**
* getAssetBuffer is used to retrieve array buffers for various assets
* that are hosted in the `public` folder.
*
* This exists due to a breakage with `import.meta.url` imports and open graph images,
* once we can identify a fix for this, we can remove this helper.
*
* @param path The path to the asset, relative to the `public` folder.
*/
export const getAssetBuffer = async (path: string) => {
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
return fetch(new URL(path, baseUrl)).then(async (res) => res.arrayBuffer());
};

View File

@ -1,44 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import handlerFeatureFlagAll from '@documenso/lib/server-only/feature-flags/all';
import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '~/helpers/get-post-hog-server-client';
import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
export const config = { export const config = {
runtime: 'edge', runtime: 'edge',
}; };
/** export default handlerFeatureFlagAll;
* Get all the evaluated feature flags based on the current user if possible.
*/
export default async function handler(req: Request) {
const requestHeaders = Object.fromEntries(req.headers.entries());
const nextReq = new NextRequest(req, {
headers: requestHeaders,
});
const token = await getToken({ req: nextReq });
const postHog = PostHogServerClient();
// Return the local feature flags if PostHog is not enabled, true by default.
// The front end should not call this API if PostHog is not enabled to reduce network requests.
if (!postHog) {
return NextResponse.json(LOCAL_FEATURE_FLAGS);
}
const distinctId = extractDistinctUserId(token, nextReq);
const featureFlags = await postHog.getAllFlags(distinctId, mapJwtToFlagProperties(token));
const res = NextResponse.json(featureFlags);
res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
return res;
}

View File

@ -1,122 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import handlerFeatureFlagGet from '@documenso/lib/server-only/feature-flags/get';
import { JWT, getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
import { nanoid } from '@documenso/lib/universal/id';
import PostHogServerClient from '~/helpers/get-post-hog-server-client';
export const config = { export const config = {
runtime: 'edge', runtime: 'edge',
}; };
/** export default handlerFeatureFlagGet;
* Evaluate a single feature flag based on the current user if possible.
*
* @param req The request with a query parameter `flag`. Example request URL: /api/feature-flag/get?flag=flag-name
* @returns A Response with the feature flag value.
*/
export default async function handler(req: Request) {
const { searchParams } = new URL(req.url ?? '');
const flag = searchParams.get('flag');
const requestHeaders = Object.fromEntries(req.headers.entries());
const nextReq = new NextRequest(req, {
headers: requestHeaders,
});
const token = await getToken({ req: nextReq });
if (!flag) {
return NextResponse.json(
{
error: 'Missing flag query parameter.',
},
{
status: 400,
headers: {
'content-type': 'application/json',
},
},
);
}
const postHog = PostHogServerClient();
// Return the local feature flags if PostHog is not enabled, true by default.
// The front end should not call this API if PostHog is disabled to reduce network requests.
if (!postHog) {
return NextResponse.json(LOCAL_FEATURE_FLAGS[flag] ?? true);
}
const distinctId = extractDistinctUserId(token, nextReq);
const featureFlag = await postHog.getFeatureFlag(flag, distinctId, mapJwtToFlagProperties(token));
const res = NextResponse.json(featureFlag);
res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
return res;
}
/**
* Map a JWT to properties which are consumed by PostHog to evaluate feature flags.
*
* @param jwt The JWT of the current user.
* @returns A map of properties which are consumed by PostHog.
*/
export const mapJwtToFlagProperties = (
jwt?: JWT | null,
): {
groups?: Record<string, string>;
personProperties?: Record<string, string>;
groupProperties?: Record<string, Record<string, string>>;
} => {
return {
personProperties: {
email: jwt?.email ?? '',
},
groupProperties: {
// Add properties to group users into different groups, such as billing plan.
},
};
};
/**
* Extract a distinct ID from a JWT and request.
*
* Will fallback to a random ID if no ID could be extracted from either the JWT or request.
*
* @param jwt The JWT of the current user.
* @param request Request potentially containing a PostHog `distinct_id` cookie.
* @returns A distinct user ID.
*/
export const extractDistinctUserId = (jwt: JWT | null, request: NextRequest): string => {
const config = extractPostHogConfig();
const email = jwt?.email;
const userId = jwt?.id.toString();
let fallbackDistinctId = nanoid();
if (config) {
try {
const postHogCookie = JSON.parse(
request.cookies.get(`ph_${config.key}_posthog`)?.value ?? '',
);
const postHogDistinctId = postHogCookie['distinct_id'];
if (typeof postHogDistinctId === 'string') {
fallbackDistinctId = postHogDistinctId;
}
} catch {
// Do nothing.
}
}
return email ?? userId ?? fallbackDistinctId;
};

547
package-lock.json generated
View File

@ -22,6 +22,7 @@
"husky": "^8.0.0", "husky": "^8.0.0",
"lint-staged": "^14.0.0", "lint-staged": "^14.0.0",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"rimraf": "^5.0.1",
"turbo": "^1.9.3" "turbo": "^1.9.3"
}, },
"engines": { "engines": {
@ -41,14 +42,16 @@
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",
"contentlayer": "^0.3.4", "contentlayer": "^0.3.4",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"lucide-react": "^0.214.0", "lucide-react": "^0.279.0",
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "13.4.12", "next": "13.4.19",
"next-auth": "4.22.3", "next-auth": "4.22.3",
"next-contentlayer": "^0.3.4", "next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
"posthog-js": "^1.77.3",
"react": "18.2.0", "react": "18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
@ -78,10 +81,10 @@
"@tanstack/react-query": "^4.29.5", "@tanstack/react-query": "^4.29.5",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"lucide-react": "^0.214.0", "lucide-react": "^0.279.0",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "13.4.12", "next": "13.4.19",
"next-auth": "4.22.3", "next-auth": "4.22.3",
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
@ -1873,6 +1876,10 @@
"resolved": "packages/prisma", "resolved": "packages/prisma",
"link": true "link": true
}, },
"node_modules/@documenso/signing": {
"resolved": "packages/signing",
"link": true
},
"node_modules/@documenso/tailwind-config": { "node_modules/@documenso/tailwind-config": {
"resolved": "packages/tailwind-config", "resolved": "packages/tailwind-config",
"link": true "link": true
@ -2444,9 +2451,9 @@
} }
}, },
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "3.1.1", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz",
"integrity": "sha512-tS16bAUkqjITNSvbJuO1x7MXbn7Oe8ZziDTJdA9mMvsoYthnOOiznOTGBYwbdlYBgU+tgpI/BtTU3paRbCuSlg==", "integrity": "sha512-tgK3nWlfFLlqhqpXZmFMP3RN5E7mlbGfnM2h2ILVsW1TNGuFSod0ePW0grlIY2GAbL4pJdtmOT4HQSZsTwOiKg==",
"peerDependencies": { "peerDependencies": {
"react-hook-form": "^7.0.0" "react-hook-form": "^7.0.0"
} }
@ -2528,6 +2535,96 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
}, },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -2746,6 +2843,20 @@
"node-pre-gyp": "bin/node-pre-gyp" "node-pre-gyp": "bin/node-pre-gyp"
} }
}, },
"node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@mdx-js/esbuild": { "node_modules/@mdx-js/esbuild": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@mdx-js/esbuild/-/esbuild-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@mdx-js/esbuild/-/esbuild-2.3.0.tgz",
@ -2818,22 +2929,22 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
"integrity": "sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==" "integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ=="
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.19.tgz",
"integrity": "sha512-6rhK9CdxEgj/j1qvXIyLTWEaeFv7zOK8yJMulz3Owel0uek0U9MJCGzmKgYxM3aAUBo3gKeywCZKyQnJKto60A==", "integrity": "sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ==",
"dependencies": { "dependencies": {
"glob": "7.1.7" "glob": "7.1.7"
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz",
"integrity": "sha512-deUrbCXTMZ6ZhbOoloqecnUeNpUOupi8SE2tx4jPfNS9uyUR9zK4iXBvH65opVcA/9F5I/p8vDXSYbUlbmBjZg==", "integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2846,9 +2957,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz",
"integrity": "sha512-WRvH7RxgRHlC1yb5oG0ZLx8F7uci9AivM5/HGGv9ZyG2Als8Ij64GC3d+mQ5sJhWjusyU6T6V1WKTUoTmOB0zQ==", "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2861,9 +2972,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz",
"integrity": "sha512-YEKracAWuxp54tKiAvvq73PUs9lok57cc8meYRibTWe/VdPB2vLgkTVWFcw31YDuRXdEhdX0fWS6Q+ESBhnEig==", "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2876,9 +2987,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz",
"integrity": "sha512-LhJR7/RAjdHJ2Isl2pgc/JaoxNk0KtBgkVpiDJPVExVWA1c6gzY57+3zWuxuyWzTG+fhLZo2Y80pLXgIJv7g3g==", "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2891,9 +3002,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz",
"integrity": "sha512-1DWLL/B9nBNiQRng+1aqs3OaZcxC16Nf+mOnpcrZZSdyKHek3WQh6j/fkbukObgNGwmCoVevLUa/p3UFTTqgqg==", "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2906,9 +3017,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz",
"integrity": "sha512-kEAJmgYFhp0VL+eRWmUkVxLVunn7oL9Mdue/FS8yzRBVj7Z0AnIrHpTIeIUl1bbdQq1VaoOztnKicAjfkLTRCQ==", "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2921,9 +3032,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz",
"integrity": "sha512-GMLuL/loR6yIIRTnPRY6UGbLL9MBdw2anxkOnANxvLvsml4F0HNIgvnU3Ej4BjbqMTNjD4hcPFdlEow4XHPdZA==", "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2936,9 +3047,9 @@
} }
}, },
"node_modules/@next/swc-win32-ia32-msvc": { "node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz",
"integrity": "sha512-PhgNqN2Vnkm7XaMdRmmX0ZSwZXQAtamBVSa9A/V1dfKQCV1rjIZeiy/dbBnVYGdj63ANfsOR/30XpxP71W0eww==", "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -2951,9 +3062,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.12.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz",
"integrity": "sha512-Z+56e/Ljt0bUs+T+jPjhFyxYBcdY2RIq9ELFU+qAMQMteHo7ymbV7CKmlcX59RI9C4YzN8PgMgLyAoi916b5HA==", "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3646,6 +3757,16 @@
"pako": "^1.0.10" "pako": "^1.0.10"
} }
}, },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@pkgr/utils": { "node_modules/@pkgr/utils": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
@ -6272,9 +6393,9 @@
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
}, },
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.3.1", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz",
"integrity": "sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==", "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==",
"dev": true "dev": true
}, },
"node_modules/@types/mdast": { "node_modules/@types/mdast": {
@ -6306,6 +6427,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==" "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A=="
}, },
"node_modules/@types/node-forge": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.4.tgz",
"integrity": "sha512-08scBQriFsBbm/CuBWOXRMD1RG7ydFW01EDR6vGX27nxcj6E/jGSCOLdICNd8ETwQlLFXVBVA854RX6Y7vPSrQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/nodemailer": { "node_modules/@types/nodemailer": {
"version": "6.4.9", "version": "6.4.9",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
@ -9594,19 +9724,19 @@
} }
}, },
"node_modules/eslint-config-next": { "node_modules/eslint-config-next": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.12.tgz", "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.19.tgz",
"integrity": "sha512-ZF0r5vxKaVazyZH/37Au/XItiG7qUOBw+HaH3PeyXltIMwXorsn6bdrl0Nn9N5v5v9spc+6GM2ryjugbjF6X2g==", "integrity": "sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "13.4.12", "@next/eslint-plugin-next": "13.4.19",
"@rushstack/eslint-patch": "^1.1.3", "@rushstack/eslint-patch": "^1.1.3",
"@typescript-eslint/parser": "^5.42.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0",
"eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2", "eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.31.7", "eslint-plugin-react": "^7.31.7",
"eslint-plugin-react-hooks": "5.0.0-canary-7118f5dd7-20230705" "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^7.23.0 || ^8.0.0", "eslint": "^7.23.0 || ^8.0.0",
@ -10442,6 +10572,20 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/flat-cache/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.2.7", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
@ -10474,6 +10618,34 @@
"is-callable": "^1.1.3" "is-callable": "^1.1.3"
} }
}, },
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@ -11957,6 +12129,24 @@
"ws": "*" "ws": "*"
} }
}, },
"node_modules/jackspeak": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz",
"integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/javascript-natural-sort": { "node_modules/javascript-natural-sort": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
@ -12685,17 +12875,17 @@
} }
}, },
"node_modules/lucide-react": { "node_modules/lucide-react": {
"version": "0.214.0", "version": "0.279.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.214.0.tgz", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.279.0.tgz",
"integrity": "sha512-/vRi1wnFV2lqyIIkghQ3dDLu0eA9zykRQN9GZBwydzv+kB/2Q3S4X6OYB+aRqLXwl438vfVBqyYov2z0LJeoqA==", "integrity": "sha512-LJ8g66+Bxc3t3x9vKTeK3wn3xucrOQGfJ9ou9GsBwCt2offsrT2BB90XrTrIzE1noYYDe2O8jZaRHi6sAHXNxw==",
"peerDependencies": { "peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0" "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.4.0", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.0.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.2.tgz",
"integrity": "sha512-7eDo4Pt7aGhoCheGFIuq4Xa2fJm4ZpmldpGhjTYBNUYNCN6TIEP6v7chwwwt3KRp7YR+rghbfvjyo3V5y9hgBw==", "integrity": "sha512-uBoAVCVcajsrqy3pv7eo5jEUz1oeLmCcnMv8n4AJpT5hbpN9lUssAXibNElpbLce3Mhm9dyBzwYLs9zctM/0tA==",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@ -13981,11 +14171,11 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
}, },
"node_modules/next": { "node_modules/next": {
"version": "13.4.12", "version": "13.4.19",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.12.tgz", "resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz",
"integrity": "sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==", "integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==",
"dependencies": { "dependencies": {
"@next/env": "13.4.12", "@next/env": "13.4.19",
"@swc/helpers": "0.5.1", "@swc/helpers": "0.5.1",
"busboy": "1.6.0", "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406", "caniuse-lite": "^1.0.30001406",
@ -14001,19 +14191,18 @@
"node": ">=16.8.0" "node": ">=16.8.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "13.4.12", "@next/swc-darwin-arm64": "13.4.19",
"@next/swc-darwin-x64": "13.4.12", "@next/swc-darwin-x64": "13.4.19",
"@next/swc-linux-arm64-gnu": "13.4.12", "@next/swc-linux-arm64-gnu": "13.4.19",
"@next/swc-linux-arm64-musl": "13.4.12", "@next/swc-linux-arm64-musl": "13.4.19",
"@next/swc-linux-x64-gnu": "13.4.12", "@next/swc-linux-x64-gnu": "13.4.19",
"@next/swc-linux-x64-musl": "13.4.12", "@next/swc-linux-x64-musl": "13.4.19",
"@next/swc-win32-arm64-msvc": "13.4.12", "@next/swc-win32-arm64-msvc": "13.4.19",
"@next/swc-win32-ia32-msvc": "13.4.12", "@next/swc-win32-ia32-msvc": "13.4.19",
"@next/swc-win32-x64-msvc": "13.4.12" "@next/swc-win32-x64-msvc": "13.4.19"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",
"fibers": ">= 3.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"sass": "^1.3.0" "sass": "^1.3.0"
@ -14022,9 +14211,6 @@
"@opentelemetry/api": { "@opentelemetry/api": {
"optional": true "optional": true
}, },
"fibers": {
"optional": true
},
"sass": { "sass": {
"optional": true "optional": true
} }
@ -14197,11 +14383,30 @@
} }
} }
}, },
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"engines": {
"node": ">= 6.13.0"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.13", "version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
}, },
"node_modules/node-signpdf": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-2.0.0.tgz",
"integrity": "sha512-B6fDvD8z2v0pRntQjgPO2ARqxh0pfNwfSn6YEbP7cv6xmPgcphFwcrMA3N47LztmpVbAM3vUFUslX32L6NRYDg==",
"engines": {
"node": ">=12"
},
"peerDependencies": {
"node-forge": "^1.2.1"
}
},
"node_modules/node-sql-parser": { "node_modules/node-sql-parser": {
"version": "4.9.0", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.9.0.tgz", "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.9.0.tgz",
@ -14753,6 +14958,31 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dev": true,
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
"integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@ -15043,9 +15273,9 @@
} }
}, },
"node_modules/posthog-js": { "node_modules/posthog-js": {
"version": "1.75.3", "version": "1.77.3",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.75.3.tgz", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.77.3.tgz",
"integrity": "sha512-q5xP4R/Tx8E6H0goZQjY+URMLATFiYXc2raHA+31aNvpBs118fPTmExa4RK6MgRZDFhBiMUBZNT6aj7dM3SyUQ==", "integrity": "sha512-DKsGpBIUjQSihhGruEW8wpVCkeDxU4jz7gADdXX2jEWV6bl4WpUPxjo1ukidVDFvvc/ihCM5PQWMQrItexdpSA==",
"dependencies": { "dependencies": {
"fflate": "^0.4.1" "fflate": "^0.4.1"
} }
@ -15559,6 +15789,20 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-confetti": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz",
"integrity": "sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==",
"dependencies": {
"tween-functions": "^1.2.0"
},
"engines": {
"node": ">=10.18"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.1 || ^18.0.0"
}
},
"node_modules/react-day-picker": { "node_modules/react-day-picker": {
"version": "8.8.0", "version": "8.8.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.0.tgz", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.0.tgz",
@ -16089,9 +16333,9 @@
} }
}, },
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.45.2", "version": "7.45.4",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz",
"integrity": "sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A==", "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ==",
"engines": { "engines": {
"node": ">=12.22.0" "node": ">=12.22.0"
}, },
@ -16821,14 +17065,64 @@
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
}, },
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "3.0.2", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==",
"dev": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^10.2.5"
}, },
"bin": { "bin": {
"rimraf": "bin.js" "rimraf": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.5.tgz",
"integrity": "sha512-bYUpUD7XDEHI4Q2O5a7PXGvyw4deKR70kHiDxzQbe925wbZknhOzUt2xBgTkYL6RBcVeXYuD9iNYeqoWbBZQnA==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.0.3",
"minimatch": "^9.0.1",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@ -17439,6 +17733,27 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width/node_modules/emoji-regex": { "node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -17528,6 +17843,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-bom": { "node_modules/strip-bom": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@ -18639,6 +18967,11 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/tween-functions": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
"integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA=="
},
"node_modules/typanion": { "node_modules/typanion": {
"version": "3.13.0", "version": "3.13.0",
"resolved": "https://registry.npmjs.org/typanion/-/typanion-3.13.0.tgz", "resolved": "https://registry.npmjs.org/typanion/-/typanion-3.13.0.tgz",
@ -19290,6 +19623,24 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -19439,7 +19790,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2", "@typescript-eslint/parser": "^5.59.2",
"eslint": "^8.40.0", "eslint": "^8.40.0",
"eslint-config-next": "13.4.12", "eslint-config-next": "13.4.19",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "^1.9.3", "eslint-config-turbo": "^1.9.3",
"eslint-plugin-package-json": "^0.1.4", "eslint-plugin-package-json": "^0.1.4",
@ -19466,7 +19817,7 @@
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"next": "13.4.12", "next": "13.4.19",
"next-auth": "4.22.3", "next-auth": "4.22.3",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"react": "18.2.0", "react": "18.2.0",
@ -19508,6 +19859,20 @@
"typescript": "^5.1.6" "typescript": "^5.1.6"
} }
}, },
"packages/signing": {
"name": "@documenso/signing",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@documenso/tsconfig": "*",
"node-forge": "^1.3.1",
"node-signpdf": "^2.0.0",
"pdf-lib": "^1.17.1"
},
"devDependencies": {
"@types/node-forge": "^1.3.4"
}
},
"packages/tailwind-config": { "packages/tailwind-config": {
"name": "@documenso/tailwind-config", "name": "@documenso/tailwind-config",
"version": "0.0.0", "version": "0.0.0",
@ -19549,6 +19914,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@documenso/lib": "*", "@documenso/lib": "*",
"@hookform/resolvers": "^3.3.0",
"@radix-ui/react-accordion": "^1.1.1", "@radix-ui/react-accordion": "^1.1.1",
"@radix-ui/react-alert-dialog": "^1.0.3", "@radix-ui/react-alert-dialog": "^1.0.3",
"@radix-ui/react-aspect-ratio": "^1.0.2", "@radix-ui/react-aspect-ratio": "^1.0.2",
@ -19569,27 +19935,32 @@
"@radix-ui/react-select": "^1.2.1", "@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-separator": "^1.0.2", "@radix-ui/react-separator": "^1.0.2",
"@radix-ui/react-slider": "^1.1.1", "@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.2", "@radix-ui/react-switch": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.3", "@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toast": "^1.1.3", "@radix-ui/react-toast": "^1.1.3",
"@radix-ui/react-toggle": "^1.0.2", "@radix-ui/react-toggle": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.5", "@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-table": "^8.9.1", "@tanstack/react-table": "^8.9.1",
"class-variance-authority": "^0.6.0", "class-variance-authority": "^0.6.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"lucide-react": "^0.214.0", "lucide-react": "^0.279.0",
"next": "13.4.12", "luxon": "^3.4.2",
"next": "13.4.19",
"pdfjs-dist": "3.6.172", "pdfjs-dist": "3.6.172",
"react-day-picker": "^8.7.1", "react-day-picker": "^8.7.1",
"react-hook-form": "^7.45.4",
"react-pdf": "^7.3.3", "react-pdf": "^7.3.3",
"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"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",
"@documenso/tsconfig": "*", "@documenso/tsconfig": "*",
"@types/luxon": "^3.3.2",
"@types/react": "18.2.18", "@types/react": "18.2.18",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"react": "18.2.0", "react": "18.2.0",

View File

@ -7,7 +7,8 @@
"lint": "turbo run lint", "lint": "turbo run lint",
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"", "format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
"prepare": "husky install", "prepare": "husky install",
"commitlint": "commitlint --edit" "commitlint": "commitlint --edit",
"clean": "turbo run clean && rimraf node_modules"
}, },
"engines": { "engines": {
"npm": ">=8.6.0", "npm": ">=8.6.0",
@ -23,6 +24,7 @@
"husky": "^8.0.0", "husky": "^8.0.0",
"lint-staged": "^14.0.0", "lint-staged": "^14.0.0",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"rimraf": "^5.0.1",
"turbo": "^1.9.3" "turbo": "^1.9.3"
}, },
"name": "@documenso/root", "name": "@documenso/root",

View File

@ -9,7 +9,9 @@
"server-only/", "server-only/",
"universal/" "universal/"
], ],
"scripts": {}, "scripts": {
"clean": "rimraf node_modules"
},
"dependencies": { "dependencies": {
"@documenso/lib": "*", "@documenso/lib": "*",
"@documenso/prisma": "*" "@documenso/prisma": "*"

View File

@ -13,6 +13,7 @@
], ],
"scripts": { "scripts": {
"dev": "email dev --port 3002 --dir templates", "dev": "email dev --port 3002 --dir templates",
"clean": "rimraf node_modules",
"worker:test": "tsup worker/index.ts --format esm" "worker:test": "tsup worker/index.ts --format esm"
}, },
"dependencies": { "dependencies": {

View File

@ -1,17 +1,15 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
export interface TemplateDocumentCompletedProps { export interface TemplateDocumentCompletedProps {
downloadLink: string; downloadLink: string;
reviewLink: string;
documentName: string; documentName: string;
assetBaseUrl: string; assetBaseUrl: string;
} }
export const TemplateDocumentCompleted = ({ export const TemplateDocumentCompleted = ({
downloadLink, downloadLink,
reviewLink,
documentName, documentName,
assetBaseUrl, assetBaseUrl,
}: TemplateDocumentCompletedProps) => { }: TemplateDocumentCompletedProps) => {
@ -29,11 +27,23 @@ export const TemplateDocumentCompleted = ({
}, },
}} }}
> >
<Section className="flex-row items-center justify-center"> <Section>
<div className="flex items-center justify-center p-4"> <Row className="table-fixed">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" /> <Column />
</div>
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]"> <Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" /> <Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Completed Completed
@ -44,17 +54,17 @@ export const TemplateDocumentCompleted = ({
</Text> </Text>
<Text className="my-1 text-center text-base text-slate-400"> <Text className="my-1 text-center text-base text-slate-400">
Continue by downloading or reviewing the document. Continue by downloading the document.
</Text> </Text>
<Section className="mb-6 mt-8 text-center"> <Section className="mb-6 mt-8 text-center">
<Button {/* <Button
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline" className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href={reviewLink} href={reviewLink}
> >
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" /> <Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" />
Review Review
</Button> </Button> */}
<Button <Button
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline" className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href={downloadLink} href={downloadLink}

View File

@ -1,4 +1,4 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
@ -30,13 +30,26 @@ export const TemplateDocumentInvite = ({
}, },
}} }}
> >
<Section className="mt-4 flex-row items-center justify-center"> <Section className="mt-4">
<div className="flex items-center justify-center p-4"> <Row className="table-fixed">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" /> <Column />
</div>
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold"> <Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{inviterName} has invited you to sign "{documentName}" {inviterName} has invited you to sign
<br />"{documentName}"
</Text> </Text>
<Text className="my-1 text-center text-base text-slate-400"> <Text className="my-1 text-center text-base text-slate-400">

View File

@ -1,4 +1,4 @@
import { Img, Section, Tailwind, Text } from '@react-email/components'; import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
@ -25,11 +25,23 @@ export const TemplateDocumentPending = ({
}, },
}} }}
> >
<Section className="flex-row items-center justify-center"> <Section>
<div className="flex items-center justify-center p-4"> <Row className="table-fixed">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" /> <Column />
</div>
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500"> <Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" /> <Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Waiting for others Waiting for others

View File

@ -0,0 +1,69 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
export interface TemplateDocumentSelfSignedProps {
downloadLink: string;
documentName: string;
assetBaseUrl: string;
}
export const TemplateDocumentSelfSigned = ({
downloadLink,
documentName,
assetBaseUrl,
}: TemplateDocumentSelfSignedProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Section className="flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Completed
</Text>
<Text className="text-primary mb-0 text-center text-lg font-semibold">
You have signed {documentName}
</Text>
<Text className="my-1 text-center text-base text-slate-400">
Check out our plans to access the full suite of features.
</Text>
<Section className="mb-6 mt-8 text-center">
<Button
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href="https://documenso.com/pricing"
>
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" />
View plans
</Button>
<Button
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href={downloadLink}
>
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
Download
</Button>
</Section>
</Section>
</Tailwind>
);
};
export default TemplateDocumentSelfSigned;

View File

@ -17,7 +17,7 @@ export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
)} )}
<Text className="my-8 text-sm text-slate-400"> <Text className="my-8 text-sm text-slate-400">
Documenso Documenso, Inc.
<br /> <br />
2261 Market Street, #5211, San Francisco, CA 94114, USA 2261 Market Street, #5211, San Francisco, CA 94114, USA
</Text> </Text>

View File

@ -21,7 +21,6 @@ export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentComple
export const DocumentCompletedEmailTemplate = ({ export const DocumentCompletedEmailTemplate = ({
downloadLink = 'https://documenso.com', downloadLink = 'https://documenso.com',
reviewLink = 'https://documenso.com',
documentName = 'Open Source Pledge.pdf', documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002', assetBaseUrl = 'http://localhost:3002',
}: DocumentCompletedEmailTemplateProps) => { }: DocumentCompletedEmailTemplateProps) => {
@ -56,7 +55,6 @@ export const DocumentCompletedEmailTemplate = ({
<TemplateDocumentCompleted <TemplateDocumentCompleted
downloadLink={downloadLink} downloadLink={downloadLink}
reviewLink={reviewLink}
documentName={documentName} documentName={documentName}
assetBaseUrl={assetBaseUrl} assetBaseUrl={assetBaseUrl}
/> />

View File

@ -20,7 +20,9 @@ import {
} from '../template-components/template-document-invite'; } from '../template-components/template-document-invite';
import TemplateFooter from '../template-components/template-footer'; import TemplateFooter from '../template-components/template-footer';
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps>; export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
customBody?: string;
};
export const DocumentInviteEmailTemplate = ({ export const DocumentInviteEmailTemplate = ({
inviterName = 'Lucas Smith', inviterName = 'Lucas Smith',
@ -28,6 +30,7 @@ export const DocumentInviteEmailTemplate = ({
documentName = 'Open Source Pledge.pdf', documentName = 'Open Source Pledge.pdf',
signDocumentLink = 'https://documenso.com', signDocumentLink = 'https://documenso.com',
assetBaseUrl = 'http://localhost:3002', assetBaseUrl = 'http://localhost:3002',
customBody,
}: DocumentInviteEmailTemplateProps) => { }: DocumentInviteEmailTemplateProps) => {
const previewText = `Completed Document`; const previewText = `Completed Document`;
@ -78,7 +81,11 @@ export const DocumentInviteEmailTemplate = ({
</Text> </Text>
<Text className="mt-2 text-base text-slate-400"> <Text className="mt-2 text-base text-slate-400">
{inviterName} has invited you to sign the document "{documentName}". {customBody ? (
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
) : (
`${inviterName} has invited you to sign the document "${documentName}".`
)}
</Text> </Text>
</Section> </Section>
</Container> </Container>

Some files were not shown because too many files have changed in this diff Show More