Compare commits
7 Commits
feat/next-
...
blog/upcom
| Author | SHA1 | Date | |
|---|---|---|---|
| 31829f99cf | |||
| e2237ee67b | |||
| 89fe8842f2 | |||
| db6ef10829 | |||
| 9742898cb6 | |||
| 0bf1310ae0 | |||
| b6ed07b46b |
@ -6,7 +6,7 @@ NEXTAUTH_SECRET="secret"
|
|||||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||||
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
|
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
|
||||||
|
|
||||||
# [[URLS]]
|
# [[APP]]
|
||||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||||
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
|
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
|
||||||
|
|
||||||
|
|||||||
6
.github/dependabot.yml
vendored
@ -9,7 +9,7 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- "ci dependencies"
|
- "ci dependencies"
|
||||||
- "ci"
|
- "ci"
|
||||||
open-pull-requests-limit: 2
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/apps/marketing"
|
directory: "/apps/marketing"
|
||||||
@ -19,7 +19,7 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- "npm dependencies"
|
- "npm dependencies"
|
||||||
- "frontend"
|
- "frontend"
|
||||||
open-pull-requests-limit: 2
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/apps/web"
|
directory: "/apps/web"
|
||||||
@ -29,4 +29,4 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- "npm dependencies"
|
- "npm dependencies"
|
||||||
- "frontend"
|
- "frontend"
|
||||||
open-pull-requests-limit: 2
|
open-pull-requests-limit: 10
|
||||||
|
|||||||
101
apps/marketing/content/blog/launch-week.mdx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
title: A week of announcements
|
||||||
|
description: An overview of what's new at Documenso.
|
||||||
|
authorName: 'Flo Merian'
|
||||||
|
authorImage: '/blog/blog-author-flo.jpeg'
|
||||||
|
authorRole: 'Go-to-market'
|
||||||
|
date: 2023-09-20
|
||||||
|
tags:
|
||||||
|
- Announcement
|
||||||
|
- Community
|
||||||
|
---
|
||||||
|
|
||||||
|
We just spent the week announcing something new every day. This blog post gives an overview of what's new at Documenso.
|
||||||
|
|
||||||
|
## A week of announcements
|
||||||
|
|
||||||
|
### Day 1: Documenso Design System
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<MdxNextImage
|
||||||
|
src="/blog/blog-fig-launch-week-documenso-design-system.webp"
|
||||||
|
width="2000"
|
||||||
|
height="1126"
|
||||||
|
alt="Documenso Design System"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<figcaption className="text-center">Documenso Design System</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
We open-sourced Documenso's design system.
|
||||||
|
|
||||||
|
Documenso isn't just an open-source alternative to DocuSign. It's a beautiful document signing experience.
|
||||||
|
|
||||||
|
We welcome every developer AND designer to contribute to building the product.
|
||||||
|
|
||||||
|
[This Figma file]() serves as an open source guide for the design of the product across website, desktop & mobile apps, and brand.
|
||||||
|
|
||||||
|
It includes design tokens, primitives and components, screens, and brand assets.
|
||||||
|
|
||||||
|
Go duplicate and remix it!
|
||||||
|
|
||||||
|
### Day 2: Malfunction Mania
|
||||||
|
|
||||||
|
Documenso 1.0 just hit the testing environment.
|
||||||
|
|
||||||
|
We want to ensure the best possible release. That's why we started this week a public testing phase, the most significant community project yet, where we invite you to try out the new version, report and fix bugs and give feedback before release.
|
||||||
|
|
||||||
|
We call it "Malfunction Mania."
|
||||||
|
|
||||||
|
Early contributors love it so far:
|
||||||
|
|
||||||
|
"I love the refresh. The UI is much cleaner and the possibility to select the signature spot, date, name, email, and change the size, is a life-saver..."
|
||||||
|
|
||||||
|
Documenso's co-founder and CEO Timur shared the details in [this announcement]().
|
||||||
|
|
||||||
|
### Day 3: Documenso Shop
|
||||||
|
|
||||||
|
Supporting and contributing to open-source projects like Documenso is cool for sure.
|
||||||
|
|
||||||
|
Do you know what's *even* cooler tho? To wear swag from open-source projects.
|
||||||
|
|
||||||
|
We announced the [Documenso Shop](https://documen.so/shop), a place where you can buy some merch. Suit up!
|
||||||
|
|
||||||
|
### Day 4: Early Adopter Plan
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<MdxNextImage
|
||||||
|
src="/blog/blog-fig-launch-week-early-adopter-plan.webp"
|
||||||
|
width="2000"
|
||||||
|
height="1126"
|
||||||
|
alt="Early Adopter Plan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<figcaption className="text-center">The Early Adopter Plan. $30/month, forever. Available for the first 100 new signups.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
As we ramp up development speed, we introduced the [Early Adopter Plan](https://documen.so/pricing), a special, fixed $30/mo offer for 100 early adopters who want to get deep hands-on feedback.
|
||||||
|
|
||||||
|
Timur published a blog post [here]() to explain how we plan to build the core version of [documenso.com](http://documenso.com/).
|
||||||
|
|
||||||
|
### Day 5: Upcoming Launches
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<MdxNextImage
|
||||||
|
src="/blog/blog-fig-launch-week-upcoming-launches.webp"
|
||||||
|
width="2000"
|
||||||
|
height="1126"
|
||||||
|
alt="Documenso Upcoming Launches"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Say hello! hey! yo! to the public roadmap.
|
||||||
|
|
||||||
|
Go to the repository to find it and preview what's next at Documenso, the features we're working on and upcoming launches.
|
||||||
|
|
||||||
|
Make sure to [star the repo on GitHub](https://documen.so/github) and watch it to get the latest updates.
|
||||||
|
|
||||||
|
## Wrapping up
|
||||||
|
|
||||||
|
So this week was Documenso's first Launch Week. We hope you enjoyed it as much as we did.
|
||||||
@ -2,7 +2,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { withContentlayer } = require('next-contentlayer');
|
const { withContentlayer } = require('next-contentlayer');
|
||||||
|
|
||||||
require('dotenv').config({
|
const { parsed: env } = require('dotenv').config({
|
||||||
path: path.join(__dirname, '../../.env.local'),
|
path: path.join(__dirname, '../../.env.local'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -10,13 +10,9 @@ require('dotenv').config({
|
|||||||
const config = {
|
const config = {
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: true,
|
serverActions: true,
|
||||||
serverActionsBodySizeLimit: '10mb',
|
|
||||||
},
|
},
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'],
|
transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'],
|
||||||
env: {
|
|
||||||
NEXT_PUBLIC_PROJECT: 'marketing',
|
|
||||||
},
|
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
'lucide-react': {
|
'lucide-react': {
|
||||||
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
"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": {
|
||||||
@ -19,16 +18,14 @@
|
|||||||
"@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.279.0",
|
"lucide-react": "^0.214.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "13.5.4",
|
"next": "13.4.12",
|
||||||
"next-auth": "4.22.3",
|
"next-auth": "4.22.3",
|
||||||
"next-contentlayer": "^0.3.4",
|
"next-contentlayer": "^0.3.4",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
"perfect-freehand": "^1.2.0",
|
"perfect-freehand": "^1.2.0",
|
||||||
"posthog-js": "^1.77.3",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-confetti": "^6.1.0",
|
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 15 KiB |
@ -39,7 +39,7 @@ export default function ContentPage({ params }: { params: { content: string } })
|
|||||||
const MDXContent = useMDXComponent(post.body.code);
|
const MDXContent = useMDXComponent(post.body.code);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="prose dark:prose-invert mx-auto">
|
<article className="prose prose-slate mx-auto">
|
||||||
<MDXContent components={mdxComponents} />
|
<MDXContent components={mdxComponents} />
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 dark:prose-invert mx-auto py-8">
|
<article className="prose prose-slate mx-auto py-8">
|
||||||
<div className="mb-6 text-center">
|
<div className="mb-6 text-center">
|
||||||
<time dateTime={post.date} className="text-muted-foreground mb-1 text-xs">
|
<time dateTime={post.date} className="mb-1 text-xs text-gray-600">
|
||||||
{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="bg-foreground h-10 w-10 rounded-full">
|
<div className="h-10 w-10 rounded-full bg-gray-50">
|
||||||
{post.authorImage && (
|
{post.authorImage && (
|
||||||
<img
|
<img
|
||||||
src={post.authorImage}
|
src={post.authorImage}
|
||||||
alt={`Image of ${post.authorName}`}
|
alt={`Image of ${post.authorName}`}
|
||||||
className="bg-foreground/10 h-10 w-10 rounded-full"
|
className="h-10 w-10 rounded-full bg-gray-50"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</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="text-muted-foreground mx-auto mt-4 max-w-xl text-center text-lg leading-normal">
|
<p className="mx-auto mt-4 max-w-xl text-center text-lg leading-normal text-[#31373D]">
|
||||||
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="divide-muted-foreground/20 border-muted-foreground/20 mt-10 divide-y border-t">
|
<div className="mt-10 divide-y divide-slate-100 border-t border-slate-200 ">
|
||||||
{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="bg-foreground/5 h-10 w-10 rounded-full">
|
<div className="h-10 w-10 rounded-full bg-slate-50">
|
||||||
{post.authorImage && (
|
{post.authorImage && (
|
||||||
<img
|
<img
|
||||||
src={post.authorImage}
|
src={post.authorImage}
|
||||||
alt={`Image of ${post.authorName}`}
|
alt={`Image of ${post.authorName}`}
|
||||||
className="bg-foreground/5 h-10 w-10 rounded-full"
|
className="h-10 w-10 rounded-full bg-slate-50"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</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-foreground text-3xl font-bold md:text-4xl">
|
<h1 className="text-3xl font-bold text-slate-900 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="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
|
<p className="mt-4 max-w-prose text-base text-slate-500 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="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
|
<p className="mt-4 max-w-prose text-base text-slate-500 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="text-muted-foreground mt-4 max-w-prose text-base md:text-lg">
|
<p className="mt-4 max-w-prose text-base text-slate-500 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-foreground text-2xl font-bold">Let's do it together</h2>
|
<h2 className="text-2xl font-bold text-slate-900">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-foreground text-4xl font-semibold md:text-5xl',
|
'text-4xl font-semibold text-slate-900 md:text-5xl',
|
||||||
fontCaveat.className,
|
fontCaveat.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Timur
|
Timur
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm md:text-lg">
|
<p className="text-sm text-slate-500 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-foreground text-4xl font-semibold md:text-5xl',
|
'text-4xl font-semibold text-slate-900 md:text-5xl',
|
||||||
fontCaveat.className,
|
fontCaveat.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Lucas
|
Lucas
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm md:text-lg">
|
<p className="text-sm text-slate-500 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,16 +119,12 @@ 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
|
<img src={signatureDataUrl} alt="your-signature" className="max-w-[172px]" />
|
||||||
src={signatureDataUrl}
|
|
||||||
alt="your-signature"
|
|
||||||
className="max-w-[172px] dark:invert"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{!signatureDataUrl && (
|
{!signatureDataUrl && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground text-4xl font-semibold md:text-5xl',
|
'text-4xl font-semibold text-slate-900 md:text-5xl',
|
||||||
fontCaveat.className,
|
fontCaveat.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -136,7 +132,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm md:text-lg">
|
<p className="text-sm text-slate-500 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>
|
||||||
@ -147,20 +143,20 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<h2 className="text-foreground text-2xl font-bold">Your sign in details</h2>
|
<h2 className="text-2xl font-bold text-slate-900">Your sign in details</h2>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="text-muted-foreground text-lg">
|
<p className="text-lg text-slate-500">
|
||||||
<span className="font-bold">Email:</span> {user.email}
|
<span className="font-bold">Email:</span> {user.email}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-lg">
|
<p className="mt-2 text-lg text-slate-500">
|
||||||
<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="text-muted-foreground mt-4 text-sm italic">
|
<p className="mt-4 text-sm italic text-slate-500">
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
'use client';
|
import React from 'react';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
import { Footer } from '~/components/(marketing)/footer';
|
import { Footer } from '~/components/(marketing)/footer';
|
||||||
import { Header } from '~/components/(marketing)/header';
|
import { Header } from '~/components/(marketing)/header';
|
||||||
@ -12,31 +8,15 @@ export type MarketingLayoutProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||||
const [scrollY, setScrollY] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onScroll = () => {
|
|
||||||
setScrollY(window.scrollY);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', onScroll);
|
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', onScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
|
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
|
||||||
<div
|
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md">
|
||||||
className={cn('fixed left-0 top-0 z-50 w-full bg-transparent', {
|
|
||||||
'bg-background/50 backdrop-blur-md': scrollY > 5,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div>
|
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div>
|
||||||
|
|
||||||
<Footer className="bg-background border-muted mt-24 border-t" />
|
<Footer className="mt-24 bg-transparent backdrop-blur-[2px]" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export default async function OpenPage() {
|
|||||||
.then((res) => ZStargazersLiveResponse.parse(res));
|
.then((res) => ZStargazersLiveResponse.parse(res));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
<div className="mx-auto mt-12 max-w-screen-lg">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
|
<h1 className="text-3xl font-bold lg:text-5xl">Open Startup</h1>
|
||||||
|
|
||||||
|
|||||||
@ -1,84 +0,0 @@
|
|||||||
'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,23 +1,152 @@
|
|||||||
import Image from 'next/image';
|
'use client';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Variants, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
import { OSSFriendsContainer } from './container';
|
const OSSFriends = [
|
||||||
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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default async function OSSFriendsPage() {
|
const ContainerVariants: Variants = {
|
||||||
const ossFriends: TOSSFriendsSchema = await fetch('https://formbricks.com/api/oss-friends', {
|
initial: {
|
||||||
next: {
|
opacity: 0,
|
||||||
revalidate: 3600,
|
},
|
||||||
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.075,
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
.then(async (res) => res.json())
|
};
|
||||||
.then(async (data) => z.object({ data: ZOSSFriendsSchema }).parseAsync(data))
|
|
||||||
.then(({ data }) => data)
|
|
||||||
.catch(() => []);
|
|
||||||
|
|
||||||
|
const CardVariants: Variants = {
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 50,
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const randomDegrees = () => {
|
||||||
|
const degrees = [45, 120, -140, -45];
|
||||||
|
|
||||||
|
return degrees[Math.floor(Math.random() * degrees.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OSSFriendsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="relative mt-12">
|
<div className="relative mt-12">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -25,19 +154,49 @@ export default async function OSSFriendsPage() {
|
|||||||
Our <span title="Open Source Software">OSS</span> Friends
|
Our <span title="Open Source Software">OSS</span> Friends
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-foreground mx-auto mt-4 max-w-[55ch] text-lg leading-normal">
|
<p className="mx-auto mt-4 max-w-[55ch] text-lg leading-normal text-[#31373D]">
|
||||||
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>
|
||||||
|
|
||||||
<OSSFriendsContainer className="mt-12" ossFriends={ossFriends} />
|
<motion.div
|
||||||
|
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 dark:contrast-[70%] dark:invert dark:sepia md:-mr-[50vw] md:scale-150 lg:scale-[175%]"
|
className="-mr-[15vw] -mt-[15vh] h-full max-h-[150vh] scale-125 object-cover md:-mr-[50vw] md:scale-150 lg:scale-[175%]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
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>;
|
|
||||||
@ -20,14 +20,14 @@ export type PricingPageProps = {
|
|||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 sm:mt-12">
|
<div className="mt-12">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
|
<h1 className="text-3xl font-bold lg:text-5xl">Pricing</h1>
|
||||||
|
|
||||||
<p className="text-foreground mt-4 text-lg leading-normal">
|
<p className="mt-4 text-lg leading-normal text-[#31373D]">
|
||||||
Designed for every stage of your journey.
|
Designed for every stage of your journey.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-foreground text-lg leading-normal">Get started today.</p>
|
<p className="text-lg leading-normal text-[#31373D]">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="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||||
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="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||||
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="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||||
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="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||||
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="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||||
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="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||||
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="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="max-w-prose text-sm leading-relaxed text-slate-500">
|
||||||
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"
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import { notFound } from 'next/navigation';
|
|
||||||
|
|
||||||
import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { SinglePlayerModeSuccess } from '~/components/(marketing)/single-player-mode/single-player-mode-success';
|
|
||||||
|
|
||||||
export type SinglePlayerModeSuccessPageProps = {
|
|
||||||
params: {
|
|
||||||
token?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function SinglePlayerModeSuccessPage({
|
|
||||||
params: { token },
|
|
||||||
}: SinglePlayerModeSuccessPageProps) {
|
|
||||||
if (!token) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const document = await getDocumentAndRecipientByToken({
|
|
||||||
token,
|
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
if (!document || document.status !== DocumentStatus.COMPLETED) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SinglePlayerModeSuccess document={document} />;
|
|
||||||
}
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
|
||||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import { Field, Prisma, Recipient } from '@documenso/prisma/client';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
|
||||||
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
|
|
||||||
import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
|
||||||
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
|
|
||||||
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
|
||||||
import {
|
|
||||||
DocumentFlowFormContainer,
|
|
||||||
DocumentFlowFormContainerHeader,
|
|
||||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
|
||||||
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { createSinglePlayerDocument } from '~/components/(marketing)/single-player-mode/create-single-player-document.action';
|
|
||||||
|
|
||||||
type SinglePlayerModeStep = 'fields' | 'sign';
|
|
||||||
|
|
||||||
export default function SinglePlayerModePage() {
|
|
||||||
const analytics = useAnalytics();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
|
|
||||||
|
|
||||||
const [step, setStep] = useState<SinglePlayerModeStep>('fields');
|
|
||||||
const [fields, setFields] = useState<Field[]>([]);
|
|
||||||
|
|
||||||
const documentFlow: Record<SinglePlayerModeStep, DocumentFlowStep> = {
|
|
||||||
fields: {
|
|
||||||
title: 'Add document',
|
|
||||||
description: 'Upload a document and add fields.',
|
|
||||||
stepIndex: 1,
|
|
||||||
onBackStep: uploadedFile
|
|
||||||
? () => {
|
|
||||||
setUploadedFile(null);
|
|
||||||
setFields([]);
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
onNextStep: () => setStep('sign'),
|
|
||||||
},
|
|
||||||
sign: {
|
|
||||||
title: 'Sign',
|
|
||||||
description: 'Enter your details.',
|
|
||||||
stepIndex: 2,
|
|
||||||
onBackStep: () => setStep('fields'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentDocumentFlow = documentFlow[step];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
analytics.startSessionRecording('marketing_session_recording_spm');
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
analytics.stopSessionRecording();
|
|
||||||
};
|
|
||||||
}, [analytics]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert the selected fields into the local state.
|
|
||||||
*/
|
|
||||||
const onFieldsSubmit = (data: TAddFieldsFormSchema) => {
|
|
||||||
if (!uploadedFile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFields(
|
|
||||||
data.fields.map((field, i) => ({
|
|
||||||
id: i,
|
|
||||||
documentId: -1,
|
|
||||||
recipientId: -1,
|
|
||||||
type: field.type,
|
|
||||||
page: field.pageNumber,
|
|
||||||
positionX: new Prisma.Decimal(field.pageX),
|
|
||||||
positionY: new Prisma.Decimal(field.pageY),
|
|
||||||
width: new Prisma.Decimal(field.pageWidth),
|
|
||||||
height: new Prisma.Decimal(field.pageHeight),
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
analytics.capture('Marketing: SPM - Fields added');
|
|
||||||
|
|
||||||
documentFlow.fields.onNextStep?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload, create, sign and send the document.
|
|
||||||
*/
|
|
||||||
const onSignSubmit = async (data: TAddSignatureFormSchema) => {
|
|
||||||
if (!uploadedFile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const putFileData = await putFile(uploadedFile.file);
|
|
||||||
|
|
||||||
const documentToken = await createSinglePlayerDocument({
|
|
||||||
documentData: {
|
|
||||||
type: putFileData.type,
|
|
||||||
data: putFileData.data,
|
|
||||||
},
|
|
||||||
documentName: uploadedFile.file.name,
|
|
||||||
signer: data,
|
|
||||||
fields: fields.map((field) => ({
|
|
||||||
page: field.page,
|
|
||||||
type: field.type,
|
|
||||||
positionX: field.positionX.toNumber(),
|
|
||||||
positionY: field.positionY.toNumber(),
|
|
||||||
width: field.width.toNumber(),
|
|
||||||
height: field.height.toNumber(),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
analytics.capture('Marketing: SPM - Document signed', {
|
|
||||||
signer: data.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push(`/single-player-mode/${documentToken}/success`);
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: 'Please try again later.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const placeholderRecipient: Recipient = {
|
|
||||||
id: -1,
|
|
||||||
documentId: -1,
|
|
||||||
email: '',
|
|
||||||
name: '',
|
|
||||||
token: '',
|
|
||||||
expired: null,
|
|
||||||
signedAt: null,
|
|
||||||
readStatus: 'OPENED',
|
|
||||||
signingStatus: 'NOT_SIGNED',
|
|
||||||
sendStatus: 'NOT_SENT',
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const base64String = base64.encode(new Uint8Array(arrayBuffer));
|
|
||||||
|
|
||||||
setUploadedFile({
|
|
||||||
file,
|
|
||||||
fileBase64: `data:application/pdf;base64,${base64String}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
analytics.capture('Marketing: SPM - Document uploaded');
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: 'Please try again later.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-6 sm:mt-12">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-3xl font-bold lg:text-5xl">Single Player Mode</h1>
|
|
||||||
|
|
||||||
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
|
|
||||||
View our{' '}
|
|
||||||
<Link
|
|
||||||
href={'/pricing'}
|
|
||||||
target="_blank"
|
|
||||||
className="hover:text-foreground/80 font-semibold transition-colors"
|
|
||||||
>
|
|
||||||
community plan
|
|
||||||
</Link>{' '}
|
|
||||||
for exclusive features, including the ability to collaborate with multiple signers.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 grid w-full grid-cols-12 gap-8">
|
|
||||||
<div className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7">
|
|
||||||
{uploadedFile ? (
|
|
||||||
<Card gradient>
|
|
||||||
<CardContent className="p-2">
|
|
||||||
<LazyPDFViewer document={uploadedFile.fileBase64} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<DocumentDropzone className="h-[80vh] max-h-[60rem]" onDrop={onFileDrop} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
|
||||||
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
|
|
||||||
<DocumentFlowFormContainerHeader
|
|
||||||
title={currentDocumentFlow.title}
|
|
||||||
description={currentDocumentFlow.description}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Add fields to PDF page. */}
|
|
||||||
{step === 'fields' && (
|
|
||||||
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
|
|
||||||
<AddFieldsFormPartial
|
|
||||||
documentFlow={documentFlow.fields}
|
|
||||||
hideRecipients={true}
|
|
||||||
recipients={uploadedFile ? [placeholderRecipient] : []}
|
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
fields={fields}
|
|
||||||
onSubmit={onFieldsSubmit}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Enter user details and signature. */}
|
|
||||||
{step === 'sign' && (
|
|
||||||
<AddSignatureFormPartial
|
|
||||||
documentFlow={documentFlow.sign}
|
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
fields={fields}
|
|
||||||
onSubmit={onSignSubmit}
|
|
||||||
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
|
|
||||||
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DocumentFlowFormContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,20 +1,12 @@
|
|||||||
import { Suspense } from 'react';
|
import { Inter } from 'next/font/google';
|
||||||
|
|
||||||
import { Caveat, Inter } from 'next/font/google';
|
|
||||||
|
|
||||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Toaster } from '@documenso/ui/primitives/toaster';
|
import { Toaster } from '@documenso/ui/primitives/toaster';
|
||||||
|
|
||||||
import { ThemeProvider } from '~/providers/next-theme';
|
|
||||||
import { PlausibleProvider } from '~/providers/plausible';
|
import { PlausibleProvider } from '~/providers/plausible';
|
||||||
import { PostHogPageview } from '~/providers/posthog';
|
|
||||||
|
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
||||||
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Documenso - The Open Source DocuSign Alternative',
|
title: 'Documenso - The Open Source DocuSign Alternative',
|
||||||
@ -40,15 +32,9 @@ export const metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
const flags = await getAllAnonymousFlags();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html lang="en" className={fontInter.variable} suppressHydrationWarning>
|
||||||
lang="en"
|
|
||||||
className={cn(fontInter.variable, fontCaveat.variable)}
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<head>
|
<head>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
@ -56,17 +42,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<Suspense>
|
|
||||||
<PostHogPageview />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<FeatureFlagProvider initialFlags={flags}>
|
<PlausibleProvider>{children}</PlausibleProvider>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
||||||
<PlausibleProvider>{children}</PlausibleProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</FeatureFlagProvider>
|
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import backgroundPattern from '~/assets/background-pattern.png';
|
|
||||||
|
|
||||||
export default function NotFound() {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
|
|
||||||
<div className="absolute -inset-24 -z-10">
|
|
||||||
<motion.div
|
|
||||||
className="flex h-full w-full items-center justify-center"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={backgroundPattern}
|
|
||||||
alt="background pattern"
|
|
||||||
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
|
|
||||||
<div>
|
|
||||||
<p className="text-muted-foreground font-semibold">404 Page not found</p>
|
|
||||||
|
|
||||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4 text-sm">
|
|
||||||
The page you are looking for was moved, removed, renamed or might never have existed.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-32"
|
|
||||||
onClick={() => {
|
|
||||||
void router.back();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button className="w-32" asChild>
|
|
||||||
<Link href="/">Home</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 MiB |
@ -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 dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
<span className="bg-primary -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 dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
<span className="bg-primary -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { usePlausible } from 'next-plausible';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -44,10 +43,8 @@ export type ClaimPlanDialogProps = {
|
|||||||
|
|
||||||
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
|
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
|
||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
const analytics = useAnalytics();
|
|
||||||
const event = usePlausible();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const event = usePlausible();
|
||||||
|
|
||||||
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
|
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
|
||||||
|
|
||||||
@ -76,12 +73,10 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
event('claim-plan-pricing');
|
event('claim-plan-pricing');
|
||||||
analytics.capture('Marketing: Claim plan', { planId, email });
|
|
||||||
|
|
||||||
window.location.href = redirectUrl;
|
window.location.href = redirectUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
event('claim-plan-failed');
|
event('claim-plan-failed');
|
||||||
analytics.capture('Marketing: Claim plan failure', { planId, email });
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Something went wrong',
|
title: 'Something went wrong',
|
||||||
@ -123,7 +118,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground">Name</Label>
|
<Label className="text-slate-500">Name</Label>
|
||||||
|
|
||||||
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
||||||
|
|
||||||
@ -131,7 +126,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground">Email</Label>
|
<Label className="text-slate-500">Email</Label>
|
||||||
|
|
||||||
<Input type="email" className="mt-2" {...register('email')} />
|
<Input type="email" className="mt-2" {...register('email')} />
|
||||||
|
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import Confetti from 'react-confetti';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
|
|
||||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
|
||||||
import { useWindowSize } from '@documenso/lib/client-only/hooks/use-window-size';
|
|
||||||
|
|
||||||
export default function ConfettiScreen({
|
|
||||||
numberOfPieces: numberOfPiecesProp = 200,
|
|
||||||
...props
|
|
||||||
}: React.ComponentPropsWithoutRef<typeof Confetti> & { duration?: number }) {
|
|
||||||
const isMounted = useIsMounted();
|
|
||||||
const { width, height } = useWindowSize();
|
|
||||||
|
|
||||||
const [numberOfPieces, setNumberOfPieces] = useState(numberOfPiecesProp);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!props.duration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setNumberOfPieces(0);
|
|
||||||
}, props.duration);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [props.duration]);
|
|
||||||
|
|
||||||
if (!isMounted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<Confetti
|
|
||||||
{...props}
|
|
||||||
className="w-full"
|
|
||||||
numberOfPieces={numberOfPieces}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
/>,
|
|
||||||
document.body,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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 dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
|
className="h-full scale-125 object-cover 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,53 +33,41 @@ 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="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
|
<p className="col-span-12 leading-relaxed text-[#555E67] 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
|
<Image src={cardFastFigure} alt="its fast" className="max-w-[80%] lg:max-w-none" />
|
||||||
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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardBeautifulFigure} alt="its fast" className="w-full max-w-xs" />
|
||||||
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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardSmartFigure} alt="its fast" className="w-full max-w-[16rem]" />
|
||||||
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,12 +1,9 @@
|
|||||||
'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, Moon, Sun, Twitter } from 'lucide-react';
|
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
@ -20,7 +17,6 @@ const SOCIAL_LINKS = [
|
|||||||
|
|
||||||
const FOOTER_LINKS = [
|
const FOOTER_LINKS = [
|
||||||
{ href: '/pricing', text: 'Pricing' },
|
{ href: '/pricing', text: 'Pricing' },
|
||||||
{ href: '/single-player-mode', text: 'Single Player Mode' },
|
|
||||||
{ href: '/blog', text: 'Blog' },
|
{ href: '/blog', text: 'Blog' },
|
||||||
{ href: '/open', text: 'Open' },
|
{ href: '/open', text: 'Open' },
|
||||||
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
|
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
|
||||||
@ -30,30 +26,17 @@ 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
|
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></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">
|
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4 text-[#8D8D8D]">
|
||||||
{SOCIAL_LINKS.map((link, index) => (
|
{SOCIAL_LINKS.map((link, index) => (
|
||||||
<Link
|
<Link key={index} href={link.href} target="_blank" className="hover:text-[#6D6D6D]">
|
||||||
key={index}
|
|
||||||
href={link.href}
|
|
||||||
target="_blank"
|
|
||||||
className="text-muted-foreground hover:text-muted-foreground/80"
|
|
||||||
>
|
|
||||||
{link.icon}
|
{link.icon}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@ -66,29 +49,17 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
key={index}
|
key={index}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
target={link.target}
|
target={link.target}
|
||||||
className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 text-sm"
|
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
>
|
>
|
||||||
{link.text}
|
{link.text}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24">
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-sm text-[#8D8D8D]">
|
||||||
© {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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { HTMLAttributes, useState } from 'react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
import { HamburgerMenu } from './mobile-hamburger';
|
import { HamburgerMenu } from './mobile-hamburger';
|
||||||
@ -16,59 +15,29 @@ export type HeaderProps = HTMLAttributes<HTMLElement>;
|
|||||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
export const Header = ({ className, ...props }: HeaderProps) => {
|
||||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||||
|
|
||||||
const { getFlag } = useFeatureFlags();
|
|
||||||
|
|
||||||
const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||||
<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
|
</Link>
|
||||||
src="/logo.png"
|
|
||||||
alt="Documenso Logo"
|
|
||||||
className="dark:invert"
|
|
||||||
width={170}
|
|
||||||
height={25}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{isSinglePlayerModeMarketingEnabled && (
|
|
||||||
<Link
|
|
||||||
href="/single-player-mode"
|
|
||||||
className="bg-primary dark:text-background rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
|
||||||
>
|
|
||||||
Try now!
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden items-center gap-x-6 md:flex">
|
<div className="hidden items-center gap-x-6 md:flex">
|
||||||
<Link
|
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
||||||
href="/pricing"
|
|
||||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
|
||||||
>
|
|
||||||
Pricing
|
Pricing
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link href="/blog" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
||||||
href="/blog"
|
|
||||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
|
||||||
>
|
|
||||||
Blog
|
Blog
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link href="/open" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
|
||||||
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-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -6,9 +6,7 @@ import Link from 'next/link';
|
|||||||
import { Variants, motion } from 'framer-motion';
|
import { Variants, motion } from 'framer-motion';
|
||||||
import { Github } from 'lucide-react';
|
import { Github } from 'lucide-react';
|
||||||
import { usePlausible } from 'next-plausible';
|
import { usePlausible } from 'next-plausible';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
@ -53,10 +51,6 @@ const HeroTitleVariants: Variants = {
|
|||||||
export const Hero = ({ className, ...props }: HeroProps) => {
|
export const Hero = ({ className, ...props }: HeroProps) => {
|
||||||
const event = usePlausible();
|
const event = usePlausible();
|
||||||
|
|
||||||
const { getFlag } = useFeatureFlags();
|
|
||||||
|
|
||||||
const heroMarketingCTA = getFlag('marketing_landing_hero_cta');
|
|
||||||
|
|
||||||
const onSignUpClick = () => {
|
const onSignUpClick = () => {
|
||||||
const el = document.getElementById('email');
|
const el = document.getElementById('email');
|
||||||
|
|
||||||
@ -86,7 +80,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 dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
|
className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@ -115,7 +109,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
|||||||
onClick={onSignUpClick}
|
onClick={onSignUpClick}
|
||||||
>
|
>
|
||||||
Get the Community Plan
|
Get the Community Plan
|
||||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
<span className="bg-primary -mr-2 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||||
$30/mo. forever!
|
$30/mo. forever!
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -128,45 +122,23 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{match(heroMarketingCTA)
|
<motion.div
|
||||||
.with('spm', () => (
|
variants={HeroTitleVariants}
|
||||||
<motion.div
|
initial="initial"
|
||||||
variants={HeroTitleVariants}
|
animate="animate"
|
||||||
initial="initial"
|
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
|
||||||
animate="animate"
|
>
|
||||||
className="border-primary bg-background hover:bg-muted mx-auto mt-8 w-60 rounded-xl border transition duration-300"
|
<Link
|
||||||
>
|
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
|
||||||
<Link href="/single-player-mode" className="block px-4 py-2 text-center">
|
target="_blank"
|
||||||
<h2 className="text-muted-foreground text-xs font-semibold">
|
>
|
||||||
Introducing Single Player Mode
|
<img
|
||||||
</h2>
|
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
|
||||||
|
alt="Documenso - The open source DocuSign alternative | Product Hunt"
|
||||||
<h1 className="text-foreground mt-1.5 font-medium leading-5">
|
style={{ width: '250px', height: '54px' }}
|
||||||
Self sign for free!
|
/>
|
||||||
</h1>
|
</Link>
|
||||||
</Link>
|
</motion.div>
|
||||||
</motion.div>
|
|
||||||
))
|
|
||||||
.with('productHunt', () => (
|
|
||||||
<motion.div
|
|
||||||
variants={HeroTitleVariants}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
|
|
||||||
alt="Documenso - The open source DocuSign alternative | Product Hunt"
|
|
||||||
style={{ width: '250px', height: '54px' }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
))
|
|
||||||
.otherwise(() => null)}
|
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="mt-12"
|
className="mt-12"
|
||||||
|
|||||||
@ -14,10 +14,6 @@ export type MobileNavigationProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MENU_NAVIGATION_LINKS = [
|
export const MENU_NAVIGATION_LINKS = [
|
||||||
{
|
|
||||||
href: '/single-player-mode',
|
|
||||||
text: 'Single Player Mode',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: '/blog',
|
href: '/blog',
|
||||||
text: 'Blog',
|
text: 'Blog',
|
||||||
@ -59,13 +55,7 @@ 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
|
<Image src="/logo.png" alt="Documenso Logo" width={170} height={25} />
|
||||||
src="/logo.png"
|
|
||||||
alt="Documenso Logo"
|
|
||||||
className="dark:invert"
|
|
||||||
width={170}
|
|
||||||
height={25}
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -95,7 +85,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
className="text-2xl font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
href={href}
|
href={href}
|
||||||
onClick={() => handleMenuItemClick()}
|
onClick={() => handleMenuItemClick()}
|
||||||
>
|
>
|
||||||
@ -109,7 +99,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-foreground hover:text-foreground/80"
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
>
|
>
|
||||||
<Twitter className="h-6 w-6" />
|
<Twitter className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -117,7 +107,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-foreground hover:text-foreground/80"
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
>
|
>
|
||||||
<Github className="h-6 w-6" />
|
<Github className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -125,7 +115,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-foreground hover:text-foreground/80"
|
className="text-[#8D8D8D] hover:text-[#6D6D6D]"
|
||||||
>
|
>
|
||||||
<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 dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
|
className="h-full scale-125 object-cover 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,53 +30,41 @@ 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="text-foreground/80 col-span-12 leading-relaxed lg:col-span-6">
|
<p className="col-span-12 leading-relaxed text-[#555E67] 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
|
<Image src={cardOpenFigure} alt="its fast" className="max-w-[80%] lg:max-w-full" />
|
||||||
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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardBuildFigure} alt="its fast" className="w-full max-w-xs" />
|
||||||
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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardTemplateFigure} alt="its fast" className="w-full max-w-sm" />
|
||||||
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,13 +41,10 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.button
|
<motion.button
|
||||||
key="MONTHLY"
|
key="MONTHLY"
|
||||||
className={cn(
|
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
|
||||||
'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
|
'text-slate-900': period === 'MONTHLY',
|
||||||
{
|
'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
|
||||||
@ -61,17 +58,14 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|||||||
|
|
||||||
<motion.button
|
<motion.button
|
||||||
key="YEARLY"
|
key="YEARLY"
|
||||||
className={cn(
|
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
|
||||||
'text-muted-foreground relative flex items-center gap-x-2.5 px-1 py-2.5',
|
'text-slate-900': period === 'YEARLY',
|
||||||
{
|
'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="bg-muted text-foreground block rounded-full px-2 py-0.5 text-xs">
|
<div className="block rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-700">
|
||||||
Save $60
|
Save $60
|
||||||
</div>
|
</div>
|
||||||
{period === 'YEARLY' && (
|
{period === 'YEARLY' && (
|
||||||
@ -87,12 +81,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="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
|
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
|
||||||
>
|
>
|
||||||
<p className="text-foreground text-4xl font-medium">Self Hosted</p>
|
<p className="text-4xl font-medium text-slate-900">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="text-foreground mt-4 max-w-[30ch] text-center">
|
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
|
||||||
For small teams and individuals who need a simple solution
|
For small teams and individuals who need a simple solution
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -106,20 +100,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="text-foreground py-4 font-medium">Host your own instance</p>
|
<p className="py-4 font-medium text-slate-900">Host your own instance</p>
|
||||||
<p className="text-foreground py-4">Full Control</p>
|
<p className="py-4 text-slate-900">Full Control</p>
|
||||||
<p className="text-foreground py-4">Customizability</p>
|
<p className="py-4 text-slate-900">Customizability</p>
|
||||||
<p className="text-foreground py-4">Docker Ready</p>
|
<p className="py-4 text-slate-900">Docker Ready</p>
|
||||||
<p className="text-foreground py-4">Community Support</p>
|
<p className="py-4 text-slate-900">Community Support</p>
|
||||||
<p className="text-foreground py-4">Free, Forever</p>
|
<p className="py-4 text-slate-900">Free, Forever</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-plan="community"
|
data-plan="community"
|
||||||
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]"
|
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"
|
||||||
>
|
>
|
||||||
<p className="text-foreground text-4xl font-medium">Community</p>
|
<p className="text-4xl font-medium text-slate-900">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>}
|
||||||
@ -127,7 +121,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-foreground mt-4 max-w-[30ch] text-center">
|
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
|
||||||
For fast-growing companies that aim to scale across multiple teams.
|
For fast-growing companies that aim to scale across multiple teams.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -136,25 +130,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="text-foreground py-4 font-medium">Documenso Early Adopter Deal:</p>
|
<p className="py-4 font-medium text-slate-900">Documenso Early Adopter Deal:</p>
|
||||||
<p className="text-foreground py-4">Join the movement</p>
|
<p className="py-4 text-slate-900">Join the movement</p>
|
||||||
<p className="text-foreground py-4">Simple signing solution</p>
|
<p className="py-4 text-slate-900">Simple signing solution</p>
|
||||||
<p className="text-foreground py-4">Email and Slack assistance</p>
|
<p className="py-4 text-slate-900">Email and Slack assistance</p>
|
||||||
<p className="text-foreground py-4">
|
<p className="py-4 text-slate-900">
|
||||||
<strong>Includes all upcoming features</strong>
|
<strong>Includes all upcoming features</strong>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-foreground py-4">Fixed, straightforward pricing</p>
|
<p className="py-4 text-slate-900">Fixed, straightforward pricing</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-plan="enterprise"
|
data-plan="enterprise"
|
||||||
className="bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border px-8 py-12 shadow-lg"
|
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
|
||||||
>
|
>
|
||||||
<p className="text-foreground text-4xl font-medium">Enterprise</p>
|
<p className="text-4xl font-medium text-slate-900">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="text-foreground mt-4 max-w-[30ch] text-center">
|
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
|
||||||
For large organizations that need extra flexibility and control.
|
For large organizations that need extra flexibility and control.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -168,12 +162,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="text-foreground py-4 font-medium">Everything in Community, plus:</p>
|
<p className="py-4 font-medium text-slate-900">Everything in Community, plus:</p>
|
||||||
<p className="text-foreground py-4">Custom Subdomain</p>
|
<p className="py-4 text-slate-900">Custom Subdomain</p>
|
||||||
<p className="text-foreground py-4">Compliance Check</p>
|
<p className="py-4 text-slate-900">Compliance Check</p>
|
||||||
<p className="text-foreground py-4">Guaranteed Uptime</p>
|
<p className="py-4 text-slate-900">Guaranteed Uptime</p>
|
||||||
<p className="text-foreground py-4">Reporting & Analysis</p>
|
<p className="py-4 text-slate-900">Reporting & Analysis</p>
|
||||||
<p className="text-foreground py-4">24/7 Support</p>
|
<p className="py-4 text-slate-900">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 dark:contrast-[70%] dark:invert dark:sepia md:scale-150 lg:scale-[175%]"
|
className="h-full scale-125 object-cover 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,70 +34,54 @@ 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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardSharingFigure} alt="its fast" className="w-full max-w-xs" />
|
||||||
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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardConnectionsFigure} alt="its fast" className="w-full max-w-sm" />
|
||||||
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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardPaidFigure} alt="its fast" className="w-full max-w-[14rem]" />
|
||||||
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="text-foreground/80 leading-relaxed">
|
<p className="leading-relaxed text-[#555E67]">
|
||||||
<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
|
<Image src={cardWidgetFigure} alt="its fast" className="w-full max-w-xs" />
|
||||||
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>
|
||||||
|
|||||||
@ -1,234 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { PDFDocument } from 'pdf-lib';
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import { render } from '@documenso/email/render';
|
|
||||||
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
|
|
||||||
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
|
|
||||||
import { alphaid } from '@documenso/lib/universal/id';
|
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import {
|
|
||||||
DocumentDataType,
|
|
||||||
DocumentStatus,
|
|
||||||
FieldType,
|
|
||||||
Prisma,
|
|
||||||
ReadStatus,
|
|
||||||
SendStatus,
|
|
||||||
SigningStatus,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
const ZCreateSinglePlayerDocumentSchema = z.object({
|
|
||||||
documentData: z.object({
|
|
||||||
data: z.string(),
|
|
||||||
type: z.nativeEnum(DocumentDataType),
|
|
||||||
}),
|
|
||||||
documentName: z.string(),
|
|
||||||
signer: z.object({
|
|
||||||
email: z.string().email().min(1),
|
|
||||||
name: z.string(),
|
|
||||||
signature: z.string(),
|
|
||||||
}),
|
|
||||||
fields: z.array(
|
|
||||||
z.object({
|
|
||||||
page: z.number(),
|
|
||||||
type: z.nativeEnum(FieldType),
|
|
||||||
positionX: z.number(),
|
|
||||||
positionY: z.number(),
|
|
||||||
width: z.number(),
|
|
||||||
height: z.number(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCreateSinglePlayerDocumentSchema = z.infer<typeof ZCreateSinglePlayerDocumentSchema>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and self signs a document.
|
|
||||||
*
|
|
||||||
* Returns the document token.
|
|
||||||
*/
|
|
||||||
export const createSinglePlayerDocument = async (
|
|
||||||
value: TCreateSinglePlayerDocumentSchema,
|
|
||||||
): Promise<string> => {
|
|
||||||
const { signer, fields, documentData, documentName } =
|
|
||||||
ZCreateSinglePlayerDocumentSchema.parse(value);
|
|
||||||
|
|
||||||
const document = await getFile({
|
|
||||||
data: documentData.data,
|
|
||||||
type: documentData.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const doc = await PDFDocument.load(document);
|
|
||||||
const createdAt = new Date();
|
|
||||||
|
|
||||||
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
|
|
||||||
const signatureImageAsBase64 = isBase64 ? signer.signature : null;
|
|
||||||
const typedSignature = !isBase64 ? signer.signature : null;
|
|
||||||
|
|
||||||
// Update the document with the fields inserted.
|
|
||||||
for (const field of fields) {
|
|
||||||
const isSignatureField = field.type === FieldType.SIGNATURE;
|
|
||||||
|
|
||||||
await insertFieldInPDF(doc, {
|
|
||||||
...mapField(field, signer),
|
|
||||||
Signature: isSignatureField
|
|
||||||
? {
|
|
||||||
created: createdAt,
|
|
||||||
signatureImageAsBase64,
|
|
||||||
typedSignature,
|
|
||||||
// Dummy data.
|
|
||||||
id: -1,
|
|
||||||
recipientId: -1,
|
|
||||||
fieldId: -1,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
// Dummy data.
|
|
||||||
id: -1,
|
|
||||||
documentId: -1,
|
|
||||||
recipientId: -1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdfBytes = await doc.save();
|
|
||||||
|
|
||||||
const documentToken = await prisma.$transaction(
|
|
||||||
async (tx) => {
|
|
||||||
const documentToken = alphaid();
|
|
||||||
|
|
||||||
// Fetch service user who will be the owner of the document.
|
|
||||||
const serviceUser = await tx.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
email: SERVICE_USER_EMAIL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentDataBytes = Buffer.from(pdfBytes).toString('base64');
|
|
||||||
|
|
||||||
const { id: documentDataId } = await tx.documentData.create({
|
|
||||||
data: {
|
|
||||||
type: DocumentDataType.BYTES_64,
|
|
||||||
data: documentDataBytes,
|
|
||||||
initialData: documentDataBytes,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create document.
|
|
||||||
const document = await tx.document.create({
|
|
||||||
data: {
|
|
||||||
title: documentName,
|
|
||||||
status: DocumentStatus.COMPLETED,
|
|
||||||
documentDataId,
|
|
||||||
userId: serviceUser.id,
|
|
||||||
createdAt,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create recipient.
|
|
||||||
const recipient = await tx.recipient.create({
|
|
||||||
data: {
|
|
||||||
documentId: document.id,
|
|
||||||
name: signer.name,
|
|
||||||
email: signer.email,
|
|
||||||
token: documentToken,
|
|
||||||
signedAt: createdAt,
|
|
||||||
readStatus: ReadStatus.OPENED,
|
|
||||||
signingStatus: SigningStatus.SIGNED,
|
|
||||||
sendStatus: SendStatus.SENT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create fields and signatures.
|
|
||||||
await Promise.all(
|
|
||||||
fields.map(async (field) => {
|
|
||||||
const insertedField = await tx.field.create({
|
|
||||||
data: {
|
|
||||||
documentId: document.id,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
...mapField(field, signer),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
|
|
||||||
await tx.signature.create({
|
|
||||||
data: {
|
|
||||||
fieldId: insertedField.id,
|
|
||||||
signatureImageAsBase64,
|
|
||||||
typedSignature,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return documentToken;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
maxWait: 5000,
|
|
||||||
timeout: 30000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Todo: Handle `downloadLink`
|
|
||||||
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
|
||||||
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
|
|
||||||
documentName: documentName,
|
|
||||||
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email to signer.
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
address: signer.email,
|
|
||||||
name: signer.name,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: 'Document signed',
|
|
||||||
html: render(template),
|
|
||||||
text: render(template, { plainText: true }),
|
|
||||||
});
|
|
||||||
|
|
||||||
return documentToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map the fields provided by the user to fields compatible with Prisma.
|
|
||||||
*
|
|
||||||
* Signature fields are handled separately.
|
|
||||||
*
|
|
||||||
* @param field The field passed in by the user.
|
|
||||||
* @param signer The details of the person who is signing this document.
|
|
||||||
* @returns A field compatible with Prisma.
|
|
||||||
*/
|
|
||||||
const mapField = (
|
|
||||||
field: TCreateSinglePlayerDocumentSchema['fields'][number],
|
|
||||||
signer: TCreateSinglePlayerDocumentSchema['signer'],
|
|
||||||
) => {
|
|
||||||
const customText = match(field.type)
|
|
||||||
.with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a'))
|
|
||||||
.with(FieldType.EMAIL, () => signer.email)
|
|
||||||
.with(FieldType.NAME, () => signer.name)
|
|
||||||
.otherwise(() => '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: field.type,
|
|
||||||
page: field.page,
|
|
||||||
positionX: new Prisma.Decimal(field.positionX),
|
|
||||||
positionY: new Prisma.Decimal(field.positionY),
|
|
||||||
width: new Prisma.Decimal(field.width),
|
|
||||||
height: new Prisma.Decimal(field.height),
|
|
||||||
customText,
|
|
||||||
inserted: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Share } from 'lucide-react';
|
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
|
||||||
import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
|
|
||||||
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
|
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import signingCelebration from '~/assets/signing-celebration.png';
|
|
||||||
import ConfettiScreen from '~/components/(marketing)/confetti-screen';
|
|
||||||
|
|
||||||
import { DocumentStatus } from '.prisma/client';
|
|
||||||
|
|
||||||
interface SinglePlayerModeSuccessProps {
|
|
||||||
className?: string;
|
|
||||||
document: DocumentWithRecipient;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerModeSuccessProps) => {
|
|
||||||
const { getFlag } = useFeatureFlags();
|
|
||||||
|
|
||||||
const isConfettiEnabled = getFlag('marketing_spm_confetti');
|
|
||||||
|
|
||||||
const [showDocumentDialog, setShowDocumentDialog] = useState(false);
|
|
||||||
const [isFetchingDocumentFile, setIsFetchingDocumentFile] = useState(false);
|
|
||||||
const [documentFile, setDocumentFile] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const onShowDocumentClick = async () => {
|
|
||||||
if (isFetchingDocumentFile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFetchingDocumentFile(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await getFile(document.documentData);
|
|
||||||
|
|
||||||
setDocumentFile(base64.encode(data));
|
|
||||||
|
|
||||||
setShowDocumentDialog(true);
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong.',
|
|
||||||
description: 'We were unable to retrieve the document at this time. Please try again.',
|
|
||||||
variant: 'destructive',
|
|
||||||
duration: 7500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFetchingDocumentFile(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo({ top: 0 });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[calc(100vh-10rem)] flex-col items-center justify-center sm:min-h-[calc(100vh-13rem)]">
|
|
||||||
{isConfettiEnabled && (
|
|
||||||
<ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h2 className="text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
|
|
||||||
You have signed
|
|
||||||
<span className="mt-2 block">{document.title}</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<SigningCard3D
|
|
||||||
className="mt-8"
|
|
||||||
name={document.Recipient.name || document.Recipient.email}
|
|
||||||
signingCelebrationImage={signingCelebration}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-8 w-full">
|
|
||||||
<div className={cn('flex flex-col items-center', className)}>
|
|
||||||
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
|
|
||||||
{/* TODO: Hook this up */}
|
|
||||||
<Button variant="outline" className="flex-1" disabled>
|
|
||||||
<Share className="mr-2 h-5 w-5" />
|
|
||||||
Share
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DocumentDownloadButton
|
|
||||||
className="flex-1"
|
|
||||||
fileName={document.title}
|
|
||||||
documentData={document.documentData}
|
|
||||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={async () => onShowDocumentClick()}
|
|
||||||
loading={isFetchingDocumentFile}
|
|
||||||
className="col-span-2"
|
|
||||||
>
|
|
||||||
Show document
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-16 text-center text-sm">
|
|
||||||
Create a{' '}
|
|
||||||
<Link
|
|
||||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
|
|
||||||
target="_blank"
|
|
||||||
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
free account
|
|
||||||
</Link>{' '}
|
|
||||||
to access your signed documents at any time
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<DocumentDialog
|
|
||||||
document={documentFile ?? ''}
|
|
||||||
open={showDocumentDialog}
|
|
||||||
onOpenChange={setShowDocumentDialog}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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="text-muted-foreground col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed lg:col-span-7">
|
<div className="col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed text-[#727272] lg:col-span-7">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
|
className="col-span-12 flex flex-col rounded-2xl bg-[#F7F7F7] 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="text-muted-foreground mt-2 text-xs">
|
<p className="mt-2 text-xs text-[#AFAFAF]">
|
||||||
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-foreground text-lg font-semibold lg:text-xl">
|
<label htmlFor="email" className="text-lg font-semibold text-slate-900 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="bg-background w-full pr-16"
|
className="w-full bg-white pr-16"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
field.value !== '' &&
|
field.value !== '' &&
|
||||||
@ -255,10 +255,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
transform: 'translateX(25%)',
|
transform: 'translateX(25%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label
|
<label htmlFor="name" className="text-lg font-semibold text-slate-900 lg:text-xl">
|
||||||
htmlFor="name"
|
|
||||||
className="text-foreground text-lg font-semibold lg:text-xl"
|
|
||||||
>
|
|
||||||
and your name?
|
and your name?
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@ -271,7 +268,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
className="bg-background w-full pr-16"
|
className="w-full bg-white pr-16"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
field.value !== '' &&
|
field.value !== '' &&
|
||||||
@ -303,11 +300,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-muted-foreground text-xs">{stepsRemaining} step(s) until signed</p>
|
<p className="text-xs text-[#AFAFAF]">{stepsRemaining} step(s) until signed</p>
|
||||||
<p className="text-muted-foreground block text-xs md:hidden">Minimise contract</p>
|
<p className="block text-xs text-[#AFAFAF] md:hidden">Minimise contract</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-background relative mt-2.5 h-[2px] w-full">
|
<div className="relative mt-2.5 h-[2px] w-full bg-[#E9E9E9]">
|
||||||
<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,
|
||||||
@ -325,17 +322,13 @@ 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
|
<img src={signatureDataUrl} alt="user signature" className="h-full" />
|
||||||
src={signatureDataUrl}
|
|
||||||
alt="user signature"
|
|
||||||
className="h-full dark:invert"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{signatureText && (
|
{signatureText && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground text-4xl font-semibold [font-family:var(--font-caveat)]',
|
'text-4xl font-semibold text-slate-900 [font-family:var(--font-caveat)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{signatureText}
|
{signatureText}
|
||||||
@ -349,7 +342,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id="signatureText"
|
id="signatureText"
|
||||||
className="text-foreground placeholder:text-muted-foreground border-none p-0 text-sm focus-visible:ring-0"
|
className="border-none p-0 text-sm text-slate-700 placeholder:text-[#D6D6D6] focus-visible:ring-0"
|
||||||
placeholder="Draw or type name here"
|
placeholder="Draw or type name here"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...register('signatureText', {
|
{...register('signatureText', {
|
||||||
@ -363,7 +356,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted h-8"
|
className="h-8 disabled:bg-[#ECEEED] disabled:text-[#C6C6C6] disabled:hover:bg-[#ECEEED]"
|
||||||
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" />}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import handlerFeatureFlagAll from '@documenso/lib/server-only/feature-flags/all';
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
runtime: 'edge',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default handlerFeatureFlagAll;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import handlerFeatureFlagGet from '@documenso/lib/server-only/feature-flags/get';
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
runtime: 'edge',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default handlerFeatureFlagGet;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
'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>;
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { usePathname, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import posthog from 'posthog-js';
|
|
||||||
|
|
||||||
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
|
||||||
|
|
||||||
export function PostHogPageview() {
|
|
||||||
const postHogConfig = extractPostHogConfig();
|
|
||||||
|
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && postHogConfig) {
|
|
||||||
posthog.init(postHogConfig.key, {
|
|
||||||
api_host: postHogConfig.host,
|
|
||||||
disable_session_recording: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!postHogConfig || !pathname) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = window.origin + pathname;
|
|
||||||
if (searchParams && searchParams.toString()) {
|
|
||||||
url = url + `?${searchParams.toString()}`;
|
|
||||||
}
|
|
||||||
posthog.capture('$pageview', {
|
|
||||||
$current_url: url,
|
|
||||||
});
|
|
||||||
}, [pathname, searchParams, postHogConfig]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { version } = require('./package.json');
|
const { version } = require('./package.json');
|
||||||
|
|
||||||
require('dotenv').config({
|
const { parsed: env } = require('dotenv').config({
|
||||||
path: path.join(__dirname, '../../.env.local'),
|
path: path.join(__dirname, '../../.env.local'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -22,7 +22,6 @@ const config = {
|
|||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
APP_VERSION: version,
|
APP_VERSION: version,
|
||||||
NEXT_PUBLIC_PROJECT: 'web',
|
|
||||||
},
|
},
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
'lucide-react': {
|
'lucide-react': {
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
"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": {
|
||||||
@ -22,10 +21,10 @@
|
|||||||
"@tanstack/react-query": "^4.29.5",
|
"@tanstack/react-query": "^4.29.5",
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.279.0",
|
"lucide-react": "^0.214.0",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "13.5.4",
|
"next": "13.4.12",
|
||||||
"next-auth": "4.22.3",
|
"next-auth": "4.22.3",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
|
|||||||
1
apps/web/public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 743 KiB |
1
apps/web/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 629 B |
@ -55,18 +55,21 @@ export const EditDocumentForm = ({
|
|||||||
title: 'Add Signers',
|
title: 'Add Signers',
|
||||||
description: 'Add the people who will sign the document.',
|
description: 'Add the people who will sign the document.',
|
||||||
stepIndex: 1,
|
stepIndex: 1,
|
||||||
|
onSubmit: () => onAddSignersFormSubmit,
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
title: 'Add Fields',
|
title: 'Add Fields',
|
||||||
description: 'Add all relevant fields for each recipient.',
|
description: 'Add all relevant fields for each recipient.',
|
||||||
stepIndex: 2,
|
stepIndex: 2,
|
||||||
onBackStep: () => setStep('signers'),
|
onBackStep: () => setStep('signers'),
|
||||||
|
onSubmit: () => onAddFieldsFormSubmit,
|
||||||
},
|
},
|
||||||
subject: {
|
subject: {
|
||||||
title: 'Add Subject',
|
title: 'Add Subject',
|
||||||
description: 'Add the subject and message you wish to send to signers.',
|
description: 'Add the subject and message you wish to send to signers.',
|
||||||
stepIndex: 3,
|
stepIndex: 3,
|
||||||
onBackStep: () => setStep('fields'),
|
onBackStep: () => setStep('fields'),
|
||||||
|
onSubmit: () => onAddSubjectFormSubmit,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -166,7 +169,6 @@ 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}
|
||||||
@ -177,7 +179,6 @@ 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,11 +7,7 @@ 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 & {
|
||||||
@ -22,16 +18,11 @@ 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;
|
||||||
@ -41,20 +32,6 @@ 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,
|
||||||
@ -80,8 +57,8 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
<Button className="w-24" loading={isCreatingShareLink} onClick={onShareClick}>
|
<Button className="w-24" disabled>
|
||||||
{!isCreatingShareLink && <Share className="-ml-1 mr-2 h-4 w-4" />}
|
<Share className="-ml-1 mr-2 h-4 w-4" />
|
||||||
Share
|
Share
|
||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
History,
|
History,
|
||||||
Loader,
|
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
Share,
|
Share,
|
||||||
@ -19,8 +18,7 @@ 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 as trpcClient } from '@documenso/trpc/client';
|
import { trpc } from '@documenso/trpc/client';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -28,9 +26,6 @@ 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 & {
|
||||||
@ -41,16 +36,11 @@ 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;
|
||||||
@ -60,29 +50,15 @@ 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 trpcClient.document.getDocumentById.query({
|
document = await trpc.document.getDocumentById.query({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
document = await trpcClient.document.getDocumentByToken.query({
|
document = await trpc.document.getDocumentByToken.query({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -112,7 +88,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
|
<MoreHorizontal className="h-5 w-5 text-gray-500" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
@ -159,12 +135,8 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
Resend
|
Resend
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onShareClick}>
|
<DropdownMenuItem disabled>
|
||||||
{isCreatingShareLink ? (
|
<Share className="mr-2 h-4 w-4" />
|
||||||
<Loader 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="bg-background/50 absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||||
<Loader className="text-muted-foreground h-8 w-8 animate-spin" />
|
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
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,7 +12,6 @@ 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 = {
|
||||||
@ -63,44 +62,41 @@ 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 />
|
||||||
|
|
||||||
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
|
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
|
||||||
<h1 className="text-4xl font-semibold">Documents</h1>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-6 overflow-hidden">
|
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
|
||||||
<Tabs defaultValue={status} className="overflow-x-auto">
|
<Tabs defaultValue={status} className="overflow-x-auto">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{[
|
{[
|
||||||
ExtendedDocumentStatus.INBOX,
|
ExtendedDocumentStatus.INBOX,
|
||||||
ExtendedDocumentStatus.PENDING,
|
ExtendedDocumentStatus.PENDING,
|
||||||
ExtendedDocumentStatus.COMPLETED,
|
ExtendedDocumentStatus.COMPLETED,
|
||||||
ExtendedDocumentStatus.DRAFT,
|
ExtendedDocumentStatus.DRAFT,
|
||||||
ExtendedDocumentStatus.ALL,
|
ExtendedDocumentStatus.ALL,
|
||||||
].map((value) => (
|
].map((value) => (
|
||||||
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
|
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
|
||||||
<Link href={getTabHref(value)} scroll={false}>
|
<Link href={getTabHref(value)} scroll={false}>
|
||||||
<DocumentStatus status={value} />
|
<DocumentStatus status={value} />
|
||||||
|
|
||||||
{value !== ExtendedDocumentStatus.ALL && (
|
{value !== ExtendedDocumentStatus.ALL && (
|
||||||
<span className="ml-1 hidden opacity-50 md:inline-block">
|
<span className="ml-1 hidden opacity-50 md:inline-block">
|
||||||
{Math.min(stats[value], 99)}
|
{Math.min(stats[value], 99)}
|
||||||
{stats[value] > 99 && '+'}
|
{stats[value] > 99 && '+'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
<div className="flex flex-1 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">
|
||||||
{results.count > 0 && <DocumentsDataTable results={results} />}
|
<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="bg-background/50 absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||||
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
|
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -4,12 +4,12 @@ import { redirect } from 'next/navigation';
|
|||||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
|
||||||
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag';
|
||||||
|
|
||||||
export default async function BillingSettingsPage() {
|
export default async function BillingSettingsPage() {
|
||||||
const user = await getRequiredServerComponentSession();
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|||||||
@ -1,153 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { Metadata } from 'next';
|
|
||||||
|
|
||||||
import { Redirect } from './redirect';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Documenso - Share',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SharePage() {
|
|
||||||
return <Redirect />;
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export const Redirect = () => {
|
|
||||||
useEffect(() => {
|
|
||||||
window.location.href = process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@ -15,7 +15,7 @@ export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
|||||||
documentData?: DocumentData;
|
documentData?: DocumentData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentDownloadButton = ({
|
export const DownloadButton = ({
|
||||||
className,
|
className,
|
||||||
fileName,
|
fileName,
|
||||||
documentData,
|
documentData,
|
||||||
@ -1,19 +1,17 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { CheckCircle2, Clock8 } from 'lucide-react';
|
import { CheckCircle2, Clock8, Share } from 'lucide-react';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { SigningCard } from '@documenso/ui/components/signing-card';
|
|
||||||
|
|
||||||
import signingCelebration from '~/assets/signing-celebration.png';
|
import { DownloadButton } from './download-button';
|
||||||
|
import { SigningCard } from './signing-card';
|
||||||
import { ShareButton } from './share-button';
|
|
||||||
|
|
||||||
export type CompletedSigningPageProps = {
|
export type CompletedSigningPageProps = {
|
||||||
params: {
|
params: {
|
||||||
@ -55,7 +53,7 @@ export default async function CompletedSigningPage({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center pt-24">
|
<div className="flex flex-col items-center pt-24">
|
||||||
{/* Card with recipient */}
|
{/* Card with recipient */}
|
||||||
<SigningCard name={recipientName} signingCelebrationImage={signingCelebration} />
|
<SigningCard name={recipientName} />
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{match(document.status)
|
{match(document.status)
|
||||||
@ -90,9 +88,13 @@ 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">
|
||||||
<ShareButton documentId={document.id} token={recipient.token} />
|
{/* TODO: Hook this up */}
|
||||||
|
<Button variant="outline" className="flex-1">
|
||||||
|
<Share className="mr-2 h-5 w-5" />
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
|
||||||
<DocumentDownloadButton
|
<DownloadButton
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
fileName={document.title}
|
fileName={document.title}
|
||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
@ -101,7 +103,7 @@ export default async function CompletedSigningPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-36 text-sm">
|
<p className="text-muted-foreground/60 mt-36 text-sm">
|
||||||
Want to send slick signing links like this one?{' '}
|
Want so send slick signing links like this one?{' '}
|
||||||
<Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600">
|
<Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600">
|
||||||
Check out Documenso.
|
Check out Documenso.
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,146 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { HTMLAttributes, useState } from 'react';
|
|
||||||
|
|
||||||
import { Copy, Share, Twitter } from 'lucide-react';
|
|
||||||
|
|
||||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@documenso/ui/primitives/dialog';
|
|
||||||
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 [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: createOrGetShareLink,
|
|
||||||
data: shareLink,
|
|
||||||
isLoading,
|
|
||||||
} = trpc.shareLink.createOrGetShareLink.useMutation();
|
|
||||||
|
|
||||||
const onOpenChange = (nextOpen: boolean) => {
|
|
||||||
if (nextOpen) {
|
|
||||||
void createOrGetShareLink({
|
|
||||||
token,
|
|
||||||
documentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsOpen(nextOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCopyClick = async () => {
|
|
||||||
let { slug = '' } = shareLink || {};
|
|
||||||
|
|
||||||
if (!slug) {
|
|
||||||
const result = await createOrGetShareLink({
|
|
||||||
token,
|
|
||||||
documentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
slug = result.slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Copied to clipboard',
|
|
||||||
description: 'The sharing link has been copied to your clipboard.',
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTweetClick = async () => {
|
|
||||||
let { slug = '' } = shareLink || {};
|
|
||||||
|
|
||||||
if (!slug) {
|
|
||||||
const result = await createOrGetShareLink({
|
|
||||||
token,
|
|
||||||
documentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
slug = result.slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(
|
|
||||||
generateTwitterIntent(
|
|
||||||
`I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`,
|
|
||||||
`${window.location.origin}/share/${slug}`,
|
|
||||||
),
|
|
||||||
'_blank',
|
|
||||||
);
|
|
||||||
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
disabled={!token || !documentId}
|
|
||||||
className="flex-1"
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{!isLoading && <Share className="mr-2 h-5 w-5" />}
|
|
||||||
Share
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Share</DialogTitle>
|
|
||||||
|
|
||||||
<DialogDescription className="mt-4">Share your signing experience!</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex w-full flex-col">
|
|
||||||
<div className="rounded-md border p-4">
|
|
||||||
I just {token ? 'signed' : 'sent'} a document with{' '}
|
|
||||||
<span className="font-medium text-blue-400">@documenso</span>
|
|
||||||
. Check it out!
|
|
||||||
<span className="mt-2 block" />
|
|
||||||
<span className="font-medium text-blue-400">
|
|
||||||
{window.location.origin}/share/{shareLink?.slug || '...'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant="outline" className="mt-4" onClick={onTweetClick}>
|
|
||||||
<Twitter className="mr-2 h-4 w-4" />
|
|
||||||
Tweet
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-4 text-xs uppercase">
|
|
||||||
<div className="bg-border h-px flex-1" />
|
|
||||||
<span className="text-muted-foreground bg-transparent">Or</span>
|
|
||||||
<div className="bg-border h-px flex-1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant="outline" onClick={onCopyClick}>
|
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
|
||||||
Copy Link
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
|
import signingCelebration from '~/assets/signing-celebration.png';
|
||||||
|
|
||||||
|
export type SigningCardProps = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SigningCard = ({ name }: SigningCardProps) => {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full max-w-xs md:max-w-sm">
|
||||||
|
<Card
|
||||||
|
className="group mx-auto flex aspect-[21/9] w-full items-center justify-center"
|
||||||
|
degrees={-145}
|
||||||
|
gradient
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
className="font-signature p-6 text-center"
|
||||||
|
style={{
|
||||||
|
container: 'main',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="text-muted-foreground/60 group-hover:text-primary/80 break-all font-semibold duration-300"
|
||||||
|
style={{
|
||||||
|
fontSize: `max(min(4rem, ${(100 / name.length / 2).toFixed(4)}cqw), 1.875rem)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute -inset-32 -z-10 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.8,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: 1,
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
delay: 0.5,
|
||||||
|
duration: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={signingCelebration}
|
||||||
|
alt="background pattern"
|
||||||
|
className="w-full"
|
||||||
|
style={{
|
||||||
|
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||||
|
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -77,7 +77,7 @@ export const DateField = ({ field, recipient }: DateFieldProps) => {
|
|||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -81,7 +81,7 @@ export const EmailField = ({ field, recipient }: EmailFieldProps) => {
|
|||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
|
||||||
import { Document, Field, Recipient } from '@documenso/prisma/client';
|
import { Document, Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@ -30,22 +27,15 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||||
|
|
||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
} = useForm();
|
} = useForm();
|
||||||
|
|
||||||
const uninsertedFields = useMemo(() => {
|
const isComplete = fields.every((f) => f.inserted);
|
||||||
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
|
|
||||||
}, [fields]);
|
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
const onFormSubmit = async () => {
|
||||||
setValidateUninsertedFields(true);
|
if (!isComplete) {
|
||||||
const isFieldsValid = validateFieldsInserted(fields);
|
|
||||||
|
|
||||||
if (!isFieldsValid) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,16 +54,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
)}
|
)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
>
|
>
|
||||||
{validateUninsertedFields && uninsertedFields[0] && (
|
<div className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}>
|
||||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
|
||||||
Click to insert field
|
|
||||||
</FieldToolTip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<fieldset
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
|
|
||||||
>
|
|
||||||
<div className={cn('flex flex-1 flex-col')}>
|
<div className={cn('flex flex-1 flex-col')}>
|
||||||
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
||||||
|
|
||||||
@ -120,18 +101,23 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={() => router.back()}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button className="w-full" type="submit" size="lg" loading={isSubmitting}>
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
disabled={!isComplete || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
|
||||||
Complete
|
Complete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
|
|||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 }).catch(() => null),
|
viewedDocument({ token }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!document || !document.documentData || !recipient) {
|
if (!document || !document.documentData || !recipient) {
|
||||||
|
|||||||
@ -115,7 +115,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -130,7 +130,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
<img
|
<img
|
||||||
src={signature.signatureImageAsBase64}
|
src={signature.signatureImageAsBase64}
|
||||||
alt={`Signature for ${recipient.name}`}
|
alt={`Signature for ${recipient.name}`}
|
||||||
className="h-full w-full object-contain dark:invert"
|
className="h-full w-full object-contain"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
|
import { useFieldPageCoords } from '~/hooks/use-field-page-coords';
|
||||||
|
|
||||||
export type SignatureFieldProps = {
|
export type SignatureFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
@ -20,6 +23,8 @@ export const SigningFieldContainer = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
children,
|
children,
|
||||||
}: SignatureFieldProps) => {
|
}: SignatureFieldProps) => {
|
||||||
|
const coords = useFieldPageCoords(field);
|
||||||
|
|
||||||
const onSignFieldClick = async () => {
|
const onSignFieldClick = async () => {
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
return;
|
return;
|
||||||
@ -37,25 +42,40 @@ export const SigningFieldContainer = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldRootContainer field={field}>
|
<div
|
||||||
{!field.inserted && !loading && (
|
className="absolute"
|
||||||
<button
|
style={{
|
||||||
type="submit"
|
top: `${coords.y}px`,
|
||||||
className="absolute inset-0 z-10 h-full w-full"
|
left: `${coords.x}px`,
|
||||||
onClick={onSignFieldClick}
|
height: `${coords.height}px`,
|
||||||
/>
|
width: `${coords.width}px`,
|
||||||
)}
|
}}
|
||||||
|
>
|
||||||
{field.inserted && !loading && (
|
<Card
|
||||||
<button
|
className="bg-background relative h-full w-full"
|
||||||
className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
|
data-inserted={field.inserted ? 'true' : 'false'}
|
||||||
onClick={onRemoveSignedFieldClick}
|
>
|
||||||
|
<CardContent
|
||||||
|
className={cn(
|
||||||
|
'text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Remove
|
{!field.inserted && !loading && (
|
||||||
</button>
|
<button type="submit" className="absolute inset-0 z-10" onClick={onSignFieldClick} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
{field.inserted && !loading && (
|
||||||
</FieldRootContainer>
|
<button
|
||||||
|
className="text-destructive bg-background/40 absolute inset-0 z-10 flex items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
|
||||||
|
onClick={onRemoveSignedFieldClick}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default function UnauthenticatedLayout({ children }: UnauthenticatedLayou
|
|||||||
<Image
|
<Image
|
||||||
src={backgroundPattern}
|
src={backgroundPattern}
|
||||||
alt="background pattern"
|
alt="background pattern"
|
||||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
className="dark:brightness-95 dark:invert dark:sepia"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -2,15 +2,15 @@ import { Suspense } from 'react';
|
|||||||
|
|
||||||
import { Caveat, Inter } from 'next/font/google';
|
import { Caveat, Inter } from 'next/font/google';
|
||||||
|
|
||||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
|
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
|
||||||
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
|
||||||
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
|
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
import { TrpcProvider } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Toaster } from '@documenso/ui/primitives/toaster';
|
import { Toaster } from '@documenso/ui/primitives/toaster';
|
||||||
import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
|
import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
|
import { getServerComponentAllFlags } from '~/helpers/get-server-component-feature-flag';
|
||||||
|
import { FeatureFlagProvider } from '~/providers/feature-flag';
|
||||||
import { ThemeProvider } from '~/providers/next-theme';
|
import { ThemeProvider } from '~/providers/next-theme';
|
||||||
import { PlausibleProvider } from '~/providers/plausible';
|
import { PlausibleProvider } from '~/providers/plausible';
|
||||||
import { PostHogPageview } from '~/providers/posthog';
|
import { PostHogPageview } from '~/providers/posthog';
|
||||||
@ -76,7 +76,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</FeatureFlagProvider>
|
</FeatureFlagProvider>
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 3.3 MiB |
|
Before 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-muted-foreground text-sm">{recipient.email}</span>
|
<span className="text-sm text-gray-500">{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-muted-foreground text-sm">{recipient.email}</span>
|
<span className="text-sm text-gray-500">{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-muted-foreground text-sm">{recipient.email}</span>
|
<span className="text-sm text-gray-500">{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-muted-foreground text-sm">{recipient.email}</span>
|
<span className="text-sm text-gray-500">{recipient.email}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,14 +10,12 @@ import {
|
|||||||
User as LucideUser,
|
User as LucideUser,
|
||||||
Monitor,
|
Monitor,
|
||||||
Moon,
|
Moon,
|
||||||
Palette,
|
|
||||||
Sun,
|
Sun,
|
||||||
UserCog,
|
UserCog,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut } from 'next-auth/react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||||
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import { User } from '@documenso/prisma/client';
|
import { User } from '@documenso/prisma/client';
|
||||||
@ -28,23 +26,19 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
|
|
||||||
|
import { useFeatureFlags } from '~/providers/feature-flag';
|
||||||
|
|
||||||
export type ProfileDropdownProps = {
|
export type ProfileDropdownProps = {
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
||||||
const { getFlag } = useFeatureFlags();
|
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
const { getFlag } = useFeatureFlags();
|
||||||
const isUserAdmin = isAdmin(user);
|
const isUserAdmin = isAdmin(user);
|
||||||
|
|
||||||
const isBillingEnabled = getFlag('app_billing');
|
const isBillingEnabled = getFlag('app_billing');
|
||||||
@ -104,30 +98,28 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
|||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuSub>
|
{theme === 'light' ? null : (
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||||
<Palette className="mr-2 h-4 w-4" />
|
<Sun className="mr-2 h-4 w-4" />
|
||||||
Themes
|
Light Mode
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuPortal>
|
)}
|
||||||
<DropdownMenuSubContent>
|
{theme === 'dark' ? null : (
|
||||||
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
|
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||||
<DropdownMenuRadioItem value="light">
|
<Moon className="mr-2 h-4 w-4" />
|
||||||
<Sun className="mr-2 h-4 w-4" /> Light
|
Dark Mode
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuRadioItem value="dark">
|
)}
|
||||||
<Moon className="mr-2 h-4 w-4" />
|
|
||||||
Dark
|
{theme === 'system' ? null : (
|
||||||
</DropdownMenuRadioItem>
|
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||||
<DropdownMenuRadioItem value="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" />
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import { usePathname } from 'next/navigation';
|
|||||||
|
|
||||||
import { CreditCard, Key, User } from 'lucide-react';
|
import { CreditCard, Key, User } from 'lucide-react';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { useFeatureFlags } from '~/providers/feature-flag';
|
||||||
|
|
||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import { usePathname } from 'next/navigation';
|
|||||||
|
|
||||||
import { CreditCard, Key, User } from 'lucide-react';
|
import { CreditCard, Key, User } from 'lucide-react';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { useFeatureFlags } from '~/providers/feature-flag';
|
||||||
|
|
||||||
export type MobileNavProps = HTMLAttributes<HTMLDivElement>;
|
export type MobileNavProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||||
|
|||||||
@ -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 dark:text-blue-300',
|
color: 'text-blue-600',
|
||||||
},
|
},
|
||||||
COMPLETED: {
|
COMPLETED: {
|
||||||
label: 'Completed',
|
label: 'Completed',
|
||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
color: 'text-green-500 dark:text-green-300',
|
color: 'text-green-500',
|
||||||
},
|
},
|
||||||
DRAFT: {
|
DRAFT: {
|
||||||
label: 'Draft',
|
label: 'Draft',
|
||||||
icon: File,
|
icon: File,
|
||||||
color: 'text-yellow-500 dark:text-yellow-200',
|
color: 'text-yellow-500',
|
||||||
},
|
},
|
||||||
INBOX: {
|
INBOX: {
|
||||||
label: 'Inbox',
|
label: 'Inbox',
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
'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';
|
||||||
|
|
||||||
@ -9,20 +8,12 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
export const completeDocument = async ({ documentId }: CompleteDocumentActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { id: userId } = await getRequiredServerComponentSession();
|
const { id: userId } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
if (email.message || email.subject) {
|
await sendDocument({
|
||||||
await upsertDocumentMeta({
|
|
||||||
documentId,
|
|
||||||
subject: email.subject,
|
|
||||||
message: email.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await sendDocument({
|
|
||||||
userId,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Eye, EyeOff, Loader } from 'lucide-react';
|
import { Eye, EyeOff, Loader } from 'lucide-react';
|
||||||
@ -38,6 +40,8 @@ export type SignInFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SignInForm = ({ className }: SignInFormProps) => {
|
export const SignInForm = ({ className }: SignInFormProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
@ -53,29 +57,36 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
resolver: zodResolver(ZSignInFormSchema),
|
resolver: zodResolver(ZSignInFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorCode = searchParams?.get('error');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
if (isErrorCode(errorCode)) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred',
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [errorCode, toast]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
|
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const result = await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
callbackUrl: LOGIN_REDIRECT_PATH,
|
callbackUrl: LOGIN_REDIRECT_PATH,
|
||||||
redirect: false,
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error && isErrorCode(result.error)) {
|
|
||||||
toast({
|
|
||||||
variant: 'destructive',
|
|
||||||
description: ERROR_MESSAGES[result.error],
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result?.url) {
|
|
||||||
throw new Error('An unknown error occurred');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = result.url;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'An unknown error occurred',
|
title: 'An unknown error occurred',
|
||||||
|
|||||||
7
apps/web/src/components/motion.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export * from 'framer-motion';
|
||||||
|
|
||||||
|
export const MotionDiv = motion.div;
|
||||||
@ -1,66 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import backgroundPattern from '~/assets/background-pattern.png';
|
|
||||||
|
|
||||||
export type NotFoundPartialProps = {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function NotFoundPartial({ children }: NotFoundPartialProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('relative max-w-[100vw] overflow-hidden')}>
|
|
||||||
<div className="absolute -inset-24 -z-10">
|
|
||||||
<motion.div
|
|
||||||
className="flex h-full w-full items-center justify-center"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 0.8, transition: { duration: 0.5, delay: 0.5 } }}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={backgroundPattern}
|
|
||||||
alt="background pattern"
|
|
||||||
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container mx-auto flex h-full min-h-screen items-center px-6 py-32">
|
|
||||||
<div>
|
|
||||||
<p className="text-muted-foreground font-semibold">404 Page not found</p>
|
|
||||||
|
|
||||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4 text-sm">
|
|
||||||
The page you are looking for was moved, removed, renamed or might never have existed.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-32"
|
|
||||||
onClick={() => {
|
|
||||||
void router.back();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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());
|
|
||||||
};
|
|
||||||
@ -1,12 +1,9 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
|
||||||
TFeatureFlagValue,
|
|
||||||
ZFeatureFlagValueSchema,
|
|
||||||
} from '@documenso/lib/client-only/providers/feature-flag.types';
|
|
||||||
import { APP_BASE_URL } from '@documenso/lib/constants/app';
|
|
||||||
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
|
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
|
||||||
|
|
||||||
|
import { TFeatureFlagValue, ZFeatureFlagValueSchema } from '~/providers/feature-flag.types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate whether a flag is enabled for the current user.
|
* Evaluate whether a flag is enabled for the current user.
|
||||||
*
|
*
|
||||||
@ -24,7 +21,7 @@ export const getFlag = async (
|
|||||||
return LOCAL_FEATURE_FLAGS[flag] ?? true;
|
return LOCAL_FEATURE_FLAGS[flag] ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(`${APP_BASE_URL}/api/feature-flag/get`);
|
const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/get`);
|
||||||
url.searchParams.set('flag', flag);
|
url.searchParams.set('flag', flag);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@ -57,7 +54,7 @@ export const getAllFlags = async (
|
|||||||
return LOCAL_FEATURE_FLAGS;
|
return LOCAL_FEATURE_FLAGS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(`${APP_BASE_URL}/api/feature-flag/all`);
|
const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/all`);
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -72,28 +69,6 @@ export const getAllFlags = async (
|
|||||||
.catch(() => LOCAL_FEATURE_FLAGS);
|
.catch(() => LOCAL_FEATURE_FLAGS);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all feature flags for anonymous users.
|
|
||||||
*
|
|
||||||
* @returns A record of flags and their values.
|
|
||||||
*/
|
|
||||||
export const getAllAnonymousFlags = async (): Promise<Record<string, TFeatureFlagValue>> => {
|
|
||||||
if (!isFeatureFlagEnabled()) {
|
|
||||||
return LOCAL_FEATURE_FLAGS;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(`${APP_BASE_URL}/api/feature-flag/all`);
|
|
||||||
|
|
||||||
return fetch(url, {
|
|
||||||
next: {
|
|
||||||
revalidate: 60,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (res) => res.json())
|
|
||||||
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
|
|
||||||
.catch(() => LOCAL_FEATURE_FLAGS);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface GetFlagOptions {
|
interface GetFlagOptions {
|
||||||
/**
|
/**
|
||||||
* The headers to attach to the request to evaluate flags.
|
* The headers to attach to the request to evaluate flags.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
|
|
||||||
import { getAllFlags, getFlag } from '@documenso/lib/universal/get-feature-flag';
|
import { getAllFlags, getFlag } from './get-feature-flag';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate whether a flag is enabled for the current user in a server component.
|
* Evaluate whether a flag is enabled for the current user in a server component.
|
||||||