Merge remote-tracking branch 'origin/feat/refresh' into feat/single-player-mode
This commit is contained in:
@@ -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,7 +19,7 @@
|
|||||||
"@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.277.0",
|
"lucide-react": "^0.279.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"next-auth": "4.22.3",
|
"next-auth": "4.22.3",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export type MarketingLayoutProps = {
|
|||||||
|
|
||||||
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative max-w-[100vw] 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="bg-background/50 fixed left-0 top-0 z-50 w-full backdrop-blur-md">
|
||||||
<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>
|
||||||
|
|
||||||
|
|||||||
84
apps/marketing/src/app/(marketing)/oss-friends/container.tsx
Normal file
84
apps/marketing/src/app/(marketing)/oss-friends/container.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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:
|
|
||||||
'BoxyHQ’s 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,
|
.then(async (res) => res.json())
|
||||||
transition: {
|
.then(async (data) => z.object({ data: ZOSSFriendsSchema }).parseAsync(data))
|
||||||
staggerChildren: 0.075,
|
.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:invert dark:sepia md:-mr-[50vw] md:scale-150 lg:scale-[175%]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
apps/marketing/src/app/(marketing)/oss-friends/schema.ts
Normal file
11
apps/marketing/src/app/(marketing)/oss-friends/schema.ts
Normal 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>;
|
||||||
@@ -24,10 +24,10 @@ export default function PricingPage() {
|
|||||||
<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"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag'
|
|||||||
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 { 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';
|
||||||
|
|
||||||
@@ -61,9 +62,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<FeatureFlagProvider initialFlags={flags}>
|
<FeatureFlagProvider initialFlags={flags}>
|
||||||
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<PlausibleProvider>{children}</PlausibleProvider>
|
<PlausibleProvider>{children}</PlausibleProvider>
|
||||||
<Toaster />
|
</ThemeProvider>
|
||||||
</FeatureFlagProvider>
|
</FeatureFlagProvider>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
65
apps/marketing/src/app/not-found.tsx
Normal file
65
apps/marketing/src/app/not-found.tsx
Normal 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: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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: '*',
|
userAgent: '*',
|
||||||
allow: '/*',
|
|
||||||
disallow: ['/_next/*'],
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
sitemap: `${getBaseUrl()}/sitemap.xml`,
|
sitemap: `${getBaseUrl()}/sitemap.xml`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -123,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 />
|
||||||
|
|
||||||
@@ -131,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')} />
|
||||||
|
|
||||||
|
|||||||
@@ -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: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. That’s why we care about the smallest detail in
|
Because signing should be celebrated. That’s 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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -27,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>
|
||||||
))}
|
))}
|
||||||
@@ -50,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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
|||||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
|
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
|
||||||
<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>
|
||||||
|
|
||||||
{isSinglePlayerModeMarketingEnabled && (
|
{isSinglePlayerModeMarketingEnabled && (
|
||||||
@@ -38,22 +44,31 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
|||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -86,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:invert dark:sepia md:scale-150 lg:scale-[175%]"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,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 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||||
$30/mo. forever!
|
$30/mo. forever!
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -59,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
|
||||||
@@ -89,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()}
|
||||||
>
|
>
|
||||||
@@ -103,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>
|
||||||
@@ -111,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>
|
||||||
@@ -119,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>
|
||||||
|
|||||||
@@ -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: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>
|
||||||
It’s up to you. Either clone our repository or rely on our easy to use hosting
|
It’s 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: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 don’t have to worry about getting paid.
|
Integrated payments with stripe so you don’t 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>
|
||||||
|
|||||||
@@ -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">
|
||||||
What’s your email?
|
What’s 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" />}
|
||||||
|
|||||||
10
apps/marketing/src/providers/next-theme.tsx
Normal file
10
apps/marketing/src/providers/next-theme.tsx
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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,9 @@
|
|||||||
"@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.277.0",
|
"lucide-react": "^0.279.0",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"next-auth": "4.22.3",
|
"next-auth": "4.22.3",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
|
|||||||
BIN
apps/web/public/fonts/caveat-regular.ttf
Normal file
BIN
apps/web/public/fonts/caveat-regular.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/inter-bold.ttf
Normal file
BIN
apps/web/public/fonts/inter-bold.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/inter-regular.ttf
Normal file
BIN
apps/web/public/fonts/inter-regular.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/inter-semibold.ttf
Normal file
BIN
apps/web/public/fonts/inter-semibold.ttf
Normal file
Binary file not shown.
@@ -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 |
BIN
apps/web/public/static/og-share-frame.png
Normal file
BIN
apps/web/public/static/og-share-frame.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 743 KiB |
@@ -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 |
@@ -166,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}
|
||||||
@@ -176,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}
|
||||||
|
|||||||
@@ -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>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
{isCreatingShareLink ? (
|
||||||
|
<Loader className="mr-2 h-4 w-4" />
|
||||||
|
) : (
|
||||||
<Share className="mr-2 h-4 w-4" />
|
<Share className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
Share
|
Share
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
50
apps/web/src/app/(dashboard)/documents/empty-state.tsx
Normal file
50
apps/web/src/app/(dashboard)/documents/empty-state.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,9 +63,10 @@ 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>
|
||||||
{[
|
{[
|
||||||
@@ -90,13 +92,15 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
153
apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx
Normal file
153
apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
11
apps/web/src/app/(share)/share/[slug]/page.tsx
Normal file
11
apps/web/src/app/(share)/share/[slug]/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
11
apps/web/src/app/(share)/share/[slug]/redirect.tsx
Normal file
11
apps/web/src/app/(share)/share/[slug]/redirect.tsx
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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';
|
||||||
@@ -10,10 +10,11 @@ import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-re
|
|||||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { SigningCard } from '@documenso/ui/components/signing-card';
|
import { SigningCard } from '@documenso/ui/components/signing-card';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import signingCelebration from '~/assets/signing-celebration.png';
|
import signingCelebration from '~/assets/signing-celebration.png';
|
||||||
|
|
||||||
|
import { ShareButton } from './share-button';
|
||||||
|
|
||||||
export type CompletedSigningPageProps = {
|
export type CompletedSigningPageProps = {
|
||||||
params: {
|
params: {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -89,11 +90,7 @@ 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>
|
|
||||||
|
|
||||||
<DocumentDownloadButton
|
<DocumentDownloadButton
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -120,6 +120,7 @@ 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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
26
apps/web/src/app/not-found.tsx
Normal file
26
apps/web/src/app/not-found.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
apps/web/src/assets/background-pattern-og.png
Normal file
BIN
apps/web/src/assets/background-pattern-og.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
BIN
apps/web/src/assets/caveat-regular.ttf
Normal file
BIN
apps/web/src/assets/caveat-regular.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/inter-bold.ttf
Normal file
BIN
apps/web/src/assets/inter-bold.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/inter-regular.ttf
Normal file
BIN
apps/web/src/assets/inter-regular.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/inter-semibold.ttf
Normal file
BIN
apps/web/src/assets/inter-semibold.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/og-share-frame.png
Normal file
BIN
apps/web/src/assets/og-share-frame.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 743 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
User as LucideUser,
|
User as LucideUser,
|
||||||
Monitor,
|
Monitor,
|
||||||
Moon,
|
Moon,
|
||||||
|
Palette,
|
||||||
Sun,
|
Sun,
|
||||||
UserCog,
|
UserCog,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -27,7 +28,13 @@ 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';
|
||||||
|
|
||||||
@@ -36,8 +43,8 @@ export type ProfileDropdownProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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');
|
||||||
@@ -97,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}>
|
||||||
|
<DropdownMenuRadioItem value="light">
|
||||||
|
<Sun className="mr-2 h-4 w-4" /> Light
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value="dark">
|
||||||
<Moon className="mr-2 h-4 w-4" />
|
<Moon className="mr-2 h-4 w-4" />
|
||||||
Dark Mode
|
Dark
|
||||||
</DropdownMenuItem>
|
</DropdownMenuRadioItem>
|
||||||
)}
|
<DropdownMenuRadioItem value="system">
|
||||||
|
|
||||||
{theme === 'system' ? null : (
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
|
||||||
<Monitor className="mr-2 h-4 w-4" />
|
<Monitor className="mr-2 h-4 w-4" />
|
||||||
System Theme
|
System
|
||||||
</DropdownMenuItem>
|
</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" />
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export * from 'framer-motion';
|
|
||||||
|
|
||||||
export const MotionDiv = motion.div;
|
|
||||||
66
apps/web/src/components/partials/not-found.tsx
Normal file
66
apps/web/src/components/partials/not-found.tsx
Normal 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 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
apps/web/src/helpers/get-asset-buffer.ts
Normal file
14
apps/web/src/helpers/get-asset-buffer.ts
Normal 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());
|
||||||
|
};
|
||||||
325
package-lock.json
generated
325
package-lock.json
generated
@@ -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,7 +42,7 @@
|
|||||||
"@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.277.0",
|
"lucide-react": "^0.279.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"next-auth": "4.22.3",
|
"next-auth": "4.22.3",
|
||||||
@@ -80,10 +81,9 @@
|
|||||||
"@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.277.0",
|
"lucide-react": "^0.279.0",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"next-auth": "4.22.3",
|
"next-auth": "4.22.3",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
@@ -2531,6 +2531,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",
|
||||||
@@ -2749,6 +2839,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",
|
||||||
@@ -3649,6 +3753,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",
|
||||||
@@ -10445,6 +10559,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",
|
||||||
@@ -10477,6 +10605,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",
|
||||||
@@ -11960,6 +12116,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",
|
||||||
@@ -12688,9 +12862,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.277.0",
|
"version": "0.279.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.277.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.279.0.tgz",
|
||||||
"integrity": "sha512-9epmznme+vW14V9d2rsMeLr3fMnf59lYDUOVUg6s7oVN22Zq8h4B30+3CIdFFV9UXCjPG5ZNKHfO/hf96cl46A==",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -14752,6 +14926,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",
|
||||||
@@ -16834,14 +17033,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"
|
||||||
@@ -17452,6 +17701,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",
|
||||||
@@ -17541,6 +17811,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",
|
||||||
@@ -19308,6 +19591,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",
|
||||||
@@ -19599,7 +19900,7 @@
|
|||||||
"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.277.0",
|
"lucide-react": "^0.279.0",
|
||||||
"luxon": "^3.4.2",
|
"luxon": "^3.4.2",
|
||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"pdfjs-dist": "3.6.172",
|
"pdfjs-dist": "3.6.172",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "*"
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -27,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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.cjs",
|
"main": "./index.cjs",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf node_modules"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
"universal/",
|
"universal/",
|
||||||
"next-auth/"
|
"next-auth/"
|
||||||
],
|
],
|
||||||
"scripts": {},
|
"scripts": {
|
||||||
|
"clean": "rimraf node_modules"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.410.0",
|
"@aws-sdk/client-s3": "^3.410.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export type CreateDocumentMetaOptions = {
|
||||||
|
documentId: number;
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upsertDocumentMeta = async ({
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
documentId,
|
||||||
|
}: CreateDocumentMetaOptions) => {
|
||||||
|
return await prisma.documentMeta.upsert({
|
||||||
|
where: {
|
||||||
|
documentId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
documentId,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma';
|
|||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { sealDocument } from './seal-document';
|
import { sealDocument } from './seal-document';
|
||||||
|
import { sendPendingEmail } from './send-pending-email';
|
||||||
|
|
||||||
export type CompleteDocumentWithTokenOptions = {
|
export type CompleteDocumentWithTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -69,6 +70,19 @@ export const completeDocumentWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pendingRecipients = await prisma.recipient.count({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
signingStatus: {
|
||||||
|
not: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pendingRecipients > 0) {
|
||||||
|
await sendPendingEmail({ documentId, recipientId: recipient.id });
|
||||||
|
}
|
||||||
|
|
||||||
const documents = await prisma.document.updateMany({
|
const documents = await prisma.document.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: document.id,
|
id: document.id,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) =>
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentData: true,
|
documentData: true,
|
||||||
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
|||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { putFile } from '../../universal/upload/put-file';
|
import { putFile } from '../../universal/upload/put-file';
|
||||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||||
|
import { sendCompletedEmail } from './send-completed-email';
|
||||||
|
|
||||||
export type SealDocumentOptions = {
|
export type SealDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@@ -86,4 +87,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
|
|||||||
data: newData,
|
data: newData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await sendCompletedEmail({ documentId });
|
||||||
};
|
};
|
||||||
|
|||||||
57
packages/lib/server-only/document/send-completed-email.ts
Normal file
57
packages/lib/server-only/document/send-completed-email.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import { render } from '@documenso/email/render';
|
||||||
|
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export interface SendDocumentOptions {
|
||||||
|
documentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => {
|
||||||
|
const document = await prisma.document.findUnique({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Recipient: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.Recipient.length === 0) {
|
||||||
|
throw new Error('Document has no recipients');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
document.Recipient.map(async (recipient) => {
|
||||||
|
const { email, name, token } = recipient;
|
||||||
|
|
||||||
|
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||||
|
documentName: document.title,
|
||||||
|
assetBaseUrl,
|
||||||
|
downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: {
|
||||||
|
address: email,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||||
|
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||||
|
},
|
||||||
|
subject: 'Signing Complete!',
|
||||||
|
html: render(template),
|
||||||
|
text: render(template, { plainText: true }),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -4,13 +4,14 @@ import { mailer } from '@documenso/email/mailer';
|
|||||||
import { render } from '@documenso/email/render';
|
import { render } from '@documenso/email/render';
|
||||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||||
|
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export interface SendDocumentOptions {
|
export type SendDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => {
|
export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => {
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@@ -26,9 +27,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customEmail = document?.documentMeta;
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
@@ -45,6 +49,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
|
const customEmailTemplate = {
|
||||||
|
'signer.name': name,
|
||||||
|
'signer.email': email,
|
||||||
|
'document.name': document.title,
|
||||||
|
};
|
||||||
|
|
||||||
if (recipient.sendStatus === SendStatus.SENT) {
|
if (recipient.sendStatus === SendStatus.SENT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,6 +68,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
inviterEmail: user.email,
|
inviterEmail: user.email,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
|
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
||||||
});
|
});
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
@@ -69,7 +80,9 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
name: FROM_NAME,
|
name: FROM_NAME,
|
||||||
address: FROM_ADDRESS,
|
address: FROM_ADDRESS,
|
||||||
},
|
},
|
||||||
subject: 'Please sign this document',
|
subject: customEmail?.subject
|
||||||
|
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||||
|
: 'Please sign this document',
|
||||||
html: render(template),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
});
|
});
|
||||||
|
|||||||
64
packages/lib/server-only/document/send-pending-email.ts
Normal file
64
packages/lib/server-only/document/send-pending-email.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import { render } from '@documenso/email/render';
|
||||||
|
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export interface SendPendingEmailOptions {
|
||||||
|
documentId: number;
|
||||||
|
recipientId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => {
|
||||||
|
const document = await prisma.document.findFirst({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
id: recipientId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Recipient: {
|
||||||
|
where: {
|
||||||
|
id: recipientId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.Recipient.length === 0) {
|
||||||
|
throw new Error('Document has no recipients');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [recipient] = document.Recipient;
|
||||||
|
|
||||||
|
const { email, name } = recipient;
|
||||||
|
|
||||||
|
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
const template = createElement(DocumentPendingEmailTemplate, {
|
||||||
|
documentName: document.title,
|
||||||
|
assetBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: {
|
||||||
|
address: email,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||||
|
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||||
|
},
|
||||||
|
subject: 'Waiting for others to complete signing.',
|
||||||
|
html: render(template),
|
||||||
|
text: render(template, { plainText: true }),
|
||||||
|
});
|
||||||
|
};
|
||||||
21
packages/lib/server-only/document/update-document.ts
Normal file
21
packages/lib/server-only/document/update-document.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export type UpdateDocumentOptions = {
|
||||||
|
documentId: number;
|
||||||
|
data: Prisma.DocumentUpdateInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDocument = async ({ documentId, data }: UpdateDocumentOptions) => {
|
||||||
|
return await prisma.document.update({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -13,6 +13,9 @@ export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForD
|
|||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
import { FieldType, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export interface SetFieldsForDocumentOptions {
|
export interface SetFieldsForDocumentOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
fields: {
|
fields: {
|
||||||
id?: number | null;
|
id?: number | null;
|
||||||
|
type: FieldType;
|
||||||
signerEmail: string;
|
signerEmail: string;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
pageX: number;
|
pageX: number;
|
||||||
@@ -54,39 +55,34 @@ export const setFieldsForDocument = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
...existing,
|
_persisted: existing,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((field) => {
|
.filter((field) => {
|
||||||
return (
|
return (
|
||||||
field.Recipient?.sendStatus !== SendStatus.SENT &&
|
field._persisted?.Recipient?.sendStatus !== SendStatus.SENT &&
|
||||||
field.Recipient?.signingStatus !== SigningStatus.SIGNED
|
field._persisted?.Recipient?.signingStatus !== SigningStatus.SIGNED
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedFields = await prisma.$transaction(
|
const persistedFields = await prisma.$transaction(
|
||||||
|
// Disabling as wrapping promises here causes type issues
|
||||||
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||||
linkedFields.map((field) =>
|
linkedFields.map((field) =>
|
||||||
field.id
|
prisma.field.upsert({
|
||||||
? prisma.field.update({
|
|
||||||
where: {
|
where: {
|
||||||
id: field.id,
|
id: field._persisted?.id ?? -1,
|
||||||
recipientId: field.recipientId,
|
|
||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
data: {
|
update: {
|
||||||
type: field.type,
|
|
||||||
page: field.pageNumber,
|
page: field.pageNumber,
|
||||||
positionX: field.pageX,
|
positionX: field.pageX,
|
||||||
positionY: field.pageY,
|
positionY: field.pageY,
|
||||||
width: field.pageWidth,
|
width: field.pageWidth,
|
||||||
height: field.pageHeight,
|
height: field.pageHeight,
|
||||||
},
|
},
|
||||||
})
|
create: {
|
||||||
: prisma.field.create({
|
type: field.type,
|
||||||
data: {
|
|
||||||
// TODO: Rewrite this entire transaction because this is a mess
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
type: field.type!,
|
|
||||||
page: field.pageNumber,
|
page: field.pageNumber,
|
||||||
positionX: field.pageX,
|
positionX: field.pageX,
|
||||||
positionY: field.pageY,
|
positionY: field.pageY,
|
||||||
@@ -94,17 +90,16 @@ export const setFieldsForDocument = async ({
|
|||||||
height: field.pageHeight,
|
height: field.pageHeight,
|
||||||
customText: '',
|
customText: '',
|
||||||
inserted: false,
|
inserted: false,
|
||||||
|
|
||||||
Document: {
|
Document: {
|
||||||
connect: {
|
connect: {
|
||||||
id: document.id,
|
id: documentId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Recipient: {
|
Recipient: {
|
||||||
connect: {
|
connect: {
|
||||||
documentId_email: {
|
documentId_email: {
|
||||||
documentId: document.id,
|
documentId,
|
||||||
email: field.signerEmail,
|
email: field.signerEmail.toLowerCase(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export const getRecipientsForDocument = async ({
|
|||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return recipients;
|
return recipients;
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export const setRecipientsForDocument = async ({
|
|||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedRecipients = recipients.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
email: recipient.email.toLowerCase(),
|
||||||
|
}));
|
||||||
|
|
||||||
const existingRecipients = await prisma.recipient.findMany({
|
const existingRecipients = await prisma.recipient.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
@@ -37,13 +42,13 @@ export const setRecipientsForDocument = async ({
|
|||||||
|
|
||||||
const removedRecipients = existingRecipients.filter(
|
const removedRecipients = existingRecipients.filter(
|
||||||
(existingRecipient) =>
|
(existingRecipient) =>
|
||||||
!recipients.find(
|
!normalizedRecipients.find(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const linkedRecipients = recipients
|
const linkedRecipients = normalizedRecipients
|
||||||
.map((recipient) => {
|
.map((recipient) => {
|
||||||
const existing = existingRecipients.find(
|
const existing = existingRecipients.find(
|
||||||
(existingRecipient) =>
|
(existingRecipient) =>
|
||||||
@@ -62,21 +67,20 @@ export const setRecipientsForDocument = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const persistedRecipients = await prisma.$transaction(
|
const persistedRecipients = await prisma.$transaction(
|
||||||
|
// Disabling as wrapping promises here causes type issues
|
||||||
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||||
linkedRecipients.map((recipient) =>
|
linkedRecipients.map((recipient) =>
|
||||||
recipient.id
|
prisma.recipient.upsert({
|
||||||
? prisma.recipient.update({
|
|
||||||
where: {
|
where: {
|
||||||
id: recipient.id,
|
id: recipient.id ?? -1,
|
||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
data: {
|
update: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
})
|
create: {
|
||||||
: prisma.recipient.create({
|
|
||||||
data: {
|
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
token: nanoid(),
|
token: nanoid(),
|
||||||
|
|||||||
58
packages/lib/server-only/share/create-or-get-share-link.ts
Normal file
58
packages/lib/server-only/share/create-or-get-share-link.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { alphaid } from '../../universal/id';
|
||||||
|
|
||||||
|
export type CreateSharingIdOptions =
|
||||||
|
| {
|
||||||
|
documentId: number;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
documentId: number;
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createOrGetShareLink = async ({ documentId, ...options }: CreateSharingIdOptions) => {
|
||||||
|
const email = await match(options)
|
||||||
|
.with({ token: P.string }, async ({ token }) => {
|
||||||
|
return await prisma.recipient
|
||||||
|
.findFirst({
|
||||||
|
where: {
|
||||||
|
documentId,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((recipient) => recipient?.email);
|
||||||
|
})
|
||||||
|
.with({ userId: P.number }, async ({ userId }) => {
|
||||||
|
return await prisma.user
|
||||||
|
.findFirst({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((user) => user?.email);
|
||||||
|
})
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new Error('Unable to create share link for document with the given email');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.documentShareLink.upsert({
|
||||||
|
where: {
|
||||||
|
documentId_email: {
|
||||||
|
email,
|
||||||
|
documentId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
email,
|
||||||
|
documentId,
|
||||||
|
slug: alphaid(14),
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export type GetRecipientOrSenderByShareLinkSlugOptions = {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecipientOrSenderByShareLinkSlug = async ({
|
||||||
|
slug,
|
||||||
|
}: GetRecipientOrSenderByShareLinkSlugOptions) => {
|
||||||
|
const { documentId, email } = await prisma.documentShareLink.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipient = await prisma.recipient.findFirst({
|
||||||
|
where: {
|
||||||
|
documentId,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Signature: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recipient) {
|
||||||
|
return recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sender = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
Document: { some: { id: documentId } },
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sender) {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Recipient or sender not found');
|
||||||
|
};
|
||||||
13
packages/lib/server-only/share/get-share-link-by-slug.ts
Normal file
13
packages/lib/server-only/share/get-share-link-by-slug.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export type GetShareLinkBySlugOptions = {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShareLinkBySlug = async ({ slug }: GetShareLinkBySlugOptions) => {
|
||||||
|
return await prisma.documentShareLink.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ export type GetSubscriptionByUserIdOptions = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSubscriptionByUserId = ({ userId }: GetSubscriptionByUserIdOptions) => {
|
export const getSubscriptionByUserId = async ({ userId }: GetSubscriptionByUserIdOptions) => {
|
||||||
return prisma.subscription.findFirst({
|
return prisma.subscription.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
1
packages/lib/types/font.d.ts
vendored
Normal file
1
packages/lib/types/font.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module '*.ttf';
|
||||||
12
packages/lib/utils/render-custom-email-template.ts
Normal file
12
packages/lib/utils/render-custom-email-template.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const renderCustomEmailTemplate = <T extends Record<string, string>>(
|
||||||
|
template: string,
|
||||||
|
variables: T,
|
||||||
|
): string => {
|
||||||
|
return template.replace(/\{(\S+)\}/g, (_, key) => {
|
||||||
|
if (key in variables) {
|
||||||
|
return variables[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.cjs",
|
"main": "./index.cjs",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf node_modules"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Share" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"link" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"documentId" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "Share_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Share_link_key" ON "Share"("link");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Share" ADD CONSTRAINT "Share_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Share" ADD CONSTRAINT "Share_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `userId` on the `Share` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `recipientId` to the `Share` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Share" DROP CONSTRAINT "Share_userId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Share" DROP COLUMN "userId",
|
||||||
|
ADD COLUMN "recipientId" INTEGER NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Share" ADD CONSTRAINT "Share_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" ADD COLUMN "documentMetaId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DocumentMeta" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"customEmailSubject" TEXT,
|
||||||
|
"customEmailBody" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "DocumentMeta_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Document" ADD CONSTRAINT "Document_documentMetaId_fkey" FOREIGN KEY ("documentMetaId") REFERENCES "DocumentMeta"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `Share` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Share" DROP CONSTRAINT "Share_documentId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Share" DROP CONSTRAINT "Share_recipientId_fkey";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "Share";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DocumentShareLink" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"documentId" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "DocumentShareLink_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DocumentShareLink_slug_key" ON "DocumentShareLink"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DocumentShareLink_documentId_email_key" ON "DocumentShareLink"("documentId", "email");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DocumentShareLink" ADD CONSTRAINT "DocumentShareLink_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[documentMetaId]` on the table `Document` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Document_documentMetaId_key" ON "Document"("documentMetaId");
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `documentMetaId` on the `Document` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `customEmailBody` on the `DocumentMeta` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `customEmailSubject` on the `DocumentMeta` table. All the data in the column will be lost.
|
||||||
|
- A unique constraint covering the columns `[documentId]` on the table `DocumentMeta` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `documentId` to the `DocumentMeta` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Document" DROP CONSTRAINT "Document_documentMetaId_fkey";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "Document_documentMetaId_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentMeta"
|
||||||
|
ADD COLUMN "documentId" INTEGER,
|
||||||
|
ADD COLUMN "message" TEXT,
|
||||||
|
ADD COLUMN "subject" TEXT;
|
||||||
|
|
||||||
|
-- Migrate data
|
||||||
|
UPDATE "DocumentMeta" SET "documentId" = (
|
||||||
|
SELECT "id" FROM "Document" WHERE "Document"."documentMetaId" = "DocumentMeta"."id"
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrate data
|
||||||
|
UPDATE "DocumentMeta" SET "message" = "customEmailBody";
|
||||||
|
|
||||||
|
-- Migrate data
|
||||||
|
UPDATE "DocumentMeta" SET "subject" = "customEmailSubject";
|
||||||
|
|
||||||
|
-- Prune data
|
||||||
|
DELETE FROM "DocumentMeta" WHERE "documentId" IS NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" DROP COLUMN "documentMetaId";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentMeta"
|
||||||
|
DROP COLUMN "customEmailBody",
|
||||||
|
DROP COLUMN "customEmailSubject";
|
||||||
|
|
||||||
|
-- AlterColumn
|
||||||
|
ALTER TABLE "DocumentMeta" ALTER COLUMN "documentId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DocumentMeta_documentId_key" ON "DocumentMeta"("documentId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DocumentMeta" ADD CONSTRAINT "DocumentMeta_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "prisma generate",
|
"build": "prisma generate",
|
||||||
"format": "prisma format",
|
"format": "prisma format",
|
||||||
|
"clean": "rimraf node_modules",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate-dev": "prisma migrate dev",
|
"prisma:migrate-dev": "prisma migrate dev",
|
||||||
"prisma:migrate-deploy": "prisma migrate deploy",
|
"prisma:migrate-deploy": "prisma migrate deploy",
|
||||||
|
|||||||
@@ -108,8 +108,10 @@ model Document {
|
|||||||
status DocumentStatus @default(DRAFT)
|
status DocumentStatus @default(DRAFT)
|
||||||
Recipient Recipient[]
|
Recipient Recipient[]
|
||||||
Field Field[]
|
Field Field[]
|
||||||
|
ShareLink DocumentShareLink[]
|
||||||
documentDataId String
|
documentDataId String
|
||||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||||
|
documentMeta DocumentMeta?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
@@ -130,6 +132,14 @@ model DocumentData {
|
|||||||
Document Document?
|
Document Document?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DocumentMeta {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
subject String?
|
||||||
|
message String?
|
||||||
|
documentId Int @unique
|
||||||
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
enum ReadStatus {
|
enum ReadStatus {
|
||||||
NOT_OPENED
|
NOT_OPENED
|
||||||
OPENED
|
OPENED
|
||||||
@@ -200,3 +210,16 @@ model Signature {
|
|||||||
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
||||||
Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict)
|
Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DocumentShareLink {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String
|
||||||
|
slug String @unique
|
||||||
|
documentId Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
document Document @relation(fields: [documentId], references: [id])
|
||||||
|
|
||||||
|
@@unique([documentId, email])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Document, DocumentData } from '@documenso/prisma/client';
|
import { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type DocumentWithData = Document & {
|
export type DocumentWithData = Document & {
|
||||||
documentData?: DocumentData | null;
|
documentData?: DocumentData | null;
|
||||||
|
documentMeta?: DocumentMeta | null;
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user